Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 49 commits into from
Jun 17, 2020
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5b02a67
Init localStorage epic
cnasikas May 21, 2020
48508fd
Persist removed columns
cnasikas May 21, 2020
3c9d544
Persist added columns
cnasikas May 21, 2020
a2735b9
Create helpers
cnasikas May 21, 2020
50223b7
Support multiple page timelines
cnasikas May 21, 2020
ce60056
Persist order of columns
cnasikas May 21, 2020
1048da7
Filter page timelines
cnasikas May 21, 2020
72f6cfb
Persist deltas
cnasikas May 24, 2020
6cec55b
Persist reset fields
cnasikas May 24, 2020
3af9d9b
Improve filtering
cnasikas May 25, 2020
b7a94a6
Persist itemsPerPage
cnasikas May 25, 2020
dccf233
Persist sort
cnasikas May 25, 2020
7956547
Use helpers
cnasikas May 25, 2020
0ee6927
Fix prettier linting warnings
cnasikas May 25, 2020
96fdc65
Refactor code
cnasikas May 26, 2020
54e0926
Improve filtering to include any timeline expect the flyout
cnasikas May 28, 2020
cec55f0
Dynamic ids to alert tables
cnasikas May 28, 2020
637189a
Move type to types
cnasikas May 28, 2020
0bbd5ab
Improve localStorage
cnasikas May 28, 2020
660344f
Improve tests
cnasikas May 28, 2020
67a8815
Add cypress test
cnasikas Jun 2, 2020
c6a9994
Test siem local storage
cnasikas Jun 3, 2020
0298a7b
Test epic
cnasikas Jun 3, 2020
c5f8248
Remove unecessary cypress tests
cnasikas Jun 3, 2020
85392d8
Fix detections page timelines
cnasikas Jun 4, 2020
d4e89d8
Fix rebase
cnasikas Jun 4, 2020
4a780b9
Rename to new naming
cnasikas Jun 4, 2020
0c16de3
Clean cypress tests
cnasikas Jun 5, 2020
c20ae9d
Proper reload
cnasikas Jun 5, 2020
93a77ef
Merge branch 'master' into persistent_timeline
elasticmachine Jun 7, 2020
0cf818e
Merge branch 'master' into persistent_timeline
cnasikas Jun 9, 2020
ec2df5b
Improve cypress tests
cnasikas Jun 9, 2020
91a892a
Minor fixes
cnasikas Jun 9, 2020
9a8c4d0
Rename ids
cnasikas Jun 9, 2020
56d2c29
Improve localStorage
cnasikas Jun 9, 2020
ae37657
Merge branch 'master' into persistent_timeline
cnasikas Jun 9, 2020
605466c
re-structure code
XavierM Jun 9, 2020
b9fd416
Draft
cnasikas Jun 9, 2020
60d6319
Fix types and tests to align to the new restructure of code
cnasikas Jun 10, 2020
40b02fb
Improve code
cnasikas Jun 10, 2020
e86ad72
Merge branch 'master' into persistent_timeline
cnasikas Jun 11, 2020
1d636e7
Better naming
cnasikas Jun 11, 2020
ba2a556
Merge branch 'master' into persistent_timeline
cnasikas Jun 12, 2020
3630cfd
Improve cypress tests
cnasikas Jun 12, 2020
f27e8e7
Fix types
cnasikas Jun 12, 2020
3a92165
Review improvements
cnasikas Jun 13, 2020
1c17b14
Merge branch 'master' into persistent_timeline
elasticmachine Jun 15, 2020
f81b1c1
Move TimelineId to common folder
cnasikas Jun 15, 2020
9c92dab
Merge branch 'master' into persistent_timeline
cnasikas Jun 16, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ import {
dragAndDropColumn,
openEventsViewerFieldsBrowser,
opensInspectQueryModal,
resetFields,
waitsForEventsToBeLoaded,
} from '../tasks/hosts/events';
import { clearSearchBar, kqlSearch } from '../tasks/siem_header';

import { HOSTS_PAGE } from '../urls/navigation';
import { resetFields } from '../tasks/timeline';

const defaultHeadersInDefaultEcsCategory = [
{ id: '@timestamp' },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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)
.eq(TABLE_COLUMN_EVENTS_MESSAGE)
.invoke('text')
.should('equal', 'message');
removeColumn(TABLE_COLUMN_EVENTS_MESSAGE);
reload(waitsForEventsToBeLoaded);
cy.get(DRAGGABLE_HEADER).each(($el) => {
expect($el.text()).not.equal('message');
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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"]';
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 5 additions & 0 deletions x-pack/plugins/security_solution/cypress/screens/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]';

Expand Down
6 changes: 6 additions & 0 deletions x-pack/plugins/security_solution/cypress/tasks/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
};
Expand Down
12 changes: 12 additions & 0 deletions x-pack/plugins/security_solution/cypress/tasks/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -101,3 +103,13 @@ 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 });
cy.wait(3000); // wait for DOM updates
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
};

export const resetFields = () => {
cy.get(RESET_FIELDS).click({ force: true });
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('AlertsTableComponent', () => {
it('renders correctly', () => {
const wrapper = shallow(
<AlertsTableComponent
timelineId={'test'}
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
canUserCRUD
hasIndexWrite
from={0}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,8 @@ import {
displayErrorToast,
} from '../../../common/components/toasters';

export const ALERTS_TABLE_TIMELINE_ID = 'alerts-table';

interface OwnProps {
timelineId: string;
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
canUserCRUD: boolean;
defaultFilters?: Filter[];
hasIndexWrite: boolean;
Expand All @@ -68,6 +67,7 @@ interface OwnProps {
type AlertsTableComponentProps = OwnProps & PropsFromRedux;

export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
timelineId,
canUserCRUD,
clearEventsDeleted,
clearEventsLoading,
Expand Down Expand Up @@ -141,16 +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 });
},
[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 });
},
[setEventsDeleted, ALERTS_TABLE_TIMELINE_ID]
[setEventsDeleted, timelineId]
);

const onAlertStatusUpdateSuccess = useCallback(
Expand Down Expand Up @@ -186,17 +186,17 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
// Callback for when open/closed filter changes
const onFilterGroupChangedCallback = useCallback(
(newFilterGroup: AlertFilterOption) => {
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]
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
);

// 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]);
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -307,7 +307,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,
Expand All @@ -317,7 +317,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
}, []);
useEffect(() => {
setTimelineRowActions({
id: ALERTS_TABLE_TIMELINE_ID,
id: timelineId,
queryFields: requiredFieldsForActions,
timelineRowActions: additionalActions,
});
Expand All @@ -343,7 +343,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
defaultModel={alertsDefaultModel}
end={to}
headerFilterGroup={headerFilterGroup}
id={ALERTS_TABLE_TIMELINE_ID}
id={timelineId}
start={from}
utilityBar={utilityBarCallback}
/>
Expand All @@ -353,9 +353,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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 SINGLE_RULE_ALERTS_TABLE_ID = 'detections-page-rule-details';
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
export const ALERTS_TABLE_ID = 'detections-page-alerts';
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { DetectionEngineNoIndex } from './detection_engine_no_signal_index';
import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page';
import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated';
import * as i18n from './translations';
import { ALERTS_TABLE_ID } from './constants';

export const DetectionEnginePageComponent: React.FC<PropsFromRedux> = ({
filters,
Expand Down Expand Up @@ -138,6 +139,7 @@ export const DetectionEnginePageComponent: React.FC<PropsFromRedux> = ({
/>
<EuiSpacer size="l" />
<AlertsTable
timelineId={ALERTS_TABLE_ID}
loading={loading}
hasIndexWrite={hasIndexWrite ?? false}
canUserCRUD={(canUserCRUD ?? false) && (hasEncryptionKey ?? false)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import { FailureHistory } from './failure_history';
import { RuleStatus } from '../../../../components/rules//rule_status';
import { useMlCapabilities } from '../../../../../common/components/ml_popover/hooks/use_ml_capabilities';
import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions';
import { SINGLE_RULE_ALERTS_TABLE_ID } from '../../constants';

enum RuleDetailTabs {
alerts = 'alerts',
Expand Down Expand Up @@ -374,6 +375,7 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
<EuiSpacer />
{ruleId != null && (
<AlertsTable
timelineId={SINGLE_RULE_ALERTS_TABLE_ID}
canUserCRUD={canUserCRUD ?? false}
defaultFilters={alertDefaultFilters}
hasIndexWrite={hasIndexWrite ?? false}
Expand Down
15 changes: 14 additions & 1 deletion x-pack/plugins/security_solution/public/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { ManageGlobalTimeline } from '../timelines/components/manage_timeline';

import { ApolloClientContext } from '../common/utils/apollo_context';
import { SecuritySubPlugins } from './types';
import { createSecuritySolutionStorage } from '../common/lib/local_storage';
import { initialTimelineState } from '../timelines/store/timeline/reducer';

interface AppPluginRootComponentProps {
apolloClient: AppApolloClient;
Expand Down Expand Up @@ -79,11 +81,22 @@ const StartAppComponent: FC<StartAppComponent> = ({ subPlugins, ...libs }) => {
const { i18n } = useKibana().services;
const history = createHashHistory();
const libs$ = new BehaviorSubject(libs);
const storage = createSecuritySolutionStorage(localStorage);

const store = createStore(
createInitialState(subPluginsStore.initialState),
createInitialState({
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
...subPluginsStore.initialState,
timeline: {
...(subPluginsStore.initialState.timeline ?? initialTimelineState),
timelineById: {
...(subPluginsStore.initialState.timeline?.timelineById ?? {}),
...(storage.getAllTimelines() ?? {}),
},
},
}),
subPluginsStore.reducer,
libs$.pipe(pluck('apolloClient')),
storage,
subPluginsStore.middlewares
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
mockGlobalState,
TestProviders,
SUB_PLUGINS_REDUCER,
createSecuritySolutionStorageMock,
} from '../../mock';
import { createStore, State } from '../../store';
import { AddFilterToGlobalSearchBar } from '.';
Expand All @@ -33,10 +34,21 @@ jest.mock('../../lib/kibana', () => ({

describe('AddFilterToGlobalSearchBar Component', () => {
const state: State = mockGlobalState;
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable);
const securitySolutionLocalStorageMock = createSecuritySolutionStorageMock();
let store = createStore(
state,
SUB_PLUGINS_REDUCER,
apolloClientObservable,
securitySolutionLocalStorageMock
);

beforeEach(() => {
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable);
store = createStore(
state,
SUB_PLUGINS_REDUCER,
apolloClientObservable,
securitySolutionLocalStorageMock
);
mockAddFilters.mockClear();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export interface OwnProps {
start: number;
}

const ALERTS_TABLE_ID = 'alerts-table';
const defaultAlertsFilters: Filter[] = [
{
meta: {
Expand Down Expand Up @@ -52,18 +51,19 @@ const defaultAlertsFilters: Filter[] = [
];

interface Props {
id: string;
endDate: number;
startDate: number;
pageFilters?: Filter[];
}

const AlertsTableComponent: React.FC<Props> = ({ endDate, startDate, pageFilters = [] }) => {
const AlertsTableComponent: React.FC<Props> = ({ id, endDate, startDate, pageFilters = [] }) => {
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]);
const { initializeTimeline } = useManageTimeline();

useEffect(() => {
initializeTimeline({
id: ALERTS_TABLE_ID,
id,
documentType: i18n.ALERTS_DOCUMENT_TYPE,
footerText: i18n.TOTAL_COUNT_OF_ALERTS,
title: i18n.ALERTS_TABLE_TITLE,
Expand All @@ -75,7 +75,7 @@ const AlertsTableComponent: React.FC<Props> = ({ endDate, startDate, pageFilters
pageFilters={alertsFilter}
defaultModel={alertsDefaultModel}
end={endDate}
id={ALERTS_TABLE_ID}
id={id}
start={startDate}
/>
);
Expand Down
Loading