[SIEM][Timeline] Persist timeline to localStorage (#67156)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
This commit is contained in:
Christos Nasikas 2020-06-17 14:55:46 +03:00 committed by GitHub
parent ab1270e566
commit 47e50f8102
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 949 additions and 163 deletions

View file

@ -197,6 +197,32 @@ export interface SavedTimeline extends runtimeTypes.TypeOf<typeof SavedTimelineR
export interface SavedTimelineNote extends runtimeTypes.TypeOf<typeof SavedTimelineRuntimeType> {}
/*
* 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<typeof TimelineIdLiteralRt>;
/**
* Timeline Saved object type with metadata
*/

View file

@ -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' },

View file

@ -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');
});
});
});
});

View file

@ -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"]';

View file

@ -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;

View file

@ -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 =

View file

@ -48,3 +48,9 @@ export const drop = (dropTarget: JQuery<HTMLElement>) => {
.trigger('mouseup', { force: true })
.wait(1000);
};
export const reload = (afterReload: () => void) => {
cy.reload();
cy.contains('a', 'Security');
afterReload();
};

View file

@ -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');
};

View file

@ -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 });
};

View file

@ -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(
<AlertsTableComponent
timelineId={TimelineId.test}
canUserCRUD
hasIndexWrite
from={0}

View file

@ -12,6 +12,7 @@ import { Dispatch } from 'redux';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
import { Filter, esQuery } from '../../../../../../../src/plugins/data/public';
import { TimelineIdLiteral } from '../../../../common/types/timeline';
import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns';
import { StatefulEventsViewer } from '../../../common/components/events_viewer';
import { HeaderSection } from '../../../common/components/header_section';
@ -48,9 +49,8 @@ import {
displayErrorToast,
} from '../../../common/components/toasters';
export const ALERTS_TABLE_TIMELINE_ID = 'alerts-table';
interface OwnProps {
timelineId: TimelineIdLiteral;
canUserCRUD: boolean;
defaultFilters?: Filter[];
hasIndexWrite: boolean;
@ -63,6 +63,7 @@ interface OwnProps {
type AlertsTableComponentProps = OwnProps & PropsFromRedux;
export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
timelineId,
canUserCRUD,
clearEventsDeleted,
clearEventsLoading,
@ -140,18 +141,16 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
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<AlertsTableComponentProps> = ({
// 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<AlertsTableComponentProps> = ({
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<AlertsTableComponentProps> = ({
}, []);
useEffect(() => {
setTimelineRowActions({
id: ALERTS_TABLE_TIMELINE_ID,
id: timelineId,
queryFields: requiredFieldsForActions,
timelineRowActions: additionalActions,
});
@ -365,7 +364,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
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<AlertsTableComponentProps> = ({
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);

View file

@ -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';

View file

@ -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),
},
};
}
}

View file

@ -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<PropsFromRedux> = ({
/>
<EuiSpacer size="l" />
<AlertsTable
timelineId={TimelineId.alertsPage}
loading={loading}
hasIndexWrite={hasIndexWrite ?? false}
canUserCRUD={(canUserCRUD ?? false) && (hasEncryptionKey ?? false)}

View file

@ -24,6 +24,7 @@ import { Redirect, useParams } from 'react-router-dom';
import { StickyContainer } from 'react-sticky';
import { connect, ConnectedProps } from 'react-redux';
import { TimelineId } from '../../../../../../common/types/timeline';
import { UpdateDateRange } from '../../../../../common/components/charts/common';
import { FiltersGlobal } from '../../../../../common/components/filters_global';
import { FormattedDate } from '../../../../../common/components/formatted_date';
@ -386,6 +387,7 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
<EuiSpacer />
{ruleId != null && (
<AlertsTable
timelineId={TimelineId.alertsRulesDetailsPage}
canUserCRUD={canUserCRUD ?? false}
defaultFilters={alertDefaultFilters}
hasIndexWrite={hasIndexWrite ?? false}

View file

@ -72,9 +72,10 @@ const AppPluginRoot = memo(AppPluginRootComponent);
interface StartAppComponent extends AppFrontendLibs {
subPlugins: SecuritySubPlugins;
storage: Storage;
}
const StartAppComponent: FC<StartAppComponent> = ({ subPlugins, ...libs }) => {
const StartAppComponent: FC<StartAppComponent> = ({ subPlugins, storage, ...libs }) => {
const { routes: subPluginRoutes, store: subPluginsStore } = subPlugins;
const { i18n } = useKibana().services;
const history = createHashHistory();
@ -84,6 +85,7 @@ const StartAppComponent: FC<StartAppComponent> = ({ 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<SiemAppComponentProps> = ({ services, subPlugins }) => (
<KibanaContextProvider
services={{
appName: 'siem',
storage: new Storage(localStorage),
...services,
}}
>
<StartApp subPlugins={subPlugins} {...compose(services)} />
</KibanaContextProvider>
);
const SiemAppComponent: React.FC<SiemAppComponentProps> = ({ services, subPlugins }) => {
return (
<KibanaContextProvider
services={{
appName: 'siem',
...services,
}}
>
<StartApp subPlugins={subPlugins} storage={services.storage} {...compose(services)} />
</KibanaContextProvider>
);
};
export const SiemApp = memo(SiemAppComponent);

View file

@ -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<K extends SecuritySubPluginKeyStore, T>
export interface SecuritySubPlugin {
routes: React.ReactElement[];
storageTimelines?: Pick<TimelineState, 'timelineById'>;
}
type SecuritySubPluginKeyStore =

View file

@ -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();
});

View file

@ -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<Props> = ({ endDate, startDate, pageFilters = [] }) => {
const AlertsTableComponent: React.FC<Props> = ({
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<Props> = ({ endDate, startDate, pageFilters
pageFilters={alertsFilter}
defaultModel={alertsDefaultModel}
end={endDate}
id={ALERTS_TABLE_ID}
id={timelineId}
start={startDate}
/>
);

View file

@ -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$<string>(DEFAULT_NUMBER_FORMAT);
const getSubtitle = useCallback(
(totalCount: number) =>
@ -61,7 +62,12 @@ export const AlertsView = ({
type={type}
{...alertsHistogramConfigs}
/>
<AlertsTable endDate={endDate} startDate={startDate} pageFilters={pageFilters} />
<AlertsTable
timelineId={timelineId}
endDate={endDate}
startDate={startDate}
pageFilters={pageFilters}
/>
</>
);
};

View file

@ -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[];

View file

@ -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', () => {

View file

@ -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(

View file

@ -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([
[

View file

@ -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();

View file

@ -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"

View file

@ -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 }) => (

View file

@ -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';

View file

@ -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<string, unknown> = {};
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),
};
};

View file

@ -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<string, unknown> = {};
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<Props> = ({
children,
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable),
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage),
onDragEnd = jest.fn(),
}) => (
<I18nProvider>
@ -83,7 +69,7 @@ export const TestProviders = React.memo(TestProvidersComponent);
const TestProviderWithoutDragAndDropComponent: React.FC<Props> = ({
children,
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable),
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage),
}) => (
<I18nProvider>
<ReduxStoreProvider store={store}>{children}</ReduxStoreProvider>

View file

@ -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 = <State>() =>
combineEpics(
createTimelineEpic<State>(),
createTimelineFavoriteEpic<State>(),
createTimelineNoteEpic<State>(),
createTimelinePinnedEventEpic<State>()
createTimelinePinnedEventEpic<State>(),
createTimelineLocalStorageEpic<State>()
);

View file

@ -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<State>,
pluginsReducer: SubPluginsInitReducer,
apolloClient: Observable<AppApolloClient>,
storage: Storage,
additionalMiddleware?: Array<Middleware<{}, State, Dispatch<AppAction | Immutable<AppAction>>>>
): Store<State, Action> => {
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<Action, Action, State, typeof middlewareDependencies>(

View file

@ -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(() => <div />);

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -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 },

View file

@ -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(
<TestProviders store={myStore}>
<MockedProvider mocks={localSource} addTypename={false}>

View file

@ -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 <AlertsView {...rest} pageFilters={hostPageFilters} />;
return (
<AlertsView
timelineId={TimelineId.hostsPageExternalAlerts}
{...rest}
pageFilters={hostPageFilters}
/>
);
});
HostAlertsQueryTabBody.displayName = 'HostAlertsQueryTabBody';

View file

@ -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 = ({
<StatefulEventsViewer
defaultModel={eventsDefaultModel}
end={endDate}
id={HOSTS_PAGE_TIMELINE_ID}
id={TimelineId.hostsPageEvents}
start={startDate}
pageFilters={pageFilters}
/>

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -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 },

View file

@ -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);
});

View file

@ -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) => (
<AlertsView {...alertsProps} pageFilters={filterNetworkData} />
<AlertsView
timelineId={TimelineId.networkPageExternalAlerts}
{...alertsProps}
pageFilters={filterNetworkData}
/>
));
NetworkAlertsQueryTabBody.displayName = 'NetworkAlertsQueryTabBody';

View file

@ -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(
<TestProviders store={myStore}>
<MockedProvider mocks={localSource} addTypename={false}>

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -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<PluginSetup, PluginStart, SetupPlugins, S
plugins.triggers_actions_ui.actionTypeRegistry.register(jiraActionType());
const mountSecurityApp = async (params: AppMountParameters) => {
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<PluginSetup, PluginStart, SetupPlugins, S
const endpointAlertsSubPlugin = new (await import('./endpoint_alerts')).EndpointAlerts();
const managementSubPlugin = new (await import('./management')).Management();
const alertsStart = alertsSubPlugin.start();
const alertsStart = alertsSubPlugin.start(storage);
const casesStart = casesSubPlugin.start();
const hostsStart = hostsSubPlugin.start();
const networkStart = networkSubPlugin.start();
const hostsStart = hostsSubPlugin.start(storage);
const networkStart = networkSubPlugin.start(storage);
const overviewStart = overviewSubPlugin.start();
const timelinesStart = timelinesSubPlugin.start();
const endpointAlertsStart = endpointAlertsSubPlugin.start(coreStart, startPlugins);
const managementSubPluginStart = managementSubPlugin.start(coreStart, startPlugins);
const timelineInitialState = {
timeline: {
...timelinesStart.store.initialState.timeline!,
timelineById: {
...timelinesStart.store.initialState.timeline!.timelineById,
...alertsStart.storageTimelines!.timelineById,
...hostsStart.storageTimelines!.timelineById,
...networkStart.storageTimelines!.timelineById,
},
},
};
return renderApp(services, params, {
routes: [
...alertsStart.routes,
@ -91,7 +106,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
initialState: {
...hostsStart.store.initialState,
...networkStart.store.initialState,
...timelinesStart.store.initialState,
...timelineInitialState,
...endpointAlertsStart.store.initialState,
...managementSubPluginStart.store.initialState,
},

View file

@ -14,6 +14,7 @@ import {
mockGlobalState,
TestProviders,
SUB_PLUGINS_REDUCER,
createSecuritySolutionStorageMock,
} from '../../../common/mock';
import { createStore, State } from '../../../common/store';
import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers';
@ -31,6 +32,7 @@ const usersViewing = ['elastic'];
describe('Flyout', () => {
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(

View file

@ -48,7 +48,7 @@ export const Actions = React.memo<Props>(({ header, onColumnRemoved, sort, isLoa
<>
{sort.columnId === header.id && isLoading ? (
<EventsHeadingExtra className="siemEventsHeading__extra--loading">
<EventsLoading />
<EventsLoading data-test-subj="timeline-loading-spinner" />
</EventsHeadingExtra>
) : (
<EventsHeadingExtra className="siemEventsHeading__extra--close">

View file

@ -33,24 +33,28 @@ exports[`Footer Timeline Component rendering it renders the default timeline foo
items={
Array [
<EuiContextMenuItem
data-test-subj="items-per-page-option-1"
icon="empty"
onClick={[Function]}
>
1 rows
</EuiContextMenuItem>,
<EuiContextMenuItem
data-test-subj="items-per-page-option-5"
icon="empty"
onClick={[Function]}
>
5 rows
</EuiContextMenuItem>,
<EuiContextMenuItem
data-test-subj="items-per-page-option-10"
icon="empty"
onClick={[Function]}
>
10 rows
</EuiContextMenuItem>,
<EuiContextMenuItem
data-test-subj="items-per-page-option-20"
icon="empty"
onClick={[Function]}
>

View file

@ -144,6 +144,7 @@ export const EventsCountComponent = ({
iconType="arrowDown"
iconSide="right"
onClick={onClick}
data-test-subj="local-events-count-button"
/>
</EuiBadge>
{` ${i18n.OF} `}
@ -289,6 +290,7 @@ export const FooterComponent = ({
<EuiContextMenuItem
key={item}
icon={itemsPerPage === item ? 'check' : 'empty'}
data-test-subj={`items-per-page-option-${item}`}
onClick={() => {
closePopover();
onChangeItemsPerPage(item);

View file

@ -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 });
});

View file

@ -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;

View file

@ -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 }));
}

View file

@ -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,
});
});
});
});

View file

@ -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 };

View file

@ -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<TimelineIdLiteral, TimelineModel>;
getTimelineById: (id: TimelineIdLiteral) => TimelineModel | null;
addTimeline: (id: TimelineIdLiteral, timeline: TimelineModel) => void;
}

View file

@ -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<State> {
timelineByIdSelector: (state: State) => TimelineById;
timelineTimeRangeSelector: (state: State) => inputsModel.TimeRange;
selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery;
selectNotesByIdSelector: (state: State) => NotesById;
apolloClient$: Observable<AppApolloClient>;
}
const timelineActionsType = [
applyKqlFilterQuery.type,
addProvider.type,

View file

@ -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<void> => {
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(
<TestProviders store={store}>
<TimelineComponent {...props} />
</TestProviders>
);
store.dispatch(upsertColumn({ id: 'test', index: 1, column: defaultHeaders[0] }));
await wait();
expect(addTimelineInStorageMock).toHaveBeenCalled();
});
it('persist timeline when removing a column ', async () => {
shallow(
<TestProviders store={store}>
<TimelineComponent {...props} />
</TestProviders>
);
store.dispatch(removeColumn({ id: 'test', columnId: '@timestamp' }));
await wait();
expect(addTimelineInStorageMock).toHaveBeenCalled();
});
it('persists resizing of a column', async () => {
shallow(
<TestProviders store={store}>
<TimelineComponent {...props} />
</TestProviders>
);
store.dispatch(applyDeltaToColumnWidth({ id: 'test', columnId: '@timestamp', delta: 80 }));
await wait();
expect(addTimelineInStorageMock).toHaveBeenCalled();
});
it('persist the resetting of the fields', async () => {
shallow(
<TestProviders store={store}>
<TimelineComponent {...props} />
</TestProviders>
);
store.dispatch(updateColumns({ id: 'test', columns: defaultHeaders }));
await wait();
expect(addTimelineInStorageMock).toHaveBeenCalled();
});
it('persist items per page', async () => {
shallow(
<TestProviders store={store}>
<TimelineComponent {...props} />
</TestProviders>
);
store.dispatch(updateItemsPerPage({ id: 'test', itemsPerPage: 50 }));
await wait();
expect(addTimelineInStorageMock).toHaveBeenCalled();
});
it('persist the sorting of a column', async () => {
shallow(
<TestProviders store={store}>
<TimelineComponent {...props} />
</TestProviders>
);
store.dispatch(
updateSort({
id: 'test',
sort: {
columnId: 'event.severity',
sortDirection: Direction.desc,
},
})
);
await wait();
expect(addTimelineInStorageMock).toHaveBeenCalled();
});
});

View file

@ -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 = <State>(): Epic<
Action,
Action,
State,
TimelineEpicDependencies<State>
> => (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()
);
};

View file

@ -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 = <T>(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;

View file

@ -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<string> {
noteId: string;
};
}
export interface TimelineEpicDependencies<State> {
timelineByIdSelector: (state: State) => TimelineById;
timelineTimeRangeSelector: (state: State) => inputsModel.TimeRange;
selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery;
selectNotesByIdSelector: (state: State) => NotesById;
apolloClient$: Observable<AppApolloClient>;
storage: Storage;
}

View file

@ -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