From 07c77df01e33637c680ec224240fe2b35d142f32 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 6 May 2019 15:38:35 -0600 Subject: [PATCH 01/18] WIP, definitely wrong --- .../components/link_to/query_params.tsx | 29 +++ .../components/super_date_picker/index.tsx | 24 ++- .../plugins/siem/public/pages/home/index.tsx | 26 ++- .../siem/public/utils/hooks/use_interval.ts | 32 ++++ .../siem/public/utils/page_providers.tsx | 19 ++ .../plugins/siem/public/utils/url_state.tsx | 165 +++++++++++++++++ .../plugins/siem/public/utils/with_time.tsx | 167 ++++++++++++++++++ 7 files changed, 458 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/siem/public/components/link_to/query_params.tsx create mode 100644 x-pack/plugins/siem/public/utils/hooks/use_interval.ts create mode 100644 x-pack/plugins/siem/public/utils/page_providers.tsx create mode 100644 x-pack/plugins/siem/public/utils/url_state.tsx create mode 100644 x-pack/plugins/siem/public/utils/with_time.tsx diff --git a/x-pack/plugins/siem/public/components/link_to/query_params.tsx b/x-pack/plugins/siem/public/components/link_to/query_params.tsx new file mode 100644 index 0000000000000..5675a71965e22 --- /dev/null +++ b/x-pack/plugins/siem/public/components/link_to/query_params.tsx @@ -0,0 +1,29 @@ +/* + * 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 { Location } from 'history'; + +import { getParamFromQueryString, getQueryStringFromLocation } from '../../utils/url_state'; + +export const getTimeFromLocation = (location: Location) => { + const timeParam = getParamFromQueryString(getQueryStringFromLocation(location), 'time'); + return timeParam ? parseFloat(timeParam) : NaN; +}; + +export const getFilterFromLocation = (location: Location) => { + const param = getParamFromQueryString(getQueryStringFromLocation(location), 'filter'); + return param ? param : ''; +}; + +export const getToFromLocation = (location: Location) => { + const timeParam = getParamFromQueryString(getQueryStringFromLocation(location), 'to'); + return timeParam ? parseFloat(timeParam) : NaN; +}; + +export const getFromFromLocation = (location: Location) => { + const timeParam = getParamFromQueryString(getQueryStringFromLocation(location), 'from'); + return timeParam ? parseFloat(timeParam) : NaN; +}; diff --git a/x-pack/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/plugins/siem/public/components/super_date_picker/index.tsx index 39f459b4ec261..c6240fa69c894 100644 --- a/x-pack/plugins/siem/public/components/super_date_picker/index.tsx +++ b/x-pack/plugins/siem/public/components/super_date_picker/index.tsx @@ -18,6 +18,7 @@ import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; import { inputsActions, inputsModel, State } from '../../store'; +import { MetricsTimeState } from '../../utils/with_time'; const MAX_RECENTLY_USED_RANGES = 9; @@ -78,6 +79,7 @@ interface TimeArgs { export type SuperDatePickerProps = OwnProps & SuperDatePickerDispatchProps & + MetricsTimeState & SuperDatePickerStateRedux; export interface SuperDatePickerState { @@ -101,9 +103,16 @@ export const SuperDatePickerComponent = class extends Component< } public render() { - const { duration, end, start, kind, fromStr, policy, toStr, isLoading } = this.props; - const endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); - const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); + const { duration, end, start, kind, fromStr, policy, toStr, isLoading, timeRange } = this.props; + let endDate; + let startDate; + if (!end && !start && timeRange) { + endDate = new Date(timeRange.from).toISOString(); + startDate = new Date(timeRange.to).toISOString(); + } else { + endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); + startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); + } return ( { if (!isInvalid) { this.updateReduxTime({ start, end, isQuickSelection, isInvalid }); + console.log('this.props', this.props); + this.props.setTimeRange({ + from: this.formatDate(start), + to: this.formatDate(end), + interval: '>=1m', + }); this.setState((prevState: SuperDatePickerState) => { const recentlyUsedRanges = [ { start, end }, diff --git a/x-pack/plugins/siem/public/pages/home/index.tsx b/x-pack/plugins/siem/public/pages/home/index.tsx index eabf3ec14b858..730155d625955 100644 --- a/x-pack/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/plugins/siem/public/pages/home/index.tsx @@ -33,6 +33,7 @@ import { HostsContainer } from '../hosts'; import { NetworkContainer } from '../network'; import { Overview } from '../overview'; import { Timelines } from '../timelines'; +import { WithMetricsTime, WithMetricsTimeUrlState } from '../../utils/with_time'; const WrappedByAutoSizer = styled.div` height: 100%; @@ -88,7 +89,30 @@ export const HomePage = pure(() => ( - + + + + {({ + timeRange, + setTimeRange, + refreshInterval, + setRefreshInterval, + isAutoReloading, + setAutoReload, + }) => ( + + )} + diff --git a/x-pack/plugins/siem/public/utils/hooks/use_interval.ts b/x-pack/plugins/siem/public/utils/hooks/use_interval.ts new file mode 100644 index 0000000000000..4e063d6d51ce3 --- /dev/null +++ b/x-pack/plugins/siem/public/utils/hooks/use_interval.ts @@ -0,0 +1,32 @@ +/* + * 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 { useEffect, useRef } from 'react'; + +export function useInterval(callback: () => void, delay: number | null) { + const savedCallback = useRef(callback); + + useEffect( + () => { + savedCallback.current = callback; + }, + [callback] + ); + + useEffect( + () => { + function tick() { + savedCallback.current(); + } + + if (delay !== null) { + const id = setInterval(tick, delay); + return () => clearInterval(id); + } + }, + [delay] + ); +} diff --git a/x-pack/plugins/siem/public/utils/page_providers.tsx b/x-pack/plugins/siem/public/utils/page_providers.tsx new file mode 100644 index 0000000000000..c899ee8b6361e --- /dev/null +++ b/x-pack/plugins/siem/public/utils/page_providers.tsx @@ -0,0 +1,19 @@ +/* + * 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 React from 'react'; + +import { SourceConfigurationFlyoutState } from '../components/source_configuration'; +import { MetricsTimeContainer } from '../containers/metrics/with_metrics_time'; +import { WithSource } from '../containers/source'; + +export const MetricDetailPageProviders: React.FunctionComponent = ({ children }) => ( + + + {children} + + +); diff --git a/x-pack/plugins/siem/public/utils/url_state.tsx b/x-pack/plugins/siem/public/utils/url_state.tsx new file mode 100644 index 0000000000000..43a803c81230b --- /dev/null +++ b/x-pack/plugins/siem/public/utils/url_state.tsx @@ -0,0 +1,165 @@ +/* + * 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 throttle from 'lodash/fp/throttle'; +import React from 'react'; +import { Route, RouteProps } from 'react-router-dom'; +import { decode, encode, RisonValue } from 'rison-node'; + +import { QueryString } from 'ui/utils/query_string'; + +interface UrlStateContainerProps { + urlState: UrlState | undefined; + urlStateKey: string; + mapToUrlState?: (value: any) => UrlState | undefined; + onChange?: (urlState: UrlState, previousUrlState: UrlState | undefined) => void; + onInitialize?: (urlState: UrlState | undefined) => void; +} + +interface UrlStateContainerLifecycleProps extends UrlStateContainerProps { + location: Location; + history: History; +} + +class UrlStateContainerLifecycle extends React.Component< + UrlStateContainerLifecycleProps +> { + public render() { + return null; + } + + public componentDidUpdate({ + location: prevLocation, + urlState: prevUrlState, + }: UrlStateContainerLifecycleProps) { + const { history, location, urlState } = this.props; + + if (urlState !== prevUrlState) { + this.replaceStateInLocation(urlState); + } + + if (history.action === 'POP' && location !== prevLocation) { + this.handleLocationChange(prevLocation, location); + } + } + + public componentDidMount() { + const { location } = this.props; + + this.handleInitialize(location); + } + + // eslint-disable-next-line @typescript-eslint/member-ordering this is really a method despite what eslint thinks + private replaceStateInLocation = throttle(1000, (urlState: UrlState | undefined) => { + const { history, location, urlStateKey } = this.props; + + const newLocation = replaceQueryStringInLocation( + location, + replaceStateKeyInQueryString(urlStateKey, urlState)(getQueryStringFromLocation(location)) + ); + + if (newLocation !== location) { + history.replace(newLocation); + } + }); + + private handleInitialize = (location: Location) => { + const { onInitialize, mapToUrlState, urlStateKey } = this.props; + + if (!onInitialize || !mapToUrlState) { + return; + } + + const newUrlStateString = getParamFromQueryString( + getQueryStringFromLocation(location), + urlStateKey + ); + const newUrlState = mapToUrlState(decodeRisonUrlState(newUrlStateString)); + + onInitialize(newUrlState); + }; + + private handleLocationChange = (prevLocation: Location, newLocation: Location) => { + const { onChange, mapToUrlState, urlStateKey } = 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 UrlStateContainer = ( + props: UrlStateContainerProps +) => ( + > + {({ history, location }) => ( + history={history} location={location} {...props} /> + )} + +); + +export const decodeRisonUrlState = (value: string | undefined): RisonValue | 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/utils/with_time.tsx b/x-pack/plugins/siem/public/utils/with_time.tsx new file mode 100644 index 0000000000000..6422eced34333 --- /dev/null +++ b/x-pack/plugins/siem/public/utils/with_time.tsx @@ -0,0 +1,167 @@ +/* + * 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 createContainer from 'constate-latest'; +import moment from 'moment'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { TimerangeInput } from '../graphql/types'; +import { useInterval } from './hooks/use_interval'; +import { replaceStateKeyInQueryString, UrlStateContainer } from './url_state'; + +export interface MetricsTimeState { + isAutoReloading: boolean; + refreshInterval: number; + setAutoReload: (isAutoReloading: boolean) => void; + setRefreshInterval: (refreshInterval: number) => void; + setTimeRange: (timeRange: TimerangeInput) => void; + timeRange: TimerangeInput; +} + +export const useMetricsTime = () => { + const [isAutoReloading, setAutoReload] = useState(false); + const [refreshInterval, setRefreshInterval] = useState(5000); + const [timeRange, setTimeRange] = useState({ + from: moment() + .subtract(1, 'hour') + .valueOf(), + to: moment().valueOf(), + interval: '>=1m', + }); + + const setTimeRangeToNow = useCallback( + () => { + const range = timeRange.to - timeRange.from; + const nowInMs = moment().valueOf(); + setTimeRange({ + from: nowInMs - range, + to: nowInMs, + interval: '>=1m', + }); + }, + [timeRange.from, timeRange.to] + ); + + useInterval(setTimeRangeToNow, isAutoReloading ? refreshInterval : null); + + useEffect( + () => { + if (isAutoReloading) { + setTimeRangeToNow(); + } + }, + [isAutoReloading] + ); + + return { + timeRange, + setTimeRange, + refreshInterval, + setRefreshInterval, + isAutoReloading, + setAutoReload, + }; +}; + +export const MetricsTimeContainer = createContainer(useMetricsTime); + +interface WithMetricsTimeProps { + children: (args: MetricsTimeState) => React.ReactElement; +} +export const WithMetricsTime: React.FunctionComponent = ({ + children, +}: WithMetricsTimeProps) => { + const metricsTimeState = useContext(MetricsTimeContainer.Context); + return children({ ...metricsTimeState }); +}; + +/** + * Url State + */ + +interface MetricsTimeUrlState { + time?: MetricsTimeState['timeRange']; + autoReload?: boolean; + refreshInterval?: number; +} + +export const WithMetricsTimeUrlState = () => ( + + {({ + timeRange, + setTimeRange, + refreshInterval, + setRefreshInterval, + isAutoReloading, + setAutoReload, + }) => ( + { + if (newUrlState && newUrlState.time) { + setTimeRange(newUrlState.time); + } + if (newUrlState && newUrlState.autoReload) { + setAutoReload(true); + } else if ( + newUrlState && + typeof newUrlState.autoReload !== 'undefined' && + !newUrlState.autoReload + ) { + setAutoReload(false); + } + if (newUrlState && newUrlState.refreshInterval) { + setRefreshInterval(newUrlState.refreshInterval); + } + }} + onInitialize={initialUrlState => { + if (initialUrlState && initialUrlState.time) { + setTimeRange(initialUrlState.time); + } + if (initialUrlState && initialUrlState.autoReload) { + setAutoReload(true); + } + if (initialUrlState && initialUrlState.refreshInterval) { + setRefreshInterval(initialUrlState.refreshInterval); + } + }} + /> + )} + +); + +const mapToUrlState = (value: any): MetricsTimeUrlState | undefined => + value + ? { + time: mapToTimeUrlState(value.time), + autoReload: mapToAutoReloadUrlState(value.autoReload), + refreshInterval: mapToRefreshInterval(value.refreshInterval), + } + : undefined; + +const mapToTimeUrlState = (value: any) => + value && (typeof value.to === 'number' && typeof value.from === 'number') ? value : undefined; + +const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); + +const mapToRefreshInterval = (value: any) => (typeof value === 'number' ? value : undefined); + +export const replaceMetricTimeInQueryString = (from: number, to: number) => + Number.isNaN(from) || Number.isNaN(to) + ? (value: string) => value + : replaceStateKeyInQueryString('metricTime', { + autoReload: false, + time: { + interval: '>=1m', + from, + to, + }, + }); From 1b9128868beee14365b13390682151595d77a515 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 6 May 2019 15:54:31 -0600 Subject: [PATCH 02/18] Metric to Global --- .../components/super_date_picker/index.tsx | 4 +-- .../plugins/siem/public/pages/home/index.tsx | 8 +++--- .../{with_time.tsx => with_global_time.tsx} | 28 +++++++++---------- 3 files changed, 20 insertions(+), 20 deletions(-) rename x-pack/plugins/siem/public/utils/{with_time.tsx => with_global_time.tsx} (85%) diff --git a/x-pack/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/plugins/siem/public/components/super_date_picker/index.tsx index c6240fa69c894..5a42673eeaf49 100644 --- a/x-pack/plugins/siem/public/components/super_date_picker/index.tsx +++ b/x-pack/plugins/siem/public/components/super_date_picker/index.tsx @@ -18,7 +18,7 @@ import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; import { inputsActions, inputsModel, State } from '../../store'; -import { MetricsTimeState } from '../../utils/with_time'; +import { GlobalTimeState } from '../../utils/with_global_time'; const MAX_RECENTLY_USED_RANGES = 9; @@ -79,7 +79,7 @@ interface TimeArgs { export type SuperDatePickerProps = OwnProps & SuperDatePickerDispatchProps & - MetricsTimeState & + GlobalTimeState & SuperDatePickerStateRedux; export interface SuperDatePickerState { diff --git a/x-pack/plugins/siem/public/pages/home/index.tsx b/x-pack/plugins/siem/public/pages/home/index.tsx index 730155d625955..b3d3ac22146fa 100644 --- a/x-pack/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/plugins/siem/public/pages/home/index.tsx @@ -33,7 +33,7 @@ import { HostsContainer } from '../hosts'; import { NetworkContainer } from '../network'; import { Overview } from '../overview'; import { Timelines } from '../timelines'; -import { WithMetricsTime, WithMetricsTimeUrlState } from '../../utils/with_time'; +import { WithGlobalTime, WithGlobalTimeUrlState } from '../../utils/with_global_time'; const WrappedByAutoSizer = styled.div` height: 100%; @@ -89,9 +89,9 @@ export const HomePage = pure(() => ( - + - + {({ timeRange, setTimeRange, @@ -112,7 +112,7 @@ export const HomePage = pure(() => ( }} /> )} - + diff --git a/x-pack/plugins/siem/public/utils/with_time.tsx b/x-pack/plugins/siem/public/utils/with_global_time.tsx similarity index 85% rename from x-pack/plugins/siem/public/utils/with_time.tsx rename to x-pack/plugins/siem/public/utils/with_global_time.tsx index 6422eced34333..6b4f47953998a 100644 --- a/x-pack/plugins/siem/public/utils/with_time.tsx +++ b/x-pack/plugins/siem/public/utils/with_global_time.tsx @@ -11,7 +11,7 @@ import { TimerangeInput } from '../graphql/types'; import { useInterval } from './hooks/use_interval'; import { replaceStateKeyInQueryString, UrlStateContainer } from './url_state'; -export interface MetricsTimeState { +export interface GlobalTimeState { isAutoReloading: boolean; refreshInterval: number; setAutoReload: (isAutoReloading: boolean) => void; @@ -20,7 +20,7 @@ export interface MetricsTimeState { timeRange: TimerangeInput; } -export const useMetricsTime = () => { +export const useGlobalTime = () => { const [isAutoReloading, setAutoReload] = useState(false); const [refreshInterval, setRefreshInterval] = useState(5000); const [timeRange, setTimeRange] = useState({ @@ -65,15 +65,15 @@ export const useMetricsTime = () => { }; }; -export const MetricsTimeContainer = createContainer(useMetricsTime); +export const GlobalTimeContainer = createContainer(useGlobalTime); interface WithMetricsTimeProps { - children: (args: MetricsTimeState) => React.ReactElement; + children: (args: GlobalTimeState) => React.ReactElement; } -export const WithMetricsTime: React.FunctionComponent = ({ +export const WithGlobalTime: React.FunctionComponent = ({ children, }: WithMetricsTimeProps) => { - const metricsTimeState = useContext(MetricsTimeContainer.Context); + const metricsTimeState = useContext(GlobalTimeContainer.Context); return children({ ...metricsTimeState }); }; @@ -81,14 +81,14 @@ export const WithMetricsTime: React.FunctionComponent = ({ * Url State */ -interface MetricsTimeUrlState { - time?: MetricsTimeState['timeRange']; +interface GlobalTimeUrlState { + time?: GlobalTimeState['timeRange']; autoReload?: boolean; refreshInterval?: number; } -export const WithMetricsTimeUrlState = () => ( - +export const WithGlobalTimeUrlState = () => ( + {({ timeRange, setTimeRange, @@ -135,10 +135,10 @@ export const WithMetricsTimeUrlState = () => ( }} /> )} - + ); -const mapToUrlState = (value: any): MetricsTimeUrlState | undefined => +const mapToUrlState = (value: any): GlobalTimeUrlState | undefined => value ? { time: mapToTimeUrlState(value.time), @@ -154,10 +154,10 @@ const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? va const mapToRefreshInterval = (value: any) => (typeof value === 'number' ? value : undefined); -export const replaceMetricTimeInQueryString = (from: number, to: number) => +export const replaceGlobalTimeInQueryString = (from: number, to: number) => Number.isNaN(from) || Number.isNaN(to) ? (value: string) => value - : replaceStateKeyInQueryString('metricTime', { + : replaceStateKeyInQueryString('metricTime', { autoReload: false, time: { interval: '>=1m', From ac9d7ecde65afa49a696a5151af21ff0e165f2d7 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 6 May 2019 15:58:11 -0600 Subject: [PATCH 03/18] delete silliness --- .../components/link_to/query_params.tsx | 29 ------------------- .../siem/public/utils/page_providers.tsx | 19 ------------ 2 files changed, 48 deletions(-) delete mode 100644 x-pack/plugins/siem/public/components/link_to/query_params.tsx delete mode 100644 x-pack/plugins/siem/public/utils/page_providers.tsx diff --git a/x-pack/plugins/siem/public/components/link_to/query_params.tsx b/x-pack/plugins/siem/public/components/link_to/query_params.tsx deleted file mode 100644 index 5675a71965e22..0000000000000 --- a/x-pack/plugins/siem/public/components/link_to/query_params.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 { Location } from 'history'; - -import { getParamFromQueryString, getQueryStringFromLocation } from '../../utils/url_state'; - -export const getTimeFromLocation = (location: Location) => { - const timeParam = getParamFromQueryString(getQueryStringFromLocation(location), 'time'); - return timeParam ? parseFloat(timeParam) : NaN; -}; - -export const getFilterFromLocation = (location: Location) => { - const param = getParamFromQueryString(getQueryStringFromLocation(location), 'filter'); - return param ? param : ''; -}; - -export const getToFromLocation = (location: Location) => { - const timeParam = getParamFromQueryString(getQueryStringFromLocation(location), 'to'); - return timeParam ? parseFloat(timeParam) : NaN; -}; - -export const getFromFromLocation = (location: Location) => { - const timeParam = getParamFromQueryString(getQueryStringFromLocation(location), 'from'); - return timeParam ? parseFloat(timeParam) : NaN; -}; diff --git a/x-pack/plugins/siem/public/utils/page_providers.tsx b/x-pack/plugins/siem/public/utils/page_providers.tsx deleted file mode 100644 index c899ee8b6361e..0000000000000 --- a/x-pack/plugins/siem/public/utils/page_providers.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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 React from 'react'; - -import { SourceConfigurationFlyoutState } from '../components/source_configuration'; -import { MetricsTimeContainer } from '../containers/metrics/with_metrics_time'; -import { WithSource } from '../containers/source'; - -export const MetricDetailPageProviders: React.FunctionComponent = ({ children }) => ( - - - {children} - - -); From b1e8efac811b459ab12494c8051509c8e3ad9060 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 7 May 2019 11:20:32 -0600 Subject: [PATCH 04/18] starting on redux approach --- .../components/super_date_picker/index.tsx | 28 ++++--------------- .../plugins/siem/public/pages/home/index.tsx | 26 +---------------- .../siem/public/utils/hooks/use_interval.ts | 2 +- 3 files changed, 7 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/plugins/siem/public/components/super_date_picker/index.tsx index 5a42673eeaf49..d2dda33d09d72 100644 --- a/x-pack/plugins/siem/public/components/super_date_picker/index.tsx +++ b/x-pack/plugins/siem/public/components/super_date_picker/index.tsx @@ -18,7 +18,6 @@ import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; import { inputsActions, inputsModel, State } from '../../store'; -import { GlobalTimeState } from '../../utils/with_global_time'; const MAX_RECENTLY_USED_RANGES = 9; @@ -33,7 +32,7 @@ type MyEuiSuperDatePickerProps = Pick< | 'refreshInterval' | 'showUpdateButton' | 'start' -> & { + > & { isLoading?: boolean; }; const MyEuiSuperDatePicker: React.SFC = EuiSuperDatePicker; @@ -79,7 +78,6 @@ interface TimeArgs { export type SuperDatePickerProps = OwnProps & SuperDatePickerDispatchProps & - GlobalTimeState & SuperDatePickerStateRedux; export interface SuperDatePickerState { @@ -91,7 +89,7 @@ export interface SuperDatePickerState { export const SuperDatePickerComponent = class extends Component< SuperDatePickerProps, SuperDatePickerState -> { + > { constructor(props: SuperDatePickerProps) { super(props); @@ -103,16 +101,9 @@ export const SuperDatePickerComponent = class extends Component< } public render() { - const { duration, end, start, kind, fromStr, policy, toStr, isLoading, timeRange } = this.props; - let endDate; - let startDate; - if (!end && !start && timeRange) { - endDate = new Date(timeRange.from).toISOString(); - startDate = new Date(timeRange.to).toISOString(); - } else { - endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); - startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); - } + const { duration, end, start, kind, fromStr, policy, toStr, isLoading } = this.props; + const endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); + const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); return ( { if (!isInvalid) { this.updateReduxTime({ start, end, isQuickSelection, isInvalid }); - console.log('this.props', this.props); - this.props.setTimeRange({ - from: this.formatDate(start), - to: this.formatDate(end), - interval: '>=1m', - }); this.setState((prevState: SuperDatePickerState) => { const recentlyUsedRanges = [ { start, end }, diff --git a/x-pack/plugins/siem/public/pages/home/index.tsx b/x-pack/plugins/siem/public/pages/home/index.tsx index b3d3ac22146fa..eabf3ec14b858 100644 --- a/x-pack/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/plugins/siem/public/pages/home/index.tsx @@ -33,7 +33,6 @@ import { HostsContainer } from '../hosts'; import { NetworkContainer } from '../network'; import { Overview } from '../overview'; import { Timelines } from '../timelines'; -import { WithGlobalTime, WithGlobalTimeUrlState } from '../../utils/with_global_time'; const WrappedByAutoSizer = styled.div` height: 100%; @@ -89,30 +88,7 @@ export const HomePage = pure(() => ( - - - - {({ - timeRange, - setTimeRange, - refreshInterval, - setRefreshInterval, - isAutoReloading, - setAutoReload, - }) => ( - - )} - + diff --git a/x-pack/plugins/siem/public/utils/hooks/use_interval.ts b/x-pack/plugins/siem/public/utils/hooks/use_interval.ts index 4e063d6d51ce3..8e7592d7e4620 100644 --- a/x-pack/plugins/siem/public/utils/hooks/use_interval.ts +++ b/x-pack/plugins/siem/public/utils/hooks/use_interval.ts @@ -6,7 +6,7 @@ import { useEffect, useRef } from 'react'; -export function useInterval(callback: () => void, delay: number | null) { +export const useInterval = (callback: () => void, delay: number | null) => { const savedCallback = useRef(callback); useEffect( From b9d0693509770b942a76a7befff667f4631bbe52 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 7 May 2019 15:25:25 -0600 Subject: [PATCH 05/18] wip on reducer approach --- .../plugins/infra/public/utils/url_state.tsx | 39 ++++++ .../url_state/index.tsx} | 111 +++++++++++++----- .../plugins/siem/public/pages/home/index.tsx | 2 + .../plugins/siem/public/store/inputs/model.ts | 2 + .../siem/public/store/inputs/selectors.ts | 9 +- .../siem/public/utils/url_state.test.tsx | 17 +++ .../siem/public/utils/with_global_time.tsx | 2 +- 7 files changed, 150 insertions(+), 32 deletions(-) rename x-pack/plugins/siem/public/{utils/url_state.tsx => components/url_state/index.tsx} (61%) create mode 100644 x-pack/plugins/siem/public/utils/url_state.test.tsx diff --git a/x-pack/plugins/infra/public/utils/url_state.tsx b/x-pack/plugins/infra/public/utils/url_state.tsx index 43a803c81230b..9cc9aae1163c2 100644 --- a/x-pack/plugins/infra/public/utils/url_state.tsx +++ b/x-pack/plugins/infra/public/utils/url_state.tsx @@ -110,6 +110,45 @@ class UrlStateContainerLifecycle extends React.Component< }; } + +/** + urlState={{ + time: timeRange, + autoReload: isAutoReloading, + refreshInterval, + }} + urlStateKey="metricTime" + mapToUrlState={mapToUrlState} + onChange={newUrlState => { + if (newUrlState && newUrlState.time) { + setTimeRange(newUrlState.time); + } + if (newUrlState && newUrlState.autoReload) { + setAutoReload(true); + } else if ( + newUrlState && + typeof newUrlState.autoReload !== 'undefined' && + !newUrlState.autoReload + ) { + setAutoReload(false); + } + if (newUrlState && newUrlState.refreshInterval) { + setRefreshInterval(newUrlState.refreshInterval); + } +}} + onInitialize={initialUrlState => { + if (initialUrlState && initialUrlState.time) { + setTimeRange(initialUrlState.time); + } + if (initialUrlState && initialUrlState.autoReload) { + setAutoReload(true); + } + if (initialUrlState && initialUrlState.refreshInterval) { + setRefreshInterval(initialUrlState.refreshInterval); + } +}} + */ + export const UrlStateContainer = ( props: UrlStateContainerProps ) => ( diff --git a/x-pack/plugins/siem/public/utils/url_state.tsx b/x-pack/plugins/siem/public/components/url_state/index.tsx similarity index 61% rename from x-pack/plugins/siem/public/utils/url_state.tsx rename to x-pack/plugins/siem/public/components/url_state/index.tsx index 43a803c81230b..67bfc2e1b2d96 100644 --- a/x-pack/plugins/siem/public/utils/url_state.tsx +++ b/x-pack/plugins/siem/public/components/url_state/index.tsx @@ -5,14 +5,24 @@ */ import { History, Location } from 'history'; -import throttle from 'lodash/fp/throttle'; +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 { inputsActions, inputsSelectors, State } from '../../store'; +import { InputsModel, InputsModelId, TimeRangeKinds, RelativeTimeRange } from '../../store/inputs/model'; -interface UrlStateContainerProps { +interface UrlState { + global: any; + timeline: any; +} + +interface UrlStateContainerProps { urlState: UrlState | undefined; urlStateKey: string; mapToUrlState?: (value: any) => UrlState | undefined; @@ -20,14 +30,33 @@ interface UrlStateContainerProps { onInitialize?: (urlState: UrlState | undefined) => void; } -interface UrlStateContainerLifecycleProps extends UrlStateContainerProps { +interface UrlStateReduxProps { + limit: number; +} + +interface UrlStateDispatchProps { + setAbsoluteTimerange: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + setRelativeTimerange: ActionCreator<{ + id: InputsModelId; + fromStr: string; + toStr: string; + from: number; + to: number; + }>; +} + +interface UrlStateContainerLifecycleProps extends UrlStateContainerProps { location: Location; history: History; } -class UrlStateContainerLifecycle extends React.Component< - UrlStateContainerLifecycleProps -> { +type UrlStateProps = UrlStateContainerLifecycleProps & UrlStateReduxProps & UrlStateDispatchProps; + +class UrlStateContainerLifecycle extends React.Component { public render() { return null; } @@ -35,7 +64,7 @@ class UrlStateContainerLifecycle extends React.Component< public componentDidUpdate({ location: prevLocation, urlState: prevUrlState, - }: UrlStateContainerLifecycleProps) { + }: UrlStateContainerLifecycleProps) { const { history, location, urlState } = this.props; if (urlState !== prevUrlState) { @@ -49,7 +78,6 @@ class UrlStateContainerLifecycle extends React.Component< public componentDidMount() { const { location } = this.props; - this.handleInitialize(location); } @@ -67,20 +95,32 @@ class UrlStateContainerLifecycle extends React.Component< } }); - private handleInitialize = (location: Location) => { - const { onInitialize, mapToUrlState, urlStateKey } = this.props; - - if (!onInitialize || !mapToUrlState) { - return; - } - - const newUrlStateString = getParamFromQueryString( - getQueryStringFromLocation(location), - urlStateKey - ); - const newUrlState = mapToUrlState(decodeRisonUrlState(newUrlStateString)); + private urlStateMappedToActions = { + timerange: { + absolute: this.props.setAbsoluteTimerange, + relative: this.props.setRelativeTimerange, + }, + another: { + nope: true, + }, + }; - onInitialize(newUrlState); + private handleInitialize = (location: Location) => { + Object.keys(this.urlStateMappedToActions).map(key => { + const newUrlStateString = getParamFromQueryString(getQueryStringFromLocation(location), key); + if (newUrlStateString) { + switch (key) { + case 'timerange': + const urlStateData: InputsModel = decodeRisonUrlState(newUrlStateString) + const globalType: TimeRangeKinds = get('global.kind', urlStateData); + const globalRange: RelativeTimeRange = urlStateData.global; + if (globalType !== null) { + return this.urlStateMappedToActions.timerange[globalType](globalRange); + } + } + return; + } + }); }; private handleLocationChange = (prevLocation: Location, newLocation: Location) => { @@ -110,17 +150,32 @@ class UrlStateContainerLifecycle extends React.Component< }; } -export const UrlStateContainer = ( - props: UrlStateContainerProps -) => ( +export const UrlStateComponents = pure(props => ( > {({ history, location }) => ( - history={history} location={location} {...props} /> + )} -); +)); + +const makeMapStateToProps = () => { + const getInputsSelector = inputsSelectors.inputsSelector(); + const mapStateToProps = (state: State) => ({ + ...getInputsSelector(state), + urlStateKey: 'qwrty', + }); + + return mapStateToProps; +}; +export const UrlStateContainer = connect( + makeMapStateToProps, + { + setAbsoluteTimerange: inputsActions.setAbsoluteRangeDatePicker, + setRelativeTimerange: inputsActions.setRelativeRangeDatePicker, + } +)(UrlStateComponents); -export const decodeRisonUrlState = (value: string | undefined): RisonValue | undefined => { +export const decodeRisonUrlState = (value: string | undefined): RisonValue | any | undefined => { try { return value ? decode(value) : undefined; } catch (error) { @@ -144,9 +199,11 @@ export const replaceStateKeyInQueryString = ( stateKey: string, urlState: UrlState | undefined ) => (queryString: string) => { + debugger; const previousQueryValues = QueryString.decode(queryString); const encodedUrlState = typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; + debugger; return QueryString.encode({ ...previousQueryValues, [stateKey]: encodedUrlState, diff --git a/x-pack/plugins/siem/public/pages/home/index.tsx b/x-pack/plugins/siem/public/pages/home/index.tsx index eabf3ec14b858..070e501457106 100644 --- a/x-pack/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/plugins/siem/public/pages/home/index.tsx @@ -28,6 +28,7 @@ import { SiemNavigation } from '../../components/navigation'; import { PageHeadline } from '../../components/page_headline'; import { SuperDatePicker } from '../../components/super_date_picker'; import { StatefulTimeline } from '../../components/timeline'; +import { UrlStateContainer } from '../../components/url_state'; import { NotFoundPage } from '../404'; import { HostsContainer } from '../hosts'; import { NetworkContainer } from '../network'; @@ -108,6 +109,7 @@ export const HomePage = pure(() => ( + diff --git a/x-pack/plugins/siem/public/store/inputs/model.ts b/x-pack/plugins/siem/public/store/inputs/model.ts index 895b97e7c44af..15c6ceddca9ba 100644 --- a/x-pack/plugins/siem/public/store/inputs/model.ts +++ b/x-pack/plugins/siem/public/store/inputs/model.ts @@ -22,6 +22,8 @@ interface RelativeTimeRange { export type InputsModelId = 'global' | 'timeline'; +export type TimeRangeKinds = 'absolute' | 'relative'; + export type TimeRange = AbsoluteTimeRange | RelativeTimeRange; export interface Policy { 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/utils/url_state.test.tsx b/x-pack/plugins/siem/public/utils/url_state.test.tsx new file mode 100644 index 0000000000000..db86d28cbad34 --- /dev/null +++ b/x-pack/plugins/siem/public/utils/url_state.test.tsx @@ -0,0 +1,17 @@ +/* + * 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 { shallow } from 'enzyme'; +// import * as React from 'react'; + +import { replaceStateKeyInQueryString } from '../components/url_state'; + +describe('url_state', () => { + test('url_state', () => { + const result = replaceStateKeyInQueryString('yourmom', 2); + expect(result).toEqual(12); + }); +}); diff --git a/x-pack/plugins/siem/public/utils/with_global_time.tsx b/x-pack/plugins/siem/public/utils/with_global_time.tsx index 6b4f47953998a..d524d0135ca86 100644 --- a/x-pack/plugins/siem/public/utils/with_global_time.tsx +++ b/x-pack/plugins/siem/public/utils/with_global_time.tsx @@ -9,7 +9,7 @@ import moment from 'moment'; import React, { useCallback, useContext, useEffect, useState } from 'react'; import { TimerangeInput } from '../graphql/types'; import { useInterval } from './hooks/use_interval'; -import { replaceStateKeyInQueryString, UrlStateContainer } from './url_state'; +import { replaceStateKeyInQueryString, UrlStateContainer } from '../components/url_state'; export interface GlobalTimeState { isAutoReloading: boolean; From 5d6b75e9db22e685b3eaec33fd6a918919c076da Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 7 May 2019 15:43:16 -0600 Subject: [PATCH 06/18] timerange global is working --- .../plugins/siem/public/components/url_state/index.tsx | 9 +++++---- x-pack/plugins/siem/public/store/inputs/model.ts | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/siem/public/components/url_state/index.tsx b/x-pack/plugins/siem/public/components/url_state/index.tsx index 67bfc2e1b2d96..b0b91b6365bd7 100644 --- a/x-pack/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/plugins/siem/public/components/url_state/index.tsx @@ -15,7 +15,7 @@ import { decode, encode, RisonValue } from 'rison-node'; import { QueryString } from 'ui/utils/query_string'; import { ActionCreator } from 'typescript-fsa'; import { inputsActions, inputsSelectors, State } from '../../store'; -import { InputsModel, InputsModelId, TimeRangeKinds, RelativeTimeRange } from '../../store/inputs/model'; +import { UrlInputsModel, InputsModelId, TimeRangeKinds, TimeRange } from '../../store/inputs/model'; interface UrlState { global: any; @@ -111,11 +111,12 @@ class UrlStateContainerLifecycle extends React.Component { if (newUrlStateString) { switch (key) { case 'timerange': - const urlStateData: InputsModel = decodeRisonUrlState(newUrlStateString) + const urlStateData: UrlInputsModel = decodeRisonUrlState(newUrlStateString) const globalType: TimeRangeKinds = get('global.kind', urlStateData); - const globalRange: RelativeTimeRange = urlStateData.global; + const globalRange: TimeRange = urlStateData.global; + const globalId: InputsModelId = 'global'; if (globalType !== null) { - return this.urlStateMappedToActions.timerange[globalType](globalRange); + return this.urlStateMappedToActions.timerange[globalType]({...globalRange, id: globalId}); } } return; diff --git a/x-pack/plugins/siem/public/store/inputs/model.ts b/x-pack/plugins/siem/public/store/inputs/model.ts index 15c6ceddca9ba..7e8291888aa8b 100644 --- a/x-pack/plugins/siem/public/store/inputs/model.ts +++ b/x-pack/plugins/siem/public/store/inputs/model.ts @@ -49,3 +49,7 @@ export interface InputsModel { global: InputsRange; timeline: InputsRange; } +export interface UrlInputsModel { + global: TimeRange; + timeline: TimeRange; +} From 12f8e6102e977011490666c26eaeb6054cc2550c Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 9 May 2019 14:20:34 -0600 Subject: [PATCH 07/18] timerange working --- .../public/components/url_state/index.tsx | 102 +++++++++++------- 1 file changed, 64 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/siem/public/components/url_state/index.tsx b/x-pack/plugins/siem/public/components/url_state/index.tsx index b0b91b6365bd7..3591fa2a457f2 100644 --- a/x-pack/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/plugins/siem/public/components/url_state/index.tsx @@ -15,28 +15,25 @@ import { decode, encode, RisonValue } from 'rison-node'; import { QueryString } from 'ui/utils/query_string'; import { ActionCreator } from 'typescript-fsa'; import { inputsActions, inputsSelectors, State } from '../../store'; -import { UrlInputsModel, InputsModelId, TimeRangeKinds, TimeRange } from '../../store/inputs/model'; +import { InputsModelId, TimeRange, TimeRangeKinds, UrlInputsModel } from '../../store/inputs/model'; interface UrlState { - global: any; - timeline: any; + timerange: UrlInputsModel; + [key: string]: any; } -interface UrlStateContainerProps { - urlState: UrlState | undefined; - urlStateKey: string; +interface UrlStateProps { + urlState: UrlState; mapToUrlState?: (value: any) => UrlState | undefined; onChange?: (urlState: UrlState, previousUrlState: UrlState | undefined) => void; onInitialize?: (urlState: UrlState | undefined) => void; } -interface UrlStateReduxProps { - limit: number; -} - interface UrlStateDispatchProps { setAbsoluteTimerange: ActionCreator<{ id: InputsModelId; + fromStr: undefined; + toStr: undefined; from: number; to: number; }>; @@ -49,14 +46,16 @@ interface UrlStateDispatchProps { }>; } -interface UrlStateContainerLifecycleProps extends UrlStateContainerProps { +type UrlStateContainerProps = UrlStateProps & UrlStateDispatchProps; + +interface UrlStateContainerLifecycles { location: Location; history: History; } -type UrlStateProps = UrlStateContainerLifecycleProps & UrlStateReduxProps & UrlStateDispatchProps; +type UrlStateContainerLifecycleProps = UrlStateContainerLifecycles & UrlStateContainerProps; -class UrlStateContainerLifecycle extends React.Component { +class UrlStateContainerLifecycle extends React.Component { public render() { return null; } @@ -66,9 +65,12 @@ class UrlStateContainerLifecycle extends React.Component { urlState: prevUrlState, }: UrlStateContainerLifecycleProps) { const { history, location, urlState } = this.props; - - if (urlState !== prevUrlState) { - this.replaceStateInLocation(urlState); + if (JSON.stringify(urlState) !== JSON.stringify(prevUrlState)) { + Object.keys(urlState).map((urlKey: string) => { + if (JSON.stringify(urlState[urlKey]) !== JSON.stringify(prevUrlState[urlKey])) { + this.replaceStateInLocation(urlState[urlKey], urlKey); + } + }); } if (history.action === 'POP' && location !== prevLocation) { @@ -82,18 +84,20 @@ class UrlStateContainerLifecycle extends React.Component { } // eslint-disable-next-line @typescript-eslint/member-ordering this is really a method despite what eslint thinks - private replaceStateInLocation = throttle(1000, (urlState: UrlState | undefined) => { - const { history, location, urlStateKey } = this.props; - - const newLocation = replaceQueryStringInLocation( - location, - replaceStateKeyInQueryString(urlStateKey, urlState)(getQueryStringFromLocation(location)) - ); - - if (newLocation !== location) { - history.replace(newLocation); + private replaceStateInLocation = throttle( + 1000, + (urlState: UrlState | undefined, urlStateKey: string) => { + const { history, location } = this.props; + const newLocation = replaceQueryStringInLocation( + location, + replaceStateKeyInQueryString(urlStateKey, urlState)(getQueryStringFromLocation(location)) + ); + + if (newLocation !== location) { + history.replace(newLocation); + } } - }); + ); private urlStateMappedToActions = { timerange: { @@ -111,12 +115,26 @@ class UrlStateContainerLifecycle extends React.Component { if (newUrlStateString) { switch (key) { case 'timerange': - const urlStateData: UrlInputsModel = decodeRisonUrlState(newUrlStateString) + const urlStateData: UrlInputsModel = decodeRisonUrlState(newUrlStateString); const globalType: TimeRangeKinds = get('global.kind', urlStateData); const globalRange: TimeRange = urlStateData.global; const globalId: InputsModelId = 'global'; if (globalType !== null) { - return this.urlStateMappedToActions.timerange[globalType]({...globalRange, id: globalId}); + // @ts-ignore + this.urlStateMappedToActions.timerange[globalType]({ + ...globalRange, + id: globalId, + }); + } + const timelineRange: TimeRange = urlStateData.timeline; + const timelineType: TimeRangeKinds = get('timeline.kind', urlStateData); + const timelineId: InputsModelId = 'timeline'; + if (timelineType !== null) { + // @ts-ignore + this.urlStateMappedToActions.timerange[timelineType]({ + ...timelineRange, + id: timelineId, + }); } } return; @@ -125,7 +143,7 @@ class UrlStateContainerLifecycle extends React.Component { }; private handleLocationChange = (prevLocation: Location, newLocation: Location) => { - const { onChange, mapToUrlState, urlStateKey } = this.props; + const { onChange, mapToUrlState } = this.props; if (!onChange || !mapToUrlState) { return; @@ -133,11 +151,11 @@ class UrlStateContainerLifecycle extends React.Component { const previousUrlStateString = getParamFromQueryString( getQueryStringFromLocation(prevLocation), - urlStateKey + 'urlStateKey' ); const newUrlStateString = getParamFromQueryString( getQueryStringFromLocation(newLocation), - urlStateKey + 'urlStateKey' ); if (previousUrlStateString !== newUrlStateString) { @@ -161,10 +179,19 @@ export const UrlStateComponents = pure(props => ( const makeMapStateToProps = () => { const getInputsSelector = inputsSelectors.inputsSelector(); - const mapStateToProps = (state: State) => ({ - ...getInputsSelector(state), - urlStateKey: 'qwrty', - }); + const mapStateToProps = (state: State) => { + const inputState = getInputsSelector(state); + return { + urlState: { + timerange: inputState + ? { + global: get('global.timerange', inputState), + timeline: get('timeline.timerange', inputState), + } + : {}, + }, + }; + }; return mapStateToProps; }; @@ -174,6 +201,7 @@ export const UrlStateContainer = connect( setAbsoluteTimerange: inputsActions.setAbsoluteRangeDatePicker, setRelativeTimerange: inputsActions.setRelativeRangeDatePicker, } + // @ts-ignore )(UrlStateComponents); export const decodeRisonUrlState = (value: string | undefined): RisonValue | any | undefined => { @@ -200,11 +228,9 @@ export const replaceStateKeyInQueryString = ( stateKey: string, urlState: UrlState | undefined ) => (queryString: string) => { - debugger; const previousQueryValues = QueryString.decode(queryString); const encodedUrlState = typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; - debugger; return QueryString.encode({ ...previousQueryValues, [stateKey]: encodedUrlState, From b9c82f54da474b026816091260938175d25c13f6 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 9 May 2019 14:22:01 -0600 Subject: [PATCH 08/18] rm unnecessary files --- .../siem/public/utils/hooks/use_interval.ts | 32 ---- .../siem/public/utils/url_state.test.tsx | 17 -- .../siem/public/utils/with_global_time.tsx | 167 ------------------ 3 files changed, 216 deletions(-) delete mode 100644 x-pack/plugins/siem/public/utils/hooks/use_interval.ts delete mode 100644 x-pack/plugins/siem/public/utils/url_state.test.tsx delete mode 100644 x-pack/plugins/siem/public/utils/with_global_time.tsx diff --git a/x-pack/plugins/siem/public/utils/hooks/use_interval.ts b/x-pack/plugins/siem/public/utils/hooks/use_interval.ts deleted file mode 100644 index 8e7592d7e4620..0000000000000 --- a/x-pack/plugins/siem/public/utils/hooks/use_interval.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 { useEffect, useRef } from 'react'; - -export const useInterval = (callback: () => void, delay: number | null) => { - const savedCallback = useRef(callback); - - useEffect( - () => { - savedCallback.current = callback; - }, - [callback] - ); - - useEffect( - () => { - function tick() { - savedCallback.current(); - } - - if (delay !== null) { - const id = setInterval(tick, delay); - return () => clearInterval(id); - } - }, - [delay] - ); -} diff --git a/x-pack/plugins/siem/public/utils/url_state.test.tsx b/x-pack/plugins/siem/public/utils/url_state.test.tsx deleted file mode 100644 index db86d28cbad34..0000000000000 --- a/x-pack/plugins/siem/public/utils/url_state.test.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -// import * as React from 'react'; - -import { replaceStateKeyInQueryString } from '../components/url_state'; - -describe('url_state', () => { - test('url_state', () => { - const result = replaceStateKeyInQueryString('yourmom', 2); - expect(result).toEqual(12); - }); -}); diff --git a/x-pack/plugins/siem/public/utils/with_global_time.tsx b/x-pack/plugins/siem/public/utils/with_global_time.tsx deleted file mode 100644 index d524d0135ca86..0000000000000 --- a/x-pack/plugins/siem/public/utils/with_global_time.tsx +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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 createContainer from 'constate-latest'; -import moment from 'moment'; -import React, { useCallback, useContext, useEffect, useState } from 'react'; -import { TimerangeInput } from '../graphql/types'; -import { useInterval } from './hooks/use_interval'; -import { replaceStateKeyInQueryString, UrlStateContainer } from '../components/url_state'; - -export interface GlobalTimeState { - isAutoReloading: boolean; - refreshInterval: number; - setAutoReload: (isAutoReloading: boolean) => void; - setRefreshInterval: (refreshInterval: number) => void; - setTimeRange: (timeRange: TimerangeInput) => void; - timeRange: TimerangeInput; -} - -export const useGlobalTime = () => { - const [isAutoReloading, setAutoReload] = useState(false); - const [refreshInterval, setRefreshInterval] = useState(5000); - const [timeRange, setTimeRange] = useState({ - from: moment() - .subtract(1, 'hour') - .valueOf(), - to: moment().valueOf(), - interval: '>=1m', - }); - - const setTimeRangeToNow = useCallback( - () => { - const range = timeRange.to - timeRange.from; - const nowInMs = moment().valueOf(); - setTimeRange({ - from: nowInMs - range, - to: nowInMs, - interval: '>=1m', - }); - }, - [timeRange.from, timeRange.to] - ); - - useInterval(setTimeRangeToNow, isAutoReloading ? refreshInterval : null); - - useEffect( - () => { - if (isAutoReloading) { - setTimeRangeToNow(); - } - }, - [isAutoReloading] - ); - - return { - timeRange, - setTimeRange, - refreshInterval, - setRefreshInterval, - isAutoReloading, - setAutoReload, - }; -}; - -export const GlobalTimeContainer = createContainer(useGlobalTime); - -interface WithMetricsTimeProps { - children: (args: GlobalTimeState) => React.ReactElement; -} -export const WithGlobalTime: React.FunctionComponent = ({ - children, -}: WithMetricsTimeProps) => { - const metricsTimeState = useContext(GlobalTimeContainer.Context); - return children({ ...metricsTimeState }); -}; - -/** - * Url State - */ - -interface GlobalTimeUrlState { - time?: GlobalTimeState['timeRange']; - autoReload?: boolean; - refreshInterval?: number; -} - -export const WithGlobalTimeUrlState = () => ( - - {({ - timeRange, - setTimeRange, - refreshInterval, - setRefreshInterval, - isAutoReloading, - setAutoReload, - }) => ( - { - if (newUrlState && newUrlState.time) { - setTimeRange(newUrlState.time); - } - if (newUrlState && newUrlState.autoReload) { - setAutoReload(true); - } else if ( - newUrlState && - typeof newUrlState.autoReload !== 'undefined' && - !newUrlState.autoReload - ) { - setAutoReload(false); - } - if (newUrlState && newUrlState.refreshInterval) { - setRefreshInterval(newUrlState.refreshInterval); - } - }} - onInitialize={initialUrlState => { - if (initialUrlState && initialUrlState.time) { - setTimeRange(initialUrlState.time); - } - if (initialUrlState && initialUrlState.autoReload) { - setAutoReload(true); - } - if (initialUrlState && initialUrlState.refreshInterval) { - setRefreshInterval(initialUrlState.refreshInterval); - } - }} - /> - )} - -); - -const mapToUrlState = (value: any): GlobalTimeUrlState | undefined => - value - ? { - time: mapToTimeUrlState(value.time), - autoReload: mapToAutoReloadUrlState(value.autoReload), - refreshInterval: mapToRefreshInterval(value.refreshInterval), - } - : undefined; - -const mapToTimeUrlState = (value: any) => - value && (typeof value.to === 'number' && typeof value.from === 'number') ? value : undefined; - -const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); - -const mapToRefreshInterval = (value: any) => (typeof value === 'number' ? value : undefined); - -export const replaceGlobalTimeInQueryString = (from: number, to: number) => - Number.isNaN(from) || Number.isNaN(to) - ? (value: string) => value - : replaceStateKeyInQueryString('metricTime', { - autoReload: false, - time: { - interval: '>=1m', - from, - to, - }, - }); From 01076b593bae00e0394cb8f2f1fa382b9b71ee63 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 9 May 2019 14:23:03 -0600 Subject: [PATCH 09/18] rm unnecessary files --- .../plugins/infra/public/utils/url_state.tsx | 39 ------------------- 1 file changed, 39 deletions(-) diff --git a/x-pack/plugins/infra/public/utils/url_state.tsx b/x-pack/plugins/infra/public/utils/url_state.tsx index 9cc9aae1163c2..43a803c81230b 100644 --- a/x-pack/plugins/infra/public/utils/url_state.tsx +++ b/x-pack/plugins/infra/public/utils/url_state.tsx @@ -110,45 +110,6 @@ class UrlStateContainerLifecycle extends React.Component< }; } - -/** - urlState={{ - time: timeRange, - autoReload: isAutoReloading, - refreshInterval, - }} - urlStateKey="metricTime" - mapToUrlState={mapToUrlState} - onChange={newUrlState => { - if (newUrlState && newUrlState.time) { - setTimeRange(newUrlState.time); - } - if (newUrlState && newUrlState.autoReload) { - setAutoReload(true); - } else if ( - newUrlState && - typeof newUrlState.autoReload !== 'undefined' && - !newUrlState.autoReload - ) { - setAutoReload(false); - } - if (newUrlState && newUrlState.refreshInterval) { - setRefreshInterval(newUrlState.refreshInterval); - } -}} - onInitialize={initialUrlState => { - if (initialUrlState && initialUrlState.time) { - setTimeRange(initialUrlState.time); - } - if (initialUrlState && initialUrlState.autoReload) { - setAutoReload(true); - } - if (initialUrlState && initialUrlState.refreshInterval) { - setRefreshInterval(initialUrlState.refreshInterval); - } -}} - */ - export const UrlStateContainer = ( props: UrlStateContainerProps ) => ( From 2041a2556de6bf1b0ce632db0ea26ad0e9fa5678 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Fri, 17 May 2019 13:08:43 -0400 Subject: [PATCH 10/18] wooohooo kql working --- .../public/components/url_state/index.tsx | 208 +++++++++++++++--- .../plugins/siem/public/pages/home/index.tsx | 2 - .../siem/public/pages/hosts/host_details.tsx | 2 + .../plugins/siem/public/pages/hosts/hosts.tsx | 2 + .../siem/public/pages/network/ip_details.tsx | 2 + .../siem/public/pages/network/network.tsx | 2 + .../siem/public/store/hosts/selectors.ts | 6 + .../plugins/siem/public/store/inputs/model.ts | 10 +- .../siem/public/store/network/selectors.ts | 6 + 9 files changed, 211 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/siem/public/components/url_state/index.tsx b/x-pack/plugins/siem/public/components/url_state/index.tsx index 3591fa2a457f2..fc4f785450c30 100644 --- a/x-pack/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/plugins/siem/public/components/url_state/index.tsx @@ -14,48 +14,130 @@ import { decode, encode, RisonValue } from 'rison-node'; import { QueryString } from 'ui/utils/query_string'; import { ActionCreator } from 'typescript-fsa'; -import { inputsActions, inputsSelectors, State } from '../../store'; -import { InputsModelId, TimeRange, TimeRangeKinds, UrlInputsModel } from '../../store/inputs/model'; +import { StaticIndexPattern } from 'ui/index_patterns'; +import { inputsActions, hostsActions, networkActions } from '../../store/actions'; +import { + hostsModel, + hostsSelectors, + inputsSelectors, + 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; + type: hostsModel.HostsType; + model: 'hosts'; +} + +interface KqlQueryNetwork { + filterQuery: KueryFilterQuery; + type: networkModel.NetworkType; + model: 'network'; +} + +type KqlQuery = KqlQueryHosts | KqlQueryNetwork; interface UrlState { timerange: UrlInputsModel; + kqlQuery: KqlQuery[]; [key: string]: any; } interface UrlStateProps { - urlState: UrlState; + indexPattern: StaticIndexPattern; mapToUrlState?: (value: any) => UrlState | undefined; onChange?: (urlState: UrlState, previousUrlState: UrlState | undefined) => void; onInitialize?: (urlState: UrlState | undefined) => void; + urlState: UrlState; } interface UrlStateDispatchProps { + setHostsKql: ActionCreator<{ + filterQuery: SerializedFilterQuery; + hostsType: hostsModel.HostsType; + }>; + setNetworkKql: ActionCreator<{ + filterQuery: SerializedFilterQuery; + networkType: networkModel.NetworkType; + }>; setAbsoluteTimerange: ActionCreator<{ - id: InputsModelId; - fromStr: undefined; - toStr: undefined; from: number; + fromStr: undefined; + id: InputsModelId; to: number; + toStr: undefined; }>; setRelativeTimerange: ActionCreator<{ - id: InputsModelId; - fromStr: string; - toStr: string; from: number; + fromStr: string; + id: InputsModelId; to: number; + toStr: string; + }>; + toggleTimelineLinkTo: ActionCreator<{ + linkToId: InputsModelId; }>; } type UrlStateContainerProps = UrlStateProps & UrlStateDispatchProps; interface UrlStateContainerLifecycles { - location: Location; history: History; + location: Location; } - type UrlStateContainerLifecycleProps = UrlStateContainerLifecycles & 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 + ) { + return true; + } + if ( + trailingPath[0] === 'network' && + kql.model === 'network' && + kql.type === networkModel.NetworkType.page + ) { + return true; + } + if ( + pathname.match(/hosts\/.*?/) && + kql.model === 'hosts' && + kql.type === hostsModel.HostsType.details + ) { + return true; + } + if ( + pathname.match(/network\/ip\/.*?/) && + kql.model === 'network' && + kql.type === networkModel.NetworkType.details + ) { + return true; + } + } + return false; +}; + class UrlStateContainerLifecycle extends React.Component { + public readonly state = { + kqlQuery: '', + }; + public render() { return null; } @@ -66,9 +148,20 @@ class UrlStateContainerLifecycle extends React.Component { + Object.keys(urlState).forEach((urlKey: string) => { if (JSON.stringify(urlState[urlKey]) !== JSON.stringify(prevUrlState[urlKey])) { - this.replaceStateInLocation(urlState[urlKey], 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); + } } }); } @@ -86,7 +179,7 @@ class UrlStateContainerLifecycle extends React.Component { + (urlState: UrlInputsModel | KqlQuery, urlStateKey: string) => { const { history, location } = this.props; const newLocation = replaceQueryStringInLocation( location, @@ -104,8 +197,9 @@ class UrlStateContainerLifecycle extends React.Component(props => ( const makeMapStateToProps = () => { const getInputsSelector = inputsSelectors.inputsSelector(); + const getHostsFilterQueryAsKuery = hostsSelectors.hostsFilterQueryAsKuery(); + const getNetworkFilterQueryAsKuery = networkSelectors.networkFilterQueryAsKuery(); const mapStateToProps = (state: State) => { const inputState = getInputsSelector(state); return { urlState: { timerange: inputState ? { - global: get('global.timerange', inputState), - timeline: get('timeline.timerange', inputState), + global: { + ...get('global.timerange', inputState), + linkTo: get('global.linkTo', inputState), + }, + timeline: { + ...get('timeline.timerange', inputState), + linkTo: get('timeline.linkTo', inputState), + }, } : {}, + kqlQuery: [ + { + filterQuery: getHostsFilterQueryAsKuery(state, hostsModel.HostsType.details) || '', + type: hostsModel.HostsType.details, + model: 'hosts', + }, + { + filterQuery: getHostsFilterQueryAsKuery(state, hostsModel.HostsType.page) || '', + type: hostsModel.HostsType.page, + model: 'hosts', + }, + { + filterQuery: + getNetworkFilterQueryAsKuery(state, networkModel.NetworkType.details) || '', + type: networkModel.NetworkType.details, + model: 'network', + }, + { + filterQuery: getNetworkFilterQueryAsKuery(state, networkModel.NetworkType.page) || '', + type: networkModel.NetworkType.page, + model: 'network', + }, + ], }, }; }; @@ -200,6 +355,9 @@ export const UrlStateContainer = connect( { setAbsoluteTimerange: inputsActions.setAbsoluteRangeDatePicker, setRelativeTimerange: inputsActions.setRelativeRangeDatePicker, + setNetworkKql: networkActions.applyNetworkFilterQuery, + setHostsKql: hostsActions.applyHostsFilterQuery, + toggleTimelineLinkTo: inputsActions.toggleTimelineLinkTo, } // @ts-ignore )(UrlStateComponents); diff --git a/x-pack/plugins/siem/public/pages/home/index.tsx b/x-pack/plugins/siem/public/pages/home/index.tsx index fee358b9804b7..5641057760d40 100644 --- a/x-pack/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/plugins/siem/public/pages/home/index.tsx @@ -19,7 +19,6 @@ import { HelpMenu } from '../../components/help_menu'; import { LinkToPage } from '../../components/link_to'; import { SiemNavigation } from '../../components/navigation'; import { StatefulTimeline } from '../../components/timeline'; -import { UrlStateContainer } from '../../components/url_state'; import { NotFoundPage } from '../404'; import { HostsContainer } from '../hosts'; import { NetworkContainer } from '../network'; @@ -117,7 +116,6 @@ export const HomePage = pure(() => ( - 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( + (({ filterQuery }) => ( + ( + (({ 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 7e8291888aa8b..8193acdd87050 100644 --- a/x-pack/plugins/siem/public/store/inputs/model.ts +++ b/x-pack/plugins/siem/public/store/inputs/model.ts @@ -26,6 +26,8 @@ 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 @@ -45,11 +47,15 @@ export interface InputsRange { linkTo: InputsModelId[]; } +interface LinkTo { + linkTo: InputsModelId[]; +} + export interface InputsModel { global: InputsRange; timeline: InputsRange; } export interface UrlInputsModel { - global: TimeRange; - timeline: TimeRange; + global: TimeRange & LinkTo; + timeline: TimeRange & LinkTo; } 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, From 02055f453bbd96cf2d9bcfb6435f9c6b51aff498 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Fri, 17 May 2019 14:13:00 -0400 Subject: [PATCH 11/18] fix current tests --- .../siem/public/pages/hosts/hosts.test.tsx | 35 +++++++++++++++++-- .../public/pages/network/network.test.tsx | 35 +++++++++++++++++-- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx b/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx index e1c80c1317411..b8ad66839aa1f 100644 --- a/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx +++ b/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx @@ -6,6 +6,7 @@ import { mount } from 'enzyme'; import * as React from 'react'; +import { Router } from 'react-router-dom'; import '../../mock/match_media'; import { Hosts } from './hosts'; @@ -38,6 +39,28 @@ let localSource: Array<{ }; }>; +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(), +}; + describe('Hosts', () => { describe('rendering', () => { beforeEach(() => { @@ -51,7 +74,9 @@ describe('Hosts', () => { const wrapper = mount( - + + + ); @@ -68,7 +93,9 @@ describe('Hosts', () => { const wrapper = mount( - + + + ); @@ -85,7 +112,9 @@ describe('Hosts', () => { const wrapper = mount( - + + + ); diff --git a/x-pack/plugins/siem/public/pages/network/network.test.tsx b/x-pack/plugins/siem/public/pages/network/network.test.tsx index 2c329a0bc9cba..452319fcc1ad9 100644 --- a/x-pack/plugins/siem/public/pages/network/network.test.tsx +++ b/x-pack/plugins/siem/public/pages/network/network.test.tsx @@ -6,6 +6,7 @@ import { mount } from 'enzyme'; import * as React from 'react'; +import { Router } from 'react-router-dom'; import '../../mock/match_media'; import { Network } from './network'; @@ -38,6 +39,28 @@ let localSource: Array<{ }; }>; +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(), +}; + describe('Network', () => { describe('rendering', () => { beforeEach(() => { @@ -51,7 +74,9 @@ describe('Network', () => { const wrapper = mount( - + + + ); @@ -68,7 +93,9 @@ describe('Network', () => { const wrapper = mount( - + + + ); @@ -85,7 +112,9 @@ describe('Network', () => { const wrapper = mount( - + + + ); From 1f91df9e295acb51b03b96f4620068dff11f5f00 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Fri, 17 May 2019 14:43:52 -0400 Subject: [PATCH 12/18] add url state test --- .../__snapshots__/index.test.tsx.snap | 498 ++++++++++++++++++ .../components/url_state/index.test.tsx | 60 +++ .../public/components/url_state/index.tsx | 29 +- 3 files changed, 575 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/siem/public/components/url_state/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/plugins/siem/public/components/url_state/index.test.tsx 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..283cc3aab4d37 --- /dev/null +++ b/x-pack/plugins/siem/public/components/url_state/__snapshots__/index.test.tsx.snap @@ -0,0 +1,498 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UrlStateComponents 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..8f406b4352234 --- /dev/null +++ b/x-pack/plugins/siem/public/components/url_state/index.test.tsx @@ -0,0 +1,60 @@ +/* + * 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 } 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 { UrlStateContainer } from './'; +import { mockGlobalState, TestProviders } from '../../mock'; +import { createStore, State } from '../../store'; + +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(), +}; +describe('UrlStateComponents', () => { + const state: State = mockGlobalState; + + let store = createStore(state); + + beforeEach(() => { + store = createStore(state); + }); + test('mounts and renders', () => { + const wrapper = mount( + + + + + + + + ); + const urlStateComponents = wrapper.find('[data-test-subj="urlStateComponents"]'); + urlStateComponents.exists(); + expect(toJson(wrapper)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/siem/public/components/url_state/index.tsx b/x-pack/plugins/siem/public/components/url_state/index.tsx index fc4f785450c30..0db4a8d894691 100644 --- a/x-pack/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/plugins/siem/public/components/url_state/index.tsx @@ -15,7 +15,7 @@ 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 { inputsActions, hostsActions, networkActions } from '../../store/actions'; +import { hostsActions, inputsActions, networkActions } from '../../store/actions'; import { hostsModel, hostsSelectors, @@ -36,22 +36,22 @@ import { convertKueryToElasticSearchQuery } from '../../lib/keury'; interface KqlQueryHosts { filterQuery: KueryFilterQuery; - type: hostsModel.HostsType; model: 'hosts'; + type: hostsModel.HostsType; } interface KqlQueryNetwork { filterQuery: KueryFilterQuery; - type: networkModel.NetworkType; model: 'network'; + type: networkModel.NetworkType; } type KqlQuery = KqlQueryHosts | KqlQueryNetwork; interface UrlState { - timerange: UrlInputsModel; - kqlQuery: KqlQuery[]; [key: string]: any; + kqlQuery: KqlQuery[]; + timerange: UrlInputsModel; } interface UrlStateProps { @@ -193,14 +193,14 @@ class UrlStateContainerLifecycle extends React.Component { @@ -296,7 +296,12 @@ class UrlStateContainerLifecycle extends React.Component(props => ( > {({ history, location }) => ( - + )} )); @@ -354,9 +359,9 @@ export const UrlStateContainer = connect( makeMapStateToProps, { setAbsoluteTimerange: inputsActions.setAbsoluteRangeDatePicker, - setRelativeTimerange: inputsActions.setRelativeRangeDatePicker, - setNetworkKql: networkActions.applyNetworkFilterQuery, setHostsKql: hostsActions.applyHostsFilterQuery, + setNetworkKql: networkActions.applyNetworkFilterQuery, + setRelativeTimerange: inputsActions.setRelativeRangeDatePicker, toggleTimelineLinkTo: inputsActions.toggleTimelineLinkTo, } // @ts-ignore From 2e7f74df87be27221d0b4a44f2cc0ad279c92a69 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Sun, 19 May 2019 21:36:58 -0600 Subject: [PATCH 13/18] add tests --- .../__snapshots__/index.test.tsx.snap | 2 +- .../components/url_state/index.test.tsx | 272 ++++++++++++++++-- .../public/components/url_state/index.tsx | 11 +- 3 files changed, 256 insertions(+), 29 deletions(-) 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 index 283cc3aab4d37..32351ae637a6a 100644 --- 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 @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UrlStateComponents mounts and renders 1`] = ` +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 index 8f406b4352234..67936106c4cf3 100644 --- a/x-pack/plugins/siem/public/components/url_state/index.test.tsx +++ b/x-pack/plugins/siem/public/components/url_state/index.test.tsx @@ -4,15 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; +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 { UrlStateContainer } from './'; +import { + isKqlForRoute, + UrlStateContainer, + UrlStateContainerLifecycle, + UrlStateContainerLifecycleProps, +} from './'; import { mockGlobalState, TestProviders } from '../../mock'; -import { createStore, State } from '../../store'; +import { + createStore, + hostsModel, + networkModel, + State, + KueryFilterQuery, + SerializedFilterQuery, +} from '../../store'; +import { ActionCreator } from 'typescript-fsa'; +import { InputsModelId } from '../../store/inputs/model'; type Action = 'PUSH' | 'POP' | 'REPLACE'; const pop: Action = 'POP'; @@ -35,26 +49,244 @@ const mockHistory = { 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: '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', () => { - const state: State = mockGlobalState; + 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', () => { + 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(); + expect(mockHistory.replace).toBeCalledWith({ + hash: '', + pathname: '/network', + search: + '?timerange=(timeline:(from:1558048243696,fromStr:now-24h,kind:relative,linkTo:!(global),to:1558134643697,toStr:now))', + state: '', + }); + }); + test('componentDidUpdate - kql query redux state updates the url', () => { + const wrapper = shallow(); - let store = createStore(state); + const newUrlState = { + kqlQuery: [ + { + filterQuery, + type: networkModel.NetworkType.details, + model: 'network', + }, + ], + }; - beforeEach(() => { - store = createStore(state); + wrapper.setProps({ urlState: newUrlState }); + wrapper.update(); + expect(mockHistory.replace).toBeCalledWith({ + hash: '', + pathname: '/network', + search: + "?kqlQuery=(filterQuery:(expression:'host.name:%22siem-es%22',kind:kuery),model:network,type:details)", + state: '', + }); + }); }); - test('mounts and renders', () => { - const wrapper = mount( - - - - - - - - ); - const urlStateComponents = wrapper.find('[data-test-subj="urlStateComponents"]'); - urlStateComponents.exists(); - expect(toJson(wrapper)).toMatchSnapshot(); + describe('isKqlForRoute', () => { + test('host page and host page kuery', () => { + const result = isKqlForRoute('/hosts', { + filterQuery: { + expression: 'host.name:"siem-kibana"', + kind: 'kuery', + }, + model: '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: '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: '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: '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: '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: '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: '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: '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 index 0db4a8d894691..96a782e8ac185 100644 --- a/x-pack/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/plugins/siem/public/components/url_state/index.tsx @@ -46,7 +46,7 @@ interface KqlQueryNetwork { type: networkModel.NetworkType; } -type KqlQuery = KqlQueryHosts | KqlQueryNetwork; +export type KqlQuery = KqlQueryHosts | KqlQueryNetwork; interface UrlState { [key: string]: any; @@ -96,7 +96,7 @@ interface UrlStateContainerLifecycles { history: History; location: Location; } -type UrlStateContainerLifecycleProps = UrlStateContainerLifecycles & UrlStateContainerProps; +export type UrlStateContainerLifecycleProps = UrlStateContainerLifecycles & UrlStateContainerProps; export const isKqlForRoute = (pathname: string, kql: KqlQuery): boolean => { const trailingPath = pathname.match(/([^\/]+$)/); @@ -133,11 +133,7 @@ export const isKqlForRoute = (pathname: string, kql: KqlQuery): boolean => { return false; }; -class UrlStateContainerLifecycle extends React.Component { - public readonly state = { - kqlQuery: '', - }; - +export class UrlStateContainerLifecycle extends React.Component { public render() { return null; } @@ -176,7 +172,6 @@ class UrlStateContainerLifecycle extends React.Component { From 497d952eca0397e2efe373aea188a5c1153a3418 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 20 May 2019 09:01:44 -0600 Subject: [PATCH 14/18] man linter, make up your mind --- .../siem/public/components/super_date_picker/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/plugins/siem/public/components/super_date_picker/index.tsx index 46007e6f2137c..f96265d6fd790 100644 --- a/x-pack/plugins/siem/public/components/super_date_picker/index.tsx +++ b/x-pack/plugins/siem/public/components/super_date_picker/index.tsx @@ -33,7 +33,7 @@ type MyEuiSuperDatePickerProps = Pick< | 'refreshInterval' | 'showUpdateButton' | 'start' - > & { +> & { isLoading?: boolean; }; const MyEuiSuperDatePicker: React.SFC = EuiSuperDatePicker; @@ -90,7 +90,7 @@ export interface SuperDatePickerState { export const SuperDatePickerComponent = class extends Component< SuperDatePickerProps, SuperDatePickerState - > { +> { constructor(props: SuperDatePickerProps) { super(props); From d7d1fe6daa1d3d359676f5df02a883bd5c2aae22 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 20 May 2019 09:22:13 -0600 Subject: [PATCH 15/18] further cleanup --- .../components/url_state/index.test.tsx | 21 +-- .../public/components/url_state/index.tsx | 141 ++++++++---------- x-pack/plugins/siem/public/store/model.ts | 5 + 3 files changed, 80 insertions(+), 87 deletions(-) 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 index 67936106c4cf3..43ba951769e29 100644 --- a/x-pack/plugins/siem/public/components/url_state/index.test.tsx +++ b/x-pack/plugins/siem/public/components/url_state/index.test.tsx @@ -24,6 +24,7 @@ import { State, KueryFilterQuery, SerializedFilterQuery, + KueryFilterModel, } from '../../store'; import { ActionCreator } from 'typescript-fsa'; import { InputsModelId } from '../../store/inputs/model'; @@ -92,7 +93,7 @@ const mockProps: UrlStateContainerLifecycleProps = { { filterQuery, type: networkModel.NetworkType.page, - model: 'network', + model: KueryFilterModel.network, }, ], }, @@ -183,7 +184,7 @@ describe('UrlStateComponents', () => { { filterQuery, type: networkModel.NetworkType.details, - model: 'network', + model: KueryFilterModel.network, }, ], }; @@ -206,7 +207,7 @@ describe('UrlStateComponents', () => { expression: 'host.name:"siem-kibana"', kind: 'kuery', }, - model: 'hosts', + model: KueryFilterModel.hosts, type: hostsModel.HostsType.page, }); expect(result).toBeTruthy(); @@ -217,7 +218,7 @@ describe('UrlStateComponents', () => { expression: 'host.name:"siem-kibana"', kind: 'kuery', }, - model: 'hosts', + model: KueryFilterModel.hosts, type: hostsModel.HostsType.details, }); expect(result).toBeFalsy(); @@ -228,7 +229,7 @@ describe('UrlStateComponents', () => { expression: 'host.name:"siem-kibana"', kind: 'kuery', }, - model: 'hosts', + model: KueryFilterModel.hosts, type: hostsModel.HostsType.details, }); expect(result).toBeTruthy(); @@ -239,7 +240,7 @@ describe('UrlStateComponents', () => { expression: 'host.name:"siem-kibana"', kind: 'kuery', }, - model: 'hosts', + model: KueryFilterModel.hosts, type: hostsModel.HostsType.page, }); expect(result).toBeFalsy(); @@ -250,7 +251,7 @@ describe('UrlStateComponents', () => { expression: 'network.name:"siem-kibana"', kind: 'kuery', }, - model: 'network', + model: KueryFilterModel.network, type: networkModel.NetworkType.page, }); expect(result).toBeTruthy(); @@ -261,7 +262,7 @@ describe('UrlStateComponents', () => { expression: 'network.name:"siem-kibana"', kind: 'kuery', }, - model: 'network', + model: KueryFilterModel.network, type: networkModel.NetworkType.details, }); expect(result).toBeFalsy(); @@ -272,7 +273,7 @@ describe('UrlStateComponents', () => { expression: 'network.name:"siem-kibana"', kind: 'kuery', }, - model: 'network', + model: KueryFilterModel.network, type: networkModel.NetworkType.details, }); expect(result).toBeTruthy(); @@ -283,7 +284,7 @@ describe('UrlStateComponents', () => { expression: 'network.name:"siem-kibana"', kind: 'kuery', }, - model: 'network', + 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 index 96a782e8ac185..7ade2d66fc2eb 100644 --- a/x-pack/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/plugins/siem/public/components/url_state/index.tsx @@ -20,6 +20,7 @@ import { hostsModel, hostsSelectors, inputsSelectors, + KueryFilterModel, KueryFilterQuery, networkModel, networkSelectors, @@ -36,13 +37,13 @@ import { convertKueryToElasticSearchQuery } from '../../lib/keury'; interface KqlQueryHosts { filterQuery: KueryFilterQuery; - model: 'hosts'; + model: KueryFilterModel.hosts; type: hostsModel.HostsType; } interface KqlQueryNetwork { filterQuery: KueryFilterQuery; - model: 'network'; + model: KueryFilterModel.network; type: networkModel.NetworkType; } @@ -102,30 +103,18 @@ 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 - ) { - return true; - } - if ( - trailingPath[0] === 'network' && - kql.model === 'network' && - kql.type === networkModel.NetworkType.page - ) { - return true; - } - if ( - pathname.match(/hosts\/.*?/) && - kql.model === 'hosts' && - kql.type === hostsModel.HostsType.details - ) { - return true; - } - if ( - pathname.match(/network\/ip\/.*?/) && - kql.model === 'network' && - kql.type === networkModel.NetworkType.details + (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; } @@ -199,64 +188,62 @@ export class UrlStateContainerLifecycle extends React.Component { - Object.keys(this.urlStateMappedToActions).map(key => { + Object.keys(this.urlStateMappedToActions).forEach(key => { const newUrlStateString = getParamFromQueryString(getQueryStringFromLocation(location), key); if (newUrlStateString) { - switch (key) { - case '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[globalType]({ - ...globalRange, - id: globalId, - }); + if (key === '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' }); } - 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, + // @ts-ignore + this.urlStateMappedToActions.timerange[globalType]({ + ...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 (key === 'kqlQuery') { + const kqlQueryStateData: KqlQuery = decodeRisonUrlState(newUrlStateString); + if (isKqlForRoute(location.pathname, kqlQueryStateData)) { + const filterQuery = { + query: kqlQueryStateData.filterQuery, + serializedQuery: convertKueryToElasticSearchQuery( + kqlQueryStateData.filterQuery.expression, + this.props.indexPattern + ), + }; + if (kqlQueryStateData.model === 'hosts') { + this.urlStateMappedToActions.kqlQuery.hosts({ + filterQuery, + hostsType: kqlQueryStateData.type, }); } - return; - case 'kqlQuery': - const kqlQueryStateData: KqlQuery = decodeRisonUrlState(newUrlStateString); - if (isKqlForRoute(location.pathname, kqlQueryStateData)) { - const filterQuery = { - query: kqlQueryStateData.filterQuery, - serializedQuery: convertKueryToElasticSearchQuery( - 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, - }); - } + if (kqlQueryStateData.model === 'network') { + this.urlStateMappedToActions.kqlQuery.network({ + filterQuery, + networkType: kqlQueryStateData.type, + }); } + } } - return; } }); }; diff --git a/x-pack/plugins/siem/public/store/model.ts b/x-pack/plugins/siem/public/store/model.ts index 7bc8d065e8685..25100df94a270 100644 --- a/x-pack/plugins/siem/public/store/model.ts +++ b/x-pack/plugins/siem/public/store/model.ts @@ -10,6 +10,11 @@ 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; From 2f33f6938e02abfb60d3cb0afe6dd1f302f5ed11 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 20 May 2019 10:39:15 -0600 Subject: [PATCH 16/18] suppress silly enzyme warning --- .../siem/public/pages/hosts/hosts.test.tsx | 127 +++++++++-------- .../public/pages/network/network.test.tsx | 128 ++++++++++-------- 2 files changed, 137 insertions(+), 118 deletions(-) diff --git a/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx b/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx index b8ad66839aa1f..3f7ebe6cab50a 100644 --- a/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx +++ b/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx @@ -61,67 +61,76 @@ const mockHistory = { listen: jest.fn(), }; -describe('Hosts', () => { - describe('rendering', () => { - beforeEach(() => { - localSource = cloneDeep(mocksSource); - }); +// 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( - - - - - - - - ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise(resolve => setTimeout(resolve)); - wrapper.update(); - expect(wrapper.text()).toContain(i18n.SETUP_INSTRUCTIONS); - }); +describe('Hosts - rendering', () => { + beforeAll(() => { + console.error = jest.fn(); + }); + + afterAll(() => { + console.error = originalError; + }); + beforeEach(() => { + localSource = cloneDeep(mocksSource); + }); - 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); - }); + 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); + }); + + 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); + }); - 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); - }); + 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); }); }); diff --git a/x-pack/plugins/siem/public/pages/network/network.test.tsx b/x-pack/plugins/siem/public/pages/network/network.test.tsx index 452319fcc1ad9..d3d789bc55040 100644 --- a/x-pack/plugins/siem/public/pages/network/network.test.tsx +++ b/x-pack/plugins/siem/public/pages/network/network.test.tsx @@ -60,68 +60,78 @@ const mockHistory = { 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; -describe('Network', () => { - describe('rendering', () => { - beforeEach(() => { - localSource = cloneDeep(mocksSource); - }); +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); - }); + 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); + }); - 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); - }); + 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); + }); - 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); - }); + 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); }); }); From c597fdfbdb55a0b63a9c2fe4b72951bf032cb58a Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 21 May 2019 10:42:03 -0600 Subject: [PATCH 17/18] ts issues and initial url state --- .../public/components/url_state/index.tsx | 210 +++++++++++------- 1 file changed, 127 insertions(+), 83 deletions(-) diff --git a/x-pack/plugins/siem/public/components/url_state/index.tsx b/x-pack/plugins/siem/public/components/url_state/index.tsx index 7ade2d66fc2eb..567eaf8404319 100644 --- a/x-pack/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/plugins/siem/public/components/url_state/index.tsx @@ -36,13 +36,13 @@ import { import { convertKueryToElasticSearchQuery } from '../../lib/keury'; interface KqlQueryHosts { - filterQuery: KueryFilterQuery; + filterQuery: KueryFilterQuery | null; model: KueryFilterModel.hosts; type: hostsModel.HostsType; } interface KqlQueryNetwork { - filterQuery: KueryFilterQuery; + filterQuery: KueryFilterQuery | null; model: KueryFilterModel.network; type: networkModel.NetworkType; } @@ -50,16 +50,16 @@ interface KqlQueryNetwork { export type KqlQuery = KqlQueryHosts | KqlQueryNetwork; interface UrlState { - [key: string]: any; kqlQuery: KqlQuery[]; timerange: UrlInputsModel; } +type KeyUrlState = keyof UrlState; interface UrlStateProps { indexPattern: StaticIndexPattern; - mapToUrlState?: (value: any) => UrlState | undefined; - onChange?: (urlState: UrlState, previousUrlState: UrlState | undefined) => void; - onInitialize?: (urlState: UrlState | undefined) => void; + mapToUrlState?: (value: any) => UrlState; + onChange?: (urlState: UrlState, previousUrlState: UrlState) => void; + onInitialize?: (urlState: UrlState) => void; urlState: UrlState; } @@ -91,13 +91,47 @@ interface UrlStateDispatchProps { }>; } +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 UrlStateContainerLifecycles { +interface UrlStateRouterProps { history: History; location: Location; } -export type UrlStateContainerLifecycleProps = UrlStateContainerLifecycles & UrlStateContainerProps; + +export type UrlStateContainerLifecycleProps = UrlStateRouterProps & UrlStateContainerProps; export const isKqlForRoute = (pathname: string, kql: KqlQuery): boolean => { const trailingPath = pathname.match(/([^\/]+$)/); @@ -133,19 +167,22 @@ export class UrlStateContainerLifecycle extends React.Component { - 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); + 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); + } } } }); @@ -163,7 +200,7 @@ export class UrlStateContainerLifecycle extends React.Component { + (urlState: UrlInputsModel | KqlQuery | KqlQuery[], urlStateKey: string) => { const { history, location } = this.props; const newLocation = replaceQueryStringInLocation( location, @@ -176,7 +213,7 @@ export class UrlStateContainerLifecycle extends React.Component { - Object.keys(this.urlStateMappedToActions).forEach(key => { - const newUrlStateString = getParamFromQueryString(getQueryStringFromLocation(location), key); - if (newUrlStateString) { - if (key === '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[globalType]({ - ...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' }); + 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' }); + } + if (globalType === 'absolute') { + this.urlStateMappedToActions.timerange.absolute({ + ...globalRange, + id: globalId, + }); + } } - // @ts-ignore - this.urlStateMappedToActions.timerange[timelineType]({ - ...timelineRange, - id: timelineId, - }); - } - } - if (key === 'kqlQuery') { - const kqlQueryStateData: KqlQuery = decodeRisonUrlState(newUrlStateString); - if (isKqlForRoute(location.pathname, kqlQueryStateData)) { - const filterQuery = { - query: kqlQueryStateData.filterQuery, - serializedQuery: convertKueryToElasticSearchQuery( - kqlQueryStateData.filterQuery.expression, - this.props.indexPattern - ), - }; - if (kqlQueryStateData.model === 'hosts') { - this.urlStateMappedToActions.kqlQuery.hosts({ - filterQuery, - hostsType: kqlQueryStateData.type, + 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 (kqlQueryStateData.model === 'network') { - this.urlStateMappedToActions.kqlQuery.network({ - filterQuery, - networkType: kqlQueryStateData.type, - }); + } + 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); } } }); @@ -310,25 +356,24 @@ const makeMapStateToProps = () => { : {}, kqlQuery: [ { - filterQuery: getHostsFilterQueryAsKuery(state, hostsModel.HostsType.details) || '', + filterQuery: getHostsFilterQueryAsKuery(state, hostsModel.HostsType.details), type: hostsModel.HostsType.details, - model: 'hosts', + model: KueryFilterModel.hosts, }, { - filterQuery: getHostsFilterQueryAsKuery(state, hostsModel.HostsType.page) || '', + filterQuery: getHostsFilterQueryAsKuery(state, hostsModel.HostsType.page), type: hostsModel.HostsType.page, - model: 'hosts', + model: KueryFilterModel.hosts, }, { - filterQuery: - getNetworkFilterQueryAsKuery(state, networkModel.NetworkType.details) || '', + filterQuery: getNetworkFilterQueryAsKuery(state, networkModel.NetworkType.details), type: networkModel.NetworkType.details, - model: 'network', + model: KueryFilterModel.network, }, { - filterQuery: getNetworkFilterQueryAsKuery(state, networkModel.NetworkType.page) || '', + filterQuery: getNetworkFilterQueryAsKuery(state, networkModel.NetworkType.page), type: networkModel.NetworkType.page, - model: 'network', + model: KueryFilterModel.network, }, ], }, @@ -346,7 +391,6 @@ export const UrlStateContainer = connect( setRelativeTimerange: inputsActions.setRelativeRangeDatePicker, toggleTimelineLinkTo: inputsActions.toggleTimelineLinkTo, } - // @ts-ignore )(UrlStateComponents); export const decodeRisonUrlState = (value: string | undefined): RisonValue | any | undefined => { From c2e58d41a02f66adfa72d25d9c754a2e7de1cfb2 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 21 May 2019 12:26:02 -0600 Subject: [PATCH 18/18] TS cleanups sort of --- .../__snapshots__/index.test.tsx.snap | 140 ++++++++++++------ .../components/url_state/index.test.tsx | 13 +- .../public/components/url_state/index.tsx | 37 ++--- x-pack/plugins/siem/public/store/model.ts | 2 +- 4 files changed, 121 insertions(+), 71 deletions(-) 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 index 32351ae637a6a..1ec61ad98e608 100644 --- 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 @@ -268,7 +268,24 @@ exports[`UrlStateComponents UrlStateContainer mounts and renders 1`] = ` "state": "", }, "push": [MockFunction], - "replace": [MockFunction], + "replace": [MockFunction] { + "calls": Array [ + Array [ + Object { + "hash": "", + "pathname": "/network", + "search": "?kqlQuery=!((filterQuery:!n,model:hosts,type:details),(filterQuery:!n,model:hosts,type:page),(filterQuery:!n,model:network,type:details),(filterQuery:!n,model:network,type:page))", + "state": "", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + }, } } > @@ -283,46 +300,50 @@ exports[`UrlStateComponents UrlStateContainer mounts and renders 1`] = ` Object { "kqlQuery": Array [ Object { - "filterQuery": "", + "filterQuery": null, "model": "hosts", "type": "details", }, Object { - "filterQuery": "", + "filterQuery": null, "model": "hosts", "type": "page", }, Object { - "filterQuery": "", + "filterQuery": null, "model": "network", "type": "details", }, Object { - "filterQuery": "", + "filterQuery": null, "model": "network", "type": "page", }, ], "timerange": Object { "global": Object { - "from": 0, - "fromStr": "now-24h", - "kind": "relative", "linkTo": Array [ "timeline", ], - "to": 1, - "toStr": "now", + "timerange": Object { + "from": 0, + "fromStr": "now-24h", + "kind": "relative", + "to": 1, + "toStr": "now", + }, }, "timeline": Object { - "from": 0, - "fromStr": "now-24h", - "kind": "relative", - "linkTo": Array [ + "linkTo": Object { + "from": 0, + "fromStr": "now-24h", + "kind": "relative", + "to": 1, + "toStr": "now", + }, + "timerange": Array [ "global", ], - "to": 1, - "toStr": "now", }, }, } @@ -338,46 +359,50 @@ exports[`UrlStateComponents UrlStateContainer mounts and renders 1`] = ` Object { "kqlQuery": Array [ Object { - "filterQuery": "", + "filterQuery": null, "model": "hosts", "type": "details", }, Object { - "filterQuery": "", + "filterQuery": null, "model": "hosts", "type": "page", }, Object { - "filterQuery": "", + "filterQuery": null, "model": "network", "type": "details", }, Object { - "filterQuery": "", + "filterQuery": null, "model": "network", "type": "page", }, ], "timerange": Object { "global": Object { - "from": 0, - "fromStr": "now-24h", - "kind": "relative", "linkTo": Array [ "timeline", ], - "to": 1, - "toStr": "now", + "timerange": Object { + "from": 0, + "fromStr": "now-24h", + "kind": "relative", + "to": 1, + "toStr": "now", + }, }, "timeline": Object { - "from": 0, - "fromStr": "now-24h", - "kind": "relative", - "linkTo": Array [ + "linkTo": Object { + "from": 0, + "fromStr": "now-24h", + "kind": "relative", + "to": 1, + "toStr": "now", + }, + "timerange": Array [ "global", ], - "to": 1, - "toStr": "now", }, }, } @@ -415,7 +440,24 @@ exports[`UrlStateComponents UrlStateContainer mounts and renders 1`] = ` "state": "", }, "push": [MockFunction], - "replace": [MockFunction], + "replace": [MockFunction] { + "calls": Array [ + Array [ + Object { + "hash": "", + "pathname": "/network", + "search": "?kqlQuery=!((filterQuery:!n,model:hosts,type:details),(filterQuery:!n,model:hosts,type:page),(filterQuery:!n,model:network,type:details),(filterQuery:!n,model:network,type:page))", + "state": "", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + }, } } location={ @@ -435,46 +477,50 @@ exports[`UrlStateComponents UrlStateContainer mounts and renders 1`] = ` Object { "kqlQuery": Array [ Object { - "filterQuery": "", + "filterQuery": null, "model": "hosts", "type": "details", }, Object { - "filterQuery": "", + "filterQuery": null, "model": "hosts", "type": "page", }, Object { - "filterQuery": "", + "filterQuery": null, "model": "network", "type": "details", }, Object { - "filterQuery": "", + "filterQuery": null, "model": "network", "type": "page", }, ], "timerange": Object { "global": Object { - "from": 0, - "fromStr": "now-24h", - "kind": "relative", "linkTo": Array [ "timeline", ], - "to": 1, - "toStr": "now", + "timerange": Object { + "from": 0, + "fromStr": "now-24h", + "kind": "relative", + "to": 1, + "toStr": "now", + }, }, "timeline": Object { - "from": 0, - "fromStr": "now-24h", - "kind": "relative", - "linkTo": Array [ + "linkTo": Object { + "from": 0, + "fromStr": "now-24h", + "kind": "relative", + "to": 1, + "toStr": "now", + }, + "timerange": Array [ "global", ], - "to": 1, - "toStr": "now", }, }, } 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 index 43ba951769e29..5840f2551b88d 100644 --- a/x-pack/plugins/siem/public/components/url_state/index.test.tsx +++ b/x-pack/plugins/siem/public/components/url_state/index.test.tsx @@ -28,6 +28,7 @@ import { } 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'; @@ -150,7 +151,7 @@ describe('UrlStateComponents', () => { urlStateComponents.exists(); expect(toJson(wrapper)).toMatchSnapshot(); }); - test('componentDidUpdate - timerange redux state updates the url', () => { + test('componentDidUpdate - timerange redux state updates the url', async () => { const wrapper = shallow(); const newUrlState = { @@ -168,15 +169,16 @@ describe('UrlStateComponents', () => { wrapper.setProps({ urlState: newUrlState }); wrapper.update(); - expect(mockHistory.replace).toBeCalledWith({ + await wait(1000); + expect(mockHistory.replace.mock.calls[1][0]).toStrictEqual({ hash: '', pathname: '/network', search: - '?timerange=(timeline:(from:1558048243696,fromStr:now-24h,kind:relative,linkTo:!(global),to:1558134643697,toStr:now))', + '?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', () => { + test('componentDidUpdate - kql query redux state updates the url', async () => { const wrapper = shallow(); const newUrlState = { @@ -191,7 +193,8 @@ describe('UrlStateComponents', () => { wrapper.setProps({ urlState: newUrlState }); wrapper.update(); - expect(mockHistory.replace).toBeCalledWith({ + await wait(1000); + expect(mockHistory.replace.mock.calls[1][0]).toStrictEqual({ hash: '', pathname: '/network', search: diff --git a/x-pack/plugins/siem/public/components/url_state/index.tsx b/x-pack/plugins/siem/public/components/url_state/index.tsx index 567eaf8404319..5df5de0f73283 100644 --- a/x-pack/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/plugins/siem/public/components/url_state/index.tsx @@ -242,12 +242,11 @@ export class UrlStateContainerLifecycle extends React.Component { 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: inputState - ? { - global: { - ...get('global.timerange', inputState), - linkTo: get('global.linkTo', inputState), - }, - timeline: { - ...get('timeline.timerange', inputState), - linkTo: get('timeline.linkTo', inputState), - }, - } - : {}, + timerange: { + global: { + timerange: globalTimerange, + linkTo: globalLinkTo, + }, + timeline: { + timerange: timelineLinkTo, + linkTo: timelineTimerange, + }, + }, kqlQuery: [ { filterQuery: getHostsFilterQueryAsKuery(state, hostsModel.HostsType.details), @@ -391,6 +391,7 @@ export const UrlStateContainer = connect( setRelativeTimerange: inputsActions.setRelativeRangeDatePicker, toggleTimelineLinkTo: inputsActions.toggleTimelineLinkTo, } + // @ts-ignore )(UrlStateComponents); export const decodeRisonUrlState = (value: string | undefined): RisonValue | any | undefined => { diff --git a/x-pack/plugins/siem/public/store/model.ts b/x-pack/plugins/siem/public/store/model.ts index 25100df94a270..68fd40d9f7c19 100644 --- a/x-pack/plugins/siem/public/store/model.ts +++ b/x-pack/plugins/siem/public/store/model.ts @@ -21,6 +21,6 @@ export interface KueryFilterQuery { } export interface SerializedFilterQuery { - query: KueryFilterQuery; + query: KueryFilterQuery | null; serializedQuery: string; }