Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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,13 +62,11 @@ export const LinkToPage = React.memo<LinkToPageProps>(({ match }) => (
component={RedirectToDetectionEnginePage}
exact
path={`${match.url}/:pageName(${SiemPageName.detections})`}
strict
/>
<Route
component={RedirectToDetectionEnginePage}
exact
path={`${match.url}/:pageName(${SiemPageName.detections})/:tabName(${DetectionEngineTab.alerts}|${DetectionEngineTab.signals})`}
strict
/>
<Route
component={RedirectToRulesPage}
Expand Down
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
137 changes: 121 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 => {
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,99 @@ export const makeMapStateToProps = () => {

return mapStateToProps;
};

export const updateTimerangeUrl = (
timeRange: UrlInputsModel,
isInitializing: boolean
): 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' && isInitializing) {
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 = ({
isInitializing,
history,
newUrlStateString,
pathName,
search,
updateTimerange,
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 && updateTimerange) {
const queryState = decodeRisonUrlState<UrlInputsModel>(newUrlStateString);
if (queryState != null && queryState.global != null) {
return replaceStateInLocation({
history,
pathName,
search,
urlStateToReplace: updateTimerangeUrl(queryState, isInitializing),
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 @@ -36,6 +36,12 @@ jest.mock('../../utils/route/use_route_spy', () => ({
useRouteSpy: () => [mockRouteSpy],
}));

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

jest.mock('../../lib/kibana', () => ({
useKibana: () => ({
services: {
Expand Down Expand Up @@ -69,19 +75,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 @@ -161,4 +167,57 @@ describe('UrlStateContainer', () => {
});
});
});

describe('After Initialization, keep Relative Date up to date for global only on detections page', () => {
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: 'hosts',
pageName: SiemPageName.hosts,
detailName: undefined,
}).relativeTimeSearch.undefinedQuery,
});
wrapper.update();
await wait();

if (CONSTANTS.detectionsPage === page) {
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: 1558732849370,
fromStr: 'now-15m',
kind: 'relative',
to: 1558733749370,
toStr: 'now',
id: 'timeline',
});
} else {
// There is no change in url state, so that's expected we only have two actions
expect(mockSetRelativeRangeDatePicker.mock.calls.length).toEqual(2);
}
}
);
});
});
Loading