[SIEM] Fix timeline persistence against url and events viewer (#45657) (#45741)

* fix timeline  persistence against url and events viewer

* fix reset fields browser for events view

* fix inspect event

* review I

* review II
This commit is contained in:
Xavier Mouligneau 2019-09-14 08:24:50 -04:00 committed by GitHub
parent ab56606513
commit a68c93de60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 379 additions and 876 deletions

View file

@ -5,7 +5,7 @@
*/
/** The `All Hosts` widget on the `Hosts` page */
export const ALL_HOSTS_WIDGET = '[data-test-subj="all-hosts"]';
export const ALL_HOSTS_WIDGET = '[data-test-subj="all-hosts-false"]';
/** A single draggable host in the `All Hosts` widget on the `Hosts` page */
export const ALL_HOSTS_WIDGET_HOST = '[data-react-beautiful-dnd-drag-handle]';

View file

@ -59,5 +59,5 @@ export const DATE_PICKER_ABSOLUTE_INPUT = '[data-test-subj="superDatePickerAbsol
export const KQL_INPUT = '[data-test-subj="kqlInput"]';
export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]';
export const HOST_DETAIL_SIEM_KIBANA = '[data-test-subj="all-hosts"] a.euiLink';
export const HOST_DETAIL_SIEM_KIBANA = '[data-test-subj="all-hosts-false"] a.euiLink';
export const BREADCRUMBS = '[data-test-subj="breadcrumbs"] a';

View file

@ -210,7 +210,7 @@ describe('url state', () => {
it('sets KQL in host page and detail page and check if href match on breadcrumb, tabs and subTabs', () => {
loginAndWaitForPage(ABSOLUTE_DATE_RANGE.urlHost);
cy.get(KQL_INPUT, { timeout: 5000 }).type('host.name: "siem-kibana" {enter}');
cy.get(NAVIGATION_HOSTS_ALL_HOSTS)
cy.get(NAVIGATION_HOSTS_ALL_HOSTS, { timeout: 5000 })
.first()
.click({ force: true });
waitForAllHostsWidget();

View file

@ -11,7 +11,7 @@ import { ActionCreator } from 'typescript-fsa';
import { WithSource } from '../../containers/source';
import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store';
import { timelineActions } from '../../store/actions';
import { timelineActions, inputsActions } from '../../store/actions';
import { KqlMode, TimelineModel } from '../../store/timeline/model';
import { ColumnHeader } from '../timeline/body/column_headers/column_header';
import { DataProvider } from '../timeline/data_providers/data_provider';
@ -19,6 +19,7 @@ import { Sort } from '../timeline/body/sort';
import { OnChangeItemsPerPage } from '../timeline/events';
import { EventsViewer } from './events_viewer';
import { InputsModelId } from '../../store/inputs/constants';
export interface OwnProps {
end: number;
@ -43,6 +44,12 @@ interface DispatchProps {
createTimeline: ActionCreator<{
id: string;
columns: ColumnHeader[];
itemsPerPage?: number;
sort?: Sort;
}>;
deleteEventQuery: ActionCreator<{
id: string;
inputId: InputsModelId;
}>;
removeColumn: ActionCreator<{
id: string;
@ -66,6 +73,7 @@ const StatefulEventsViewerComponent = React.memo<Props>(
createTimeline,
columns,
dataProviders,
deleteEventQuery,
end,
id,
isLive,
@ -83,8 +91,11 @@ const StatefulEventsViewerComponent = React.memo<Props>(
useEffect(() => {
if (createTimeline != null) {
createTimeline({ id, columns });
createTimeline({ id, columns, sort, itemsPerPage });
}
return () => {
deleteEventQuery({ id, inputId: 'global' });
};
}, []);
const onChangeItemsPerPage: OnChangeItemsPerPage = itemsChangedPerPage =>
@ -180,6 +191,7 @@ export const StatefulEventsViewer = connect(
makeMapStateToProps,
{
createTimeline: timelineActions.createTimeline,
deleteEventQuery: inputsActions.deleteOneQuery,
updateItemsPerPage: timelineActions.updateItemsPerPage,
updateSort: timelineActions.updateSort,
removeColumn: timelineActions.removeColumn,

View file

@ -45,7 +45,13 @@ PanesFlexGroup.displayName = 'PanesFlexGroup';
type Props = Pick<
FieldBrowserProps,
'browserFields' | 'height' | 'onFieldSelected' | 'onUpdateColumns' | 'timelineId' | 'width'
| 'browserFields'
| 'isEventViewer'
| 'height'
| 'onFieldSelected'
| 'onUpdateColumns'
| 'timelineId'
| 'width'
> & {
/**
* The current timeline column headers
@ -112,6 +118,7 @@ export class FieldsBrowser extends React.PureComponent<Props> {
browserFields,
filteredBrowserFields,
searchInput,
isEventViewer,
isSearching,
onCategorySelected,
onFieldSelected,
@ -133,6 +140,7 @@ export class FieldsBrowser extends React.PureComponent<Props> {
<Header
data-test-subj="header"
filteredBrowserFields={filteredBrowserFields}
isEventViewer={isEventViewer}
isSearching={isSearching}
onOutsideClick={onOutsideClick}
onSearchInputChange={this.onInputChange}

View file

@ -17,6 +17,7 @@ import { pure } from 'recompose';
import styled from 'styled-components';
import { BrowserFields } from '../../containers/source';
import { defaultHeaders as eventsDefaultHeaders } from '../events_viewer/default_headers';
import { defaultHeaders } from '../timeline/body/column_headers/default_headers';
import { OnUpdateColumns } from '../timeline/events';
@ -55,6 +56,7 @@ SearchContainer.displayName = 'SearchContainer';
interface Props {
filteredBrowserFields: BrowserFields;
isEventViewer?: boolean;
isSearching: boolean;
onOutsideClick: () => void;
onSearchInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
@ -91,39 +93,37 @@ const CountRow = pure<Pick<Props, 'filteredBrowserFields'>>(({ filteredBrowserFi
CountRow.displayName = 'CountRow';
const TitleRow = pure<{ onOutsideClick: () => void; onUpdateColumns: OnUpdateColumns }>(
({ onOutsideClick, onUpdateColumns }) => (
<EuiFlexGroup
alignItems="center"
justifyContent="spaceBetween"
direction="row"
gutterSize="none"
>
<EuiFlexItem grow={false}>
<EuiTitle data-test-subj="field-browser-title" size="s">
<h2>{i18n.CUSTOMIZE_COLUMNS}</h2>
</EuiTitle>
</EuiFlexItem>
const TitleRow = pure<{
isEventViewer?: boolean;
onOutsideClick: () => void;
onUpdateColumns: OnUpdateColumns;
}>(({ isEventViewer, onOutsideClick, onUpdateColumns }) => (
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" direction="row" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiTitle data-test-subj="field-browser-title" size="s">
<h2>{i18n.CUSTOMIZE_COLUMNS}</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="reset-fields"
onClick={() => {
onUpdateColumns(defaultHeaders);
onOutsideClick();
}}
>
{i18n.RESET_FIELDS}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
)
);
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="reset-fields"
onClick={() => {
onUpdateColumns(isEventViewer ? eventsDefaultHeaders : defaultHeaders);
onOutsideClick();
}}
>
{i18n.RESET_FIELDS}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
));
TitleRow.displayName = 'TitleRow';
export const Header = pure<Props>(
({
isEventViewer,
isSearching,
filteredBrowserFields,
onOutsideClick,
@ -133,7 +133,11 @@ export const Header = pure<Props>(
timelineId,
}) => (
<HeaderContainer>
<TitleRow onUpdateColumns={onUpdateColumns} onOutsideClick={onOutsideClick} />
<TitleRow
isEventViewer={isEventViewer}
onUpdateColumns={onUpdateColumns}
onOutsideClick={onOutsideClick}
/>
<SearchContainer>
<EuiFieldSearch
className={getFieldBrowserSearchInputClassName(timelineId)}

View file

@ -159,6 +159,7 @@ export class StatefulFieldsBrowserComponent extends React.PureComponent<
: browserFieldsWithDefaultCategory
}
height={height}
isEventViewer={isEventViewer}
isSearching={isSearching}
onCategorySelected={this.updateSelectedCategoryId}
onFieldSelected={onFieldSelected}

View file

@ -14,6 +14,7 @@ import { TimelineResult, GetOneTimeline, NoteResult } from '../../graphql/types'
import { addNotes as dispatchAddNotes } from '../../store/app/actions';
import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../store/inputs/actions';
import {
setKqlFilterQueryDraft as dispatchSetKqlFilterQueryDraft,
applyKqlFilterQuery as dispatchApplyKqlFilterQuery,
addTimeline as dispatchAddTimeline,
} from '../../store/timeline/actions';
@ -210,6 +211,15 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli
timeline.kqlQuery.filterQuery.kuery != null &&
timeline.kqlQuery.filterQuery.kuery.expression !== ''
) {
dispatch(
dispatchSetKqlFilterQueryDraft({
id,
filterQueryDraft: {
kind: 'kuery',
expression: timeline.kqlQuery.filterQuery.kuery.expression || '',
},
})
);
dispatch(
dispatchApplyKqlFilterQuery({
id,

View file

@ -41,6 +41,7 @@ import {
OpenTimelineReduxProps,
} from './types';
import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants';
import { ColumnHeader } from '../timeline/body/column_headers/column_header';
export interface OpenTimelineState {
/** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */
@ -363,8 +364,17 @@ const makeMapStateToProps = () => {
};
const mapDispatchToProps = (dispatch: Dispatch) => ({
createNewTimeline: dispatchCreateNewTimeline,
updateIsLoading: dispatchUpdateIsLoading,
createNewTimeline: ({
id,
columns,
show,
}: {
id: string;
columns: ColumnHeader[];
show?: boolean;
}) => dispatch(dispatchCreateNewTimeline({ id, columns, show })),
updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) =>
dispatch(dispatchUpdateIsLoading({ id, isLoading })),
updateTimeline: dispatchUpdateTimeline(dispatch),
});

View file

@ -397,8 +397,7 @@ describe('Paginated Table Component', () => {
});
test('should update the page when the activePage is changed from redux', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ourProps: BasicTableProps<any> = {
const ourProps: BasicTableProps<unknown> = {
activePage: 3,
columns: getHostsColumns(),
headerCount: 1,

View file

@ -21,10 +21,6 @@ interface TimelineRefetchDispatch {
loading: boolean;
refetch: inputsModel.Refetch | inputsModel.RefetchKql | null;
}>;
deleteEventQuery: ActionCreator<{
id: string;
inputId: InputsModelId;
}>;
}
export interface TimelineRefetchProps {
@ -38,14 +34,9 @@ export interface TimelineRefetchProps {
type OwnProps = TimelineRefetchProps & TimelineRefetchDispatch;
const TimelineRefetchComponent = memo<OwnProps>(
({ deleteEventQuery, id, inputId, inspect, loading, refetch, setTimelineQuery }) => {
({ id, inputId, inspect, loading, refetch, setTimelineQuery }) => {
useEffect(() => {
setTimelineQuery({ id, inputId, inspect, loading, refetch });
if (inputId === 'global') {
return () => {
deleteEventQuery({ id, inputId });
};
}
}, [id, inputId, loading, refetch, inspect]);
return null;
@ -57,7 +48,6 @@ export const TimelineRefetch = compose<React.ComponentClass<TimelineRefetchProps
null,
{
setTimelineQuery: inputsActions.setQuery,
deleteEventQuery: inputsActions.deleteOneQuery,
}
)
)(TimelineRefetchComponent);

View file

@ -1,547 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UrlStateContainer mounts and renders 1`] = `
<MockedProvider
addTypename={true}
>
<ApolloProvider
client={
ApolloClient {
"__operations_cache__": Map {},
"cache": InMemoryCache {
"addTypename": true,
"cacheKeyRoot": KeyTrie {
"weakness": true,
},
"config": Object {
"addTypename": true,
"dataIdFromObject": [Function],
"fragmentMatcher": HeuristicFragmentMatcher {},
"freezeResults": false,
"resultCaching": true,
},
"data": DepTrackingCache {
"data": Object {},
"depend": [Function],
},
"maybeBroadcastWatch": [Function],
"optimisticData": DepTrackingCache {
"data": Object {},
"depend": [Function],
},
"silenceBroadcast": false,
"storeReader": StoreReader {
"executeSelectionSet": [Function],
"executeStoreQuery": [Function],
"executeSubSelectedArray": [Function],
"freezeResults": false,
},
"storeWriter": StoreWriter {},
"typenameDocumentCache": Map {},
"watches": Set {},
},
"defaultOptions": Object {},
"disableNetworkFetches": false,
"link": ApolloLink {
"request": [Function],
},
"mutate": [Function],
"query": [Function],
"queryDeduplication": true,
"reFetchObservableQueries": [Function],
"resetStore": [Function],
"resetStoreCallbacks": Array [],
"ssrMode": false,
"store": DataStore {
"cache": InMemoryCache {
"addTypename": true,
"cacheKeyRoot": KeyTrie {
"weakness": true,
},
"config": Object {
"addTypename": true,
"dataIdFromObject": [Function],
"fragmentMatcher": HeuristicFragmentMatcher {},
"freezeResults": false,
"resultCaching": true,
},
"data": DepTrackingCache {
"data": Object {},
"depend": [Function],
},
"maybeBroadcastWatch": [Function],
"optimisticData": DepTrackingCache {
"data": Object {},
"depend": [Function],
},
"silenceBroadcast": false,
"storeReader": StoreReader {
"executeSelectionSet": [Function],
"executeStoreQuery": [Function],
"executeSubSelectedArray": [Function],
"freezeResults": false,
},
"storeWriter": StoreWriter {},
"typenameDocumentCache": Map {},
"watches": Set {},
},
},
"version": "2.3.8",
"watchQuery": [Function],
}
}
>
<pure(Component)
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(observable): [Function],
}
}
>
<Component
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(observable): [Function],
}
}
>
<I18nProvider>
<IntlProvider
defaultLocale="en"
formats={
Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"relative": Object {
"days": Object {
"units": "day",
},
"hours": Object {
"units": "hour",
},
"minutes": Object {
"units": "minute",
},
"months": Object {
"units": "month",
},
"seconds": Object {
"units": "second",
},
"years": Object {
"units": "year",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
}
}
locale="en"
messages={Object {}}
textComponent={Symbol(react.fragment)}
>
<PseudoLocaleWrapper>
<ApolloProvider
client={
ApolloClient {
"__operations_cache__": Map {},
"cache": InMemoryCache {
"addTypename": true,
"cacheKeyRoot": KeyTrie {
"weakness": true,
},
"config": Object {
"addTypename": true,
"dataIdFromObject": [Function],
"fragmentMatcher": HeuristicFragmentMatcher {},
"freezeResults": false,
"resultCaching": true,
},
"data": DepTrackingCache {
"data": Object {},
"depend": [Function],
},
"maybeBroadcastWatch": [Function],
"optimisticData": DepTrackingCache {
"data": Object {},
"depend": [Function],
},
"silenceBroadcast": false,
"storeReader": StoreReader {
"executeSelectionSet": [Function],
"executeStoreQuery": [Function],
"executeSubSelectedArray": [Function],
"freezeResults": false,
},
"storeWriter": StoreWriter {},
"typenameDocumentCache": Map {},
"watches": Set {},
},
"defaultOptions": Object {},
"disableNetworkFetches": false,
"link": ApolloLink {
"request": [Function],
},
"mutate": [Function],
"query": [Function],
"queryDeduplication": true,
"reFetchObservableQueries": [Function],
"resetStore": [Function],
"resetStoreCallbacks": Array [],
"ssrMode": false,
"store": DataStore {
"cache": InMemoryCache {
"addTypename": true,
"cacheKeyRoot": KeyTrie {
"weakness": true,
},
"config": Object {
"addTypename": true,
"dataIdFromObject": [Function],
"fragmentMatcher": HeuristicFragmentMatcher {},
"freezeResults": false,
"resultCaching": true,
},
"data": DepTrackingCache {
"data": Object {},
"depend": [Function],
},
"maybeBroadcastWatch": [Function],
"optimisticData": DepTrackingCache {
"data": Object {},
"depend": [Function],
},
"silenceBroadcast": false,
"storeReader": StoreReader {
"executeSelectionSet": [Function],
"executeStoreQuery": [Function],
"executeSubSelectedArray": [Function],
"freezeResults": false,
},
"storeWriter": StoreWriter {},
"typenameDocumentCache": Map {},
"watches": Set {},
},
},
"version": "2.3.8",
"watchQuery": [Function],
}
}
>
<Provider
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(observable): [Function],
}
}
>
<ThemeProvider
theme={[Function]}
>
<DragDropContext
onDragEnd={[MockFunction]}
>
<Router
history={
Object {
"action": "POP",
"block": [MockFunction],
"createHref": [MockFunction],
"go": [MockFunction],
"goBack": [MockFunction],
"goForward": [MockFunction],
"length": 2,
"listen": [MockFunction] {
"calls": Array [
Array [
[Function],
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
},
"location": Object {
"hash": "",
"pathname": "/network",
"search": "",
"state": "",
},
"push": [MockFunction],
"replace": [MockFunction],
}
}
>
<Memo()
indexPattern={
Object {
"fields": Array [
Object {
"aggregatable": true,
"name": "response",
"searchable": true,
"type": "number",
},
],
"title": "logstash-*",
}
}
navTabs={
Object {
"hosts": Object {
"disabled": false,
"href": "#/link-to/hosts",
"id": "hosts",
"name": "Hosts",
"urlKey": "host",
},
"network": Object {
"disabled": false,
"href": "#/link-to/network",
"id": "network",
"name": "Network",
"urlKey": "network",
},
"overview": Object {
"disabled": false,
"href": "#/link-to/overview",
"id": "overview",
"name": "Overview",
"urlKey": "overview",
},
"timelines": Object {
"disabled": false,
"href": "#/link-to/timelines",
"id": "timelines",
"name": "Timelines",
"urlKey": "timeline",
},
}
}
>
<Connect(UrlStateContainer)
indexPattern={
Object {
"fields": Array [
Object {
"aggregatable": true,
"name": "response",
"searchable": true,
"type": "number",
},
],
"title": "logstash-*",
}
}
navTabs={
Object {
"hosts": Object {
"disabled": false,
"href": "#/link-to/hosts",
"id": "hosts",
"name": "Hosts",
"urlKey": "host",
},
"network": Object {
"disabled": false,
"href": "#/link-to/network",
"id": "network",
"name": "Network",
"urlKey": "network",
},
"overview": Object {
"disabled": false,
"href": "#/link-to/overview",
"id": "overview",
"name": "Overview",
"urlKey": "overview",
},
"timelines": Object {
"disabled": false,
"href": "#/link-to/timelines",
"id": "timelines",
"name": "Timelines",
"urlKey": "timeline",
},
}
}
pageName="network"
pathName="/network"
search=""
>
<Component
addGlobalLinkTo={[Function]}
addTimelineLinkTo={[Function]}
dispatch={[Function]}
indexPattern={
Object {
"fields": Array [
Object {
"aggregatable": true,
"name": "response",
"searchable": true,
"type": "number",
},
],
"title": "logstash-*",
}
}
navTabs={
Object {
"hosts": Object {
"disabled": false,
"href": "#/link-to/hosts",
"id": "hosts",
"name": "Hosts",
"urlKey": "host",
},
"network": Object {
"disabled": false,
"href": "#/link-to/network",
"id": "network",
"name": "Network",
"urlKey": "network",
},
"overview": Object {
"disabled": false,
"href": "#/link-to/overview",
"id": "overview",
"name": "Overview",
"urlKey": "overview",
},
"timelines": Object {
"disabled": false,
"href": "#/link-to/timelines",
"id": "timelines",
"name": "Timelines",
"urlKey": "timeline",
},
}
}
pageName="network"
pathName="/network"
removeGlobalLinkTo={[Function]}
removeTimelineLinkTo={[Function]}
search=""
setAbsoluteTimerange={[Function]}
setHostsKql={[Function]}
setNetworkKql={[Function]}
setRelativeTimerange={[Function]}
updateTimeline={[Function]}
updateTimelineIsLoading={[Function]}
urlState={
Object {
"kqlQuery": Object {
"filterQuery": null,
"queryLocation": "network.page",
},
"timelineId": "",
"timerange": Object {
"global": Object {
"linkTo": Array [
"timeline",
],
"timerange": Object {
"from": 0,
"fromStr": "now-24h",
"kind": "relative",
"to": 1,
"toStr": "now",
},
},
"timeline": Object {
"linkTo": Array [
"global",
],
"timerange": Object {
"from": 0,
"fromStr": "now-24h",
"kind": "relative",
"to": 1,
"toStr": "now",
},
},
},
}
}
/>
</Connect(UrlStateContainer)>
</Memo()>
</Router>
</DragDropContext>
</ThemeProvider>
</Provider>
</ApolloProvider>
</PseudoLocaleWrapper>
</IntlProvider>
</I18nProvider>
</Component>
</pure(Component)>
</ApolloProvider>
</MockedProvider>
`;

View file

@ -5,37 +5,26 @@
*/
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 { StaticIndexPattern } from 'ui/index_patterns';
import { apolloClientObservable, HookWrapper, mockGlobalState, TestProviders } from '../../mock';
import { createStore, State } from '../../store';
import { UseUrlState } from './';
import { defaultProps, getMockPropsObj, mockHistory, testCases } from './test_dependencies';
import { HookWrapper } from '../../mock';
import {
getMockPropsObj,
mockHistory,
mockSetAbsoluteRangeDatePicker,
mockSetRelativeRangeDatePicker,
testCases,
mockApplyHostsFilterQuery,
mockApplyNetworkFilterQuery,
} from './test_dependencies';
import { UrlStateContainerPropTypes } from './types';
import { useUrlStateHooks } from './use_url_state';
import { CONSTANTS } from './constants';
import { RouteSpyState } from '../../utils/route/types';
import { navTabs, SiemPageName } from '../../pages/home/home_navigations';
import { SiemPageName } from '../../pages/home/home_navigations';
let mockProps: UrlStateContainerPropTypes;
const indexPattern: StaticIndexPattern = {
title: 'logstash-*',
fields: [
{
name: 'response',
type: 'number',
aggregatable: true,
searchable: true,
},
],
};
// const mockUseRouteSpy: jest.Mock = useRouteSpy as jest.Mock;
const mockRouteSpy: RouteSpyState = {
pageName: SiemPageName.network,
@ -49,31 +38,9 @@ jest.mock('../../utils/route/use_route_spy', () => ({
}));
describe('UrlStateContainer', () => {
const state: State = mockGlobalState;
let store = createStore(state, apolloClientObservable);
beforeEach(() => {
store = createStore(state, apolloClientObservable);
});
afterEach(() => {
jest.resetAllMocks();
});
test('mounts and renders', () => {
const wrapper = mount(
<MockedProvider>
<TestProviders store={store}>
<Router history={mockHistory}>
<UseUrlState indexPattern={indexPattern} navTabs={navTabs} />
</Router>
</TestProviders>
</MockedProvider>
);
const urlStateComponents = wrapper.find('[data-test-subj="urlStateComponents"]');
urlStateComponents.exists();
expect(toJson(wrapper)).toMatchSnapshot();
});
describe('handleInitialize', () => {
describe('URL state updates redux', () => {
describe('relative timerange actions are called with correct data on component mount', () => {
@ -90,7 +57,7 @@ describe('UrlStateContainer', () => {
mount(<HookWrapper hookProps={mockProps} hook={args => useUrlStateHooks(args)} />);
// @ts-ignore property mock does not exists
expect(defaultProps.setRelativeTimerange.mock.calls[1][0]).toEqual({
expect(mockSetRelativeRangeDatePicker.mock.calls[1][0]).toEqual({
from: 1558591200000,
fromStr: 'now-1d/d',
kind: 'relative',
@ -99,7 +66,7 @@ describe('UrlStateContainer', () => {
id: 'global',
});
// @ts-ignore property mock does not exists
expect(defaultProps.setRelativeTimerange.mock.calls[0][0]).toEqual({
expect(mockSetRelativeRangeDatePicker.mock.calls[0][0]).toEqual({
from: 1558732849370,
fromStr: 'now-15m',
kind: 'relative',
@ -120,14 +87,14 @@ describe('UrlStateContainer', () => {
mount(<HookWrapper hookProps={mockProps} hook={args => useUrlStateHooks(args)} />);
// @ts-ignore property mock does not exists
expect(defaultProps.setAbsoluteTimerange.mock.calls[1][0]).toEqual({
expect(mockSetAbsoluteRangeDatePicker.mock.calls[1][0]).toEqual({
from: 1556736012685,
kind: 'absolute',
to: 1556822416082,
id: 'global',
});
// @ts-ignore property mock does not exists
expect(defaultProps.setAbsoluteTimerange.mock.calls[0][0]).toEqual({
expect(mockSetAbsoluteRangeDatePicker.mock.calls[0][0]).toEqual({
from: 1556736012685,
kind: 'absolute',
to: 1556822416082,
@ -153,7 +120,9 @@ describe('UrlStateContainer', () => {
.relativeTimeSearch.undefinedQuery;
mount(<HookWrapper hookProps={mockProps} hook={args => useUrlStateHooks(args)} />);
const functionName =
namespaceUpper === 'Network' ? defaultProps.setNetworkKql : defaultProps.setHostsKql;
namespaceUpper === 'Network'
? mockApplyNetworkFilterQuery
: mockApplyHostsFilterQuery;
// @ts-ignore property mock does not exists
expect(functionName.mock.calls[0][0]).toEqual({
filterQuery: serializedFilterQuery,
@ -176,7 +145,9 @@ describe('UrlStateContainer', () => {
}).oppositeQueryLocationSearch.undefinedQuery;
mount(<HookWrapper hookProps={mockProps} hook={args => useUrlStateHooks(args)} />);
const functionName =
namespaceUpper === 'Network' ? defaultProps.setNetworkKql : defaultProps.setHostsKql;
namespaceUpper === 'Network'
? mockApplyNetworkFilterQuery
: mockApplyHostsFilterQuery;
// @ts-ignore property mock does not exists
expect(functionName.mock.calls.length).toEqual(0);
}

View file

@ -18,7 +18,7 @@ import {
State,
timelineSelectors,
} from '../../store';
import { hostsActions, inputsActions, networkActions, timelineActions } from '../../store/actions';
import { timelineActions } from '../../store/actions';
import { RouteSpyState } from '../../utils/route/types';
import { useRouteSpy } from '../../utils/route/use_route_spy';
@ -27,6 +27,7 @@ import { UrlStateContainerPropTypes, UrlStateProps, KqlQuery, LocationTypes } fr
import { useUrlStateHooks } from './use_url_state';
import { dispatchUpdateTimeline } from '../open_timeline/helpers';
import { getCurrentLocation } from './helpers';
import { dispatchSetInitialStateFromUrl } from './initialize_redux_by_url';
export const UrlStateContainer = React.memo<UrlStateContainerPropTypes>(
(props: UrlStateContainerPropTypes) => {
@ -108,17 +109,10 @@ const makeMapStateToProps = () => {
};
const mapDispatchToProps = (dispatch: Dispatch) => ({
addGlobalLinkTo: inputsActions.addGlobalLinkTo,
addTimelineLinkTo: inputsActions.addTimelineLinkTo,
removeGlobalLinkTo: inputsActions.removeGlobalLinkTo,
removeTimelineLinkTo: inputsActions.removeTimelineLinkTo,
setAbsoluteTimerange: inputsActions.setAbsoluteRangeDatePicker,
setHostsKql: hostsActions.applyHostsFilterQuery,
setNetworkKql: networkActions.applyNetworkFilterQuery,
setRelativeTimerange: inputsActions.setRelativeRangeDatePicker,
setInitialStateFromUrl: dispatchSetInitialStateFromUrl(dispatch),
updateTimeline: dispatchUpdateTimeline(dispatch),
updateTimelineIsLoading: timelineActions.updateIsLoading,
dispatch,
updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) =>
dispatch(timelineActions.updateIsLoading({ id, isLoading })),
});
export const UrlStateRedux = compose<React.ComponentClass<UrlStateProps & RouteSpyState>>(

View file

@ -0,0 +1,155 @@
/*
* 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 { get, isEmpty } from 'lodash/fp';
import { Dispatch } from 'redux';
import { hostsActions, inputsActions, networkActions } from '../../store/actions';
import { InputsModelId, TimeRangeKinds } from '../../store/inputs/constants';
import {
UrlInputsModel,
LinkTo,
AbsoluteTimeRange,
RelativeTimeRange,
} from '../../store/inputs/model';
import { CONSTANTS } from './constants';
import { decodeRisonUrlState, isKqlForRoute, getCurrentLocation } from './helpers';
import { normalizeTimeRange } from './normalize_time_range';
import { DispatchSetInitialStateFromUrl, KqlQuery, SetInitialStateFromUrl } from './types';
import { convertKueryToElasticSearchQuery } from '../../lib/keury';
import { HostsType } from '../../store/hosts/model';
import { NetworkType } from '../../store/network/model';
import { queryTimelineById } from '../open_timeline/helpers';
export const dispatchSetInitialStateFromUrl = (
dispatch: Dispatch
): DispatchSetInitialStateFromUrl => ({
apolloClient,
detailName,
indexPattern,
pageName,
updateTimeline,
updateTimelineIsLoading,
urlStateToUpdate,
}: SetInitialStateFromUrl<unknown>): (() => void) => () => {
urlStateToUpdate.forEach(({ urlKey, newUrlStateString }) => {
if (urlKey === CONSTANTS.timerange) {
const timerangeStateData: UrlInputsModel = decodeRisonUrlState(newUrlStateString);
const globalId: InputsModelId = 'global';
const globalLinkTo: LinkTo = { linkTo: get('global.linkTo', timerangeStateData) };
const globalType: TimeRangeKinds = get('global.timerange.kind', timerangeStateData);
const timelineId: InputsModelId = 'timeline';
const timelineLinkTo: LinkTo = { linkTo: get('timeline.linkTo', timerangeStateData) };
const timelineType: TimeRangeKinds = get('timeline.timerange.kind', timerangeStateData);
if (isEmpty(globalLinkTo.linkTo)) {
dispatch(inputsActions.removeGlobalLinkTo());
} else {
dispatch(inputsActions.addGlobalLinkTo({ linkToId: 'timeline' }));
}
if (isEmpty(timelineLinkTo.linkTo)) {
dispatch(inputsActions.removeTimelineLinkTo());
} else {
dispatch(inputsActions.addTimelineLinkTo({ linkToId: 'global' }));
}
if (timelineType) {
if (timelineType === 'absolute') {
const absoluteRange = normalizeTimeRange<AbsoluteTimeRange>(
get('timeline.timerange', timerangeStateData)
);
dispatch(
inputsActions.setAbsoluteRangeDatePicker({
...absoluteRange,
id: timelineId,
})
);
}
if (timelineType === 'relative') {
const relativeRange = normalizeTimeRange<RelativeTimeRange>(
get('timeline.timerange', timerangeStateData)
);
dispatch(
inputsActions.setRelativeRangeDatePicker({
...relativeRange,
id: timelineId,
})
);
}
}
if (globalType) {
if (globalType === 'absolute') {
const absoluteRange = normalizeTimeRange<AbsoluteTimeRange>(
get('global.timerange', timerangeStateData)
);
dispatch(
inputsActions.setAbsoluteRangeDatePicker({
...absoluteRange,
id: globalId,
})
);
}
if (globalType === 'relative') {
const relativeRange = normalizeTimeRange<RelativeTimeRange>(
get('global.timerange', timerangeStateData)
);
dispatch(
inputsActions.setRelativeRangeDatePicker({
...relativeRange,
id: globalId,
})
);
}
}
}
if (urlKey === CONSTANTS.kqlQuery && indexPattern != null) {
const kqlQueryStateData: KqlQuery = decodeRisonUrlState(newUrlStateString);
if (isKqlForRoute(pageName, detailName, kqlQueryStateData.queryLocation)) {
const filterQuery = {
kuery: kqlQueryStateData.filterQuery,
serializedQuery: convertKueryToElasticSearchQuery(
kqlQueryStateData.filterQuery ? kqlQueryStateData.filterQuery.expression : '',
indexPattern
),
};
const page = getCurrentLocation(pageName, detailName);
if ([CONSTANTS.hostsPage, CONSTANTS.hostsDetails].includes(page)) {
dispatch(
hostsActions.applyHostsFilterQuery({
filterQuery,
hostsType: page === CONSTANTS.hostsPage ? HostsType.page : HostsType.details,
})
);
} else if ([CONSTANTS.networkPage, CONSTANTS.networkDetails].includes(page)) {
dispatch(
networkActions.applyNetworkFilterQuery({
filterQuery,
networkType: page === CONSTANTS.networkPage ? NetworkType.page : NetworkType.details,
})
);
}
}
}
if (urlKey === CONSTANTS.timelineId) {
const timelineId = decodeRisonUrlState(newUrlStateString);
if (timelineId != null) {
queryTimelineById({
apolloClient,
duplicate: false,
timelineId,
updateIsLoading: updateTimelineIsLoading,
updateTimeline,
});
}
}
});
};

View file

@ -5,13 +5,14 @@
*/
import { ActionCreator } from 'typescript-fsa';
import { hostsModel, networkModel, SerializedFilterQuery } from '../../store';
import { hostsModel, networkModel } from '../../store';
import { UrlStateContainerPropTypes, LocationTypes, KqlQuery } from './types';
import { CONSTANTS } from './constants';
import { InputsModelId } from '../../store/inputs/constants';
import { DispatchUpdateTimeline } from '../open_timeline/types';
import { navTabs, SiemPageName } from '../../pages/home/home_navigations';
import { hostsActions, inputsActions, networkActions } from '../../store/actions';
import { HostsTableType } from '../../store/hosts/model';
import { dispatchSetInitialStateFromUrl } from './initialize_redux_by_url';
type Action = 'PUSH' | 'POP' | 'REPLACE';
const pop: Action = 'POP';
@ -24,6 +25,32 @@ export const getFilterQuery = (queryLocation: LocationTypes): KqlQuery => ({
queryLocation,
});
export const mockApplyHostsFilterQuery: jest.Mock = (hostsActions.applyHostsFilterQuery as unknown) as jest.Mock;
export const mockApplyNetworkFilterQuery: jest.Mock = (networkActions.applyNetworkFilterQuery as unknown) as jest.Mock;
export const mockAddGlobalLinkTo: jest.Mock = (inputsActions.addGlobalLinkTo as unknown) as jest.Mock;
export const mockAddTimelineLinkTo: jest.Mock = (inputsActions.addTimelineLinkTo as unknown) as jest.Mock;
export const mockRemoveGlobalLinkTo: jest.Mock = (inputsActions.removeGlobalLinkTo as unknown) as jest.Mock;
export const mockRemoveTimelineLinkTo: jest.Mock = (inputsActions.removeTimelineLinkTo as unknown) as jest.Mock;
export const mockSetAbsoluteRangeDatePicker: jest.Mock = (inputsActions.setAbsoluteRangeDatePicker as unknown) as jest.Mock;
export const mockSetRelativeRangeDatePicker: jest.Mock = (inputsActions.setRelativeRangeDatePicker as unknown) as jest.Mock;
jest.mock('../../store/actions', () => ({
hostsActions: {
applyHostsFilterQuery: jest.fn(),
},
networkActions: {
applyNetworkFilterQuery: jest.fn(),
},
inputsActions: {
addGlobalLinkTo: jest.fn(),
addTimelineLinkTo: jest.fn(),
removeGlobalLinkTo: jest.fn(),
removeTimelineLinkTo: jest.fn(),
setAbsoluteRangeDatePicker: jest.fn(),
setRelativeRangeDatePicker: jest.fn(),
},
}));
const defaultLocation = {
hash: '',
pathname: '/network',
@ -31,6 +58,9 @@ const defaultLocation = {
state: '',
};
const mockDispatch = jest.fn();
mockDispatch.mockImplementation(fn => fn);
export const mockHistory = {
action: pop,
block: jest.fn(),
@ -92,33 +122,7 @@ export const defaultProps: UrlStateContainerPropTypes = {
},
[CONSTANTS.timelineId]: '',
},
addGlobalLinkTo: (jest.fn() as unknown) as ActionCreator<{ linkToId: InputsModelId }>,
addTimelineLinkTo: (jest.fn() as unknown) as ActionCreator<{ linkToId: InputsModelId }>,
dispatch: jest.fn(),
removeGlobalLinkTo: (jest.fn() as unknown) as ActionCreator<void>,
removeTimelineLinkTo: (jest.fn() as unknown) as ActionCreator<void>,
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;
}>,
setInitialStateFromUrl: dispatchSetInitialStateFromUrl(mockDispatch),
updateTimeline: (jest.fn() as unknown) as DispatchUpdateTimeline,
updateTimelineIsLoading: (jest.fn() as unknown) as ActionCreator<{
id: string;

View file

@ -6,11 +6,10 @@
import { ActionCreator } from 'typescript-fsa';
import { StaticIndexPattern } from 'ui/index_patterns';
import { Dispatch } from 'redux';
import { hostsModel, KueryFilterQuery, networkModel, SerializedFilterQuery } from '../../store';
import ApolloClient from 'apollo-client';
import { KueryFilterQuery } from '../../store';
import { UrlInputsModel } from '../../store/inputs/model';
import { InputsModelId } from '../../store/inputs/constants';
import { RouteSpyState } from '../../utils/route/types';
import { DispatchUpdateTimeline } from '../open_timeline/types';
import { NavTab } from '../navigation/types';
@ -63,39 +62,15 @@ export interface UrlStateStateToPropsType {
urlState: UrlState;
}
export interface UpdateTimelineIsLoading {
id: string;
isLoading: boolean;
}
export interface UrlStateDispatchToPropsType {
addGlobalLinkTo: ActionCreator<{ linkToId: InputsModelId }>;
addTimelineLinkTo: ActionCreator<{ linkToId: InputsModelId }>;
dispatch: Dispatch;
removeGlobalLinkTo: ActionCreator<void>;
removeTimelineLinkTo: ActionCreator<void>;
setHostsKql: ActionCreator<{
filterQuery: SerializedFilterQuery;
hostsType: hostsModel.HostsType;
}>;
setNetworkKql: ActionCreator<{
filterQuery: SerializedFilterQuery;
networkType: networkModel.NetworkType;
}>;
setAbsoluteTimerange: ActionCreator<{
from: number;
fromStr: undefined;
id: InputsModelId;
to: number;
toStr: undefined;
}>;
setRelativeTimerange: ActionCreator<{
from: number;
fromStr: string;
id: InputsModelId;
to: number;
toStr: string;
}>;
setInitialStateFromUrl: DispatchSetInitialStateFromUrl;
updateTimeline: DispatchUpdateTimeline;
updateTimelineIsLoading: ActionCreator<{
id: string;
isLoading: boolean;
}>;
updateTimelineIsLoading: ActionCreator<UpdateTimelineIsLoading>;
}
export type UrlStateContainerPropTypes = RouteSpyState &
@ -107,3 +82,28 @@ export interface PreviousLocationUrlState {
pathName: string | undefined;
urlState: UrlState;
}
export interface UrlStateToRedux {
urlKey: KeyUrlState;
newUrlStateString: string;
}
export interface SetInitialStateFromUrl<TCache> {
apolloClient: ApolloClient<TCache> | ApolloClient<{}> | undefined;
detailName: string | undefined;
indexPattern: StaticIndexPattern | undefined;
pageName: string;
updateTimeline: DispatchUpdateTimeline;
updateTimelineIsLoading: ActionCreator<UpdateTimelineIsLoading>;
urlStateToUpdate: UrlStateToRedux[];
}
export type DispatchSetInitialStateFromUrl = <TCache>({
apolloClient,
detailName,
indexPattern,
pageName,
updateTimeline,
updateTimelineIsLoading,
urlStateToUpdate,
}: SetInitialStateFromUrl<TCache>) => () => void;

View file

@ -5,21 +5,11 @@
*/
import { Location } from 'history';
import { get, isEqual, difference, isEmpty } from 'lodash/fp';
import { isEqual, difference } from 'lodash/fp';
import { useEffect, useRef, useState } from 'react';
import { convertKueryToElasticSearchQuery } from '../../lib/keury';
import { InputsModelId, TimeRangeKinds } from '../../store/inputs/constants';
import {
AbsoluteTimeRange,
LinkTo,
RelativeTimeRange,
UrlInputsModel,
} from '../../store/inputs/model';
import { UrlInputsModel } from '../../store/inputs/model';
import { useApolloClient } from '../../utils/apollo_context';
import { queryTimelineById } from '../open_timeline/helpers';
import { HostsType } from '../../store/hosts/model';
import { NetworkType } from '../../store/network/model';
import { CONSTANTS, UrlStateType } from './constants';
import {
@ -29,11 +19,9 @@ import {
getParamFromQueryString,
decodeRisonUrlState,
isKqlForRoute,
getCurrentLocation,
getUrlType,
getTitle,
} from './helpers';
import { normalizeTimeRange } from './normalize_time_range';
import {
UrlStateContainerPropTypes,
PreviousLocationUrlState,
@ -41,6 +29,7 @@ import {
KeyUrlState,
KqlQuery,
ALL_URL_STATE_KEYS,
UrlStateToRedux,
} from './types';
function usePrevious(value: PreviousLocationUrlState) {
@ -52,22 +41,14 @@ function usePrevious(value: PreviousLocationUrlState) {
}
export const useUrlStateHooks = ({
addGlobalLinkTo,
addTimelineLinkTo,
detailName,
dispatch,
indexPattern,
history,
navTabs,
pageName,
pathName,
removeGlobalLinkTo,
removeTimelineLinkTo,
search,
setAbsoluteTimerange,
setHostsKql,
setNetworkKql,
setRelativeTimerange,
setInitialStateFromUrl,
tabName,
updateTimeline,
updateTimelineIsLoading,
@ -98,8 +79,7 @@ export const useUrlStateHooks = ({
getQueryStringFromLocation(latestLocation)
)
);
if (history && !isEqual(newLocation.search, latestLocation.search)) {
if (history) {
history.replace(newLocation);
}
return newLocation;
@ -107,6 +87,7 @@ export const useUrlStateHooks = ({
const handleInitialize = (initLocation: Location, type: UrlStateType) => {
let myLocation: Location = initLocation;
let urlStateToUpdate: UrlStateToRedux[] = [];
URL_STATE_KEYS[type].forEach((urlKey: KeyUrlState) => {
const newUrlStateString = getParamFromQueryString(
getQueryStringFromLocation(initLocation),
@ -129,7 +110,7 @@ export const useUrlStateHooks = ({
);
}
if (isInitializing) {
setInitialStateFromUrl(urlKey, newUrlStateString);
urlStateToUpdate = [...urlStateToUpdate, { urlKey, newUrlStateString }];
}
} else {
myLocation = replaceStateInLocation(urlState[urlKey], urlKey, myLocation);
@ -138,123 +119,16 @@ export const useUrlStateHooks = ({
difference(ALL_URL_STATE_KEYS, URL_STATE_KEYS[type]).forEach((urlKey: KeyUrlState) => {
myLocation = replaceStateInLocation('', urlKey, myLocation);
});
};
const setInitialStateFromUrl = (urlKey: KeyUrlState, newUrlStateString: string) => {
if (urlKey === CONSTANTS.timerange) {
const timerangeStateData: UrlInputsModel = decodeRisonUrlState(newUrlStateString);
const globalId: InputsModelId = 'global';
const globalLinkTo: LinkTo = { linkTo: get('global.linkTo', timerangeStateData) };
const globalType: TimeRangeKinds = get('global.timerange.kind', timerangeStateData);
const timelineId: InputsModelId = 'timeline';
const timelineLinkTo: LinkTo = { linkTo: get('timeline.linkTo', timerangeStateData) };
const timelineType: TimeRangeKinds = get('timeline.timerange.kind', timerangeStateData);
if (isEmpty(globalLinkTo.linkTo)) {
dispatch(removeGlobalLinkTo());
} else {
dispatch(addGlobalLinkTo({ linkToId: 'timeline' }));
}
if (isEmpty(timelineLinkTo.linkTo)) {
dispatch(removeTimelineLinkTo());
} else {
dispatch(addTimelineLinkTo({ linkToId: 'global' }));
}
if (timelineType) {
if (timelineType === 'absolute') {
const absoluteRange = normalizeTimeRange<AbsoluteTimeRange>(
get('timeline.timerange', timerangeStateData)
);
dispatch(
setAbsoluteTimerange({
...absoluteRange,
id: timelineId,
})
);
}
if (timelineType === 'relative') {
const relativeRange = normalizeTimeRange<RelativeTimeRange>(
get('timeline.timerange', timerangeStateData)
);
dispatch(
setRelativeTimerange({
...relativeRange,
id: timelineId,
})
);
}
}
if (globalType) {
if (globalType === 'absolute') {
const absoluteRange = normalizeTimeRange<AbsoluteTimeRange>(
get('global.timerange', timerangeStateData)
);
dispatch(
setAbsoluteTimerange({
...absoluteRange,
id: globalId,
})
);
}
if (globalType === 'relative') {
const relativeRange = normalizeTimeRange<RelativeTimeRange>(
get('global.timerange', timerangeStateData)
);
dispatch(
setRelativeTimerange({
...relativeRange,
id: globalId,
})
);
}
}
}
if (urlKey === CONSTANTS.kqlQuery && indexPattern != null) {
const kqlQueryStateData: KqlQuery = decodeRisonUrlState(newUrlStateString);
if (isKqlForRoute(pageName, detailName, kqlQueryStateData.queryLocation)) {
const filterQuery = {
kuery: kqlQueryStateData.filterQuery,
serializedQuery: convertKueryToElasticSearchQuery(
kqlQueryStateData.filterQuery ? kqlQueryStateData.filterQuery.expression : '',
indexPattern
),
};
const page = getCurrentLocation(pageName, detailName);
if ([CONSTANTS.hostsPage, CONSTANTS.hostsDetails].includes(page)) {
dispatch(
setHostsKql({
filterQuery,
hostsType: page === CONSTANTS.hostsPage ? HostsType.page : HostsType.details,
})
);
} else if ([CONSTANTS.networkPage, CONSTANTS.networkDetails].includes(page)) {
dispatch(
setNetworkKql({
filterQuery,
networkType: page === CONSTANTS.networkPage ? NetworkType.page : NetworkType.details,
})
);
}
}
}
if (urlKey === CONSTANTS.timelineId) {
const timelineId = decodeRisonUrlState(newUrlStateString);
if (timelineId != null) {
queryTimelineById({
apolloClient,
duplicate: false,
timelineId,
updateIsLoading: updateTimelineIsLoading,
updateTimeline,
});
}
}
setInitialStateFromUrl({
apolloClient,
detailName,
indexPattern,
pageName,
updateTimeline,
updateTimelineIsLoading,
urlStateToUpdate,
})();
};
useEffect(() => {
@ -269,7 +143,7 @@ export const useUrlStateHooks = ({
if (isInitializing && pageName != null && pageName !== '') {
handleInitialize(location, type);
setIsInitializing(false);
} else if (!isEqual(urlState, prevProps.urlState)) {
} else if (!isEqual(urlState, prevProps.urlState) && !isInitializing) {
let newLocation: Location = location;
URL_STATE_KEYS[type].forEach((urlKey: KeyUrlState) => {
newLocation = replaceStateInLocation(urlState[urlKey], urlKey, newLocation);

View file

@ -40,11 +40,6 @@ export class QueryTemplate<
tiebreaker?: string
) => FetchMoreOptionsArgs<TData, TVariables>;
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor(props: T) {
super(props);
}
public setFetchMore = (
val: (fetchMoreOptions: FetchMoreOptionsArgs<TData, TVariables>) => PromiseApolloQueryResult
) => {

View file

@ -49,7 +49,6 @@ export class QueryTemplatePaginated<
refetch: (variables?: TVariables) => Promise<ApolloQueryResult<TData>>
) => inputsModel.Refetch;
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor(props: T) {
super(props);
this.memoizedRefetchQuery = memoizeOne(this.refetchQuery);

View file

@ -229,7 +229,7 @@ describe('Inputs', () => {
});
});
describe('deleteOnlyOneQuery', () => {
describe('deleteOneQuery', () => {
test('make sure that we only delete one query', () => {
const refetch = jest.fn();
const newQuery: UpdateQueryParams = {

View file

@ -49,7 +49,9 @@ export const applyDeltaToColumnWidth = actionCreator<{
export const createTimeline = actionCreator<{
id: string;
columns: ColumnHeader[];
itemsPerPage?: number;
show?: boolean;
sort?: Sort;
}>('CREATE_TIMELINE');
export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT');

View file

@ -94,6 +94,9 @@ const timelineActionsType = [
upsertColumn.type,
];
const isItAtimelineAction = (timelineId: string | undefined) =>
timelineId && timelineId.toLowerCase().startsWith('timeline');
export const createTimelineEpic = <State>(): Epic<
Action,
Action,
@ -123,19 +126,24 @@ export const createTimelineEpic = <State>(): Epic<
action$.pipe(
withLatestFrom(timeline$),
filter(([action, timeline]) => {
const timelineId: TimelineModel = timeline[get('payload.id', action)];
const timelineId: string = get('payload.id', action);
const timelineObj: TimelineModel = timeline[timelineId];
if (action.type === addError.type) {
return true;
}
if (action.type === createTimeline.type) {
if (action.type === createTimeline.type && isItAtimelineAction(timelineId)) {
myEpicTimelineId.setTimelineId(null);
myEpicTimelineId.setTimelineVersion(null);
} else if (action.type === addTimeline.type) {
} else if (action.type === addTimeline.type && isItAtimelineAction(timelineId)) {
const addNewTimeline: TimelineModel = get('payload.timeline', action);
myEpicTimelineId.setTimelineId(addNewTimeline.savedObjectId);
myEpicTimelineId.setTimelineVersion(addNewTimeline.version);
return true;
} else if (timelineActionsType.includes(action.type) && !timelineId.isLoading) {
} else if (
timelineActionsType.includes(action.type) &&
!timelineObj.isLoading &&
isItAtimelineAction(timelineId)
) {
return true;
}
return false;

View file

@ -122,13 +122,16 @@ export const addTimelineToStore = ({
[id]: {
...timeline,
show: true,
isLoading: timelineById[id].isLoading,
},
});
interface AddNewTimelineParams {
columns: ColumnHeader[];
id: string;
itemsPerPage?: number;
show?: boolean;
sort?: Sort;
timelineById: TimelineById;
}
@ -136,6 +139,8 @@ interface AddNewTimelineParams {
export const addNewTimeline = ({
columns,
id,
itemsPerPage = timelineDefaults.itemsPerPage,
sort = timelineDefaults.sort,
show = false,
timelineById,
}: AddNewTimelineParams): TimelineById => ({
@ -144,6 +149,8 @@ export const addNewTimeline = ({
id,
...timelineDefaults,
columns,
itemsPerPage,
sort,
show,
savedObjectId: null,
version: null,

View file

@ -99,9 +99,16 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: addTimelineToStore({ id, timeline, timelineById: state.timelineById }),
}))
.case(createTimeline, (state, { id, show, columns }) => ({
.case(createTimeline, (state, { id, show, columns, itemsPerPage, sort }) => ({
...state,
timelineById: addNewTimeline({ columns, id, show, timelineById: state.timelineById }),
timelineById: addNewTimeline({
columns,
id,
itemsPerPage,
sort,
show,
timelineById: state.timelineById,
}),
}))
.case(upsertColumn, (state, { column, id, index }) => ({
...state,