mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:d354d27962
https://github.com/user-attachments/assets/a6d6987b-73fc-4735-9c37-973917c2fa2d #### 2. Timeline fields list will only load after the first interaction with timeline:ad557260d8
**Before:** https://github.com/user-attachments/assets/0ad2e903-ac15-4daa-925b-da8ad05e80dd **After:** https://github.com/user-attachments/assets/27d5d3d5-02c8-49b5-b699-239ebc36b16c
This commit is contained in:
parent
dab27867a6
commit
e45d97b26c
13 changed files with 206 additions and 40 deletions
|
@ -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
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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<MatrixHistogramComponentProps> =
|
|||
[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<MatrixHistogramComponentProps> =
|
|||
id={visualizationId}
|
||||
inspectTitle={title as string}
|
||||
lensAttributes={lensAttributes}
|
||||
stackByField={selectedStackByOption.value}
|
||||
stackByField={stackByField}
|
||||
timerange={timerange}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 }),
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<Props> = ({
|
|||
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) =>
|
||||
|
|
|
@ -100,8 +100,11 @@ const TestComponent = (props: Partial<ComponentProps<typeof QueryTabContent>>) =
|
|||
|
||||
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,
|
||||
|
|
|
@ -92,7 +92,10 @@ const SPECIAL_TEST_TIMEOUT = 50000;
|
|||
|
||||
const localMockedTimelineData = structuredClone(mockTimelineData);
|
||||
|
||||
const TestComponent = (props: Partial<ComponentProps<typeof UnifiedTimeline>>) => {
|
||||
const TestComponent = (
|
||||
props: Partial<ComponentProps<typeof UnifiedTimeline>> & { show?: boolean }
|
||||
) => {
|
||||
const { show, ...restProps } = props;
|
||||
const testComponentDefaultProps: ComponentProps<typeof QueryTabContent> = {
|
||||
columns: getColumnHeaders(columnsToDisplay, mockSourcererScope.browserFields),
|
||||
activeTab: TimelineTabs.query,
|
||||
|
@ -119,8 +122,11 @@ const TestComponent = (props: Partial<ComponentProps<typeof UnifiedTimeline>>) =
|
|||
|
||||
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<ComponentProps<typeof UnifiedTimeline>>) =
|
|||
},
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
}, [dispatch, show]);
|
||||
|
||||
return <UnifiedTimeline {...testComponentDefaultProps} {...props} />;
|
||||
return <UnifiedTimeline {...testComponentDefaultProps} {...restProps} />;
|
||||
};
|
||||
|
||||
const customStore = createMockStore();
|
||||
|
@ -513,6 +519,51 @@ describe('unified timeline', () => {
|
|||
});
|
||||
|
||||
describe('unified field list', () => {
|
||||
describe('render', () => {
|
||||
let TestProviderWithNewStore: FC<PropsWithChildren<unknown>>;
|
||||
beforeEach(() => {
|
||||
const freshStore = createMockStore();
|
||||
// eslint-disable-next-line react/display-name
|
||||
TestProviderWithNewStore = ({ children }) => {
|
||||
return <TestProviders store={freshStore}>{children}</TestProviders>;
|
||||
};
|
||||
});
|
||||
it(
|
||||
'should not render when timeline has never been opened',
|
||||
async () => {
|
||||
render(<TestComponent show={false} />, {
|
||||
wrapper: TestProviderWithNewStore,
|
||||
});
|
||||
expect(await screen.queryByTestId('timeline-sidebar')).not.toBeInTheDocument();
|
||||
},
|
||||
SPECIAL_TEST_TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should render when timeline has been opened',
|
||||
async () => {
|
||||
render(<TestComponent />, {
|
||||
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(<TestComponent />, {
|
||||
wrapper: TestProviderWithNewStore,
|
||||
});
|
||||
rerender(<TestComponent show={false} />);
|
||||
// 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 () => {
|
||||
|
|
|
@ -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<Props> = ({
|
|||
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 (
|
||||
<TimelineBodyContainer className="timelineBodyContainer" ref={setSidebarContainer}>
|
||||
<TimelineResizableLayout
|
||||
|
@ -351,7 +378,7 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
|
|||
sidebarPanel={
|
||||
<SidebarPanelFlexGroup gutterSize="none">
|
||||
<EuiFlexItem className="sidebarContainer">
|
||||
{dataView ? (
|
||||
{dataView && hasTimelineBeenOpenedOnce ? (
|
||||
<UnifiedFieldListSidebarContainer
|
||||
ref={unifiedFieldListContainerRef}
|
||||
showFieldList
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue