From 47e50f81029bf66d26b7976e331cc2dab3396efc Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 17 Jun 2020 14:55:46 +0300 Subject: [PATCH] [SIEM][Timeline] Persist timeline to localStorage (#67156) Co-authored-by: Elastic Machine Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> --- .../common/types/timeline/index.ts | 26 +++ .../cypress/integration/events_viewer.spec.ts | 2 +- .../timeline_local_storage.spec.ts | 46 +++++ .../cypress/screens/hosts/events.ts | 3 - .../cypress/screens/hosts/external_events.ts | 7 + .../cypress/screens/timeline.ts | 7 + .../security_solution/cypress/tasks/common.ts | 6 + .../cypress/tasks/hosts/events.ts | 5 - .../cypress/tasks/timeline.ts | 11 ++ .../components/alerts_table/index.test.tsx | 2 + .../alerts/components/alerts_table/index.tsx | 39 ++-- .../public/alerts/constants.ts | 8 + .../security_solution/public/alerts/index.ts | 13 +- .../detection_engine/detection_engine.tsx | 2 + .../detection_engine/rules/details/index.tsx | 2 + .../security_solution/public/app/app.tsx | 27 +-- .../security_solution/public/app/types.ts | 2 + .../index.test.tsx | 6 +- .../components/alerts_viewer/alerts_table.tsx | 14 +- .../common/components/alerts_viewer/index.tsx | 12 +- .../common/components/alerts_viewer/types.ts | 5 +- .../error_toast_dispatcher/index.test.tsx | 12 +- .../common/components/inspect/index.test.tsx | 8 +- .../components/stat_items/index.test.tsx | 10 +- .../super_date_picker/index.test.tsx | 12 +- .../common/components/top_n/index.test.tsx | 5 +- .../mock/endpoint/app_context_render.tsx | 7 +- .../public/common/mock/index.ts | 1 + .../public/common/mock/mock_local_storage.ts | 34 ++++ .../public/common/mock/test_providers.tsx | 22 +-- .../public/common/store/epic.ts | 4 +- .../public/common/store/store.ts | 3 + .../view/test_helpers/render_alert_page.tsx | 10 +- .../authentications_table/index.test.tsx | 12 +- .../components/hosts_table/index.test.tsx | 6 +- .../security_solution/public/hosts/index.ts | 13 +- .../public/hosts/pages/hosts.test.tsx | 4 +- .../navigation/alerts_query_tab_body.tsx | 9 +- .../navigation/events_query_tab_body.tsx | 4 +- .../components/ip_overview/index.test.tsx | 6 +- .../components/kpi_network/index.test.tsx | 12 +- .../network_dns_table/index.test.tsx | 6 +- .../network_http_table/index.test.tsx | 6 +- .../index.test.tsx | 6 +- .../network_top_n_flow_table/index.test.tsx | 6 +- .../components/tls_table/index.test.tsx | 6 +- .../components/users_table/index.test.tsx | 6 +- .../security_solution/public/network/index.ts | 8 +- .../network/pages/ip_details/index.test.tsx | 6 +- .../navigation/alerts_query_tab_body.tsx | 7 +- .../public/network/pages/network.test.tsx | 4 +- .../components/overview_host/index.test.tsx | 6 +- .../overview_network/index.test.tsx | 6 +- .../security_solution/public/plugin.tsx | 23 ++- .../components/flyout/index.test.tsx | 14 +- .../body/column_headers/actions/index.tsx | 2 +- .../footer/__snapshots__/index.test.tsx.snap | 4 + .../components/timeline/footer/index.tsx | 2 + .../timeline/properties/index.test.tsx | 7 +- .../properties/new_template_timeline.test.tsx | 4 +- .../public/timelines/containers/index.tsx | 9 +- .../containers/local_storage/index.test.ts | 168 +++++++++++++++++ .../containers/local_storage/index.tsx | 70 +++++++ .../containers/local_storage/types.ts | 14 ++ .../public/timelines/store/timeline/epic.ts | 16 +- .../timeline/epic_local_storage.test.tsx | 176 ++++++++++++++++++ .../store/timeline/epic_local_storage.ts | 60 ++++++ .../timelines/store/timeline/helpers.ts | 14 +- .../public/timelines/store/timeline/types.ts | 15 ++ .../plugins/security_solution/public/types.ts | 2 + 70 files changed, 949 insertions(+), 163 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/screens/hosts/external_events.ts create mode 100644 x-pack/plugins/security_solution/public/alerts/constants.ts create mode 100644 x-pack/plugins/security_solution/public/common/mock/mock_local_storage.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/containers/local_storage/types.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index e67eb3182ffa99..4f255bb6d68341 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -197,6 +197,32 @@ export interface SavedTimeline extends runtimeTypes.TypeOf {} +/* + * Timeline IDs + */ + +export enum TimelineId { + hostsPageEvents = 'hosts-page-events', + hostsPageExternalAlerts = 'hosts-page-external-alerts', + alertsRulesDetailsPage = 'alerts-rules-details-page', + alertsPage = 'alerts-page', + networkPageExternalAlerts = 'network-page-external-alerts', + active = 'timeline-1', + test = 'test', // Reserved for testing purposes +} + +export const TimelineIdLiteralRt = runtimeTypes.union([ + runtimeTypes.literal(TimelineId.hostsPageEvents), + runtimeTypes.literal(TimelineId.hostsPageExternalAlerts), + runtimeTypes.literal(TimelineId.alertsRulesDetailsPage), + runtimeTypes.literal(TimelineId.alertsPage), + runtimeTypes.literal(TimelineId.networkPageExternalAlerts), + runtimeTypes.literal(TimelineId.active), + runtimeTypes.literal(TimelineId.test), +]); + +export type TimelineIdLiteral = runtimeTypes.TypeOf; + /** * Timeline Saved object type with metadata */ diff --git a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts index 82395de91abfa9..b2d35f3f0c3361 100644 --- a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts @@ -29,12 +29,12 @@ import { dragAndDropColumn, openEventsViewerFieldsBrowser, opensInspectQueryModal, - resetFields, waitsForEventsToBeLoaded, } from '../tasks/hosts/events'; import { clearSearchBar, kqlSearch } from '../tasks/security_header'; import { HOSTS_PAGE } from '../urls/navigation'; +import { resetFields } from '../tasks/timeline'; const defaultHeadersInDefaultEcsCategory = [ { id: '@timestamp' }, diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts new file mode 100644 index 00000000000000..a4352f58e6fc74 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { reload } from '../tasks/common'; +import { loginAndWaitForPage } from '../tasks/login'; +import { HOSTS_PAGE } from '../urls/navigation'; +import { openEvents } from '../tasks/hosts/main'; +import { DRAGGABLE_HEADER } from '../screens/timeline'; +import { TABLE_COLUMN_EVENTS_MESSAGE } from '../screens/hosts/external_events'; +import { waitsForEventsToBeLoaded, openEventsViewerFieldsBrowser } from '../tasks/hosts/events'; +import { removeColumn, resetFields } from '../tasks/timeline'; + +describe('persistent timeline', () => { + before(() => { + loginAndWaitForPage(HOSTS_PAGE); + openEvents(); + waitsForEventsToBeLoaded(); + }); + + afterEach(() => { + openEventsViewerFieldsBrowser(); + resetFields(); + }); + + it('persist the deletion of a column', () => { + cy.get(DRAGGABLE_HEADER).then((header) => { + const currentNumberOfTimelineColumns = header.length; + const expectedNumberOfTimelineColumns = currentNumberOfTimelineColumns - 1; + + cy.wrap(header).eq(TABLE_COLUMN_EVENTS_MESSAGE).invoke('text').should('equal', 'message'); + removeColumn(TABLE_COLUMN_EVENTS_MESSAGE); + + cy.get(DRAGGABLE_HEADER).should('have.length', expectedNumberOfTimelineColumns); + + reload(waitsForEventsToBeLoaded); + + cy.get(DRAGGABLE_HEADER).should('have.length', expectedNumberOfTimelineColumns); + cy.get(DRAGGABLE_HEADER).each(($el) => { + expect($el.text()).not.equal('message'); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts index ed46a90c872c8f..a946fefe273e10 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts @@ -36,7 +36,4 @@ export const LOCAL_EVENTS_COUNT = export const LOAD_MORE = '[data-test-subj="events-viewer-panel"] [data-test-subj="TimelineMoreButton"'; -export const RESET_FIELDS = - '[data-test-subj="events-viewer-panel"] [data-test-subj="reset-fields"]'; - export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/external_events.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/external_events.ts new file mode 100644 index 00000000000000..982e1ea378e703 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/external_events.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const TABLE_COLUMN_EVENTS_MESSAGE = 1; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index bb232b752994ae..c673cf34b6daeb 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -21,6 +21,11 @@ export const ID_TOGGLE_FIELD = '[data-test-subj="toggle-field-_id"]'; export const PROVIDER_BADGE = '[data-test-subj="providerBadge"]'; +export const REMOVE_COLUMN = '[data-test-subj="remove-column"]'; + +export const RESET_FIELDS = + '[data-test-subj="events-viewer-panel"] [data-test-subj="reset-fields"]'; + export const SEARCH_OR_FILTER_CONTAINER = '[data-test-subj="timeline-search-or-filter-search-container"]'; @@ -30,6 +35,8 @@ export const TIMELINE = (id: string) => { return `[data-test-subj="title-${id}"]`; }; +export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]'; + export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; export const TIMELINE_DATA_PROVIDERS_EMPTY = diff --git a/x-pack/plugins/security_solution/cypress/tasks/common.ts b/x-pack/plugins/security_solution/cypress/tasks/common.ts index b0c64214459f0b..a385ad78f63b7b 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common.ts @@ -48,3 +48,9 @@ export const drop = (dropTarget: JQuery) => { .trigger('mouseup', { force: true }) .wait(1000); }; + +export const reload = (afterReload: () => void) => { + cy.reload(); + cy.contains('a', 'Security'); + afterReload(); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts index a5936509892596..57c819d9678835 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts @@ -13,7 +13,6 @@ import { HOST_GEO_COUNTRY_NAME_CHECKBOX, INSPECT_QUERY, LOAD_MORE, - RESET_FIELDS, SERVER_SIDE_EVENT_COUNT, } from '../../screens/hosts/events'; import { DRAGGABLE_HEADER } from '../../screens/timeline'; @@ -53,10 +52,6 @@ export const opensInspectQueryModal = () => { .click({ force: true }); }; -export const resetFields = () => { - cy.get(RESET_FIELDS).click({ force: true }); -}; - export const waitsForEventsToBeLoaded = () => { cy.get(SERVER_SIDE_EVENT_COUNT).should('exist').invoke('text').should('not.equal', '0'); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 38da611428b2ef..9e17433090c2bb 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -21,6 +21,8 @@ import { TIMELINE_TITLE, TIMESTAMP_TOGGLE_FIELD, TOGGLE_TIMELINE_EXPAND_EVENT, + REMOVE_COLUMN, + RESET_FIELDS, } from '../screens/timeline'; import { drag, drop } from '../tasks/common'; @@ -101,3 +103,12 @@ export const dragAndDropIdToggleFieldToTimeline = () => { drop(headersDropArea) ); }; + +export const removeColumn = (column: number) => { + cy.get(REMOVE_COLUMN).first().should('exist'); + cy.get(REMOVE_COLUMN).eq(column).click({ force: true }); +}; + +export const resetFields = () => { + cy.get(RESET_FIELDS).click({ force: true }); +}; diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx index 51fdd828bcddb3..f843bf68818465 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx @@ -7,12 +7,14 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { TimelineId } from '../../../../common/types/timeline'; import { AlertsTableComponent } from './index'; describe('AlertsTableComponent', () => { it('renders correctly', () => { const wrapper = shallow( = ({ + timelineId, canUserCRUD, clearEventsDeleted, clearEventsLoading, @@ -140,18 +141,16 @@ export const AlertsTableComponent: React.FC = ({ const setEventsLoadingCallback = useCallback( ({ eventIds, isLoading }: SetEventsLoadingProps) => { - setEventsLoading!({ id: ALERTS_TABLE_TIMELINE_ID, eventIds, isLoading }); + setEventsLoading!({ id: timelineId, eventIds, isLoading }); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [setEventsLoading, ALERTS_TABLE_TIMELINE_ID] + [setEventsLoading, timelineId] ); const setEventsDeletedCallback = useCallback( ({ eventIds, isDeleted }: SetEventsDeletedProps) => { - setEventsDeleted!({ id: ALERTS_TABLE_TIMELINE_ID, eventIds, isDeleted }); + setEventsDeleted!({ id: timelineId, eventIds, isDeleted }); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [setEventsDeleted, ALERTS_TABLE_TIMELINE_ID] + [setEventsDeleted, timelineId] ); const onAlertStatusUpdateSuccess = useCallback( @@ -202,20 +201,20 @@ export const AlertsTableComponent: React.FC = ({ // Callback for when open/closed filter changes const onFilterGroupChangedCallback = useCallback( (newFilterGroup: Status) => { - clearEventsLoading!({ id: ALERTS_TABLE_TIMELINE_ID }); - clearEventsDeleted!({ id: ALERTS_TABLE_TIMELINE_ID }); - clearSelected!({ id: ALERTS_TABLE_TIMELINE_ID }); + clearEventsLoading!({ id: timelineId }); + clearEventsDeleted!({ id: timelineId }); + clearSelected!({ id: timelineId }); setFilterGroup(newFilterGroup); }, - [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] + [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup, timelineId] ); // Callback for clearing entire selection from utility bar const clearSelectionCallback = useCallback(() => { - clearSelected!({ id: ALERTS_TABLE_TIMELINE_ID }); + clearSelected!({ id: timelineId }); setSelectAll(false); setShowClearSelectionAction(false); - }, [clearSelected, setSelectAll, setShowClearSelectionAction]); + }, [clearSelected, setSelectAll, setShowClearSelectionAction, timelineId]); // Callback for selecting all events on all pages from utility bar // Dispatches to stateful_body's selectAll via TimelineTypeContext props @@ -327,7 +326,7 @@ export const AlertsTableComponent: React.FC = ({ useEffect(() => { initializeTimeline({ - id: ALERTS_TABLE_TIMELINE_ID, + id: timelineId, documentType: i18n.ALERTS_DOCUMENT_TYPE, footerText: i18n.TOTAL_COUNT_OF_ALERTS, loadingText: i18n.LOADING_ALERTS, @@ -338,7 +337,7 @@ export const AlertsTableComponent: React.FC = ({ }, []); useEffect(() => { setTimelineRowActions({ - id: ALERTS_TABLE_TIMELINE_ID, + id: timelineId, queryFields: requiredFieldsForActions, timelineRowActions: additionalActions, }); @@ -365,7 +364,7 @@ export const AlertsTableComponent: React.FC = ({ defaultModel={alertsDefaultModel} end={to} headerFilterGroup={headerFilterGroup} - id={ALERTS_TABLE_TIMELINE_ID} + id={timelineId} start={from} utilityBar={utilityBarCallback} /> @@ -375,9 +374,9 @@ export const AlertsTableComponent: React.FC = ({ const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); const getGlobalInputs = inputsSelectors.globalSelector(); - const mapStateToProps = (state: State) => { - const timeline: TimelineModel = - getTimeline(state, ALERTS_TABLE_TIMELINE_ID) ?? timelineDefaults; + const mapStateToProps = (state: State, ownProps: OwnProps) => { + const { timelineId } = ownProps; + const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; const { deletedEventIds, isSelectAllChecked, loadingEventIds, selectedEventIds } = timeline; const globalInputs: inputsModel.InputsRange = getGlobalInputs(state); diff --git a/x-pack/plugins/security_solution/public/alerts/constants.ts b/x-pack/plugins/security_solution/public/alerts/constants.ts new file mode 100644 index 00000000000000..d640770a1857b5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ALERTS_RULES_DETAILS_PAGE_TIMELINE_ID = 'alerts-rules-details-page'; +export const ALERTS_TIMELINE_ID = 'alerts-page'; diff --git a/x-pack/plugins/security_solution/public/alerts/index.ts b/x-pack/plugins/security_solution/public/alerts/index.ts index c1501419a1cf6f..1409ad4f54696c 100644 --- a/x-pack/plugins/security_solution/public/alerts/index.ts +++ b/x-pack/plugins/security_solution/public/alerts/index.ts @@ -4,15 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { getTimelinesInStorageByIds } from '../timelines/containers/local_storage'; +import { TimelineIdLiteral, TimelineId } from '../../common/types/timeline'; import { getAlertsRoutes } from './routes'; import { SecuritySubPlugin } from '../app/types'; +const ALERTS_TIMELINE_IDS: TimelineIdLiteral[] = [ + TimelineId.alertsRulesDetailsPage, + TimelineId.alertsPage, +]; + export class Alerts { public setup() {} - public start(): SecuritySubPlugin { + public start(storage: Storage): SecuritySubPlugin { return { routes: getAlertsRoutes(), + storageTimelines: { + timelineById: getTimelinesInStorageByIds(storage, ALERTS_TIMELINE_IDS), + }, }; } } diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx index e3eb4666522ad6..9e1c9db168bdaf 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useMemo } from 'react'; import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; +import { TimelineId } from '../../../../common/types/timeline'; import { GlobalTime } from '../../../common/containers/global_time'; import { indicesExistOrDataTemporarilyUnavailable, @@ -138,6 +139,7 @@ export const DetectionEnginePageComponent: React.FC = ({ /> = ({ {ruleId != null && ( = ({ subPlugins, ...libs }) => { +const StartAppComponent: FC = ({ subPlugins, storage, ...libs }) => { const { routes: subPluginRoutes, store: subPluginsStore } = subPlugins; const { i18n } = useKibana().services; const history = createHashHistory(); @@ -84,6 +85,7 @@ const StartAppComponent: FC = ({ subPlugins, ...libs }) => { createInitialState(subPluginsStore.initialState), subPluginsStore.reducer, libs$.pipe(pluck('apolloClient')), + storage, subPluginsStore.middlewares ); @@ -118,16 +120,17 @@ interface SiemAppComponentProps { subPlugins: SecuritySubPlugins; } -const SiemAppComponent: React.FC = ({ services, subPlugins }) => ( - - - -); +const SiemAppComponent: React.FC = ({ services, subPlugins }) => { + return ( + + + + ); +}; export const SiemApp = memo(SiemAppComponent); diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index 4b00dee3b75108..7a905b35710c99 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -18,6 +18,7 @@ import { NavTab } from '../common/components/navigation/types'; import { State, SubPluginsInitReducer } from '../common/store'; import { Immutable } from '../../common/endpoint/types'; import { AppAction } from '../common/store/actions'; +import { TimelineState } from '../timelines/store/timeline/types'; export enum SiemPageName { overview = 'overview', @@ -48,6 +49,7 @@ export interface SecuritySubPluginStore export interface SecuritySubPlugin { routes: React.ReactElement[]; + storageTimelines?: Pick; } type SecuritySubPluginKeyStore = diff --git a/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx index 18c0032f58c3c3..c7015ed81701ef 100644 --- a/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx @@ -12,6 +12,7 @@ import { mockGlobalState, TestProviders, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../mock'; import { createStore, State } from '../../store'; import { AddFilterToGlobalSearchBar } from '.'; @@ -33,10 +34,11 @@ jest.mock('../../lib/kibana', () => ({ describe('AddFilterToGlobalSearchBar Component', () => { const state: State = mockGlobalState; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); mockAddFilters.mockClear(); }); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index bb6ba018218351..251e0278b11bab 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -7,6 +7,7 @@ import React, { useEffect, useMemo } from 'react'; import { Filter } from '../../../../../../../src/plugins/data/public'; +import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../events_viewer'; import { alertsDefaultModel } from './default_headers'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; @@ -17,7 +18,6 @@ export interface OwnProps { start: number; } -const ALERTS_TABLE_ID = 'alerts-table'; const defaultAlertsFilters: Filter[] = [ { meta: { @@ -52,18 +52,24 @@ const defaultAlertsFilters: Filter[] = [ ]; interface Props { + timelineId: TimelineIdLiteral; endDate: number; startDate: number; pageFilters?: Filter[]; } -const AlertsTableComponent: React.FC = ({ endDate, startDate, pageFilters = [] }) => { +const AlertsTableComponent: React.FC = ({ + timelineId, + endDate, + startDate, + pageFilters = [], +}) => { const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); const { initializeTimeline } = useManageTimeline(); useEffect(() => { initializeTimeline({ - id: ALERTS_TABLE_ID, + id: timelineId, documentType: i18n.ALERTS_DOCUMENT_TYPE, footerText: i18n.TOTAL_COUNT_OF_ALERTS, title: i18n.ALERTS_TABLE_TITLE, @@ -76,7 +82,7 @@ const AlertsTableComponent: React.FC = ({ endDate, startDate, pageFilters pageFilters={alertsFilter} defaultModel={alertsDefaultModel} end={endDate} - id={ALERTS_TABLE_ID} + id={timelineId} start={startDate} /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx index 8a62a05ecc2da8..a31cb4f2a8bfd8 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useCallback, useMemo } from 'react'; import numeral from '@elastic/numeral'; import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; -import { AlertsComponentsQueryProps } from './types'; +import { AlertsComponentsProps } from './types'; import { AlertsTable } from './alerts_table'; import * as i18n from './translations'; import { useUiSetting$ } from '../../lib/kibana'; @@ -17,6 +17,7 @@ import { MatrixHisrogramConfigs } from '../matrix_histogram/types'; const ID = 'alertsOverTimeQuery'; export const AlertsView = ({ + timelineId, deleteQuery, endDate, filterQuery, @@ -24,7 +25,7 @@ export const AlertsView = ({ setQuery, startDate, type, -}: AlertsComponentsQueryProps) => { +}: AlertsComponentsProps) => { const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); const getSubtitle = useCallback( (totalCount: number) => @@ -61,7 +62,12 @@ export const AlertsView = ({ type={type} {...alertsHistogramConfigs} /> - + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts index 2bc33aaf1bae77..78a6332c90fbca 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts @@ -5,16 +5,19 @@ */ import { Filter } from '../../../../../../../src/plugins/data/public'; +import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { HostsComponentsQueryProps } from '../../../hosts/pages/navigation/types'; import { NetworkComponentQueryProps } from '../../../network/pages/navigation/types'; import { MatrixHistogramOption } from '../matrix_histogram/types'; type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; -export interface AlertsComponentsQueryProps + +export interface AlertsComponentsProps extends Pick< CommonQueryProps, 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' | 'type' > { + timelineId: TimelineIdLiteral; pageFilters: Filter[]; stackByOptions?: MatrixHistogramOption[]; defaultFilters?: Filter[]; diff --git a/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx index 39b17f7008e64b..4bc77555f09bdd 100644 --- a/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx @@ -8,7 +8,12 @@ import { shallow } from 'enzyme'; import React from 'react'; import { Provider } from 'react-redux'; -import { apolloClientObservable, mockGlobalState, SUB_PLUGINS_REDUCER } from '../../mock'; +import { + apolloClientObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, +} from '../../mock'; import { createStore } from '../../store/store'; import { ErrorToastDispatcher } from '.'; @@ -16,10 +21,11 @@ import { State } from '../../store/types'; describe('Error Toast Dispatcher', () => { const state: State = mockGlobalState; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); describe('rendering', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx index d147f0224fdb65..45397921a6651d 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx @@ -14,6 +14,7 @@ import { mockGlobalState, apolloClientObservable, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../mock'; import { createStore, State } from '../../store'; import { UpdateQueryParams, upsertQuery } from '../../store/inputs/helpers'; @@ -25,6 +26,7 @@ describe('Inspect Button', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); const refetch = jest.fn(); const state: State = mockGlobalState; + const { storage } = createSecuritySolutionStorageMock(); const newQuery: UpdateQueryParams = { inputId: 'global', id: 'myQuery', @@ -34,13 +36,13 @@ describe('Inspect Button', () => { state: state.inputs, }; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); describe('Render', () => { beforeEach(() => { const myState = cloneDeep(state); myState.inputs = upsertQuery(newQuery); - store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); test('Eui Empty Button', () => { const wrapper = mount( @@ -144,7 +146,7 @@ describe('Inspect Button', () => { response: ['my response'], }; myState.inputs = upsertQuery(myQuery); - store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); test('Open Inspect Modal', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx index d81d23438bfd20..50721ef3b26ad7 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx @@ -30,7 +30,12 @@ import { mockNoChartMappings, mockNarrowDateRange, } from '../../../network/components/kpi_network/mock'; -import { mockGlobalState, apolloClientObservable, SUB_PLUGINS_REDUCER } from '../../mock'; +import { + mockGlobalState, + apolloClientObservable, + SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, +} from '../../mock'; import { State, createStore } from '../../store'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { KpiNetworkData, KpiHostsData } from '../../../graphql/types'; @@ -49,7 +54,8 @@ jest.mock('../charts/barchart', () => { describe('Stat Items Component', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); const state: State = mockGlobalState; - const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); describe.each([ [ diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx index 04cb348c3f9cd7..99510a1b4b42ef 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx @@ -10,7 +10,12 @@ import { Provider as ReduxStoreProvider } from 'react-redux'; import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../common/constants'; import { useUiSetting$ } from '../../lib/kibana'; -import { apolloClientObservable, mockGlobalState, SUB_PLUGINS_REDUCER } from '../../mock'; +import { + apolloClientObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, +} from '../../mock'; import { createUseUiSetting$Mock } from '../../mock/kibana_react'; import { createStore, State } from '../../store'; @@ -75,11 +80,12 @@ const timepickerRanges = [ describe('SIEM Super Date Picker', () => { describe('#SuperDatePicker', () => { const state: State = mockGlobalState; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); beforeEach(() => { jest.clearAllMocks(); - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); mockUseUiSetting$.mockImplementation((key, defaultValue) => { const useUiSetting$Mock = createUseUiSetting$Mock(); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index aa310d63ab2836..1c24c325dc9ec5 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -13,6 +13,7 @@ import { mockGlobalState, TestProviders, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../mock'; import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; @@ -141,7 +142,9 @@ const state: State = { }, }, }; -const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + +const { storage } = createSecuritySolutionStorageMock(); +const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); describe('StatefulTopN', () => { // Suppress warnings about "react-beautiful-dnd" diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index d930136b3c0c46..1db63897a88633 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -19,7 +19,7 @@ import { alertMiddlewareFactory } from '../../../endpoint_alerts/store/middlewar import { AppRootProvider } from './app_root_provider'; import { managementMiddlewareFactory } from '../../../management/store/middleware'; import { createKibanaContextProviderMock } from '../kibana_react'; -import { SUB_PLUGINS_REDUCER, mockGlobalState } from '..'; +import { SUB_PLUGINS_REDUCER, mockGlobalState, createSecuritySolutionStorageMock } from '..'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; @@ -56,7 +56,9 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { const coreStart = coreMock.createStart({ basePath: '/mock' }); const depsStart = depsStartMock(); const middlewareSpy = createSpyMiddleware(); - const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, apolloClientObservable, [ + const { storage } = createSecuritySolutionStorageMock(); + + const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage, [ substateMiddlewareFactory( (globalState) => globalState.alertList, alertMiddlewareFactory(coreStart, depsStart) @@ -64,6 +66,7 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { ...managementMiddlewareFactory(coreStart, depsStart), middlewareSpy.actionSpyMiddleware, ]); + const MockKibanaContextProvider = createKibanaContextProviderMock(); const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => ( diff --git a/x-pack/plugins/security_solution/public/common/mock/index.ts b/x-pack/plugins/security_solution/public/common/mock/index.ts index 30eb4c63f40b85..678ad4d84b5862 100644 --- a/x-pack/plugins/security_solution/public/common/mock/index.ts +++ b/x-pack/plugins/security_solution/public/common/mock/index.ts @@ -10,6 +10,7 @@ export * from './hook_wrapper'; export * from './index_pattern'; export * from './mock_timeline_data'; export * from './mock_detail_item'; +export * from './mock_local_storage'; export * from './netflow'; export * from './test_providers'; export * from './utils'; diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_local_storage.ts b/x-pack/plugins/security_solution/public/common/mock/mock_local_storage.ts new file mode 100644 index 00000000000000..ca44295b758891 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/mock/mock_local_storage.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IStorage, Storage } from '../../../../../../src/plugins/kibana_utils/public'; + +export const localStorageMock = (): IStorage => { + let store: Record = {}; + + return { + getItem: (key: string) => { + return store[key] || null; + }, + setItem: (key: string, value: unknown) => { + store[key] = value; + }, + clear() { + store = {}; + }, + removeItem(key: string) { + delete store[key]; + }, + }; +}; + +export const createSecuritySolutionStorageMock = () => { + const localStorage = localStorageMock(); + return { + localStorage, + storage: new Storage(localStorage), + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 679e0bdc14cd5a..0573f049c35c53 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -22,6 +22,7 @@ import { mockGlobalState } from './global_state'; import { createKibanaContextProviderMock } from './kibana_react'; import { FieldHook, useForm } from '../../shared_imports'; import { SUB_PLUGINS_REDUCER } from './utils'; +import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; const state: State = mockGlobalState; @@ -38,32 +39,17 @@ export const apolloClient = new ApolloClient({ export const apolloClientObservable = new BehaviorSubject(apolloClient); -const localStorageMock = () => { - let store: Record = {}; - - return { - getItem: (key: string) => { - return store[key] || null; - }, - setItem: (key: string, value: unknown) => { - store[key] = value; - }, - clear() { - store = {}; - }, - }; -}; - Object.defineProperty(window, 'localStorage', { value: localStorageMock(), }); const MockKibanaContextProvider = createKibanaContextProviderMock(); +const { storage } = createSecuritySolutionStorageMock(); /** A utility for wrapping children in the providers required to run most tests */ const TestProvidersComponent: React.FC = ({ children, - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable), + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage), onDragEnd = jest.fn(), }) => ( @@ -83,7 +69,7 @@ export const TestProviders = React.memo(TestProvidersComponent); const TestProviderWithoutDragAndDropComponent: React.FC = ({ children, - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable), + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage), }) => ( {children} diff --git a/x-pack/plugins/security_solution/public/common/store/epic.ts b/x-pack/plugins/security_solution/public/common/store/epic.ts index b9e8e7d88c202a..d9de7951a86f4e 100644 --- a/x-pack/plugins/security_solution/public/common/store/epic.ts +++ b/x-pack/plugins/security_solution/public/common/store/epic.ts @@ -9,11 +9,13 @@ import { createTimelineEpic } from '../../timelines/store/timeline/epic'; import { createTimelineFavoriteEpic } from '../../timelines/store/timeline/epic_favorite'; import { createTimelineNoteEpic } from '../../timelines/store/timeline/epic_note'; import { createTimelinePinnedEventEpic } from '../../timelines/store/timeline/epic_pinned_event'; +import { createTimelineLocalStorageEpic } from '../../timelines/store/timeline/epic_local_storage'; export const createRootEpic = () => combineEpics( createTimelineEpic(), createTimelineFavoriteEpic(), createTimelineNoteEpic(), - createTimelinePinnedEventEpic() + createTimelinePinnedEventEpic(), + createTimelineLocalStorageEpic() ); diff --git a/x-pack/plugins/security_solution/public/common/store/store.ts b/x-pack/plugins/security_solution/public/common/store/store.ts index 276dcdcaedb854..5f53724b287df8 100644 --- a/x-pack/plugins/security_solution/public/common/store/store.ts +++ b/x-pack/plugins/security_solution/public/common/store/store.ts @@ -28,6 +28,7 @@ import { AppApolloClient } from '../lib/lib'; import { AppAction } from './actions'; import { Immutable } from '../../../common/endpoint/types'; import { State } from './types'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; type ComposeType = typeof compose; declare global { @@ -48,6 +49,7 @@ export const createStore = ( state: PreloadedState, pluginsReducer: SubPluginsInitReducer, apolloClient: Observable, + storage: Storage, additionalMiddleware?: Array>>> ): Store => { const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; @@ -58,6 +60,7 @@ export const createStore = ( selectNotesByIdSelector: appSelectors.selectNotesByIdSelector, timelineByIdSelector: timelineSelectors.timelineByIdSelector, timelineTimeRangeSelector: inputsSelectors.timelineTimeRangeSelector, + storage, }; const epicMiddleware = createEpicMiddleware( diff --git a/x-pack/plugins/security_solution/public/endpoint_alerts/view/test_helpers/render_alert_page.tsx b/x-pack/plugins/security_solution/public/endpoint_alerts/view/test_helpers/render_alert_page.tsx index 11ddc1f2529723..acfe3f228c21fb 100644 --- a/x-pack/plugins/security_solution/public/endpoint_alerts/view/test_helpers/render_alert_page.tsx +++ b/x-pack/plugins/security_solution/public/endpoint_alerts/view/test_helpers/render_alert_page.tsx @@ -15,7 +15,12 @@ import { AlertIndex } from '../index'; import { RouteCapture } from '../../../common/components/endpoint/route_capture'; import { depsStartMock } from '../../../common/mock/endpoint'; import { createStore } from '../../../common/store'; -import { SUB_PLUGINS_REDUCER, mockGlobalState, apolloClientObservable } from '../../../common/mock'; +import { + SUB_PLUGINS_REDUCER, + mockGlobalState, + apolloClientObservable, + createSecuritySolutionStorageMock, +} from '../../../common/mock'; export const alertPageTestRender = () => { /** @@ -25,7 +30,8 @@ export const alertPageTestRender = () => { /** * Create a store, with the middleware disabled. We don't want side effects being created by our code in this test. */ - const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const depsStart = depsStartMock(); depsStart.data.ui.SearchBar.mockImplementation(() =>
); diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx index a38b25661cd5e1..3809d848759cc3 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx @@ -9,7 +9,12 @@ import { getOr } from 'lodash/fp'; import React from 'react'; import { Provider as ReduxStoreProvider } from 'react-redux'; -import { apolloClientObservable, mockGlobalState, SUB_PLUGINS_REDUCER } from '../../../common/mock'; +import { + apolloClientObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, +} from '../../../common/mock'; import { createStore, State } from '../../../common/store'; import { hostsModel } from '../../store'; import { mockData } from './mock'; @@ -20,10 +25,11 @@ describe('Authentication Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); describe('rendering', () => { diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx index 45779bf37c77f3..1168f4f7454c81 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx @@ -15,6 +15,7 @@ import { mockGlobalState, TestProviders, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../../common/mock'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { createStore, State } from '../../../common/store'; @@ -35,12 +36,13 @@ jest.mock('../../../common/components/query_bar', () => ({ describe('Hosts Table', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; + const { storage } = createSecuritySolutionStorageMock(); - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const mount = useMountAppended(); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); describe('rendering', () => { diff --git a/x-pack/plugins/security_solution/public/hosts/index.ts b/x-pack/plugins/security_solution/public/hosts/index.ts index 6f27428e71c27c..90d5f54a027d7f 100644 --- a/x-pack/plugins/security_solution/public/hosts/index.ts +++ b/x-pack/plugins/security_solution/public/hosts/index.ts @@ -4,16 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { TimelineIdLiteral, TimelineId } from '../../common/types/timeline'; import { SecuritySubPluginWithStore } from '../app/types'; +import { getTimelinesInStorageByIds } from '../timelines/containers/local_storage'; import { getHostsRoutes } from './routes'; import { initialHostsState, hostsReducer, HostsState } from './store'; +const HOST_TIMELINE_IDS: TimelineIdLiteral[] = [ + TimelineId.hostsPageEvents, + TimelineId.hostsPageExternalAlerts, +]; + export class Hosts { public setup() {} - public start(): SecuritySubPluginWithStore<'hosts', HostsState> { + public start(storage: Storage): SecuritySubPluginWithStore<'hosts', HostsState> { return { routes: getHostsRoutes(), + storageTimelines: { + timelineById: getTimelinesInStorageByIds(storage, HOST_TIMELINE_IDS), + }, store: { initialState: { hosts: initialHostsState }, reducer: { hosts: hostsReducer }, diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx index d2ccbd76fac10a..85db3b4e159f12 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx @@ -19,6 +19,7 @@ import { TestProviders, mockGlobalState, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../common/mock'; import { SiemNavigation } from '../../common/components/navigation'; import { inputsActions } from '../../common/store/inputs'; @@ -171,7 +172,8 @@ describe('Hosts - rendering', () => { ]; localSource[0].result.data.source.status.indicesExist = true; const myState: State = mockGlobalState; - const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/alerts_query_tab_body.tsx index a0d8df6b87514b..3023670f8051a0 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/alerts_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/alerts_query_tab_body.tsx @@ -7,6 +7,7 @@ import React, { useMemo } from 'react'; import { Filter } from '../../../../../../../src/plugins/data/public'; +import { TimelineId } from '../../../../common/types/timeline'; import { AlertsView } from '../../../common/components/alerts_viewer'; import { AlertsComponentQueryProps } from './types'; @@ -48,7 +49,13 @@ export const HostAlertsQueryTabBody = React.memo((alertsProps: AlertsComponentQu [pageFilters] ); - return ; + return ( + + ); }); HostAlertsQueryTabBody.displayName = 'HostAlertsQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index b8ec269508442b..574e2ec4ae250a 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -5,6 +5,7 @@ */ import React, { useEffect } from 'react'; +import { TimelineId } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; import { HostsComponentsQueryProps } from './types'; import { hostsModel } from '../../store'; @@ -17,7 +18,6 @@ import { MatrixHistogramContainer } from '../../../common/components/matrix_hist import * as i18n from '../translations'; import { HistogramType } from '../../../graphql/types'; -const HOSTS_PAGE_TIMELINE_ID = 'hosts-page'; const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery'; export const eventsStackByOptions: MatrixHistogramOption[] = [ @@ -78,7 +78,7 @@ export const EventsQueryTabBody = ({ diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx index bce811c58e4367..553cb8c63db987 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx @@ -14,6 +14,7 @@ import { mockGlobalState, TestProviders, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../../common/mock'; import { createStore, State } from '../../../common/store'; import { networkModel } from '../../store'; @@ -26,10 +27,11 @@ import { NarrowDateRange } from '../../../common/components/ml/types'; describe('IP Overview Component', () => { const state: State = mockGlobalState; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); describe('rendering', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx index 70c952b1107451..580a5420f1c345 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx @@ -8,7 +8,12 @@ import { shallow } from 'enzyme'; import React from 'react'; import { Provider as ReduxStoreProvider } from 'react-redux'; -import { apolloClientObservable, mockGlobalState, SUB_PLUGINS_REDUCER } from '../../../common/mock'; +import { + apolloClientObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, +} from '../../../common/mock'; import { createStore, State } from '../../../common/store'; import { KpiNetworkComponent } from '.'; import { mockData } from './mock'; @@ -19,10 +24,11 @@ describe('KpiNetwork Component', () => { const to = new Date('2019-06-18T06:00:00.000Z').valueOf(); const narrowDateRange = jest.fn(); - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); describe('rendering', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx index 25449214b6e773..036ebedd6b88ef 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx @@ -15,6 +15,7 @@ import { mockGlobalState, TestProviders, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../../common/mock'; import { State, createStore } from '../../../common/store'; import { networkModel } from '../../store'; @@ -26,11 +27,12 @@ import { mockData } from './mock'; describe('NetworkTopNFlow Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const mount = useMountAppended(); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); describe('rendering', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx index e9020421a411e6..39fad58ca3528b 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx @@ -15,6 +15,7 @@ import { mockGlobalState, TestProviders, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../../common/mock'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { createStore, State } from '../../../common/store'; @@ -27,11 +28,12 @@ describe('NetworkHttp Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const mount = useMountAppended(); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); describe('rendering', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx index 8552d3184fcc2d..8b1dbc8c558b6e 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx @@ -17,6 +17,7 @@ import { mockIndexPattern, TestProviders, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../../common/mock'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { createStore, State } from '../../../common/store'; @@ -30,10 +31,11 @@ describe('NetworkTopCountries Table Component', () => { const state: State = mockGlobalState; const mount = useMountAppended(); - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); describe('rendering', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx index e40bbd40f4cd28..4de04f673d879f 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx @@ -16,6 +16,7 @@ import { mockGlobalState, TestProviders, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../../common/mock'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { createStore, State } from '../../../common/store'; @@ -27,11 +28,12 @@ describe('NetworkTopNFlow Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const mount = useMountAppended(); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); describe('rendering', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx index ffb68f4df82029..acbe974f914d7e 100644 --- a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx @@ -15,6 +15,7 @@ import { mockGlobalState, TestProviders, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../../common/mock'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { createStore, State } from '../../../common/store'; @@ -26,11 +27,12 @@ describe('Tls Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const mount = useMountAppended(); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); describe('Rendering', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx index 981e182154c5e7..f0d4d7fbeefc60 100644 --- a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx @@ -16,6 +16,7 @@ import { mockGlobalState, TestProviders, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../../common/mock'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { createStore, State } from '../../../common/store'; @@ -28,11 +29,12 @@ describe('Users Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const mount = useMountAppended(); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); describe('Rendering', () => { diff --git a/x-pack/plugins/security_solution/public/network/index.ts b/x-pack/plugins/security_solution/public/network/index.ts index 6590e5ee5161c1..63291ad2d2396a 100644 --- a/x-pack/plugins/security_solution/public/network/index.ts +++ b/x-pack/plugins/security_solution/public/network/index.ts @@ -4,16 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { SecuritySubPluginWithStore } from '../app/types'; import { getNetworkRoutes } from './routes'; import { initialNetworkState, networkReducer, NetworkState } from './store'; +import { TimelineId } from '../../common/types/timeline'; +import { getTimelinesInStorageByIds } from '../timelines/containers/local_storage'; export class Network { public setup() {} - public start(): SecuritySubPluginWithStore<'network', NetworkState> { + public start(storage: Storage): SecuritySubPluginWithStore<'network', NetworkState> { return { routes: getNetworkRoutes(), + storageTimelines: { + timelineById: getTimelinesInStorageByIds(storage, [TimelineId.networkPageExternalAlerts]), + }, store: { initialState: { network: initialNetworkState }, reducer: { network: networkReducer }, diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx index 22a5d7af88eb89..bbb964ae17b9f0 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx @@ -20,6 +20,7 @@ import { mockGlobalState, TestProviders, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../../common/mock'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { createStore, State } from '../../../common/store'; @@ -118,10 +119,11 @@ describe('Ip Details', () => { }); const state: State = mockGlobalState; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); localSource = cloneDeep(mocksSource); }); diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/alerts_query_tab_body.tsx index c5f59e751ca9ae..0c9f8c194bf2b4 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/alerts_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/alerts_query_tab_body.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; +import { TimelineId } from '../../../../common/types/timeline'; import { AlertsView } from '../../../common/components/alerts_viewer'; import { NetworkComponentQueryProps } from './types'; @@ -62,7 +63,11 @@ export const filterNetworkData: Filter[] = [ ]; export const NetworkAlertsQueryTabBody = React.memo((alertsProps: NetworkComponentQueryProps) => ( - + )); NetworkAlertsQueryTabBody.displayName = 'NetworkAlertsQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx index 23948209fccfe7..e1078dee3eb0d7 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx @@ -18,6 +18,7 @@ import { mockGlobalState, apolloClientObservable, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../common/mock'; import { State, createStore } from '../../common/store'; import { inputsActions } from '../../common/store/inputs'; @@ -155,7 +156,8 @@ describe('rendering - rendering', () => { ]; localSource[0].result.data.source.status.indicesExist = true; const myState: State = mockGlobalState; - const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx index 49347ab8105479..8b04c329731ab8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx @@ -14,6 +14,7 @@ import { mockGlobalState, TestProviders, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../../common/mock'; import { OverviewHost } from '.'; @@ -92,11 +93,12 @@ const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ describe('OverviewHost', () => { const state: State = mockGlobalState; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); beforeEach(() => { const myState = cloneDeep(state); - store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); test('it renders the expected widget title', () => { diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx index 4451135c608ced..2fcf6f83f0ae0e 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx @@ -13,6 +13,7 @@ import { mockGlobalState, TestProviders, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../../common/mock'; import { OverviewNetwork } from '.'; @@ -83,11 +84,12 @@ const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ describe('OverviewNetwork', () => { const state: State = mockGlobalState; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); beforeEach(() => { const myState = cloneDeep(state); - store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); }); test('it renders the expected widget title', () => { diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index e3d3062ee9cf7f..b9eef9f799d3b9 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -14,6 +14,7 @@ import { Plugin as IPlugin, DEFAULT_APP_CATEGORIES, } from '../../../../src/core/public'; +import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { initTelemetry } from './common/lib/telemetry'; import { KibanaServices } from './common/lib/kibana/services'; @@ -50,12 +51,14 @@ export class Plugin implements IPlugin { + const storage = new Storage(localStorage); const [coreStart, startPlugins] = await core.getStartServices(); const { renderApp } = await import('./app'); const services = { ...coreStart, ...startPlugins, security: plugins.security, + storage, } as StartServices; const alertsSubPlugin = new (await import('./alerts')).Alerts(); @@ -67,15 +70,27 @@ export class Plugin implements IPlugin { const state: State = mockGlobalState; + const { storage } = createSecuritySolutionStorageMock(); describe('rendering', () => { test('it renders correctly against snapshot', () => { @@ -59,7 +61,8 @@ describe('Flyout', () => { const storeShowIsTrue = createStore( stateShowIsTrue, SUB_PLUGINS_REDUCER, - apolloClientObservable + apolloClientObservable, + storage ); const wrapper = mount( @@ -82,7 +85,8 @@ describe('Flyout', () => { const storeWithDataProviders = createStore( stateWithDataProviders, SUB_PLUGINS_REDUCER, - apolloClientObservable + apolloClientObservable, + storage ); const wrapper = mount( @@ -103,7 +107,8 @@ describe('Flyout', () => { const storeWithDataProviders = createStore( stateWithDataProviders, SUB_PLUGINS_REDUCER, - apolloClientObservable + apolloClientObservable, + storage ); const wrapper = mount( @@ -136,7 +141,8 @@ describe('Flyout', () => { const storeWithDataProviders = createStore( stateWithDataProviders, SUB_PLUGINS_REDUCER, - apolloClientObservable + apolloClientObservable, + storage ); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx index 02ebfd7f752b55..3352639fa95f82 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx @@ -48,7 +48,7 @@ export const Actions = React.memo(({ header, onColumnRemoved, sort, isLoa <> {sort.columnId === header.id && isLoading ? ( - + ) : ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/__snapshots__/index.test.tsx.snap index fcf0f1019e67ac..f155b379227f0e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/__snapshots__/index.test.tsx.snap @@ -33,24 +33,28 @@ exports[`Footer Timeline Component rendering it renders the default timeline foo items={ Array [ 1 rows , 5 rows , 10 rows , diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx index 594bf6d43e2851..4e1595eef984c0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx @@ -144,6 +144,7 @@ export const EventsCountComponent = ({ iconType="arrowDown" iconSide="right" onClick={onClick} + data-test-subj="local-events-count-button" /> {` ${i18n.OF} `} @@ -289,6 +290,7 @@ export const FooterComponent = ({ { closePopover(); onChangeItemsPerPage(item); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx index 660beffd9a0893..c67efe204eb0e2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx @@ -11,6 +11,7 @@ import { mockGlobalState, apolloClientObservable, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, TestProviders, } from '../../../../common/mock'; import { createStore, State } from '../../../../common/store'; @@ -97,12 +98,14 @@ const defaultProps = { }; describe('Properties', () => { const state: State = mockGlobalState; + const { storage } = createSecuritySolutionStorageMock(); let mockedWidth = 1000; - let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); beforeEach(() => { jest.clearAllMocks(); - store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); (useThrottledResizeObserver as jest.Mock).mockReturnValue({ width: mockedWidth }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx index 879302d4a92c1f..cd6233334c5de0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx @@ -11,6 +11,7 @@ import { mockGlobalState, apolloClientObservable, SUB_PLUGINS_REDUCER, + createSecuritySolutionStorageMock, } from '../../../../common/mock'; import { createStore, State } from '../../../../common/store'; import { useKibana } from '../../../../common/lib/kibana'; @@ -24,7 +25,8 @@ jest.mock('../../../../common/lib/kibana', () => { describe('NewTemplateTimeline', () => { const state: State = mockGlobalState; - const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const { storage } = createSecuritySolutionStorageMock(); + const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const mockClosePopover = jest.fn(); const mockTitle = 'NEW_TIMELINE'; let wrapper: ReactWrapper; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 7363a609742754..a09f2792df159a 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -27,7 +27,12 @@ import { QueryTemplate, QueryTemplateProps } from '../../common/containers/query import { EventType } from '../../timelines/store/timeline/model'; import { timelineQuery } from './index.gql_query'; import { timelineActions } from '../../timelines/store/timeline'; -import { ALERTS_TABLE_TIMELINE_ID } from '../../alerts/components/alerts_table'; +import { + ALERTS_TIMELINE_ID as DETECTION_ENGINE_TIMELINE_ID, + ALERTS_RULES_DETAILS_PAGE_TIMELINE_ID as DETECTION_ENGINE_RULES_TIMELINE_ID, +} from '../../alerts/constants'; + +const timelineIds = [DETECTION_ENGINE_TIMELINE_ID, DETECTION_ENGINE_RULES_TIMELINE_ID]; export interface TimelineArgs { events: TimelineItem[]; @@ -182,7 +187,7 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch: Dispatch) => ({ clearSignalsState: ({ id }: { id?: string }) => { - if (id != null && id === ALERTS_TABLE_TIMELINE_ID) { + if (id != null && timelineIds.some((timelineId) => timelineId === id)) { dispatch(timelineActions.clearEventsLoading({ id })); dispatch(timelineActions.clearEventsDeleted({ id })); } diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts new file mode 100644 index 00000000000000..e1bccbdff48899 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + LOCAL_STORAGE_TIMELINE_KEY, + useTimelinesStorage, + getTimelinesInStorageByIds, + getAllTimelinesInStorage, + addTimelineInStorage, +} from '.'; + +import { TimelineId } from '../../../../common/types/timeline'; +import { mockTimelineModel, createSecuritySolutionStorageMock } from '../../../common/mock'; +import { useKibana } from '../../../common/lib/kibana'; +import { createUseKibanaMock } from '../../../common/mock/kibana_react'; + +jest.mock('../../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mock; + +describe('SiemLocalStorage', () => { + const { localStorage, storage } = createSecuritySolutionStorageMock(); + + beforeEach(() => { + jest.resetAllMocks(); + useKibanaMock.mockImplementation(() => ({ + services: { + ...createUseKibanaMock()().services, + storage, + }, + })); + localStorage.clear(); + }); + + describe('addTimeline', () => { + it('adds a timeline when storage is empty', () => { + const timelineStorage = useTimelinesStorage(); + timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); + expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({ + [TimelineId.hostsPageEvents]: mockTimelineModel, + }); + }); + + it('adds a timeline when storage contains another timelines', () => { + const timelineStorage = useTimelinesStorage(); + timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); + timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); + expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({ + [TimelineId.hostsPageEvents]: mockTimelineModel, + [TimelineId.hostsPageExternalAlerts]: mockTimelineModel, + }); + }); + }); + + describe('getAllTimelines', () => { + it('gets all timelines correctly', () => { + const timelineStorage = useTimelinesStorage(); + timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); + timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); + const timelines = timelineStorage.getAllTimelines(); + expect(timelines).toEqual({ + [TimelineId.hostsPageEvents]: mockTimelineModel, + [TimelineId.hostsPageExternalAlerts]: mockTimelineModel, + }); + }); + + it('returns an empty object if there is no timelines', () => { + const timelineStorage = useTimelinesStorage(); + const timelines = timelineStorage.getAllTimelines(); + expect(timelines).toEqual({}); + }); + }); + + describe('getTimelineById', () => { + it('gets a timeline by id', () => { + const timelineStorage = useTimelinesStorage(); + timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); + const timeline = timelineStorage.getTimelineById(TimelineId.hostsPageEvents); + expect(timeline).toEqual(mockTimelineModel); + }); + }); + + describe('getTimelinesInStorageByIds', () => { + it('gets timelines correctly', () => { + const timelineStorage = useTimelinesStorage(); + timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); + timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); + const timelines = getTimelinesInStorageByIds(storage, [ + TimelineId.hostsPageEvents, + TimelineId.hostsPageExternalAlerts, + ]); + expect(timelines).toEqual({ + [TimelineId.hostsPageEvents]: mockTimelineModel, + [TimelineId.hostsPageExternalAlerts]: mockTimelineModel, + }); + }); + + it('gets an empty timelime when there is no timelines', () => { + const timelines = getTimelinesInStorageByIds(storage, [TimelineId.hostsPageEvents]); + expect(timelines).toEqual({}); + }); + + it('returns empty timelime when there is no ids', () => { + const timelineStorage = useTimelinesStorage(); + timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); + const timelines = getTimelinesInStorageByIds(storage, []); + expect(timelines).toEqual({}); + }); + + it('returns empty timelime when a specific timeline does not exists', () => { + const timelineStorage = useTimelinesStorage(); + timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); + const timelines = getTimelinesInStorageByIds(storage, [TimelineId.hostsPageExternalAlerts]); + expect(timelines).toEqual({}); + }); + + it('returns timelines correctly when one exist and another not', () => { + const timelineStorage = useTimelinesStorage(); + timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); + const timelines = getTimelinesInStorageByIds(storage, [ + TimelineId.hostsPageEvents, + TimelineId.hostsPageExternalAlerts, + ]); + expect(timelines).toEqual({ + [TimelineId.hostsPageEvents]: mockTimelineModel, + }); + }); + }); + + describe('getAllTimelinesInStorage', () => { + it('gets timelines correctly', () => { + const timelineStorage = useTimelinesStorage(); + timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); + timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); + const timelines = getAllTimelinesInStorage(storage); + expect(timelines).toEqual({ + [TimelineId.hostsPageEvents]: mockTimelineModel, + [TimelineId.hostsPageExternalAlerts]: mockTimelineModel, + }); + }); + + it('gets an empty timeline when there is no timelines', () => { + const timelines = getAllTimelinesInStorage(storage); + expect(timelines).toEqual({}); + }); + }); + + describe('addTimelineInStorage', () => { + it('adds a timeline when storage is empty', () => { + addTimelineInStorage(storage, TimelineId.hostsPageEvents, mockTimelineModel); + expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({ + [TimelineId.hostsPageEvents]: mockTimelineModel, + }); + }); + + it('adds a timeline when storage contains another timelines', () => { + addTimelineInStorage(storage, TimelineId.hostsPageEvents, mockTimelineModel); + addTimelineInStorage(storage, TimelineId.hostsPageExternalAlerts, mockTimelineModel); + expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({ + [TimelineId.hostsPageEvents]: mockTimelineModel, + [TimelineId.hostsPageExternalAlerts]: mockTimelineModel, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx new file mode 100644 index 00000000000000..1a09868da77716 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; +import { TimelinesStorage } from './types'; +import { useKibana } from '../../../common/lib/kibana'; +import { TimelineModel } from '../../store/timeline/model'; +import { TimelineIdLiteral } from '../../../../common/types/timeline'; + +export const LOCAL_STORAGE_TIMELINE_KEY = 'timelines'; +const EMPTY_TIMELINE = {} as { + [K in TimelineIdLiteral]: TimelineModel; +}; + +export const getTimelinesInStorageByIds = (storage: Storage, timelineIds: TimelineIdLiteral[]) => { + const allTimelines = storage.get(LOCAL_STORAGE_TIMELINE_KEY); + + if (!allTimelines) { + return EMPTY_TIMELINE; + } + + return timelineIds.reduce((acc, timelineId) => { + const timelineModel = allTimelines[timelineId]; + if (!timelineModel) { + return { + ...acc, + }; + } + + return { + ...acc, + [timelineId]: timelineModel, + }; + }, {} as { [K in TimelineIdLiteral]: TimelineModel }); +}; + +export const getAllTimelinesInStorage = (storage: Storage) => + storage.get(LOCAL_STORAGE_TIMELINE_KEY) ?? {}; + +export const addTimelineInStorage = ( + storage: Storage, + id: TimelineIdLiteral, + timeline: TimelineModel +) => { + const timelines = getAllTimelinesInStorage(storage); + storage.set(LOCAL_STORAGE_TIMELINE_KEY, { + ...timelines, + [id]: timeline, + }); +}; + +export const useTimelinesStorage = (): TimelinesStorage => { + const { storage } = useKibana().services; + + const getAllTimelines: TimelinesStorage['getAllTimelines'] = () => + getAllTimelinesInStorage(storage); + + const getTimelineById: TimelinesStorage['getTimelineById'] = (id: TimelineIdLiteral) => + getTimelinesInStorageByIds(storage, [id])[id] ?? null; + + const addTimeline: TimelinesStorage['addTimeline'] = (id, timeline) => + addTimelineInStorage(storage, id, timeline); + + return { getAllTimelines, getTimelineById, addTimeline }; +}; + +export { TimelinesStorage }; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/types.ts b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/types.ts new file mode 100644 index 00000000000000..d888f3bb8b332e --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimelineModel } from '../../../timelines/store/timeline/model'; +import { TimelineIdLiteral } from '../../../../common/types/timeline'; + +export interface TimelinesStorage { + getAllTimelines: () => Record; + getTimelineById: (id: TimelineIdLiteral) => TimelineModel | null; + addTimeline: (id: TimelineIdLiteral, timeline: TimelineModel) => void; +} diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts index 5a7e5e078c7994..2155dc804aa7ed 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts @@ -15,7 +15,7 @@ import { } from 'lodash/fp'; import { Action } from 'redux'; import { Epic } from 'redux-observable'; -import { from, Observable, empty, merge } from 'rxjs'; +import { from, empty, merge } from 'rxjs'; import { filter, map, @@ -34,16 +34,14 @@ import { MatchAllFilter, } from '../../../../../../.../../../src/plugins/data/public'; import { TimelineStatus } from '../../../../common/types/timeline'; +import { inputsModel } from '../../../common/store/inputs'; import { TimelineType, TimelineInput, ResponseTimeline, TimelineResult, } from '../../../graphql/types'; -import { AppApolloClient } from '../../../common/lib/lib'; import { addError } from '../../../common/store/app/actions'; -import { NotesById } from '../../../common/store/app/model'; -import { inputsModel } from '../../../common/store/inputs'; import { applyKqlFilterQuery, @@ -80,18 +78,10 @@ import { epicPersistTimelineFavorite, timelineFavoriteActionsType } from './epic import { isNotNull } from './helpers'; import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue'; import { myEpicTimelineId } from './my_epic_timeline_id'; -import { ActionTimeline, TimelineById } from './types'; +import { ActionTimeline, TimelineEpicDependencies } from './types'; import { persistTimeline } from '../../containers/api'; import { ALL_TIMELINE_QUERY_ID } from '../../containers/all'; -interface TimelineEpicDependencies { - timelineByIdSelector: (state: State) => TimelineById; - timelineTimeRangeSelector: (state: State) => inputsModel.TimeRange; - selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery; - selectNotesByIdSelector: (state: State) => NotesById; - apolloClient$: Observable; -} - const timelineActionsType = [ applyKqlFilterQuery.type, addProvider.type, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx new file mode 100644 index 00000000000000..34778aba7873cc --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { + mockGlobalState, + SUB_PLUGINS_REDUCER, + apolloClientObservable, + TestProviders, + defaultHeaders, + createSecuritySolutionStorageMock, + mockIndexPattern, +} from '../../../common/mock'; + +import { createStore, State } from '../../../common/store'; +import { + removeColumn, + upsertColumn, + applyDeltaToColumnWidth, + updateColumns, + updateItemsPerPage, + updateSort, +} from './actions'; + +import { + TimelineComponent, + Props as TimelineComponentProps, +} from '../../components/timeline/timeline'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { mockDataProviders } from '../../components/timeline/data_providers/mock/mock_data_providers'; +import { Sort } from '../../components/timeline/body/sort'; +import { Direction } from '../../../graphql/types'; + +import { addTimelineInStorage } from '../../containers/local_storage'; +import { isPageTimeline } from './epic_local_storage'; + +jest.mock('../../containers/local_storage'); + +const wait = (ms: number = 500): Promise => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +const addTimelineInStorageMock = addTimelineInStorage as jest.Mock; + +describe('epicLocalStorage', () => { + const state: State = mockGlobalState; + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); + + let props = {} as TimelineComponentProps; + const sort: Sort = { + columnId: '@timestamp', + sortDirection: Direction.desc, + }; + const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); + const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); + + const indexPattern = mockIndexPattern; + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); + props = { + browserFields: mockBrowserFields, + columns: defaultHeaders, + id: 'foo', + dataProviders: mockDataProviders, + end: endDate, + eventType: 'raw' as TimelineComponentProps['eventType'], + filters: [], + indexPattern, + indexToAdd: [], + isLive: false, + itemsPerPage: 5, + itemsPerPageOptions: [5, 10, 20], + kqlMode: 'search' as TimelineComponentProps['kqlMode'], + kqlQueryExpression: '', + loadingIndexName: false, + onChangeItemsPerPage: jest.fn(), + onClose: jest.fn(), + onDataProviderEdited: jest.fn(), + onDataProviderRemoved: jest.fn(), + onToggleDataProviderEnabled: jest.fn(), + onToggleDataProviderExcluded: jest.fn(), + show: true, + showCallOutUnauthorizedMsg: false, + start: startDate, + sort, + toggleColumn: jest.fn(), + usersViewing: ['elastic'], + }; + }); + + it('filters correctly page timelines', () => { + expect(isPageTimeline('timeline-1')).toBe(false); + expect(isPageTimeline('hosts-page-alerts')).toBe(true); + }); + + it('persist adding / reordering of a column correctly', async () => { + shallow( + + + + ); + store.dispatch(upsertColumn({ id: 'test', index: 1, column: defaultHeaders[0] })); + await wait(); + expect(addTimelineInStorageMock).toHaveBeenCalled(); + }); + + it('persist timeline when removing a column ', async () => { + shallow( + + + + ); + store.dispatch(removeColumn({ id: 'test', columnId: '@timestamp' })); + await wait(); + expect(addTimelineInStorageMock).toHaveBeenCalled(); + }); + + it('persists resizing of a column', async () => { + shallow( + + + + ); + store.dispatch(applyDeltaToColumnWidth({ id: 'test', columnId: '@timestamp', delta: 80 })); + await wait(); + expect(addTimelineInStorageMock).toHaveBeenCalled(); + }); + + it('persist the resetting of the fields', async () => { + shallow( + + + + ); + store.dispatch(updateColumns({ id: 'test', columns: defaultHeaders })); + await wait(); + expect(addTimelineInStorageMock).toHaveBeenCalled(); + }); + + it('persist items per page', async () => { + shallow( + + + + ); + store.dispatch(updateItemsPerPage({ id: 'test', itemsPerPage: 50 })); + await wait(); + expect(addTimelineInStorageMock).toHaveBeenCalled(); + }); + + it('persist the sorting of a column', async () => { + shallow( + + + + ); + store.dispatch( + updateSort({ + id: 'test', + sort: { + columnId: 'event.severity', + sortDirection: Direction.desc, + }, + }) + ); + await wait(); + expect(addTimelineInStorageMock).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts new file mode 100644 index 00000000000000..b3d1db23ffae8a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux'; +import { map, filter, ignoreElements, tap, withLatestFrom, delay } from 'rxjs/operators'; +import { Epic } from 'redux-observable'; +import { get } from 'lodash/fp'; + +import { TimelineIdLiteral } from '../../../../common/types/timeline'; +import { addTimelineInStorage } from '../../containers/local_storage'; + +import { + removeColumn, + upsertColumn, + applyDeltaToColumnWidth, + updateColumns, + updateItemsPerPage, + updateSort, +} from './actions'; +import { TimelineEpicDependencies } from './types'; +import { isNotNull } from './helpers'; + +const timelineActionTypes = [ + removeColumn.type, + upsertColumn.type, + applyDeltaToColumnWidth.type, + updateColumns.type, + updateItemsPerPage.type, + updateSort.type, +]; + +export const isPageTimeline = (timelineId: string | undefined): boolean => + // Is not a flyout timeline + !(timelineId && timelineId.toLowerCase().startsWith('timeline')); + +export const createTimelineLocalStorageEpic = (): Epic< + Action, + Action, + State, + TimelineEpicDependencies +> => (action$, state$, { timelineByIdSelector, storage }) => { + const timeline$ = state$.pipe(map(timelineByIdSelector), filter(isNotNull)); + return action$.pipe( + delay(500), + withLatestFrom(timeline$), + filter(([action]) => isPageTimeline(get('payload.id', action))), + tap(([action, timelineById]) => { + if (timelineActionTypes.includes(action.type)) { + if (storage) { + const timelineId: TimelineIdLiteral = get('payload.id', action); + addTimelineInStorage(storage, timelineId, timelineById[timelineId]); + } + } + }), + ignoreElements() + ); +}; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 03e9ca176ee821..15f956fa79d3cc 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -23,22 +23,10 @@ import { TimelineTypeLiteral } from '../../../../common/types/timeline'; import { timelineDefaults } from './defaults'; import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model'; -import { TimelineById, TimelineState } from './types'; - -const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference +import { TimelineById } from './types'; export const isNotNull = (value: T | null): value is T => value !== null; -export const initialTimelineState: TimelineState = { - timelineById: EMPTY_TIMELINE_BY_ID, - autoSavedWarningMsg: { - timelineId: null, - newTimelineModel: null, - }, - showCallOutUnauthorizedMsg: false, - insertTimeline: null, -}; - interface AddTimelineHistoryParams { id: string; historyId: string; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts index aa6c3086142872..5262c72a6140c9 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { Action } from 'redux'; +import { Observable } from 'rxjs'; + +import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; +import { AppApolloClient } from '../../../common/lib/lib'; +import { inputsModel } from '../../../common/store/inputs'; +import { NotesById } from '../../../common/store/app/model'; import { TimelineModel } from './model'; export interface AutoSavedWarningMsg { @@ -39,3 +45,12 @@ export interface ActionTimeline extends Action { noteId: string; }; } + +export interface TimelineEpicDependencies { + timelineByIdSelector: (state: State) => TimelineById; + timelineTimeRangeSelector: (state: State) => inputsModel.TimeRange; + selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery; + selectNotesByIdSelector: (state: State) => NotesById; + apolloClient$: Observable; + storage: Storage; +} diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 6c338d47cf63ce..6d59824702cfa9 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -12,6 +12,7 @@ import { Start as NewsfeedStart } from '../../../../src/plugins/newsfeed/public' import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; +import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { IngestManagerStart } from '../../ingest_manager/public'; import { TriggersAndActionsUIPublicPluginSetup as TriggersActionsSetup, @@ -39,6 +40,7 @@ export interface StartPlugins { export type StartServices = CoreStart & StartPlugins & { security: SecurityPluginSetup; + storage: Storage; }; // eslint-disable-next-line @typescript-eslint/no-empty-interface