From e45d97b26c6d0e0798d620ad0b097cad9009c179 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Fri, 27 Sep 2024 14:45:45 -0400 Subject: [PATCH] [Security Solution][Serverless] - Improve security solution performance (#194241) ## Summary The goal of this PR is to improve the default performance of many of our security solution views. 1. Upon scale testing, it was observed that the default events histogram aggregation was a source of application slowness, so to improve the performance of the default security experience, we've made the default breakdown for the events histogram `No Breakdown` similar to what is seen in the default discover histogram experience. 2. After looking through some telemetry, it was observed that the field list query run in the background for timeline can also take a significant amount of time based on the user's field count, so we now only run that query after timeline has been opened. ### Demos #### 1. By default the events visualizations on the overview and explore events pages will not have an aggregation. The user will have to manually select the breakdown they desire: https://github.com/elastic/kibana/commit/d354d27962ebbd6d5fda19e912ec344ffe8a6c75 https://github.com/user-attachments/assets/a6d6987b-73fc-4735-9c37-973917c2fa2d #### 2. Timeline fields list will only load after the first interaction with timeline: https://github.com/elastic/kibana/commit/ad557260d8f9c5dd0810a5a6aa51e5de0430000f **Before:** https://github.com/user-attachments/assets/0ad2e903-ac15-4daa-925b-da8ad05e80dd **After:** https://github.com/user-attachments/assets/27d5d3d5-02c8-49b5-b699-239ebc36b16c --- .../events_tab/events_query_tab_body.test.tsx | 3 +- .../events_tab/histogram_configurations.ts | 13 +++- .../components/events_tab/translations.ts | 7 +++ .../components/matrix_histogram/index.tsx | 10 +++- .../components/matrix_histogram/types.ts | 2 +- .../lens_attributes/common/event.test.ts | 39 +++++++++++- .../lens_attributes/common/events.ts | 48 ++++++++------- .../use_lens_attributes.test.tsx | 20 +++++++ .../use_lens_attributes.tsx | 4 +- .../components/events_by_dataset/index.tsx | 5 +- .../query_tab_unified_components.test.tsx | 5 +- .../unified_components/index.test.tsx | 59 +++++++++++++++++-- .../timeline/unified_components/index.tsx | 31 +++++++++- 13 files changed, 206 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx index 1db522f4c011e..dd5cbb4131b4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx @@ -15,6 +15,7 @@ import { EventsQueryTabBody, ALERTS_EVENTS_HISTOGRAM_ID } from './events_query_t import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { licenseService } from '../../hooks/use_license'; import { mockHistory } from '../../mock/router'; +import { DEFAULT_EVENTS_STACK_BY_VALUE } from './histogram_configurations'; const mockGetDefaultControlColumn = jest.fn(); jest.mock('../../../timelines/components/timeline/body/control_columns', () => ({ @@ -144,7 +145,7 @@ describe('EventsQueryTabBody', () => { ); expect(result.getByTestId('header-section-supplements').querySelector('select')?.value).toEqual( - 'event.action' + DEFAULT_EVENTS_STACK_BY_VALUE ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/histogram_configurations.ts b/x-pack/plugins/security_solution/public/common/components/events_tab/histogram_configurations.ts index 12458d5c52a5f..ecb3400620b28 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/histogram_configurations.ts +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/histogram_configurations.ts @@ -11,7 +11,9 @@ import { getEventsHistogramLensAttributes } from '../visualization_actions/lens_ import type { MatrixHistogramConfigs, MatrixHistogramOption } from '../matrix_histogram/types'; import * as i18n from './translations'; -const DEFAULT_EVENTS_STACK_BY = 'event.action'; +export const NO_BREAKDOWN_STACK_BY_VALUE = 'no_breakdown'; + +export const DEFAULT_EVENTS_STACK_BY_VALUE = NO_BREAKDOWN_STACK_BY_VALUE; export const getSubtitleFunction = (defaultNumberFormat: string, isAlert: boolean) => (totalCount: number) => @@ -20,6 +22,10 @@ export const getSubtitleFunction = }`; export const eventsStackByOptions: MatrixHistogramOption[] = [ + { + text: i18n.EVENTS_GRAPH_NO_BREAKDOWN_TITLE, + value: NO_BREAKDOWN_STACK_BY_VALUE, + }, { text: 'event.action', value: 'event.action', @@ -36,7 +42,8 @@ export const eventsStackByOptions: MatrixHistogramOption[] = [ export const eventsHistogramConfig: MatrixHistogramConfigs = { defaultStackByOption: - eventsStackByOptions.find((o) => o.text === DEFAULT_EVENTS_STACK_BY) ?? eventsStackByOptions[0], + eventsStackByOptions.find((o) => o.value === DEFAULT_EVENTS_STACK_BY_VALUE) ?? + eventsStackByOptions[0], stackByOptions: eventsStackByOptions, subtitle: undefined, title: i18n.EVENTS_GRAPH_TITLE, @@ -58,7 +65,7 @@ const DEFAULT_STACK_BY = 'event.module'; export const alertsHistogramConfig: MatrixHistogramConfigs = { defaultStackByOption: - alertsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0], + alertsStackByOptions.find((o) => o.value === DEFAULT_STACK_BY) ?? alertsStackByOptions[0], stackByOptions: alertsStackByOptions, title: i18n.ALERTS_GRAPH_TITLE, getLensAttributes: getExternalAlertLensAttributes, diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/translations.ts b/x-pack/plugins/security_solution/public/common/components/events_tab/translations.ts index 95300554df00c..8bfe235682a60 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/translations.ts @@ -54,3 +54,10 @@ export const SHOW_EXTERNAL_ALERTS = i18n.translate( export const EVENTS_GRAPH_TITLE = i18n.translate('xpack.securitySolution.eventsGraphTitle', { defaultMessage: 'Events', }); + +export const EVENTS_GRAPH_NO_BREAKDOWN_TITLE = i18n.translate( + 'xpack.securitySolution.eventsHistogram.selectOptions.noBreakDownLabel', + { + defaultMessage: 'No breakdown', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index 4c4b1f3e29e8a..bcc98f089cf62 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -28,6 +28,7 @@ import { VISUALIZATION_ACTIONS_BUTTON_CLASS } from '../visualization_actions/uti import { VisualizationEmbeddable } from '../visualization_actions/visualization_embeddable'; import { useVisualizationResponse } from '../visualization_actions/use_visualization_response'; import type { SourcererScopeName } from '../../../sourcerer/store/model'; +import { NO_BREAKDOWN_STACK_BY_VALUE } from '../events_tab/histogram_configurations'; export type MatrixHistogramComponentProps = MatrixHistogramQueryProps & MatrixHistogramConfigs & { @@ -165,6 +166,13 @@ export const MatrixHistogramComponent: React.FC = [isPtrIncluded, filterQuery] ); + // If the user selected the `No breakdown` option, we shouldn't perform the aggregation + const stackByField = useMemo(() => { + return selectedStackByOption.value === NO_BREAKDOWN_STACK_BY_VALUE + ? undefined + : selectedStackByOption.value; + }, [selectedStackByOption.value]); + if (hideHistogram) { return null; } @@ -216,7 +224,7 @@ export const MatrixHistogramComponent: React.FC = id={visualizationId} inspectTitle={title as string} lensAttributes={lensAttributes} - stackByField={selectedStackByOption.value} + stackByField={stackByField} timerange={timerange} /> ) : null} diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index 032a768df355e..72b2ae4184a40 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -12,7 +12,7 @@ import type { GetLensAttributes, LensAttributes } from '../visualization_actions export interface MatrixHistogramOption { text: string; - value: string; + value: string | undefined; } export type GetSubTitle = (count: number) => string; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts index f316f881ba60e..a7855ff7367bd 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts @@ -11,7 +11,7 @@ import { wrapper } from '../../mocks'; import { useLensAttributes } from '../../use_lens_attributes'; -import { getEventsHistogramLensAttributes } from './events'; +import { getEventsHistogramLensAttributes, stackByFieldAccessorId } from './events'; jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('0039eb0c-9a1a-4687-ae54-0f4e239bec75'), @@ -497,4 +497,41 @@ describe('getEventsHistogramLensAttributes', () => { }) ); }); + + it('should render the layer for the stackByField when provided', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getEventsHistogramLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current?.state?.visualization).toEqual( + expect.objectContaining({ + layers: expect.arrayContaining([ + expect.objectContaining({ splitAccessor: stackByFieldAccessorId }), + ]), + }) + ); + }); + + it('should not render the layer for the stackByField is undefined', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getEventsHistogramLensAttributes, + }), + { wrapper } + ); + + expect(result?.current?.state?.visualization).toEqual( + expect.objectContaining({ + layers: expect.arrayContaining([ + expect.not.objectContaining({ splitAccessor: stackByFieldAccessorId }), + ]), + }) + ); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts index e834fdd986061..174abce6baf62 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts @@ -8,9 +8,11 @@ import { v4 as uuidv4 } from 'uuid'; import type { GetLensAttributes } from '../../types'; const layerId = uuidv4(); +// Exported for testing purposes +export const stackByFieldAccessorId = '34919782-4546-43a5-b668-06ac934d3acd'; export const getEventsHistogramLensAttributes: GetLensAttributes = ( - stackByField = 'event.action', + stackByField, extraOptions = {} ) => { return { @@ -37,7 +39,7 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( showGridlines: false, layerType: 'data', xAccessor: 'aac9d7d0-13a3-480a-892b-08207a787926', - splitAccessor: '34919782-4546-43a5-b668-06ac934d3acd', + splitAccessor: stackByField ? stackByFieldAccessorId : undefined, }, ], yRightExtent: { @@ -83,30 +85,32 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( sourceField: '___records___', params: { emptyAsNull: true }, }, - '34919782-4546-43a5-b668-06ac934d3acd': { - label: `Top values of ${stackByField}`, - dataType: 'string', - operationType: 'terms', - scale: 'ordinal', - sourceField: `${stackByField}`, - isBucketed: true, - params: { - size: 10, - orderBy: { - type: 'column', - columnId: 'e09e0380-0740-4105-becc-0a4ca12e3944', - }, - orderDirection: 'desc', - otherBucket: true, - missingBucket: false, - parentFormat: { - id: 'terms', + ...(stackByField && { + [stackByFieldAccessorId]: { + label: `Top values of ${stackByField}`, + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: `${stackByField}`, + isBucketed: true, + params: { + size: 10, + orderBy: { + type: 'column', + columnId: 'e09e0380-0740-4105-becc-0a4ca12e3944', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, }, }, - }, + }), }, columnOrder: [ - '34919782-4546-43a5-b668-06ac934d3acd', + ...(stackByField ? [stackByFieldAccessorId] : []), 'aac9d7d0-13a3-480a-892b-08207a787926', 'e09e0380-0740-4105-becc-0a4ca12e3944', ], diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx index c7554abafafc6..22fa8c774eebe 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx @@ -22,6 +22,7 @@ import { useSourcererDataView } from '../../../sourcerer/containers'; import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric'; import { useRouteSpy } from '../../utils/route/use_route_spy'; import { SecurityPageName } from '../../../app/types'; +import { getEventsHistogramLensAttributes } from './lens_attributes/common/events'; jest.mock('../../../sourcerer/containers'); jest.mock('../../utils/route/use_route_spy', () => ({ @@ -212,6 +213,25 @@ describe('useLensAttributes', () => { ]); }); + it('should not set splitAccessor if stackByField is undefined', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getEventsHistogramLensAttributes, + stackByField: undefined, + }), + { wrapper } + ); + + expect(result?.current?.state?.visualization).toEqual( + expect.objectContaining({ + layers: expect.arrayContaining([ + expect.objectContaining({ seriesType: 'bar_stacked', splitAccessor: undefined }), + ]), + }) + ); + }); + it('should return null if no indices exist', () => { (useSourcererDataView as jest.Mock).mockReturnValue({ dataViewId: 'security-solution-default', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx index 1c6b37d4d7dd9..6a227c84681b1 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx @@ -72,7 +72,7 @@ export const useLensAttributes = ({ () => lensAttributes ?? ((getLensAttributes && - stackByField && + stackByField !== null && getLensAttributes(stackByField, extraOptions)) as LensAttributes), [extraOptions, getLensAttributes, lensAttributes, stackByField] ); @@ -82,7 +82,7 @@ export const useLensAttributes = ({ const lensAttrsWithInjectedData = useMemo(() => { if ( lensAttributes == null && - (getLensAttributes == null || stackByField == null || stackByField?.length === 0) + (getLensAttributes == null || stackByField === null || stackByField?.length === 0) ) { return null; } diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index 37051c2b5c822..eb551d4ba20aa 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -26,6 +26,7 @@ import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; import { eventsStackByOptions, eventsHistogramConfig, + NO_BREAKDOWN_STACK_BY_VALUE, } from '../../../common/components/events_tab/histogram_configurations'; import { HostsTableType } from '../../../explore/hosts/store/model'; import type { GlobalTimeArgs } from '../../../common/containers/use_global_time'; @@ -36,7 +37,7 @@ import { useFormatUrl } from '../../../common/components/link_to'; import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; import type { SourcererScopeName } from '../../../sourcerer/store/model'; -const DEFAULT_STACK_BY = 'event.dataset'; +const DEFAULT_STACK_BY = NO_BREAKDOWN_STACK_BY_VALUE; const ID = 'eventsByDatasetOverview'; const CHART_HEIGHT = 160; @@ -156,7 +157,7 @@ const EventsByDatasetComponent: React.FC = ({ defaultStackByOption: onlyField != null ? getHistogramOption(onlyField) - : eventsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? + : eventsStackByOptions.find((o) => o.value === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], legendPosition: Position.Right, subtitle: (totalCount: number) => diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx index f70f4e1f261f2..7e5e9a221ffee 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx @@ -100,8 +100,11 @@ const TestComponent = (props: Partial>) = const dispatch = useDispatch(); - // populating timeline so that it is not blank useEffect(() => { + // Unified field list can be a culprit for long load times, so we wait for the timeline to be interacted with to load + dispatch(timelineActions.showTimeline({ id: TimelineId.test, show: true })); + + // populating timeline so that it is not blank dispatch( timelineActions.applyKqlFilterQuery({ id: TimelineId.test, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx index c50c2877e2fe1..93524ac50e245 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx @@ -92,7 +92,10 @@ const SPECIAL_TEST_TIMEOUT = 50000; const localMockedTimelineData = structuredClone(mockTimelineData); -const TestComponent = (props: Partial>) => { +const TestComponent = ( + props: Partial> & { show?: boolean } +) => { + const { show, ...restProps } = props; const testComponentDefaultProps: ComponentProps = { columns: getColumnHeaders(columnsToDisplay, mockSourcererScope.browserFields), activeTab: TimelineTabs.query, @@ -119,8 +122,11 @@ const TestComponent = (props: Partial>) = const dispatch = useDispatch(); - // populating timeline so that it is not blank useEffect(() => { + // Unified field list can be a culprit for long load times, so we wait for the timeline to be interacted with to load + dispatch(timelineActions.showTimeline({ id: TimelineId.test, show: show ?? true })); + + // populating timeline so that it is not blank dispatch( timelineActions.applyKqlFilterQuery({ id: TimelineId.test, @@ -133,9 +139,9 @@ const TestComponent = (props: Partial>) = }, }) ); - }, [dispatch]); + }, [dispatch, show]); - return ; + return ; }; const customStore = createMockStore(); @@ -513,6 +519,51 @@ describe('unified timeline', () => { }); describe('unified field list', () => { + describe('render', () => { + let TestProviderWithNewStore: FC>; + beforeEach(() => { + const freshStore = createMockStore(); + // eslint-disable-next-line react/display-name + TestProviderWithNewStore = ({ children }) => { + return {children}; + }; + }); + it( + 'should not render when timeline has never been opened', + async () => { + render(, { + wrapper: TestProviderWithNewStore, + }); + expect(await screen.queryByTestId('timeline-sidebar')).not.toBeInTheDocument(); + }, + SPECIAL_TEST_TIMEOUT + ); + + it( + 'should render when timeline has been opened', + async () => { + render(, { + wrapper: TestProviderWithNewStore, + }); + expect(await screen.queryByTestId('timeline-sidebar')).toBeInTheDocument(); + }, + SPECIAL_TEST_TIMEOUT + ); + + it( + 'should not re-render when timeline has been opened at least once', + async () => { + const { rerender } = render(, { + wrapper: TestProviderWithNewStore, + }); + rerender(); + // Even after timeline is closed, it should still exist in the background + expect(await screen.queryByTestId('timeline-sidebar')).toBeInTheDocument(); + }, + SPECIAL_TEST_TIMEOUT + ); + }); + it( 'should be able to add filters', async () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx index 19a4c0bef70de..7d89da9002ba8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx @@ -6,7 +6,7 @@ */ import type { EuiDataGridProps } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiHideFor } from '@elastic/eui'; -import React, { useMemo, useCallback, useState, useRef } from 'react'; +import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { generateFilters } from '@kbn/data-plugin/public'; @@ -26,6 +26,7 @@ import { UnifiedFieldListSidebarContainer } from '@kbn/unified-field-list'; import type { EuiTheme } from '@kbn/react-kibana-context-styled'; import type { CoreStart } from '@kbn/core-lifecycle-browser'; import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { withDataView } from '../../../../common/components/with_data_view'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; import type { TimelineItem } from '../../../../../common/search_strategy'; @@ -47,6 +48,7 @@ import TimelineDataTable from './data_table'; import { timelineActions } from '../../../store'; import { getFieldsListCreationOptions } from './get_fields_list_creation_options'; import { defaultUdtHeaders } from './default_headers'; +import { getTimelineShowStatusByIdSelector } from '../../../store/selectors'; const TimelineBodyContainer = styled.div.attrs(({ className = '' }) => ({ className: `${className}`, @@ -343,6 +345,31 @@ const UnifiedTimelineComponent: React.FC = ({ onFieldEdited(); }, [onFieldEdited]); + // PERFORMANCE ONLY CODE BLOCK + /** + * We check for the timeline open status to request the fields for the fields browser as the fields request + * is often a much longer running request for customers with a significant number of indices and fields in those indices. + * This request should only be made after the user has decided to interact with timeline to prevent any performance impacts + * to the underlying security solution views, as this query will always run when the timeline exists on the page. + * + * `hasTimelineBeenOpenedOnce` - We want to keep timeline loading times as fast as possible after the user + * has chosen to interact with timeline at least once, so we use this flag to prevent re-requesting of this fields data + * every time timeline is closed and re-opened after the first interaction. + */ + + const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []); + const { show } = useDeepEqualSelector((state) => getTimelineShowStatus(state, timelineId)); + + const [hasTimelineBeenOpenedOnce, setHasTimelineBeenOpenedOnce] = useState(false); + + useEffect(() => { + if (!hasTimelineBeenOpenedOnce && show) { + setHasTimelineBeenOpenedOnce(true); + } + }, [hasTimelineBeenOpenedOnce, show]); + + // END PERFORMANCE ONLY CODE BLOCK + return ( = ({ sidebarPanel={ - {dataView ? ( + {dataView && hasTimelineBeenOpenedOnce ? (