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

[SECURITY SOLUTIONS] Keep context of timeline when switching tabs in security solutions #82237

Merged
merged 24 commits into from
Nov 6, 2020
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
036d4fe
try to keep timeline context when switching tabs
XavierM Oct 30, 2020
6865090
Merge branch 'master' of github.com:elastic/kibana into keep_state_of…
XavierM Oct 30, 2020
04adc6d
fix popover
patrykkopycinski Oct 30, 2020
f0ee419
Merge branch 'master' of github.com:elastic/kibana into keep_state_of…
XavierM Oct 30, 2020
93e68f6
simpler solution to keep timelien context between tabs
XavierM Oct 30, 2020
edf6790
fix timeline context with relative date
XavierM Oct 31, 2020
228e8bc
allow update on the kql bar when opening new timeline
XavierM Oct 31, 2020
ec6e42c
keep detail view in context when savedObjectId of the timeline does n…
XavierM Nov 1, 2020
b31b6ea
remove redux solution and just KISS it
XavierM Nov 2, 2020
d6d7254
Merge branch 'master' of github.com:elastic/kibana into keep_state_of…
XavierM Nov 2, 2020
f26dcac
add unit test for the popover
XavierM Nov 2, 2020
006d97d
add test on timeline context cache
XavierM Nov 2, 2020
15eb5e7
Merge branch 'master' of github.com:elastic/kibana into keep_state_of…
XavierM Nov 2, 2020
91ef942
final commit -> to fix context of timeline between tabs
XavierM Nov 2, 2020
1373287
keep timerange kind to absolute when refreshing
XavierM Nov 3, 2020
991cd76
fix bug today/thiw week to be absolute and not relative
XavierM Nov 3, 2020
734d4ea
add unit test for absolute date for today and this week
XavierM Nov 3, 2020
24a86d4
Merge branch 'master' of github.com:elastic/kibana into keep_state_of…
XavierM Nov 3, 2020
56662b7
fix absolute today/this week on timeline
XavierM Nov 3, 2020
d9a063f
fix refresh between page and timeline when link
XavierM Nov 5, 2020
8896fb3
Merge branch 'master' of github.com:elastic/kibana into keep_state_of…
XavierM Nov 5, 2020
466505a
clean up
XavierM Nov 5, 2020
4184c95
Merge branch 'master' of github.com:elastic/kibana into keep_state_of…
XavierM Nov 5, 2020
a5d8325
remove nit
XavierM Nov 5, 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 @@ -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,
XavierM marked this conversation as resolved.
Show resolved Hide resolved
})
);
} 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,13 @@ export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>(
toStr,
updateReduxTime,
}) => {
const [isQuickSelection, setIsQuickSelection] = useState(true);
// const [isQuickSelection, setIsQuickSelection] = useState(true);
XavierM marked this conversation as resolved.
Show resolved Hide resolved
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 +118,12 @@ 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?.includes('now') || toStr?.includes('now');
if (duration !== refreshInterval) {
setDuration({ id, duration: refreshInterval });
}
Expand All @@ -138,26 +139,22 @@ export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>(
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[id, isQuickSelection, duration, policy, toStr]
[id, fromStr, duration, policy, toStr]
);

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 +171,14 @@ export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>(
];

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

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 +228,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 +292,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 +301,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
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { Query, Filter, SavedQuery } from '../../../../../../../src/plugins/data

export interface AbsoluteTimeRange {
kind: 'absolute';
fromStr: undefined;
toStr: undefined;
fromStr?: string;
toStr?: string;
from: string;
to: string;
}
Expand Down
Loading