Skip to content

Commit f3599fe

Browse files
[SECURITY SOLUTIONS] Keep context of timeline when switching tabs in security solutions (#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 <contact@patrykkopycinski.com>
1 parent 8cdf566 commit f3599fe

File tree

23 files changed

+748
-311
lines changed

23 files changed

+748
-311
lines changed

x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ export const QueryBar = memo<QueryBarComponentProps>(
6262
const [draftQuery, setDraftQuery] = useState(filterQuery);
6363

6464
useEffect(() => {
65-
// Reset draftQuery when `Create new timeline` is clicked
65+
setDraftQuery(filterQuery);
66+
}, [filterQuery]);
67+
68+
useEffect(() => {
6669
if (filterQueryDraft == null) {
6770
setDraftQuery(filterQuery);
6871
}

x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export const SearchBarComponent = memo<SiemSearchBarProps & PropsFromRedux>(
132132

133133
if (!isStateUpdated) {
134134
// That mean we are doing a refresh!
135-
if (isQuickSelection) {
135+
if (isQuickSelection && payload.dateRange.to !== payload.dateRange.from) {
136136
updateSearchBar.updateTime = true;
137137
updateSearchBar.end = payload.dateRange.to;
138138
updateSearchBar.start = payload.dateRange.from;
@@ -313,7 +313,7 @@ const makeMapStateToProps = () => {
313313
fromStr: getFromStrSelector(inputsRange),
314314
filterQuery: getFilterQuerySelector(inputsRange),
315315
isLoading: getIsLoadingSelector(inputsRange),
316-
queries: getQueriesSelector(inputsRange),
316+
queries: getQueriesSelector(state, id),
317317
savedQuery: getSavedQuerySelector(inputsRange),
318318
start: getStartSelector(inputsRange),
319319
toStr: getToStrSelector(inputsRange),
@@ -351,15 +351,27 @@ export const dispatchUpdateSearch = (dispatch: Dispatch) => ({
351351
const fromDate = formatDate(start);
352352
let toDate = formatDate(end, { roundUp: true });
353353
if (isQuickSelection) {
354-
dispatch(
355-
inputsActions.setRelativeRangeDatePicker({
356-
id,
357-
fromStr: start,
358-
toStr: end,
359-
from: fromDate,
360-
to: toDate,
361-
})
362-
);
354+
if (end === start) {
355+
dispatch(
356+
inputsActions.setAbsoluteRangeDatePicker({
357+
id,
358+
fromStr: start,
359+
toStr: end,
360+
from: fromDate,
361+
to: toDate,
362+
})
363+
);
364+
} else {
365+
dispatch(
366+
inputsActions.setRelativeRangeDatePicker({
367+
id,
368+
fromStr: start,
369+
toStr: end,
370+
from: fromDate,
371+
to: toDate,
372+
})
373+
);
374+
}
363375
} else {
364376
toDate = formatDate(end);
365377
dispatch(

x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ describe('SIEM Super Date Picker', () => {
139139
expect(store.getState().inputs.global.timerange.toStr).toBe('now');
140140
});
141141

142-
test('Make Sure it is Today date', () => {
142+
test('Make Sure it is Today date is an absolute date', () => {
143143
wrapper
144144
.find('[data-test-subj="superDatePickerToggleQuickMenuButton"]')
145145
.first()
@@ -151,8 +151,22 @@ describe('SIEM Super Date Picker', () => {
151151
.first()
152152
.simulate('click');
153153
wrapper.update();
154-
expect(store.getState().inputs.global.timerange.fromStr).toBe('now/d');
155-
expect(store.getState().inputs.global.timerange.toStr).toBe('now/d');
154+
expect(store.getState().inputs.global.timerange.kind).toBe('absolute');
155+
});
156+
157+
test('Make Sure it is This Week date is an absolute date', () => {
158+
wrapper
159+
.find('[data-test-subj="superDatePickerToggleQuickMenuButton"]')
160+
.first()
161+
.simulate('click');
162+
wrapper.update();
163+
164+
wrapper
165+
.find('[data-test-subj="superDatePickerCommonlyUsed_This_week"]')
166+
.first()
167+
.simulate('click');
168+
wrapper.update();
169+
expect(store.getState().inputs.global.timerange.kind).toBe('absolute');
156170
});
157171

158172
test('Make Sure to (end date) is superior than from (start date)', () => {

x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,12 @@ export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>(
9191
toStr,
9292
updateReduxTime,
9393
}) => {
94-
const [isQuickSelection, setIsQuickSelection] = useState(true);
9594
const [recentlyUsedRanges, setRecentlyUsedRanges] = useState<EuiSuperDatePickerRecentRange[]>(
9695
[]
9796
);
9897
const onRefresh = useCallback(
9998
({ start: newStart, end: newEnd }: OnRefreshProps): void => {
99+
const isQuickSelection = newStart.includes('now') || newEnd.includes('now');
100100
const { kqlHaveBeenUpdated } = updateReduxTime({
101101
end: newEnd,
102102
id,
@@ -117,12 +117,13 @@ export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>(
117117
refetchQuery(queries);
118118
}
119119
},
120-
// eslint-disable-next-line react-hooks/exhaustive-deps
121-
[end, id, isQuickSelection, kqlQuery, start, timelineId]
120+
[end, id, kqlQuery, queries, start, timelineId, updateReduxTime]
122121
);
123122

124123
const onRefreshChange = useCallback(
125124
({ isPaused, refreshInterval }: OnRefreshChangeProps): void => {
125+
const isQuickSelection =
126+
(fromStr != null && fromStr.includes('now')) || (toStr != null && toStr.includes('now'));
126127
if (duration !== refreshInterval) {
127128
setDuration({ id, duration: refreshInterval });
128129
}
@@ -137,27 +138,22 @@ export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>(
137138
refetchQuery(queries);
138139
}
139140
},
140-
// eslint-disable-next-line react-hooks/exhaustive-deps
141-
[id, isQuickSelection, duration, policy, toStr]
141+
[fromStr, toStr, duration, policy, setDuration, id, stopAutoReload, startAutoReload, queries]
142142
);
143143

144-
const refetchQuery = (newQueries: inputsModel.GlobalGraphqlQuery[]) => {
144+
const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => {
145145
newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)());
146146
};
147147

148148
const onTimeChange = useCallback(
149-
({
150-
start: newStart,
151-
end: newEnd,
152-
isQuickSelection: newIsQuickSelection,
153-
isInvalid,
154-
}: OnTimeChangeProps) => {
149+
({ start: newStart, end: newEnd, isInvalid }: OnTimeChangeProps) => {
150+
const isQuickSelection = newStart.includes('now') || newEnd.includes('now');
155151
if (!isInvalid) {
156152
updateReduxTime({
157153
end: newEnd,
158154
id,
159155
isInvalid,
160-
isQuickSelection: newIsQuickSelection,
156+
isQuickSelection,
161157
kql: kqlQuery,
162158
start: newStart,
163159
timelineId,
@@ -174,15 +170,13 @@ export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>(
174170
];
175171

176172
setRecentlyUsedRanges(newRecentlyUsedRanges);
177-
setIsQuickSelection(newIsQuickSelection);
178173
}
179174
},
180-
// eslint-disable-next-line react-hooks/exhaustive-deps
181-
[recentlyUsedRanges, kqlQuery]
175+
[updateReduxTime, id, kqlQuery, timelineId, recentlyUsedRanges]
182176
);
183177

184-
const endDate = kind === 'relative' ? toStr : new Date(end).toISOString();
185-
const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString();
178+
const endDate = toStr != null ? toStr : new Date(end).toISOString();
179+
const startDate = fromStr != null ? fromStr : new Date(start).toISOString();
186180

187181
const [quickRanges] = useUiSetting$<Range[]>(DEFAULT_TIMEPICKER_QUICK_RANGES);
188182
const commonlyUsedRanges = isEmpty(quickRanges)
@@ -232,15 +226,27 @@ export const dispatchUpdateReduxTime = (dispatch: Dispatch) => ({
232226
const fromDate = formatDate(start);
233227
let toDate = formatDate(end, { roundUp: true });
234228
if (isQuickSelection) {
235-
dispatch(
236-
inputsActions.setRelativeRangeDatePicker({
237-
id,
238-
fromStr: start,
239-
toStr: end,
240-
from: fromDate,
241-
to: toDate,
242-
})
243-
);
229+
if (end === start) {
230+
dispatch(
231+
inputsActions.setAbsoluteRangeDatePicker({
232+
id,
233+
fromStr: start,
234+
toStr: end,
235+
from: fromDate,
236+
to: toDate,
237+
})
238+
);
239+
} else {
240+
dispatch(
241+
inputsActions.setRelativeRangeDatePicker({
242+
id,
243+
fromStr: start,
244+
toStr: end,
245+
from: fromDate,
246+
to: toDate,
247+
})
248+
);
249+
}
244250
} else {
245251
toDate = formatDate(end);
246252
dispatch(
@@ -284,6 +290,7 @@ export const makeMapStateToProps = () => {
284290
const getToStrSelector = toStrSelector();
285291
return (state: State, { id }: OwnProps) => {
286292
const inputsRange: InputsRange = getOr({}, `inputs.${id}`, state);
293+
287294
return {
288295
duration: getDurationSelector(inputsRange),
289296
end: getEndSelector(inputsRange),
@@ -292,7 +299,7 @@ export const makeMapStateToProps = () => {
292299
kind: getKindSelector(inputsRange),
293300
kqlQuery: getKqlQuerySelector(inputsRange) as inputsModel.GlobalKqlQuery,
294301
policy: getPolicySelector(inputsRange),
295-
queries: getQueriesSelector(inputsRange) as inputsModel.GlobalGraphqlQuery[],
302+
queries: getQueriesSelector(state, id),
296303
start: getStartSelector(inputsRange),
297304
toStr: getToStrSelector(inputsRange),
298305
};

x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.test.ts

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
} from './selectors';
1818
import { InputsRange, AbsoluteTimeRange, RelativeTimeRange } from '../../store/inputs/model';
1919
import { cloneDeep } from 'lodash/fp';
20+
import { mockGlobalState } from '../../mock';
21+
import { State } from '../../store';
2022

2123
describe('selectors', () => {
2224
let absoluteTime: AbsoluteTimeRange = {
@@ -42,6 +44,8 @@ describe('selectors', () => {
4244
filters: [],
4345
};
4446

47+
let mockState: State = mockGlobalState;
48+
4549
const getPolicySelector = policySelector();
4650
const getDurationSelector = durationSelector();
4751
const getKindSelector = kindSelector();
@@ -75,6 +79,8 @@ describe('selectors', () => {
7579
},
7680
filters: [],
7781
};
82+
83+
mockState = mockGlobalState;
7884
});
7985

8086
describe('#policySelector', () => {
@@ -375,34 +381,61 @@ describe('selectors', () => {
375381

376382
describe('#queriesSelector', () => {
377383
test('returns the same reference given the same identical input twice', () => {
378-
const result1 = getQueriesSelector(inputState);
379-
const result2 = getQueriesSelector(inputState);
384+
const myMock = {
385+
...mockState,
386+
inputs: {
387+
...mockState.inputs,
388+
global: inputState,
389+
},
390+
};
391+
const result1 = getQueriesSelector(myMock, 'global');
392+
const result2 = getQueriesSelector(myMock, 'global');
380393
expect(result1).toBe(result2);
381394
});
382395

383396
test('DOES NOT return the same reference given different input twice but with different deep copies since the query is not a primitive', () => {
384-
const clone = cloneDeep(inputState);
385-
const result1 = getQueriesSelector(inputState);
386-
const result2 = getQueriesSelector(clone);
397+
const myMock = {
398+
...mockState,
399+
inputs: {
400+
...mockState.inputs,
401+
global: inputState,
402+
},
403+
};
404+
const clone = cloneDeep(myMock);
405+
const result1 = getQueriesSelector(myMock, 'global');
406+
const result2 = getQueriesSelector(clone, 'global');
387407
expect(result1).not.toBe(result2);
388408
});
389409

390410
test('returns a different reference even if the contents are the same since query is an array and not a primitive', () => {
391-
const result1 = getQueriesSelector(inputState);
392-
const change: InputsRange = {
393-
...inputState,
394-
queries: [
395-
{
396-
loading: false,
397-
id: '1',
398-
inspect: { dsl: [], response: [] },
399-
isInspected: false,
400-
refetch: null,
401-
selectedInspectIndex: 0,
411+
const myMock = {
412+
...mockState,
413+
inputs: {
414+
...mockState.inputs,
415+
global: inputState,
416+
},
417+
};
418+
const result1 = getQueriesSelector(myMock, 'global');
419+
const myMockChange: State = {
420+
...myMock,
421+
inputs: {
422+
...mockState.inputs,
423+
global: {
424+
...mockState.inputs.global,
425+
queries: [
426+
{
427+
loading: false,
428+
id: '1',
429+
inspect: { dsl: [], response: [] },
430+
isInspected: false,
431+
refetch: null,
432+
selectedInspectIndex: 0,
433+
},
434+
],
402435
},
403-
],
436+
},
404437
};
405-
const result2 = getQueriesSelector(change);
438+
const result2 = getQueriesSelector(myMockChange, 'global');
406439
expect(result1).not.toBe(result2);
407440
});
408441
});

x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7+
import { isEmpty } from 'lodash';
78
import { createSelector } from 'reselect';
9+
import { State } from '../../store';
10+
import { InputsModelId } from '../../store/inputs/constants';
811
import { Policy, InputsRange, TimeRange, GlobalQuery } from '../../store/inputs/model';
912

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

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

19+
export const getGlobalQueries = (state: State, id: InputsModelId): GlobalQuery[] => {
20+
const inputsRange = state.inputs[id];
21+
return !isEmpty(inputsRange.linkTo)
22+
? inputsRange.linkTo.reduce<GlobalQuery[]>((acc, linkToId) => {
23+
const linkToIdInputsRange: InputsRange = state.inputs[linkToId];
24+
return [...acc, ...linkToIdInputsRange.queries];
25+
}, inputsRange.queries)
26+
: inputsRange.queries;
27+
};
28+
1629
export const policySelector = () => createSelector(getPolicy, (policy) => policy.kind);
1730

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

3346
export const queriesSelector = () =>
34-
createSelector(getQueries, (queries) => queries.filter((q) => q.id !== 'kql'));
47+
createSelector(getGlobalQueries, (queries) => queries.filter((q) => q.id !== 'kql'));
3548

3649
export const kqlQuerySelector = () =>
3750
createSelector(getQueries, (queries) => queries.find((q) => q.id === 'kql'));

x-pack/plugins/security_solution/public/common/store/inputs/actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export const setAbsoluteRangeDatePicker = actionCreator<{
1616
id: InputsModelId;
1717
from: string;
1818
to: string;
19+
fromStr?: string;
20+
toStr?: string;
1921
}>('SET_ABSOLUTE_RANGE_DATE_PICKER');
2022

2123
export const setTimelineRangeDatePicker = actionCreator<{

0 commit comments

Comments
 (0)