diff --git a/x-pack/plugins/siem/public/components/url_state/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/url_state/__snapshots__/index.test.tsx.snap
new file mode 100644
index 0000000000000..1ec61ad98e608
--- /dev/null
+++ b/x-pack/plugins/siem/public/components/url_state/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,544 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`UrlStateComponents UrlStateContainer mounts and renders 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/siem/public/components/url_state/index.test.tsx b/x-pack/plugins/siem/public/components/url_state/index.test.tsx
new file mode 100644
index 0000000000000..5840f2551b88d
--- /dev/null
+++ b/x-pack/plugins/siem/public/components/url_state/index.test.tsx
@@ -0,0 +1,296 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { mount, shallow } from 'enzyme';
+import toJson from 'enzyme-to-json';
+import * as React from 'react';
+import { Router } from 'react-router-dom';
+import { MockedProvider } from 'react-apollo/test-utils';
+
+import {
+ isKqlForRoute,
+ UrlStateContainer,
+ UrlStateContainerLifecycle,
+ UrlStateContainerLifecycleProps,
+} from './';
+import { mockGlobalState, TestProviders } from '../../mock';
+import {
+ createStore,
+ hostsModel,
+ networkModel,
+ State,
+ KueryFilterQuery,
+ SerializedFilterQuery,
+ KueryFilterModel,
+} from '../../store';
+import { ActionCreator } from 'typescript-fsa';
+import { InputsModelId } from '../../store/inputs/model';
+import { wait } from '../../lib/helpers';
+
+type Action = 'PUSH' | 'POP' | 'REPLACE';
+const pop: Action = 'POP';
+const location = {
+ pathname: '/network',
+ search: '',
+ state: '',
+ hash: '',
+};
+const mockHistory = {
+ length: 2,
+ location,
+ action: pop,
+ push: jest.fn(),
+ replace: jest.fn(),
+ go: jest.fn(),
+ goBack: jest.fn(),
+ goForward: jest.fn(),
+ block: jest.fn(),
+ createHref: jest.fn(),
+ listen: jest.fn(),
+};
+
+const filterQuery: KueryFilterQuery = {
+ expression: 'host.name:"siem-es"',
+ kind: 'kuery',
+};
+
+const mockProps: UrlStateContainerLifecycleProps = {
+ history: mockHistory,
+ location,
+ indexPattern: {
+ fields: [
+ {
+ name: '@timestamp',
+ searchable: true,
+ type: 'date',
+ aggregatable: true,
+ },
+ ],
+ title: 'filebeat-*,packetbeat-*',
+ },
+ urlState: {
+ timerange: {
+ global: {
+ kind: 'relative',
+ fromStr: 'now-24h',
+ toStr: 'now',
+ from: 1558048243696,
+ to: 1558134643697,
+ linkTo: ['timeline'],
+ },
+ timeline: {
+ kind: 'relative',
+ fromStr: 'now-24h',
+ toStr: 'now',
+ from: 1558048243696,
+ to: 1558134643697,
+ linkTo: ['global'],
+ },
+ },
+ kqlQuery: [
+ {
+ filterQuery,
+ type: networkModel.NetworkType.page,
+ model: KueryFilterModel.network,
+ },
+ ],
+ },
+ setAbsoluteTimerange: (jest.fn() as unknown) as ActionCreator<{
+ from: number;
+ fromStr: undefined;
+ id: InputsModelId;
+ to: number;
+ toStr: undefined;
+ }>,
+ setHostsKql: (jest.fn() as unknown) as ActionCreator<{
+ filterQuery: SerializedFilterQuery;
+ hostsType: hostsModel.HostsType;
+ }>,
+ setNetworkKql: (jest.fn() as unknown) as ActionCreator<{
+ filterQuery: SerializedFilterQuery;
+ networkType: networkModel.NetworkType;
+ }>,
+ setRelativeTimerange: (jest.fn() as unknown) as ActionCreator<{
+ from: number;
+ fromStr: string;
+ id: InputsModelId;
+ to: number;
+ toStr: string;
+ }>,
+ toggleTimelineLinkTo: (jest.fn() as unknown) as ActionCreator<{
+ linkToId: InputsModelId;
+ }>,
+};
+
+describe('UrlStateComponents', () => {
+ describe('UrlStateContainer', () => {
+ const state: State = mockGlobalState;
+
+ let store = createStore(state);
+
+ beforeEach(() => {
+ store = createStore(state);
+ });
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+ test('mounts and renders', () => {
+ const wrapper = mount(
+
+
+
+
+
+
+
+ );
+ const urlStateComponents = wrapper.find('[data-test-subj="urlStateComponents"]');
+ urlStateComponents.exists();
+ expect(toJson(wrapper)).toMatchSnapshot();
+ });
+ test('componentDidUpdate - timerange redux state updates the url', async () => {
+ const wrapper = shallow();
+
+ const newUrlState = {
+ timerange: {
+ timeline: {
+ kind: 'relative',
+ fromStr: 'now-24h',
+ toStr: 'now',
+ from: 1558048243696,
+ to: 1558134643697,
+ linkTo: ['global'],
+ },
+ },
+ };
+
+ wrapper.setProps({ urlState: newUrlState });
+ wrapper.update();
+ await wait(1000);
+ expect(mockHistory.replace.mock.calls[1][0]).toStrictEqual({
+ hash: '',
+ pathname: '/network',
+ search:
+ '?timerange=(global:(linkTo:!(timeline),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)),timeline:(linkTo:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now),timerange:!(global)))',
+ state: '',
+ });
+ });
+ test('componentDidUpdate - kql query redux state updates the url', async () => {
+ const wrapper = shallow();
+
+ const newUrlState = {
+ kqlQuery: [
+ {
+ filterQuery,
+ type: networkModel.NetworkType.details,
+ model: KueryFilterModel.network,
+ },
+ ],
+ };
+
+ wrapper.setProps({ urlState: newUrlState });
+ wrapper.update();
+ await wait(1000);
+ expect(mockHistory.replace.mock.calls[1][0]).toStrictEqual({
+ hash: '',
+ pathname: '/network',
+ search:
+ "?kqlQuery=(filterQuery:(expression:'host.name:%22siem-es%22',kind:kuery),model:network,type:details)",
+ state: '',
+ });
+ });
+ });
+ describe('isKqlForRoute', () => {
+ test('host page and host page kuery', () => {
+ const result = isKqlForRoute('/hosts', {
+ filterQuery: {
+ expression: 'host.name:"siem-kibana"',
+ kind: 'kuery',
+ },
+ model: KueryFilterModel.hosts,
+ type: hostsModel.HostsType.page,
+ });
+ expect(result).toBeTruthy();
+ });
+ test('host page and host details kuery', () => {
+ const result = isKqlForRoute('/hosts', {
+ filterQuery: {
+ expression: 'host.name:"siem-kibana"',
+ kind: 'kuery',
+ },
+ model: KueryFilterModel.hosts,
+ type: hostsModel.HostsType.details,
+ });
+ expect(result).toBeFalsy();
+ });
+ test('host details and host details kuery', () => {
+ const result = isKqlForRoute('/hosts/siem-kibana', {
+ filterQuery: {
+ expression: 'host.name:"siem-kibana"',
+ kind: 'kuery',
+ },
+ model: KueryFilterModel.hosts,
+ type: hostsModel.HostsType.details,
+ });
+ expect(result).toBeTruthy();
+ });
+ test('host details and host page kuery', () => {
+ const result = isKqlForRoute('/hosts/siem-kibana', {
+ filterQuery: {
+ expression: 'host.name:"siem-kibana"',
+ kind: 'kuery',
+ },
+ model: KueryFilterModel.hosts,
+ type: hostsModel.HostsType.page,
+ });
+ expect(result).toBeFalsy();
+ });
+ test('network page and network page kuery', () => {
+ const result = isKqlForRoute('/network', {
+ filterQuery: {
+ expression: 'network.name:"siem-kibana"',
+ kind: 'kuery',
+ },
+ model: KueryFilterModel.network,
+ type: networkModel.NetworkType.page,
+ });
+ expect(result).toBeTruthy();
+ });
+ test('network page and network details kuery', () => {
+ const result = isKqlForRoute('/network', {
+ filterQuery: {
+ expression: 'network.name:"siem-kibana"',
+ kind: 'kuery',
+ },
+ model: KueryFilterModel.network,
+ type: networkModel.NetworkType.details,
+ });
+ expect(result).toBeFalsy();
+ });
+ test('network details and network details kuery', () => {
+ const result = isKqlForRoute('/network/ip/10.100.7.198', {
+ filterQuery: {
+ expression: 'network.name:"siem-kibana"',
+ kind: 'kuery',
+ },
+ model: KueryFilterModel.network,
+ type: networkModel.NetworkType.details,
+ });
+ expect(result).toBeTruthy();
+ });
+ test('network details and network page kuery', () => {
+ const result = isKqlForRoute('/network/ip/123.234.34', {
+ filterQuery: {
+ expression: 'network.name:"siem-kibana"',
+ kind: 'kuery',
+ },
+ model: KueryFilterModel.network,
+ type: networkModel.NetworkType.page,
+ });
+ expect(result).toBeFalsy();
+ });
+ });
+});
diff --git a/x-pack/plugins/siem/public/components/url_state/index.tsx b/x-pack/plugins/siem/public/components/url_state/index.tsx
new file mode 100644
index 0000000000000..5df5de0f73283
--- /dev/null
+++ b/x-pack/plugins/siem/public/components/url_state/index.tsx
@@ -0,0 +1,439 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { History, Location } from 'history';
+import { get, throttle } from 'lodash/fp';
+import React from 'react';
+import { pure } from 'recompose';
+import { connect } from 'react-redux';
+import { Route, RouteProps } from 'react-router-dom';
+import { decode, encode, RisonValue } from 'rison-node';
+
+import { QueryString } from 'ui/utils/query_string';
+import { ActionCreator } from 'typescript-fsa';
+import { StaticIndexPattern } from 'ui/index_patterns';
+import { hostsActions, inputsActions, networkActions } from '../../store/actions';
+import {
+ hostsModel,
+ hostsSelectors,
+ inputsSelectors,
+ KueryFilterModel,
+ KueryFilterQuery,
+ networkModel,
+ networkSelectors,
+ SerializedFilterQuery,
+ State,
+} from '../../store';
+import {
+ InputsModelId,
+ TimeRangeKinds,
+ UrlInputsModel,
+ UrlTimeRange,
+} from '../../store/inputs/model';
+import { convertKueryToElasticSearchQuery } from '../../lib/keury';
+
+interface KqlQueryHosts {
+ filterQuery: KueryFilterQuery | null;
+ model: KueryFilterModel.hosts;
+ type: hostsModel.HostsType;
+}
+
+interface KqlQueryNetwork {
+ filterQuery: KueryFilterQuery | null;
+ model: KueryFilterModel.network;
+ type: networkModel.NetworkType;
+}
+
+export type KqlQuery = KqlQueryHosts | KqlQueryNetwork;
+
+interface UrlState {
+ kqlQuery: KqlQuery[];
+ timerange: UrlInputsModel;
+}
+type KeyUrlState = keyof UrlState;
+
+interface UrlStateProps {
+ indexPattern: StaticIndexPattern;
+ mapToUrlState?: (value: any) => UrlState;
+ onChange?: (urlState: UrlState, previousUrlState: UrlState) => void;
+ onInitialize?: (urlState: UrlState) => void;
+ urlState: UrlState;
+}
+
+interface UrlStateDispatchProps {
+ setHostsKql: ActionCreator<{
+ filterQuery: SerializedFilterQuery;
+ hostsType: hostsModel.HostsType;
+ }>;
+ setNetworkKql: ActionCreator<{
+ filterQuery: SerializedFilterQuery;
+ networkType: networkModel.NetworkType;
+ }>;
+ setAbsoluteTimerange: ActionCreator<{
+ from: number;
+ fromStr: undefined;
+ id: InputsModelId;
+ to: number;
+ toStr: undefined;
+ }>;
+ setRelativeTimerange: ActionCreator<{
+ from: number;
+ fromStr: string;
+ id: InputsModelId;
+ to: number;
+ toStr: string;
+ }>;
+ toggleTimelineLinkTo: ActionCreator<{
+ linkToId: InputsModelId;
+ }>;
+}
+
+interface TimerangeActions {
+ absolute: ActionCreator<{
+ from: number;
+ fromStr: undefined;
+ id: InputsModelId;
+ to: number;
+ toStr: undefined;
+ }>;
+ relative: ActionCreator<{
+ from: number;
+ fromStr: string;
+ id: InputsModelId;
+ to: number;
+ toStr: string;
+ }>;
+}
+
+interface KqlActions {
+ hosts: ActionCreator<{
+ filterQuery: SerializedFilterQuery;
+ hostsType: hostsModel.HostsType;
+ }>;
+ network: ActionCreator<{
+ filterQuery: SerializedFilterQuery;
+ networkType: networkModel.NetworkType;
+ }>;
+}
+
+interface UrlStateMappedToActionsType {
+ kqlQuery: KqlActions;
+ timerange: TimerangeActions;
+}
+
+type UrlStateContainerProps = UrlStateProps & UrlStateDispatchProps;
+
+interface UrlStateRouterProps {
+ history: History;
+ location: Location;
+}
+
+export type UrlStateContainerLifecycleProps = UrlStateRouterProps & UrlStateContainerProps;
+
+export const isKqlForRoute = (pathname: string, kql: KqlQuery): boolean => {
+ const trailingPath = pathname.match(/([^\/]+$)/);
+ if (trailingPath !== null) {
+ if (
+ (trailingPath[0] === 'hosts' &&
+ kql.model === 'hosts' &&
+ kql.type === hostsModel.HostsType.page) ||
+ (trailingPath[0] === 'network' &&
+ kql.model === 'network' &&
+ kql.type === networkModel.NetworkType.page) ||
+ (pathname.match(/hosts\/.*?/) &&
+ kql.model === 'hosts' &&
+ kql.type === hostsModel.HostsType.details) ||
+ (pathname.match(/network\/ip\/.*?/) &&
+ kql.model === 'network' &&
+ kql.type === networkModel.NetworkType.details)
+ ) {
+ return true;
+ }
+ }
+ return false;
+};
+
+export class UrlStateContainerLifecycle extends React.Component {
+ public render() {
+ return null;
+ }
+
+ public componentDidUpdate({
+ location: prevLocation,
+ urlState: prevUrlState,
+ }: UrlStateContainerLifecycleProps) {
+ const { history, location, urlState } = this.props;
+ if (JSON.stringify(urlState) !== JSON.stringify(prevUrlState)) {
+ Object.keys(urlState).forEach((key: string) => {
+ if ('kqlQuery' === key || 'timerange' === key) {
+ const urlKey: KeyUrlState = key;
+ if (JSON.stringify(urlState[urlKey]) !== JSON.stringify(prevUrlState[urlKey])) {
+ if (urlKey === 'kqlQuery') {
+ urlState.kqlQuery.forEach((value: KqlQuery, index: number) => {
+ if (
+ JSON.stringify(urlState.kqlQuery[index]) !==
+ JSON.stringify(prevUrlState.kqlQuery[index])
+ ) {
+ this.replaceStateInLocation(urlState.kqlQuery[index], 'kqlQuery');
+ }
+ });
+ } else {
+ this.replaceStateInLocation(urlState[urlKey], urlKey);
+ }
+ }
+ }
+ });
+ }
+
+ if (history.action === 'POP' && location !== prevLocation) {
+ this.handleLocationChange(prevLocation, location);
+ }
+ }
+
+ public componentDidMount() {
+ const { location } = this.props;
+ this.handleInitialize(location);
+ }
+
+ private replaceStateInLocation = throttle(
+ 1000,
+ (urlState: UrlInputsModel | KqlQuery | KqlQuery[], urlStateKey: string) => {
+ const { history, location } = this.props;
+ const newLocation = replaceQueryStringInLocation(
+ location,
+ replaceStateKeyInQueryString(urlStateKey, urlState)(getQueryStringFromLocation(location))
+ );
+
+ if (newLocation !== location) {
+ history.replace(newLocation);
+ }
+ }
+ );
+
+ private urlStateMappedToActions: UrlStateMappedToActionsType = {
+ kqlQuery: {
+ hosts: this.props.setHostsKql,
+ network: this.props.setNetworkKql,
+ },
+ timerange: {
+ absolute: this.props.setAbsoluteTimerange,
+ relative: this.props.setRelativeTimerange,
+ },
+ };
+
+ private handleInitialize = (location: Location) => {
+ Object.keys(this.urlStateMappedToActions).forEach((key: string) => {
+ if ('kqlQuery' === key || 'timerange' === key) {
+ const urlKey: KeyUrlState = key;
+ const newUrlStateString = getParamFromQueryString(
+ getQueryStringFromLocation(location),
+ urlKey
+ );
+ if (newUrlStateString) {
+ if (urlKey === 'timerange') {
+ const timerangeStateData: UrlInputsModel = decodeRisonUrlState(newUrlStateString);
+ const globalId: InputsModelId = 'global';
+ const globalRange: UrlTimeRange = timerangeStateData.global;
+ const globalType: TimeRangeKinds = get('global.kind', timerangeStateData);
+ if (globalType) {
+ if (globalRange.linkTo.length === 0) {
+ this.props.toggleTimelineLinkTo({ linkToId: 'global' });
+ }
+ // @ts-ignore
+ this.urlStateMappedToActions.timerange[timelineType]({
+ ...globalRange,
+ id: globalId,
+ });
+ }
+ const timelineId: InputsModelId = 'timeline';
+ const timelineRange: UrlTimeRange = timerangeStateData.timeline;
+ const timelineType: TimeRangeKinds = get('timeline.kind', timerangeStateData);
+ if (timelineType) {
+ if (timelineRange.linkTo.length === 0) {
+ this.props.toggleTimelineLinkTo({ linkToId: 'timeline' });
+ }
+ // @ts-ignore
+ this.urlStateMappedToActions.timerange[timelineType]({
+ ...timelineRange,
+ id: timelineId,
+ });
+ }
+ }
+ if (urlKey === 'kqlQuery') {
+ const kqlQueryStateData: KqlQuery = decodeRisonUrlState(newUrlStateString);
+ if (isKqlForRoute(location.pathname, kqlQueryStateData)) {
+ const filterQuery = {
+ query: kqlQueryStateData.filterQuery,
+ serializedQuery: convertKueryToElasticSearchQuery(
+ kqlQueryStateData.filterQuery ? kqlQueryStateData.filterQuery.expression : '',
+ this.props.indexPattern
+ ),
+ };
+ if (kqlQueryStateData.model === 'hosts') {
+ this.urlStateMappedToActions.kqlQuery.hosts({
+ filterQuery,
+ hostsType: kqlQueryStateData.type,
+ });
+ }
+ if (kqlQueryStateData.model === 'network') {
+ this.urlStateMappedToActions.kqlQuery.network({
+ filterQuery,
+ networkType: kqlQueryStateData.type,
+ });
+ }
+ }
+ }
+ } else {
+ this.replaceStateInLocation(this.props.urlState[urlKey], urlKey);
+ }
+ }
+ });
+ };
+
+ private handleLocationChange = (prevLocation: Location, newLocation: Location) => {
+ const { onChange, mapToUrlState } = this.props;
+
+ if (!onChange || !mapToUrlState) {
+ return;
+ }
+
+ const previousUrlStateString = getParamFromQueryString(
+ getQueryStringFromLocation(prevLocation),
+ 'urlStateKey'
+ );
+ const newUrlStateString = getParamFromQueryString(
+ getQueryStringFromLocation(newLocation),
+ 'urlStateKey'
+ );
+
+ if (previousUrlStateString !== newUrlStateString) {
+ const previousUrlState = mapToUrlState(decodeRisonUrlState(previousUrlStateString));
+ const newUrlState = mapToUrlState(decodeRisonUrlState(newUrlStateString));
+
+ if (typeof newUrlState !== 'undefined') {
+ onChange(newUrlState, previousUrlState);
+ }
+ }
+ };
+}
+
+export const UrlStateComponents = pure(props => (
+ >
+ {({ history, location }) => (
+
+ )}
+
+));
+
+const makeMapStateToProps = () => {
+ const getInputsSelector = inputsSelectors.inputsSelector();
+ const getHostsFilterQueryAsKuery = hostsSelectors.hostsFilterQueryAsKuery();
+ const getNetworkFilterQueryAsKuery = networkSelectors.networkFilterQueryAsKuery();
+ const mapStateToProps = (state: State) => {
+ const inputState = getInputsSelector(state);
+ const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global;
+ const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline;
+
+ return {
+ urlState: {
+ timerange: {
+ global: {
+ timerange: globalTimerange,
+ linkTo: globalLinkTo,
+ },
+ timeline: {
+ timerange: timelineLinkTo,
+ linkTo: timelineTimerange,
+ },
+ },
+ kqlQuery: [
+ {
+ filterQuery: getHostsFilterQueryAsKuery(state, hostsModel.HostsType.details),
+ type: hostsModel.HostsType.details,
+ model: KueryFilterModel.hosts,
+ },
+ {
+ filterQuery: getHostsFilterQueryAsKuery(state, hostsModel.HostsType.page),
+ type: hostsModel.HostsType.page,
+ model: KueryFilterModel.hosts,
+ },
+ {
+ filterQuery: getNetworkFilterQueryAsKuery(state, networkModel.NetworkType.details),
+ type: networkModel.NetworkType.details,
+ model: KueryFilterModel.network,
+ },
+ {
+ filterQuery: getNetworkFilterQueryAsKuery(state, networkModel.NetworkType.page),
+ type: networkModel.NetworkType.page,
+ model: KueryFilterModel.network,
+ },
+ ],
+ },
+ };
+ };
+
+ return mapStateToProps;
+};
+export const UrlStateContainer = connect(
+ makeMapStateToProps,
+ {
+ setAbsoluteTimerange: inputsActions.setAbsoluteRangeDatePicker,
+ setHostsKql: hostsActions.applyHostsFilterQuery,
+ setNetworkKql: networkActions.applyNetworkFilterQuery,
+ setRelativeTimerange: inputsActions.setRelativeRangeDatePicker,
+ toggleTimelineLinkTo: inputsActions.toggleTimelineLinkTo,
+ }
+ // @ts-ignore
+)(UrlStateComponents);
+
+export const decodeRisonUrlState = (value: string | undefined): RisonValue | any | undefined => {
+ try {
+ return value ? decode(value) : undefined;
+ } catch (error) {
+ if (error instanceof Error && error.message.startsWith('rison decoder error')) {
+ return {};
+ }
+ throw error;
+ }
+};
+
+const encodeRisonUrlState = (state: any) => encode(state);
+
+export const getQueryStringFromLocation = (location: Location) => location.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;
+};
+
+export const replaceStateKeyInQueryString = (
+ stateKey: string,
+ urlState: UrlState | undefined
+) => (queryString: string) => {
+ const previousQueryValues = QueryString.decode(queryString);
+ const encodedUrlState =
+ typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined;
+ return QueryString.encode({
+ ...previousQueryValues,
+ [stateKey]: encodedUrlState,
+ });
+};
+
+const replaceQueryStringInLocation = (location: Location, queryString: string): Location => {
+ if (queryString === getQueryStringFromLocation(location)) {
+ return location;
+ } else {
+ return {
+ ...location,
+ search: `?${queryString}`,
+ };
+ }
+};
diff --git a/x-pack/plugins/siem/public/pages/hosts/host_details.tsx b/x-pack/plugins/siem/public/pages/hosts/host_details.tsx
index b8dce62ccd234..e3bcadac73afa 100644
--- a/x-pack/plugins/siem/public/pages/hosts/host_details.tsx
+++ b/x-pack/plugins/siem/public/pages/hosts/host_details.tsx
@@ -35,6 +35,7 @@ import { hostsModel, hostsSelectors, State } from '../../store';
import { HostsKql } from './kql';
import * as i18n from './translations';
+import { UrlStateContainer } from '../../components/url_state';
const basePath = chrome.getBasePath();
const type = hostsModel.HostsType.details;
@@ -65,6 +66,7 @@ const HostDetailsComponent = pure(
+
;
-describe('Hosts', () => {
- describe('rendering', () => {
- beforeEach(() => {
- localSource = cloneDeep(mocksSource);
- });
+type Action = 'PUSH' | 'POP' | 'REPLACE';
+const pop: Action = 'POP';
+const location = {
+ pathname: '/network',
+ search: '',
+ state: '',
+ hash: '',
+};
+const mockHistory = {
+ length: 2,
+ location,
+ action: pop,
+ push: jest.fn(),
+ replace: jest.fn(),
+ go: jest.fn(),
+ goBack: jest.fn(),
+ goForward: jest.fn(),
+ block: jest.fn(),
+ createHref: jest.fn(),
+ listen: jest.fn(),
+};
- test('it renders the Setup Instructions text when no index is available', async () => {
- localSource[0].result.data.source.status.auditbeatIndicesExist = false;
- localSource[0].result.data.source.status.filebeatIndicesExist = false;
- localSource[0].result.data.source.status.winlogbeatIndicesExist = false;
- const wrapper = mount(
-
-
+// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769
+/* eslint-disable no-console */
+const originalError = console.error;
+
+describe('Hosts - rendering', () => {
+ beforeAll(() => {
+ console.error = jest.fn();
+ });
+
+ afterAll(() => {
+ console.error = originalError;
+ });
+ beforeEach(() => {
+ localSource = cloneDeep(mocksSource);
+ });
+
+ test('it renders the Setup Instructions text when no index is available', async () => {
+ localSource[0].result.data.source.status.auditbeatIndicesExist = false;
+ localSource[0].result.data.source.status.filebeatIndicesExist = false;
+ localSource[0].result.data.source.status.winlogbeatIndicesExist = false;
+ const wrapper = mount(
+
+
+
-
-
- );
- // Why => https://github.com/apollographql/react-apollo/issues/1711
- await new Promise(resolve => setTimeout(resolve));
- wrapper.update();
- expect(wrapper.text()).toContain(i18n.SETUP_INSTRUCTIONS);
- });
+
+
+
+ );
+ // Why => https://github.com/apollographql/react-apollo/issues/1711
+ await new Promise(resolve => setTimeout(resolve));
+ wrapper.update();
+ expect(wrapper.text()).toContain(i18n.SETUP_INSTRUCTIONS);
+ });
- test('it renders the Setup Instructions text when auditbeat index is not available', async () => {
- localSource[0].result.data.source.status.auditbeatIndicesExist = false;
- localSource[0].result.data.source.status.filebeatIndicesExist = true;
- localSource[0].result.data.source.status.winlogbeatIndicesExist = true;
- const wrapper = mount(
-
-
+ test('it renders the Setup Instructions text when auditbeat index is not available', async () => {
+ localSource[0].result.data.source.status.auditbeatIndicesExist = false;
+ localSource[0].result.data.source.status.filebeatIndicesExist = true;
+ localSource[0].result.data.source.status.winlogbeatIndicesExist = true;
+ const wrapper = mount(
+
+
+
-
-
- );
- // Why => https://github.com/apollographql/react-apollo/issues/1711
- await new Promise(resolve => setTimeout(resolve));
- wrapper.update();
- expect(wrapper.text()).toContain(i18n.SETUP_INSTRUCTIONS);
- });
+
+
+
+ );
+ // Why => https://github.com/apollographql/react-apollo/issues/1711
+ await new Promise(resolve => setTimeout(resolve));
+ wrapper.update();
+ expect(wrapper.text()).toContain(i18n.SETUP_INSTRUCTIONS);
+ });
- test('it DOES NOT render the Setup Instructions text when auditbeat index is available', async () => {
- localSource[0].result.data.source.status.auditbeatIndicesExist = true;
- localSource[0].result.data.source.status.filebeatIndicesExist = false;
- localSource[0].result.data.source.status.winlogbeatIndicesExist = false;
- const wrapper = mount(
-
-
+ test('it DOES NOT render the Setup Instructions text when auditbeat index is available', async () => {
+ localSource[0].result.data.source.status.auditbeatIndicesExist = true;
+ localSource[0].result.data.source.status.filebeatIndicesExist = false;
+ localSource[0].result.data.source.status.winlogbeatIndicesExist = false;
+ const wrapper = mount(
+
+
+
-
-
- );
- // Why => https://github.com/apollographql/react-apollo/issues/1711
- await new Promise(resolve => setTimeout(resolve));
- wrapper.update();
- expect(wrapper.text()).not.toContain(i18n.SETUP_INSTRUCTIONS);
- });
+
+
+
+ );
+ // Why => https://github.com/apollographql/react-apollo/issues/1711
+ await new Promise(resolve => setTimeout(resolve));
+ wrapper.update();
+ expect(wrapper.text()).not.toContain(i18n.SETUP_INSTRUCTIONS);
});
});
diff --git a/x-pack/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/plugins/siem/public/pages/hosts/hosts.tsx
index 91b6fba2f7a17..59fe4e8b0afd9 100644
--- a/x-pack/plugins/siem/public/pages/hosts/hosts.tsx
+++ b/x-pack/plugins/siem/public/pages/hosts/hosts.tsx
@@ -36,6 +36,7 @@ import { hostsModel, hostsSelectors, State } from '../../store';
import { HostsKql } from './kql';
import * as i18n from './translations';
+import { UrlStateContainer } from '../../components/url_state';
const basePath = chrome.getBasePath();
@@ -59,6 +60,7 @@ const HostsComponent = pure(({ filterQuery }) => (
+
(
+
;
-describe('Network', () => {
- describe('rendering', () => {
- beforeEach(() => {
- localSource = cloneDeep(mocksSource);
- });
+type Action = 'PUSH' | 'POP' | 'REPLACE';
+const pop: Action = 'POP';
+const location = {
+ pathname: '/network',
+ search: '',
+ state: '',
+ hash: '',
+};
+const mockHistory = {
+ length: 2,
+ location,
+ action: pop,
+ push: jest.fn(),
+ replace: jest.fn(),
+ go: jest.fn(),
+ goBack: jest.fn(),
+ goForward: jest.fn(),
+ block: jest.fn(),
+ createHref: jest.fn(),
+ listen: jest.fn(),
+};
+// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769
+/* eslint-disable no-console */
+const originalError = console.error;
- test('it renders the Setup Instructions text when no index is available', async () => {
- localSource[0].result.data.source.status.auditbeatIndicesExist = false;
- localSource[0].result.data.source.status.filebeatIndicesExist = false;
- localSource[0].result.data.source.status.winlogbeatIndicesExist = false;
- const wrapper = mount(
-
-
+describe('rendering - rendering', () => {
+ beforeAll(() => {
+ console.error = jest.fn();
+ });
+ afterAll(() => {
+ console.error = originalError;
+ });
+ beforeEach(() => {
+ localSource = cloneDeep(mocksSource);
+ });
+ beforeEach(() => {
+ localSource = cloneDeep(mocksSource);
+ });
+
+ test('it renders the Setup Instructions text when no index is available', async () => {
+ localSource[0].result.data.source.status.auditbeatIndicesExist = false;
+ localSource[0].result.data.source.status.filebeatIndicesExist = false;
+ localSource[0].result.data.source.status.winlogbeatIndicesExist = false;
+ const wrapper = mount(
+
+
+
-
-
- );
- // Why => https://github.com/apollographql/react-apollo/issues/1711
- await new Promise(resolve => setTimeout(resolve));
- wrapper.update();
- expect(wrapper.text()).toContain(i18n.SETUP_INSTRUCTIONS);
- });
+
+
+
+ );
+ // Why => https://github.com/apollographql/react-apollo/issues/1711
+ await new Promise(resolve => setTimeout(resolve));
+ wrapper.update();
+ expect(wrapper.text()).toContain(i18n.SETUP_INSTRUCTIONS);
+ });
- test('it renders the Setup Instructions text when filebeat index is not available', async () => {
- localSource[0].result.data.source.status.auditbeatIndicesExist = true;
- localSource[0].result.data.source.status.filebeatIndicesExist = false;
- localSource[0].result.data.source.status.winlogbeatIndicesExist = true;
- const wrapper = mount(
-
-
+ test('it renders the Setup Instructions text when filebeat index is not available', async () => {
+ localSource[0].result.data.source.status.auditbeatIndicesExist = true;
+ localSource[0].result.data.source.status.filebeatIndicesExist = false;
+ localSource[0].result.data.source.status.winlogbeatIndicesExist = true;
+ const wrapper = mount(
+
+
+
-
-
- );
- // Why => https://github.com/apollographql/react-apollo/issues/1711
- await new Promise(resolve => setTimeout(resolve));
- wrapper.update();
- expect(wrapper.text()).toContain(i18n.SETUP_INSTRUCTIONS);
- });
+
+
+
+ );
+ // Why => https://github.com/apollographql/react-apollo/issues/1711
+ await new Promise(resolve => setTimeout(resolve));
+ wrapper.update();
+ expect(wrapper.text()).toContain(i18n.SETUP_INSTRUCTIONS);
+ });
- test('it DOES NOT render the Setup Instructions text when filebeat index is available', async () => {
- localSource[0].result.data.source.status.auditbeatIndicesExist = false;
- localSource[0].result.data.source.status.filebeatIndicesExist = true;
- localSource[0].result.data.source.status.winlogbeatIndicesExist = false;
- const wrapper = mount(
-
-
+ test('it DOES NOT render the Setup Instructions text when filebeat index is available', async () => {
+ localSource[0].result.data.source.status.auditbeatIndicesExist = false;
+ localSource[0].result.data.source.status.filebeatIndicesExist = true;
+ localSource[0].result.data.source.status.winlogbeatIndicesExist = false;
+ const wrapper = mount(
+
+
+
-
-
- );
- // Why => https://github.com/apollographql/react-apollo/issues/1711
- await new Promise(resolve => setTimeout(resolve));
- wrapper.update();
- expect(wrapper.text()).not.toContain(i18n.SETUP_INSTRUCTIONS);
- });
+
+
+
+ );
+ // Why => https://github.com/apollographql/react-apollo/issues/1711
+ await new Promise(resolve => setTimeout(resolve));
+ wrapper.update();
+ expect(wrapper.text()).not.toContain(i18n.SETUP_INSTRUCTIONS);
});
});
diff --git a/x-pack/plugins/siem/public/pages/network/network.tsx b/x-pack/plugins/siem/public/pages/network/network.tsx
index 6718c3aaeb488..69569287fb952 100644
--- a/x-pack/plugins/siem/public/pages/network/network.tsx
+++ b/x-pack/plugins/siem/public/pages/network/network.tsx
@@ -29,6 +29,7 @@ import { networkModel, networkSelectors, State } from '../../store';
import { NetworkKql } from './kql';
import * as i18n from './translations';
+import { UrlStateContainer } from '../../components/url_state';
const basePath = chrome.getBasePath();
@@ -49,6 +50,7 @@ const NetworkComponent = pure(({ filterQuery }) => (
+
hosts => (hosts.filterQuery ? hosts.filterQuery.query.expression : null)
);
+export const hostsFilterQueryAsKuery = () =>
+ createSelector(
+ selectHosts,
+ hosts => (hosts.filterQuery ? hosts.filterQuery.query : null)
+ );
+
export const hostsFilterQueryAsJson = () =>
createSelector(
selectHosts,
diff --git a/x-pack/plugins/siem/public/store/inputs/model.ts b/x-pack/plugins/siem/public/store/inputs/model.ts
index 895b97e7c44af..8193acdd87050 100644
--- a/x-pack/plugins/siem/public/store/inputs/model.ts
+++ b/x-pack/plugins/siem/public/store/inputs/model.ts
@@ -22,8 +22,12 @@ interface RelativeTimeRange {
export type InputsModelId = 'global' | 'timeline';
+export type TimeRangeKinds = 'absolute' | 'relative';
+
export type TimeRange = AbsoluteTimeRange | RelativeTimeRange;
+export type UrlTimeRange = AbsoluteTimeRange & LinkTo | RelativeTimeRange & LinkTo;
+
export interface Policy {
kind: 'manual' | 'interval';
duration: number; // in ms
@@ -43,7 +47,15 @@ export interface InputsRange {
linkTo: InputsModelId[];
}
+interface LinkTo {
+ linkTo: InputsModelId[];
+}
+
export interface InputsModel {
global: InputsRange;
timeline: InputsRange;
}
+export interface UrlInputsModel {
+ global: TimeRange & LinkTo;
+ timeline: TimeRange & LinkTo;
+}
diff --git a/x-pack/plugins/siem/public/store/inputs/selectors.ts b/x-pack/plugins/siem/public/store/inputs/selectors.ts
index 4573adbb822a5..3d093a26aa034 100644
--- a/x-pack/plugins/siem/public/store/inputs/selectors.ts
+++ b/x-pack/plugins/siem/public/store/inputs/selectors.ts
@@ -16,10 +16,11 @@ const selectGlobal = (state: State): InputsRange => state.inputs.global;
const selectTimeline = (state: State): InputsRange => state.inputs.timeline;
-export const inpustSelector = createSelector(
- selectInputs,
- inputs => inputs
-);
+export const inputsSelector = () =>
+ createSelector(
+ selectInputs,
+ inputs => inputs
+ );
export const globalTimeRangeSelector = createSelector(
selectGlobal,
diff --git a/x-pack/plugins/siem/public/store/model.ts b/x-pack/plugins/siem/public/store/model.ts
index 7bc8d065e8685..68fd40d9f7c19 100644
--- a/x-pack/plugins/siem/public/store/model.ts
+++ b/x-pack/plugins/siem/public/store/model.ts
@@ -10,12 +10,17 @@ export { hostsModel } from './hosts';
export { dragAndDropModel } from './drag_and_drop';
export { networkModel } from './network';
+export enum KueryFilterModel {
+ hosts = 'hosts',
+ network = 'network',
+}
+
export interface KueryFilterQuery {
kind: 'kuery';
expression: string;
}
export interface SerializedFilterQuery {
- query: KueryFilterQuery;
+ query: KueryFilterQuery | null;
serializedQuery: string;
}
diff --git a/x-pack/plugins/siem/public/store/network/selectors.ts b/x-pack/plugins/siem/public/store/network/selectors.ts
index 48124fa2cb7a3..b242582fd39bb 100644
--- a/x-pack/plugins/siem/public/store/network/selectors.ts
+++ b/x-pack/plugins/siem/public/store/network/selectors.ts
@@ -39,6 +39,12 @@ export const networkFilterQueryAsJson = () =>
network => (network.filterQuery ? network.filterQuery.serializedQuery : null)
);
+export const networkFilterQueryAsKuery = () =>
+ createSelector(
+ selectNetworkByType,
+ network => (network.filterQuery ? network.filterQuery.query : null)
+ );
+
export const networkFilterQueryDraft = () =>
createSelector(
selectNetworkByType,