Skip to content

Commit

Permalink
[SECURITY SOLUTIONS] Keep context of timeline when switching tabs in …
Browse files Browse the repository at this point in the history
…security solutions (elastic#82237)

* try to keep timeline context when switching tabs

* fix popover

* simpler solution to keep timelien context between tabs

* fix timeline context with relative date

* allow update on the kql bar when opening new timeline

* keep detail view in context when savedObjectId of the timeline does not chnage

* remove redux solution and just KISS it

* add unit test for the popover

* add test on timeline context cache

* final commit -> to fix context of timeline between tabs

* keep timerange kind to absolute when refreshing

* fix bug today/thiw week to be absolute and not relative

* add unit test for absolute date for today and this week

* fix absolute today/this week on timeline

* fix refresh between page and timeline when link

* clean up

* remove nit

Co-authored-by: Patryk Kopycinski <[email protected]>
  • Loading branch information
XavierM and patrykkopycinski committed Nov 6, 2020
1 parent a7832ff commit 99d210b
Show file tree
Hide file tree
Showing 23 changed files with 748 additions and 311 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ export const QueryBar = memo<QueryBarComponentProps>(
const [draftQuery, setDraftQuery] = useState(filterQuery);

useEffect(() => {
// Reset draftQuery when `Create new timeline` is clicked
setDraftQuery(filterQuery);
}, [filterQuery]);

useEffect(() => {
if (filterQueryDraft == null) {
setDraftQuery(filterQuery);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export const SearchBarComponent = memo<SiemSearchBarProps & PropsFromRedux>(

if (!isStateUpdated) {
// That mean we are doing a refresh!
if (isQuickSelection) {
if (isQuickSelection && payload.dateRange.to !== payload.dateRange.from) {
updateSearchBar.updateTime = true;
updateSearchBar.end = payload.dateRange.to;
updateSearchBar.start = payload.dateRange.from;
Expand Down Expand Up @@ -313,7 +313,7 @@ const makeMapStateToProps = () => {
fromStr: getFromStrSelector(inputsRange),
filterQuery: getFilterQuerySelector(inputsRange),
isLoading: getIsLoadingSelector(inputsRange),
queries: getQueriesSelector(inputsRange),
queries: getQueriesSelector(state, id),
savedQuery: getSavedQuerySelector(inputsRange),
start: getStartSelector(inputsRange),
toStr: getToStrSelector(inputsRange),
Expand Down Expand Up @@ -351,15 +351,27 @@ export const dispatchUpdateSearch = (dispatch: Dispatch) => ({
const fromDate = formatDate(start);
let toDate = formatDate(end, { roundUp: true });
if (isQuickSelection) {
dispatch(
inputsActions.setRelativeRangeDatePicker({
id,
fromStr: start,
toStr: end,
from: fromDate,
to: toDate,
})
);
if (end === start) {
dispatch(
inputsActions.setAbsoluteRangeDatePicker({
id,
fromStr: start,
toStr: end,
from: fromDate,
to: toDate,
})
);
} else {
dispatch(
inputsActions.setRelativeRangeDatePicker({
id,
fromStr: start,
toStr: end,
from: fromDate,
to: toDate,
})
);
}
} else {
toDate = formatDate(end);
dispatch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ describe('SIEM Super Date Picker', () => {
expect(store.getState().inputs.global.timerange.toStr).toBe('now');
});

test('Make Sure it is Today date', () => {
test('Make Sure it is Today date is an absolute date', () => {
wrapper
.find('[data-test-subj="superDatePickerToggleQuickMenuButton"]')
.first()
Expand All @@ -151,8 +151,22 @@ describe('SIEM Super Date Picker', () => {
.first()
.simulate('click');
wrapper.update();
expect(store.getState().inputs.global.timerange.fromStr).toBe('now/d');
expect(store.getState().inputs.global.timerange.toStr).toBe('now/d');
expect(store.getState().inputs.global.timerange.kind).toBe('absolute');
});

test('Make Sure it is This Week date is an absolute date', () => {
wrapper
.find('[data-test-subj="superDatePickerToggleQuickMenuButton"]')
.first()
.simulate('click');
wrapper.update();

wrapper
.find('[data-test-subj="superDatePickerCommonlyUsed_This_week"]')
.first()
.simulate('click');
wrapper.update();
expect(store.getState().inputs.global.timerange.kind).toBe('absolute');
});

test('Make Sure to (end date) is superior than from (start date)', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,12 @@ export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>(
toStr,
updateReduxTime,
}) => {
const [isQuickSelection, setIsQuickSelection] = useState(true);
const [recentlyUsedRanges, setRecentlyUsedRanges] = useState<EuiSuperDatePickerRecentRange[]>(
[]
);
const onRefresh = useCallback(
({ start: newStart, end: newEnd }: OnRefreshProps): void => {
const isQuickSelection = newStart.includes('now') || newEnd.includes('now');
const { kqlHaveBeenUpdated } = updateReduxTime({
end: newEnd,
id,
Expand All @@ -117,12 +117,13 @@ export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>(
refetchQuery(queries);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[end, id, isQuickSelection, kqlQuery, start, timelineId]
[end, id, kqlQuery, queries, start, timelineId, updateReduxTime]
);

const onRefreshChange = useCallback(
({ isPaused, refreshInterval }: OnRefreshChangeProps): void => {
const isQuickSelection =
(fromStr != null && fromStr.includes('now')) || (toStr != null && toStr.includes('now'));
if (duration !== refreshInterval) {
setDuration({ id, duration: refreshInterval });
}
Expand All @@ -137,27 +138,22 @@ export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>(
refetchQuery(queries);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[id, isQuickSelection, duration, policy, toStr]
[fromStr, toStr, duration, policy, setDuration, id, stopAutoReload, startAutoReload, queries]
);

const refetchQuery = (newQueries: inputsModel.GlobalGraphqlQuery[]) => {
const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => {
newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)());
};

const onTimeChange = useCallback(
({
start: newStart,
end: newEnd,
isQuickSelection: newIsQuickSelection,
isInvalid,
}: OnTimeChangeProps) => {
({ start: newStart, end: newEnd, isInvalid }: OnTimeChangeProps) => {
const isQuickSelection = newStart.includes('now') || newEnd.includes('now');
if (!isInvalid) {
updateReduxTime({
end: newEnd,
id,
isInvalid,
isQuickSelection: newIsQuickSelection,
isQuickSelection,
kql: kqlQuery,
start: newStart,
timelineId,
Expand All @@ -174,15 +170,13 @@ export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>(
];

setRecentlyUsedRanges(newRecentlyUsedRanges);
setIsQuickSelection(newIsQuickSelection);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[recentlyUsedRanges, kqlQuery]
[updateReduxTime, id, kqlQuery, timelineId, recentlyUsedRanges]
);

const endDate = kind === 'relative' ? toStr : new Date(end).toISOString();
const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString();
const endDate = toStr != null ? toStr : new Date(end).toISOString();
const startDate = fromStr != null ? fromStr : new Date(start).toISOString();

const [quickRanges] = useUiSetting$<Range[]>(DEFAULT_TIMEPICKER_QUICK_RANGES);
const commonlyUsedRanges = isEmpty(quickRanges)
Expand Down Expand Up @@ -232,15 +226,27 @@ export const dispatchUpdateReduxTime = (dispatch: Dispatch) => ({
const fromDate = formatDate(start);
let toDate = formatDate(end, { roundUp: true });
if (isQuickSelection) {
dispatch(
inputsActions.setRelativeRangeDatePicker({
id,
fromStr: start,
toStr: end,
from: fromDate,
to: toDate,
})
);
if (end === start) {
dispatch(
inputsActions.setAbsoluteRangeDatePicker({
id,
fromStr: start,
toStr: end,
from: fromDate,
to: toDate,
})
);
} else {
dispatch(
inputsActions.setRelativeRangeDatePicker({
id,
fromStr: start,
toStr: end,
from: fromDate,
to: toDate,
})
);
}
} else {
toDate = formatDate(end);
dispatch(
Expand Down Expand Up @@ -284,6 +290,7 @@ export const makeMapStateToProps = () => {
const getToStrSelector = toStrSelector();
return (state: State, { id }: OwnProps) => {
const inputsRange: InputsRange = getOr({}, `inputs.${id}`, state);

return {
duration: getDurationSelector(inputsRange),
end: getEndSelector(inputsRange),
Expand All @@ -292,7 +299,7 @@ export const makeMapStateToProps = () => {
kind: getKindSelector(inputsRange),
kqlQuery: getKqlQuerySelector(inputsRange) as inputsModel.GlobalKqlQuery,
policy: getPolicySelector(inputsRange),
queries: getQueriesSelector(inputsRange) as inputsModel.GlobalGraphqlQuery[],
queries: getQueriesSelector(state, id),
start: getStartSelector(inputsRange),
toStr: getToStrSelector(inputsRange),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
} from './selectors';
import { InputsRange, AbsoluteTimeRange, RelativeTimeRange } from '../../store/inputs/model';
import { cloneDeep } from 'lodash/fp';
import { mockGlobalState } from '../../mock';
import { State } from '../../store';

describe('selectors', () => {
let absoluteTime: AbsoluteTimeRange = {
Expand All @@ -42,6 +44,8 @@ describe('selectors', () => {
filters: [],
};

let mockState: State = mockGlobalState;

const getPolicySelector = policySelector();
const getDurationSelector = durationSelector();
const getKindSelector = kindSelector();
Expand Down Expand Up @@ -75,6 +79,8 @@ describe('selectors', () => {
},
filters: [],
};

mockState = mockGlobalState;
});

describe('#policySelector', () => {
Expand Down Expand Up @@ -375,34 +381,61 @@ describe('selectors', () => {

describe('#queriesSelector', () => {
test('returns the same reference given the same identical input twice', () => {
const result1 = getQueriesSelector(inputState);
const result2 = getQueriesSelector(inputState);
const myMock = {
...mockState,
inputs: {
...mockState.inputs,
global: inputState,
},
};
const result1 = getQueriesSelector(myMock, 'global');
const result2 = getQueriesSelector(myMock, 'global');
expect(result1).toBe(result2);
});

test('DOES NOT return the same reference given different input twice but with different deep copies since the query is not a primitive', () => {
const clone = cloneDeep(inputState);
const result1 = getQueriesSelector(inputState);
const result2 = getQueriesSelector(clone);
const myMock = {
...mockState,
inputs: {
...mockState.inputs,
global: inputState,
},
};
const clone = cloneDeep(myMock);
const result1 = getQueriesSelector(myMock, 'global');
const result2 = getQueriesSelector(clone, 'global');
expect(result1).not.toBe(result2);
});

test('returns a different reference even if the contents are the same since query is an array and not a primitive', () => {
const result1 = getQueriesSelector(inputState);
const change: InputsRange = {
...inputState,
queries: [
{
loading: false,
id: '1',
inspect: { dsl: [], response: [] },
isInspected: false,
refetch: null,
selectedInspectIndex: 0,
const myMock = {
...mockState,
inputs: {
...mockState.inputs,
global: inputState,
},
};
const result1 = getQueriesSelector(myMock, 'global');
const myMockChange: State = {
...myMock,
inputs: {
...mockState.inputs,
global: {
...mockState.inputs.global,
queries: [
{
loading: false,
id: '1',
inspect: { dsl: [], response: [] },
isInspected: false,
refetch: null,
selectedInspectIndex: 0,
},
],
},
],
},
};
const result2 = getQueriesSelector(change);
const result2 = getQueriesSelector(myMockChange, 'global');
expect(result1).not.toBe(result2);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { isEmpty } from 'lodash';
import { createSelector } from 'reselect';
import { State } from '../../store';
import { InputsModelId } from '../../store/inputs/constants';
import { Policy, InputsRange, TimeRange, GlobalQuery } from '../../store/inputs/model';

export const getPolicy = (inputState: InputsRange): Policy => inputState.policy;
Expand All @@ -13,6 +16,16 @@ export const getTimerange = (inputState: InputsRange): TimeRange => inputState.t

export const getQueries = (inputState: InputsRange): GlobalQuery[] => inputState.queries;

export const getGlobalQueries = (state: State, id: InputsModelId): GlobalQuery[] => {
const inputsRange = state.inputs[id];
return !isEmpty(inputsRange.linkTo)
? inputsRange.linkTo.reduce<GlobalQuery[]>((acc, linkToId) => {
const linkToIdInputsRange: InputsRange = state.inputs[linkToId];
return [...acc, ...linkToIdInputsRange.queries];
}, inputsRange.queries)
: inputsRange.queries;
};

export const policySelector = () => createSelector(getPolicy, (policy) => policy.kind);

export const durationSelector = () => createSelector(getPolicy, (policy) => policy.duration);
Expand All @@ -31,7 +44,7 @@ export const isLoadingSelector = () =>
createSelector(getQueries, (queries) => queries.some((i) => i.loading === true));

export const queriesSelector = () =>
createSelector(getQueries, (queries) => queries.filter((q) => q.id !== 'kql'));
createSelector(getGlobalQueries, (queries) => queries.filter((q) => q.id !== 'kql'));

export const kqlQuerySelector = () =>
createSelector(getQueries, (queries) => queries.find((q) => q.id === 'kql'));
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const setAbsoluteRangeDatePicker = actionCreator<{
id: InputsModelId;
from: string;
to: string;
fromStr?: string;
toStr?: string;
}>('SET_ABSOLUTE_RANGE_DATE_PICKER');

export const setTimelineRangeDatePicker = actionCreator<{
Expand Down
Loading

0 comments on commit 99d210b

Please sign in to comment.