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] update url state between page if date is relative #56813

Merged
merged 10 commits into from
Feb 7, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const getSearch = (tab: SearchNavTab, urlState: TabNavigationProps): stri
replaceStateKeyInQueryString(
urlKey,
urlStateToReplace
)(getQueryStringFromLocation(myLocation))
)(getQueryStringFromLocation(myLocation.search))
);
},
{
Expand Down
132 changes: 116 additions & 16 deletions x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,32 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { decode, encode, RisonValue } from 'rison-node';
import { Location } from 'history';
import { decode, encode } from 'rison-node';
import * as H from 'history';
import { QueryString } from 'ui/utils/query_string';
import { Query, esFilters } from 'src/plugins/data/public';

import { inputsSelectors, State, timelineSelectors } from '../../store';
import { isEmpty } from 'lodash/fp';
import { SiemPageName } from '../../pages/home/types';
import { inputsSelectors, State, timelineSelectors } from '../../store';
import { UrlInputsModel } from '../../store/inputs/model';
import { formatDate } from '../super_date_picker';
import { NavTab } from '../navigation/types';
import { CONSTANTS, UrlStateType } from './constants';
import { LocationTypes, UrlStateContainerPropTypes } from './types';
import {
LocationTypes,
UrlStateContainerPropTypes,
ReplaceStateInLocation,
Timeline,
UpdateUrlStateString,
} from './types';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const decodeRisonUrlState = (value: string | undefined): RisonValue | any | undefined => {
export const decodeRisonUrlState = <T>(value: string | undefined): T | null => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice refactor!

try {
return value ? decode(value) : undefined;
return value ? ((decode(value) as unknown) as T) : null;
} catch (error) {
if (error instanceof Error && error.message.startsWith('rison decoder error')) {
return {};
return null;
}
throw error;
}
Expand All @@ -30,18 +38,16 @@ export const decodeRisonUrlState = (value: string | undefined): RisonValue | any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const encodeRisonUrlState = (state: any) => encode(state);

export const getQueryStringFromLocation = (location: Location) => location.search.substring(1);
export const getQueryStringFromLocation = (search: string) => search.substring(1);

export const getParamFromQueryString = (queryString: string, key: string): string | undefined => {
const queryParam = QueryString.decode(queryString)[key];
return Array.isArray(queryParam) ? queryParam[0] : queryParam;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const replaceStateKeyInQueryString = <UrlState extends any>(
stateKey: string,
urlState: UrlState | undefined
) => (queryString: string) => {
export const replaceStateKeyInQueryString = <T>(stateKey: string, urlState: T) => (
queryString: string
): string => {
const previousQueryValues = QueryString.decode(queryString);
if (urlState == null || (typeof urlState === 'string' && urlState === '')) {
delete previousQueryValues[stateKey];
Expand All @@ -60,8 +66,11 @@ export const replaceStateKeyInQueryString = <UrlState extends any>(
});
};

export const replaceQueryStringInLocation = (location: Location, queryString: string): Location => {
if (queryString === getQueryStringFromLocation(location)) {
export const replaceQueryStringInLocation = (
location: H.Location,
queryString: string
): H.Location => {
if (queryString === getQueryStringFromLocation(location.search)) {
return location;
} else {
return {
Expand Down Expand Up @@ -173,3 +182,94 @@ export const makeMapStateToProps = () => {

return mapStateToProps;
};

export const updateTimerangeUrl = (timeRange: UrlInputsModel): UrlInputsModel => {
if (timeRange.global.timerange.kind === 'relative') {
timeRange.global.timerange.from = formatDate(timeRange.global.timerange.fromStr);
timeRange.global.timerange.to = formatDate(timeRange.global.timerange.toStr, { roundUp: true });
}
if (timeRange.timeline.timerange.kind === 'relative') {
timeRange.timeline.timerange.from = formatDate(timeRange.timeline.timerange.fromStr);
timeRange.timeline.timerange.to = formatDate(timeRange.timeline.timerange.toStr, {
roundUp: true,
});
}
return timeRange;
};

export const updateUrlStateString = ({
history,
newUrlStateString,
pathName,
search,
urlKey,
}: UpdateUrlStateString): string => {
if (urlKey === CONSTANTS.appQuery) {
const queryState = decodeRisonUrlState<Query>(newUrlStateString);
if (queryState != null && queryState.query === '') {
return replaceStateInLocation({
history,
pathName,
search,
urlStateToReplace: '',
urlStateKey: urlKey,
});
}
} else if (urlKey === CONSTANTS.timerange) {
const queryState = decodeRisonUrlState<UrlInputsModel>(newUrlStateString);
if (queryState != null && queryState.global != null) {
return replaceStateInLocation({
history,
pathName,
search,
urlStateToReplace: updateTimerangeUrl(queryState),
urlStateKey: urlKey,
});
}
} else if (urlKey === CONSTANTS.filters) {
const queryState = decodeRisonUrlState<esFilters.Filter[]>(newUrlStateString);
if (isEmpty(queryState)) {
return replaceStateInLocation({
history,
pathName,
search,
urlStateToReplace: '',
urlStateKey: urlKey,
});
}
} else if (urlKey === CONSTANTS.timeline) {
const queryState = decodeRisonUrlState<Timeline>(newUrlStateString);
if (queryState != null && queryState.id === '') {
return replaceStateInLocation({
history,
pathName,
search,
urlStateToReplace: '',
urlStateKey: urlKey,
});
}
}
return search;
};

export const replaceStateInLocation = <T>({
history,
urlStateToReplace,
urlStateKey,
pathName,
search,
}: ReplaceStateInLocation<T>) => {
const newLocation = replaceQueryStringInLocation(
{
hash: '',
pathname: pathName,
search,
state: '',
},
replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)(getQueryStringFromLocation(search))
);
if (history) {
history.replace(newLocation);
}
return newLocation.search;
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import React from 'react';
import { HookWrapper } from '../../mock';
import { SiemPageName } from '../../pages/home/types';
import { RouteSpyState } from '../../utils/route/types';

import { CONSTANTS } from './constants';
import {
getMockPropsObj,
Expand All @@ -22,6 +21,7 @@ import {
} from './test_dependencies';
import { UrlStateContainerPropTypes } from './types';
import { useUrlStateHooks } from './use_url_state';
import { wait } from '../../lib/helpers';

let mockProps: UrlStateContainerPropTypes;

Expand All @@ -37,6 +37,12 @@ jest.mock('../../utils/route/use_route_spy', () => ({
useRouteSpy: () => [mockRouteSpy],
}));

jest.mock('../super_date_picker', () => ({
formatDate: (date: string) => {
return 11223344556677;
},
}));

jest.mock('../search_bar', () => ({
siemFilterManager: {
setFilters: jest.fn(),
Expand All @@ -63,19 +69,19 @@ describe('UrlStateContainer', () => {
mount(<HookWrapper hookProps={mockProps} hook={args => useUrlStateHooks(args)} />);

expect(mockSetRelativeRangeDatePicker.mock.calls[1][0]).toEqual({
from: 1558591200000,
from: 11223344556677,
fromStr: 'now-1d/d',
kind: 'relative',
to: 1558677599999,
to: 11223344556677,
toStr: 'now-1d/d',
id: 'global',
});

expect(mockSetRelativeRangeDatePicker.mock.calls[0][0]).toEqual({
from: 1558732849370,
from: 11223344556677,
fromStr: 'now-15m',
kind: 'relative',
to: 1558733749370,
to: 11223344556677,
toStr: 'now',
id: 'timeline',
});
Expand Down Expand Up @@ -155,4 +161,56 @@ describe('UrlStateContainer', () => {
});
});
});

describe('After Initialization, keep Relative Date up to date', () => {
test.each(testCases)(
'%o',
async (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => {
mockProps = getMockPropsObj({
page,
examplePath,
namespaceLower,
pageName,
detailName,
}).relativeTimeSearch.undefinedQuery;
const wrapper = mount(
<HookWrapper hookProps={mockProps} hook={args => useUrlStateHooks(args)} />
);

wrapper.setProps({
hookProps: getMockPropsObj({
page: CONSTANTS.hostsPage,
examplePath: '/hosts',
namespaceLower,
pageName,
detailName,
}).relativeTimeSearch.undefinedQuery,
});
wrapper.update();
await wait();
if (CONSTANTS.hostsPage === page) {
// There is no change in url state, so that's expected we only have two actions
expect(mockSetRelativeRangeDatePicker.mock.calls.length).toEqual(2);
} else {
expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({
from: 11223344556677,
fromStr: 'now-1d/d',
kind: 'relative',
to: 11223344556677,
toStr: 'now-1d/d',
id: 'global',
});

expect(mockSetRelativeRangeDatePicker.mock.calls[2][0]).toEqual({
from: 11223344556677,
fromStr: 'now-15m',
kind: 'relative',
to: 11223344556677,
toStr: 'now',
id: 'timeline',
});
}
}
);
});
});
Loading