[SIEM] Add SavedQuery in Timeline (#49813)

* Step-1: Add Search Bar in timeline instead of our own kql

* Step-2: Add the saved query with filter in timeline savedObject

* fix type

* Fix unit test

* fix bug when you use an exists filter

* Fix bug to do a search when add filter by itself

* Review I

* unit tests

* fix import for Filter

* add range as a filter

* remove comment

* forget to add range in ES mapping + allow query with only filters

* fix and/or with filter

* review with Liza
This commit is contained in:
Xavier Mouligneau 2019-11-14 20:45:21 -05:00 committed by GitHub
parent 48a2156c4c
commit 3131dd494d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 3456 additions and 263 deletions

View file

@ -82,8 +82,11 @@ function QueryBarTopRowUI(props: Props) {
const queryLanguage = props.query && props.query.language;
const persistedLog: PersistedLog | undefined = React.useMemo(
() => (queryLanguage ? getQueryLog(uiSettings!, storage, appName, queryLanguage) : undefined),
[queryLanguage]
() =>
queryLanguage && uiSettings && storage && appName
? getQueryLog(uiSettings!, storage, appName, queryLanguage)
: undefined,
[appName, queryLanguage, uiSettings, storage]
);
function onClickSubmitButton(event: React.MouseEvent<HTMLButtonElement>) {

View file

@ -72,6 +72,7 @@ export interface SearchBarOwnProps {
// Show when user has privileges to save
showSaveQuery?: boolean;
savedQuery?: SavedQuery;
onQueryChange?: (payload: { dateRange: TimeRange; query?: Query }) => void;
onQuerySubmit?: (payload: { dateRange: TimeRange; query?: Query }) => void;
// User has saved the current state as a saved query
onSaved?: (savedQuery: SavedQuery) => void;
@ -206,6 +207,18 @@ class SearchBarUI extends Component<SearchBarProps, State> {
);
}
/*
* This Function is here to show the toggle in saved query form
* in case you the date range (from/to)
*/
private shouldRenderTimeFilterInSavedQueryForm() {
const { dateRangeFrom, dateRangeTo, showDatePicker } = this.props;
return (
showDatePicker ||
(!showDatePicker && dateRangeFrom !== undefined && dateRangeTo !== undefined)
);
}
public setFilterBarHeight = () => {
requestAnimationFrame(() => {
const height =
@ -299,6 +312,9 @@ class SearchBarUI extends Component<SearchBarProps, State> {
dateRangeFrom: queryAndDateRange.dateRange.from,
dateRangeTo: queryAndDateRange.dateRange.to,
});
if (this.props.onQueryChange) {
this.props.onQueryChange(queryAndDateRange);
}
};
public onQueryBarSubmit = (queryAndDateRange: { dateRange?: TimeRange; query?: Query }) => {
@ -440,7 +456,7 @@ class SearchBarUI extends Component<SearchBarProps, State> {
onSave={this.onSave}
onClose={() => this.setState({ showSaveQueryModal: false })}
showFilterOption={this.props.showFilterBar}
showTimeFilterOption={this.props.showDatePicker}
showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()}
/>
) : null}
{this.state.showSaveNewQueryModal ? (
@ -449,7 +465,7 @@ class SearchBarUI extends Component<SearchBarProps, State> {
onSave={savedQueryMeta => this.onSave(savedQueryMeta, true)}
onClose={() => this.setState({ showSaveNewQueryModal: false })}
showFilterOption={this.props.showFilterBar}
showTimeFilterOption={this.props.showDatePicker}
showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()}
/>
) : null}
</div>

View file

@ -33,15 +33,17 @@ export interface FilterValueFormatter {
}
export interface FilterMeta {
alias: string | null;
disabled: boolean;
negate: boolean;
// controlledBy is there to identify who owns the filter
controlledBy?: string;
// index and type are optional only because when you create a new filter, there are no defaults
index?: string;
type?: string;
disabled: boolean;
negate: boolean;
alias: string | null;
key?: string;
value?: string | ((formatter?: FilterValueFormatter) => string);
params?: any;
value?: string | ((formatter?: FilterValueFormatter) => string);
}
export interface Filter {

View file

@ -25,6 +25,7 @@ export const DEFAULT_TO = 'now';
export const DEFAULT_INTERVAL_PAUSE = true;
export const DEFAULT_INTERVAL_TYPE = 'manual';
export const DEFAULT_INTERVAL_VALUE = 300000; // ms
export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges';
/**
* Id for the SIGNALS alerting type

View file

@ -9,4 +9,7 @@ import { npStart } from 'ui/new_platform';
import { Plugin } from './plugin';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new Plugin({ opaqueId: Symbol('siem'), env: {} as any }, chrome).start(npStart);
new Plugin({ opaqueId: Symbol('siem'), env: {} as any }, chrome).start(
npStart.core,
npStart.plugins
);

View file

@ -15,11 +15,6 @@ import template from './template.html';
export const ROOT_ELEMENT_ID = 'react-siem-root';
export interface StartObject {
core: LegacyCoreStart;
plugins: PluginsStart;
}
export class Plugin {
constructor(
// @ts-ignore this is added to satisfy the New Platform typing constraint,
@ -30,8 +25,7 @@ export class Plugin {
this.chrome = chrome;
}
public start(start: StartObject): void {
const { core, plugins } = start;
public start(core: LegacyCoreStart, plugins: PluginsStart) {
// @ts-ignore improper type description
this.chrome.setRootTemplate(template);
const checkForRoot = () => {

View file

@ -9,6 +9,8 @@ import React, { memo, FC } from 'react';
import { ApolloProvider } from 'react-apollo';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import { LegacyCoreStart } from 'kibana/public';
import { PluginsStart } from 'ui/new_platform/new_platform';
import { EuiErrorBoundary } from '@elastic/eui';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
@ -17,6 +19,9 @@ import { BehaviorSubject } from 'rxjs';
import { pluck } from 'rxjs/operators';
import { I18nContext } from 'ui/i18n';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { Storage } from '../../../../../../src/plugins/kibana_utils/public';
import { DEFAULT_DARK_MODE } from '../../common/constants';
import { ErrorToastDispatcher } from '../components/error_toast_dispatcher';
import { compose } from '../lib/compose/kibana_compose';
@ -31,8 +36,6 @@ import { MlCapabilitiesProvider } from '../components/ml/permissions/ml_capabili
import { ApolloClientContext } from '../utils/apollo_context';
import { StartObject } from './plugin';
const StartApp: FC<AppFrontendLibs> = memo(libs => {
const history = createHashHistory();
@ -74,10 +77,21 @@ const StartApp: FC<AppFrontendLibs> = memo(libs => {
export const ROOT_ELEMENT_ID = 'react-siem-root';
export const SiemApp = memo<StartObject>(({ core, plugins }) => (
<KibanaCoreContextProvider core={core}>
<KibanaPluginsContextProvider plugins={plugins}>
<StartApp {...compose()} />
</KibanaPluginsContextProvider>
</KibanaCoreContextProvider>
));
export const SiemApp = memo<{ core: LegacyCoreStart; plugins: PluginsStart }>(
({ core, plugins }) => (
<KibanaContextProvider
services={{
appName: 'siem',
data: plugins.data,
storage: new Storage(localStorage),
...core,
}}
>
<KibanaCoreContextProvider core={core}>
<KibanaPluginsContextProvider plugins={plugins}>
<StartApp {...compose()} />
</KibanaPluginsContextProvider>
</KibanaCoreContextProvider>
</KibanaContextProvider>
)
);

View file

@ -20,6 +20,8 @@ import { FlyoutButton } from './button';
const testFlyoutHeight = 980;
const usersViewing = ['elastic'];
jest.mock('../../lib/settings/use_kibana_ui_setting');
describe('Flyout', () => {
const state: State = mockGlobalState;

View file

@ -10,14 +10,21 @@ import 'jest-styled-components';
import * as React from 'react';
import { flyoutHeaderHeight } from '../';
import { useKibanaCore } from '../../../lib/compose/kibana_core';
import { TestProviders } from '../../../mock';
import { mockUiSettings } from '../../../mock/ui_settings';
import { Pane } from '.';
const testFlyoutHeight = 980;
const testWidth = 640;
const usersViewing = ['elastic'];
const mockUseKibanaCore = useKibanaCore as jest.Mock;
jest.mock('../../../lib/compose/kibana_core');
mockUseKibanaCore.mockImplementation(() => ({
uiSettings: mockUiSettings,
}));
describe('Pane', () => {
test('renders correctly against snapshot', () => {
const EmptyComponent = shallow(

View file

@ -235,6 +235,7 @@ describe('helpers', () => {
},
description: '',
eventIdToNoteIds: {},
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
id: 'savedObject-1',
@ -321,6 +322,7 @@ describe('helpers', () => {
},
description: '',
eventIdToNoteIds: {},
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
id: 'savedObject-1',
@ -400,6 +402,165 @@ describe('helpers', () => {
dataProviders: [],
description: '',
eventIdToNoteIds: {},
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
isLive: false,
isLoading: false,
isSaving: false,
itemsPerPage: 25,
itemsPerPageOptions: [10, 25, 50, 100],
kqlMode: 'filter',
kqlQuery: {
filterQuery: null,
filterQueryDraft: null,
},
title: '',
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
dateRange: {
start: 0,
end: 0,
},
show: false,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',
},
width: 1100,
id: 'savedObject-1',
});
});
test('should merge filters object back with json object', () => {
const timeline = {
savedObjectId: 'savedObject-1',
columns: timelineDefaults.columns.filter(column => column.id !== 'event.action'),
filters: [
{
meta: {
alias: null,
controlledBy: null,
disabled: false,
index: null,
key: 'event.category',
negate: false,
params: '{"query":"file"}',
type: 'phrase',
value: null,
},
query: '{"match_phrase":{"event.category":"file"}}',
exists: null,
},
{
meta: {
alias: null,
controlledBy: null,
disabled: false,
index: null,
key: '@timestamp',
negate: false,
params: null,
type: 'exists',
value: 'exists',
},
query: null,
exists: '{"field":"@timestamp"}',
},
],
version: '1',
};
const newTimeline = defaultTimelineToTimelineModel(timeline, false);
expect(newTimeline).toEqual({
savedObjectId: 'savedObject-1',
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
width: 190,
},
{
columnHeaderType: 'not-filtered',
id: 'message',
width: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.category',
width: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
width: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
width: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
width: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
width: 180,
},
],
version: '1',
dataProviders: [],
description: '',
eventIdToNoteIds: {},
filters: [
{
$state: {
store: 'appState',
},
meta: {
alias: null,
controlledBy: null,
disabled: false,
index: null,
key: 'event.category',
negate: false,
params: {
query: 'file',
},
type: 'phrase',
value: null,
},
query: {
match_phrase: {
'event.category': 'file',
},
},
},
{
$state: {
store: 'appState',
},
exists: {
field: '@timestamp',
},
meta: {
alias: null,
controlledBy: null,
disabled: false,
index: null,
key: '@timestamp',
negate: false,
params: null,
type: 'exists',
value: 'exists',
},
},
],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,

View file

@ -61,6 +61,14 @@ const omitTypename = (key: string, value: keyof TimelineModel) =>
const omitTypenameInTimeline = (timeline: TimelineResult): TimelineResult =>
JSON.parse(JSON.stringify(timeline), omitTypename);
const parseString = (params: string) => {
try {
return JSON.parse(params);
} catch {
return params;
}
};
export const defaultTimelineToTimelineModel = (
timeline: TimelineResult,
duplicate: boolean
@ -97,6 +105,32 @@ export const defaultTimelineToTimelineModel = (
return acc;
}, {})
: {},
filters:
timeline.filters != null
? timeline.filters.map(filter => ({
$state: {
store: 'appState',
},
meta: {
...filter.meta,
...(filter.meta && filter.meta.field != null
? { params: parseString(filter.meta.field) }
: {}),
...(filter.meta && filter.meta.params != null
? { params: parseString(filter.meta.params) }
: {}),
...(filter.meta && filter.meta.value != null
? { value: parseString(filter.meta.value) }
: {}),
},
...(filter.exists != null ? { exists: parseString(filter.exists) } : {}),
...(filter.match_all != null ? { exists: parseString(filter.match_all) } : {}),
...(filter.missing != null ? { exists: parseString(filter.missing) } : {}),
...(filter.query != null ? { query: parseString(filter.query) } : {}),
...(filter.range != null ? { range: parseString(filter.range) } : {}),
...(filter.script != null ? { exists: parseString(filter.script) } : {}),
}))
: [],
isFavorite: duplicate
? false
: timeline.favorite != null

View file

@ -30,10 +30,13 @@ mockUseKibanaCore.mockImplementation(() => ({
}));
// Test will fail because we will to need to mock some core services to make the test work
// For now let's forget about SiemSearchBar
// For now let's forget about SiemSearchBar and QueryBar
jest.mock('../../../search_bar', () => ({
SiemSearchBar: () => null,
}));
jest.mock('../../../query_bar', () => ({
QueryBar: () => null,
}));
describe('Hosts Table', () => {
const loadPage = jest.fn();

View file

@ -0,0 +1,342 @@
/*
* 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 React from 'react';
import { SearchBar } from '../../../../../../../src/legacy/core_plugins/data/public';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
import { uiSettingsServiceMock } from '../../../../../../../src/core/public/ui_settings/ui_settings_service.mock';
import { useKibanaCore } from '../../lib/compose/kibana_core';
import { TestProviders, mockIndexPattern } from '../../mock';
import { QueryBar, QueryBarComponentProps } from '.';
import { DEFAULT_FROM, DEFAULT_TO } from '../../../common/constants';
import { mockUiSettings } from '../../mock/ui_settings';
jest.mock('ui/new_platform');
const mockUseKibanaCore = useKibanaCore as jest.Mock;
const mockUiSettingsForFilterManager = uiSettingsServiceMock.createSetupContract();
jest.mock('../../lib/compose/kibana_core');
mockUseKibanaCore.mockImplementation(() => ({
uiSettings: mockUiSettings,
savedObjects: {},
}));
describe('QueryBar ', () => {
// We are doing that because we need to wrapped this component with redux
// and redux does not like to be updated and since we need to update our
// child component (BODY) and we do not want to scare anyone with this error
// we are hiding it!!!
// eslint-disable-next-line no-console
const originalError = console.error;
beforeAll(() => {
// eslint-disable-next-line no-console
console.error = (...args: string[]) => {
if (/<Provider> does not support changing `store` on the fly/.test(args[0])) {
return;
}
originalError.call(console, ...args);
};
});
const mockOnChangeQuery = jest.fn();
const mockOnSubmitQuery = jest.fn();
const mockOnSavedQuery = jest.fn();
beforeEach(() => {
mockOnChangeQuery.mockClear();
mockOnSubmitQuery.mockClear();
mockOnSavedQuery.mockClear();
});
test('check if we format the appropriate props to QueryBar', () => {
const wrapper = mount(
<TestProviders>
<QueryBar
dateRangeFrom={DEFAULT_FROM}
dateRangeTo={DEFAULT_TO}
hideSavedQuery={false}
indexPattern={mockIndexPattern}
isRefreshPaused={true}
filterQuery={{ query: 'here: query', language: 'kuery' }}
filterManager={new FilterManager(mockUiSettingsForFilterManager)}
filters={[]}
onChangedQuery={mockOnChangeQuery}
onSubmitQuery={mockOnSubmitQuery}
onSavedQuery={mockOnSavedQuery}
/>
</TestProviders>
);
const {
customSubmitButton,
timeHistory,
onClearSavedQuery,
onFiltersUpdated,
onQueryChange,
onQuerySubmit,
onSaved,
onSavedQueryUpdated,
...searchBarProps
} = wrapper.find(SearchBar).props();
expect(searchBarProps).toEqual({
dateRangeFrom: 'now-24h',
dateRangeTo: 'now',
filters: [],
indexPatterns: [
{
fields: [
{
aggregatable: true,
name: '@timestamp',
searchable: true,
type: 'date',
},
{
aggregatable: true,
name: '@version',
searchable: true,
type: 'string',
},
{
aggregatable: true,
name: 'agent.ephemeral_id',
searchable: true,
type: 'string',
},
{
aggregatable: true,
name: 'agent.hostname',
searchable: true,
type: 'string',
},
{
aggregatable: true,
name: 'agent.id',
searchable: true,
type: 'string',
},
{
aggregatable: true,
name: 'agent.test1',
searchable: true,
type: 'string',
},
{
aggregatable: true,
name: 'agent.test2',
searchable: true,
type: 'string',
},
{
aggregatable: true,
name: 'agent.test3',
searchable: true,
type: 'string',
},
{
aggregatable: true,
name: 'agent.test4',
searchable: true,
type: 'string',
},
{
aggregatable: true,
name: 'agent.test5',
searchable: true,
type: 'string',
},
{
aggregatable: true,
name: 'agent.test6',
searchable: true,
type: 'string',
},
{
aggregatable: true,
name: 'agent.test7',
searchable: true,
type: 'string',
},
{
aggregatable: true,
name: 'agent.test8',
searchable: true,
type: 'string',
},
{
aggregatable: true,
name: 'host.name',
searchable: true,
type: 'string',
},
],
title: 'filebeat-*,auditbeat-*,packetbeat-*',
},
],
isRefreshPaused: true,
query: {
language: 'kuery',
query: 'here: query',
},
refreshInterval: undefined,
showAutoRefreshOnly: false,
showDatePicker: false,
showFilterBar: true,
showQueryBar: true,
showQueryInput: true,
showSaveQuery: true,
});
});
describe('#onQueryChange', () => {
test(' is the only reference that changed when filterQueryDraft props get updated', () => {
const Proxy = (props: QueryBarComponentProps) => (
<TestProviders>
<QueryBar {...props} />
</TestProviders>
);
const wrapper = mount(
<Proxy
dateRangeFrom={DEFAULT_FROM}
dateRangeTo={DEFAULT_TO}
hideSavedQuery={false}
indexPattern={mockIndexPattern}
isRefreshPaused={true}
filterQuery={{ query: 'here: query', language: 'kuery' }}
filterManager={new FilterManager(mockUiSettingsForFilterManager)}
filters={[]}
onChangedQuery={mockOnChangeQuery}
onSubmitQuery={mockOnSubmitQuery}
onSavedQuery={mockOnSavedQuery}
/>
);
const searchBarProps = wrapper.find(SearchBar).props();
const onChangedQueryRef = searchBarProps.onQueryChange;
const onSubmitQueryRef = searchBarProps.onQuerySubmit;
const onSavedQueryRef = searchBarProps.onSavedQueryUpdated;
const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]');
queryInput.simulate('change', { target: { value: 'hello: world' } });
wrapper.update();
expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange);
expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit);
expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated);
});
});
describe('#onQuerySubmit', () => {
test(' is the only reference that changed when filterQuery props get updated', () => {
const Proxy = (props: QueryBarComponentProps) => (
<TestProviders>
<QueryBar {...props} />
</TestProviders>
);
const wrapper = mount(
<Proxy
dateRangeFrom={DEFAULT_FROM}
dateRangeTo={DEFAULT_TO}
hideSavedQuery={false}
indexPattern={mockIndexPattern}
isRefreshPaused={true}
filterQuery={{ query: 'here: query', language: 'kuery' }}
filterManager={new FilterManager(mockUiSettingsForFilterManager)}
filters={[]}
onChangedQuery={mockOnChangeQuery}
onSubmitQuery={mockOnSubmitQuery}
onSavedQuery={mockOnSavedQuery}
/>
);
const searchBarProps = wrapper.find(SearchBar).props();
const onChangedQueryRef = searchBarProps.onQueryChange;
const onSubmitQueryRef = searchBarProps.onQuerySubmit;
const onSavedQueryRef = searchBarProps.onSavedQueryUpdated;
wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } });
wrapper.update();
expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit);
expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange);
expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated);
});
test(' is only reference that changed when timelineId props get updated', () => {
const Proxy = (props: QueryBarComponentProps) => (
<TestProviders>
<QueryBar {...props} />
</TestProviders>
);
const wrapper = mount(
<Proxy
dateRangeFrom={DEFAULT_FROM}
dateRangeTo={DEFAULT_TO}
hideSavedQuery={false}
indexPattern={mockIndexPattern}
isRefreshPaused={true}
filterQuery={{ query: 'here: query', language: 'kuery' }}
filterManager={new FilterManager(mockUiSettingsForFilterManager)}
filters={[]}
onChangedQuery={mockOnChangeQuery}
onSubmitQuery={mockOnSubmitQuery}
onSavedQuery={mockOnSavedQuery}
/>
);
const searchBarProps = wrapper.find(SearchBar).props();
const onChangedQueryRef = searchBarProps.onQueryChange;
const onSubmitQueryRef = searchBarProps.onQuerySubmit;
const onSavedQueryRef = searchBarProps.onSavedQueryUpdated;
wrapper.setProps({ onSubmitQuery: jest.fn() });
wrapper.update();
expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit);
expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange);
expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated);
});
});
describe('#onSavedQueryUpdated', () => {
test('is only reference that changed when dataProviders props get updated', () => {
const Proxy = (props: QueryBarComponentProps) => (
<TestProviders>
<QueryBar {...props} />
</TestProviders>
);
const wrapper = mount(
<Proxy
dateRangeFrom={DEFAULT_FROM}
dateRangeTo={DEFAULT_TO}
hideSavedQuery={false}
indexPattern={mockIndexPattern}
isRefreshPaused={true}
filterQuery={{ query: 'here: query', language: 'kuery' }}
filterManager={new FilterManager(mockUiSettingsForFilterManager)}
filters={[]}
onChangedQuery={mockOnChangeQuery}
onSubmitQuery={mockOnSubmitQuery}
onSavedQuery={mockOnSavedQuery}
/>
);
const searchBarProps = wrapper.find(SearchBar).props();
const onChangedQueryRef = searchBarProps.onQueryChange;
const onSubmitQueryRef = searchBarProps.onQuerySubmit;
const onSavedQueryRef = searchBarProps.onSavedQueryUpdated;
wrapper.setProps({ onSavedQuery: jest.fn() });
wrapper.update();
expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated);
expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange);
expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit);
});
});
});

View file

@ -0,0 +1,146 @@
/*
* 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 { isEqual } from 'lodash/fp';
import React, { memo, useState, useEffect, useMemo, useCallback } from 'react';
import { StaticIndexPattern, IndexPattern } from 'ui/index_patterns';
import { SavedQuery, SearchBar } from '../../../../../../../src/legacy/core_plugins/data/public';
import {
esFilters,
FilterManager,
Query,
TimeHistory,
TimeRange,
} from '../../../../../../../src/plugins/data/public';
import { SavedQueryTimeFilter } from '../../../../../../../src/legacy/core_plugins/data/public/search';
import { Storage } from '../../../../../../../src/plugins/kibana_utils/public';
export interface QueryBarComponentProps {
dateRangeFrom?: string;
dateRangeTo?: string;
hideSavedQuery?: boolean;
indexPattern: StaticIndexPattern;
isRefreshPaused?: boolean;
filterQuery: Query;
filterManager: FilterManager;
filters: esFilters.Filter[];
onChangedQuery: (query: Query) => void;
onSubmitQuery: (query: Query, timefilter?: SavedQueryTimeFilter) => void;
refreshInterval?: number;
savedQuery?: SavedQuery | null;
onSavedQuery: (savedQuery: SavedQuery | null) => void;
}
export const QueryBar = memo<QueryBarComponentProps>(
({
dateRangeFrom,
dateRangeTo,
hideSavedQuery = false,
indexPattern,
isRefreshPaused,
filterQuery,
filterManager,
filters,
onChangedQuery,
onSubmitQuery,
refreshInterval,
savedQuery,
onSavedQuery,
}) => {
const [draftQuery, setDraftQuery] = useState(filterQuery);
useEffect(() => {
setDraftQuery(filterQuery);
}, [filterQuery]);
const onQuerySubmit = useCallback(
(payload: { dateRange: TimeRange; query?: Query }) => {
if (payload.query != null && !isEqual(payload.query, filterQuery)) {
onSubmitQuery(payload.query);
}
},
[filterQuery, onSubmitQuery]
);
const onQueryChange = useCallback(
(payload: { dateRange: TimeRange; query?: Query }) => {
if (payload.query != null && !isEqual(payload.query, draftQuery)) {
setDraftQuery(payload.query);
onChangedQuery(payload.query);
}
},
[draftQuery, onChangedQuery, setDraftQuery]
);
const onSaved = useCallback(
(newSavedQuery: SavedQuery) => {
onSavedQuery(newSavedQuery);
},
[onSavedQuery]
);
const onSavedQueryUpdated = useCallback(
(savedQueryUpdated: SavedQuery) => {
const { query: newQuery, filters: newFilters, timefilter } = savedQueryUpdated.attributes;
onSubmitQuery(newQuery, timefilter);
filterManager.setFilters(newFilters || []);
onSavedQuery(savedQueryUpdated);
},
[filterManager, onSubmitQuery, onSavedQuery]
);
const onClearSavedQuery = useCallback(() => {
if (savedQuery != null) {
onSubmitQuery({
query: '',
language: savedQuery.attributes.query.language,
});
filterManager.setFilters([]);
onSavedQuery(null);
}
}, [filterManager, onSubmitQuery, onSavedQuery, savedQuery]);
const onFiltersUpdated = useCallback(
(newFilters: esFilters.Filter[]) => {
filterManager.setFilters(newFilters);
},
[filterManager]
);
const CustomButton = <>{null}</>;
const indexPatterns = useMemo(() => [indexPattern as IndexPattern], [indexPattern]);
const searchBarProps = savedQuery != null ? { savedQuery } : {};
return (
<SearchBar
customSubmitButton={CustomButton}
dateRangeFrom={dateRangeFrom}
dateRangeTo={dateRangeTo}
filters={filters}
indexPatterns={indexPatterns}
isRefreshPaused={isRefreshPaused}
query={draftQuery}
onClearSavedQuery={onClearSavedQuery}
onFiltersUpdated={onFiltersUpdated}
onQueryChange={onQueryChange}
onQuerySubmit={onQuerySubmit}
onSaved={onSaved}
onSavedQueryUpdated={onSavedQueryUpdated}
refreshInterval={refreshInterval}
showAutoRefreshOnly={false}
showFilterBar={!hideSavedQuery}
showDatePicker={false}
showQueryBar={true}
showQueryInput={true}
showSaveQuery={true}
timeHistory={new TimeHistory(new Storage(localStorage))}
{...searchBarProps}
/>
);
}
);

View file

@ -234,7 +234,7 @@ const SearchBarComponent = memo<SiemSearchBarProps & SiemSearchBarRedux & SiemSe
savedQuery: undefined,
});
}
}, [id, end, fromStr, start, toStr]);
}, [id, end, fromStr, start, toStr, savedQuery]);
useEffect(() => {
let isSubscribed = true;
@ -258,13 +258,13 @@ const SearchBarComponent = memo<SiemSearchBarProps & SiemSearchBarRedux & SiemSe
subscriptions.unsubscribe();
};
}, []);
const IndexPatterns = useMemo(() => [indexPattern as IndexPattern], [indexPattern]);
const indexPatterns = useMemo(() => [indexPattern as IndexPattern], [indexPattern]);
return (
<SearchBarContainer data-test-subj={`${id}DatePicker`}>
<SearchBar
appName="siem"
isLoading={isLoading}
indexPatterns={IndexPatterns}
indexPatterns={indexPatterns}
query={filterQuery}
onClearSavedQuery={onClearSavedQuery}
onQuerySubmit={onQuerySubmit}

View file

@ -14,6 +14,8 @@ import { createStore, State } from '../../store';
import { SuperDatePicker, makeMapStateToProps } from '.';
import { cloneDeep } from 'lodash/fp';
jest.mock('../../lib/settings/use_kibana_ui_setting');
describe('SIEM Super Date Picker', () => {
describe('#SuperDatePicker', () => {
const state: State = mockGlobalState;
@ -74,38 +76,6 @@ describe('SIEM Super Date Picker', () => {
expect(store.getState().inputs.global.timerange.toStr).toBe('now/d');
});
test('Make Sure it is this week', () => {
wrapper
.find('[data-test-subj="superDatePickerToggleQuickMenuButton"]')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="superDatePickerCommonlyUsed_This_week"]')
.first()
.simulate('click');
wrapper.update();
expect(store.getState().inputs.global.timerange.fromStr).toBe('now/w');
expect(store.getState().inputs.global.timerange.toStr).toBe('now/w');
});
test('Make Sure it is week to date', () => {
wrapper
.find('[data-test-subj="superDatePickerToggleQuickMenuButton"]')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="superDatePickerCommonlyUsed_Week_to date"]')
.first()
.simulate('click');
wrapper.update();
expect(store.getState().inputs.global.timerange.fromStr).toBe('now/w');
expect(store.getState().inputs.global.timerange.toStr).toBe('now');
});
test('Make Sure to (end date) is superior than from (start date)', () => {
expect(store.getState().inputs.global.timerange.to).toBeGreaterThan(
store.getState().inputs.global.timerange.from
@ -168,60 +138,6 @@ describe('SIEM Super Date Picker', () => {
).toBe('Last 15 minutesToday');
});
test('Today and Year to date is in Recently used date ranges', () => {
wrapper
.find('[data-test-subj="superDatePickerToggleQuickMenuButton"]')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="superDatePickerCommonlyUsed_Year_to date"]')
.first()
.simulate('click');
wrapper.update();
expect(
wrapper
.find('div.euiQuickSelectPopover__section')
.at(1)
.text()
).toBe('Year to dateToday');
});
test('Today and Last 15 minutes and Year to date is in Recently used date ranges', () => {
wrapper
.find('[data-test-subj="superDatePickerToggleQuickMenuButton"]')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('button.euiQuickSelect__applyButton')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="superDatePickerToggleQuickMenuButton"]')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="superDatePickerCommonlyUsed_Year_to date"]')
.first()
.simulate('click');
wrapper.update();
expect(
wrapper
.find('div.euiQuickSelectPopover__section')
.at(1)
.text()
).toBe('Year to dateLast 15 minutesToday');
});
test('Make sure that it does not add any duplicate if you click again on today', () => {
wrapper
.find('[data-test-subj="superDatePickerToggleQuickMenuButton"]')

View file

@ -12,11 +12,13 @@ import {
OnRefreshProps,
OnTimeChangeProps,
} from '@elastic/eui';
import { getOr, take } from 'lodash/fp';
import { getOr, take, isEmpty } from 'lodash/fp';
import React, { useState, useCallback } from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../common/constants';
import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting';
import { inputsModel, State } from '../../store';
import { inputsActions, timelineActions } from '../../store/actions';
import { InputsModelId } from '../../store/inputs/constants';
@ -194,8 +196,18 @@ export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>(
const endDate = kind === 'relative' ? toStr : new Date(end).toISOString();
const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString();
const [quickRanges] = useKibanaUiSetting(DEFAULT_TIMEPICKER_QUICK_RANGES);
const commonlyUsedRanges = isEmpty(quickRanges)
? []
: quickRanges.map(({ from, to, display }: { from: string; to: string; display: string }) => ({
start: from,
end: to,
label: display,
}));
return (
<EuiSuperDatePicker
commonlyUsedRanges={commonlyUsedRanges}
end={endDate}
isLoading={isLoading}
isPaused={policy === 'manual'}

View file

@ -50,7 +50,6 @@ const TimelineKqlFetchComponent = memo<OwnProps>(
kueryFilterQuery,
kueryFilterQueryDraft,
storeType: 'timelineType',
type: null,
timelineId: id,
}),
});

View file

@ -150,6 +150,7 @@ exports[`Header rendering renders correctly against snapshot 1`] = `
show={true}
/>
<Connect(StatefulSearchOrFilterComponent)
browserFields={Object {}}
indexPattern={
Object {
"fields": Array [

View file

@ -9,12 +9,21 @@ import toJson from 'enzyme-to-json';
import * as React from 'react';
import { Direction } from '../../../graphql/types';
import { useKibanaCore } from '../../../lib/compose/kibana_core';
import { mockIndexPattern } from '../../../mock';
import { TestProviders } from '../../../mock/test_providers';
import { mockUiSettings } from '../../../mock/ui_settings';
import { mockDataProviders } from '../data_providers/mock/mock_data_providers';
import { TimelineHeader } from '.';
const mockUseKibanaCore = useKibanaCore as jest.Mock;
jest.mock('../../../lib/compose/kibana_core');
mockUseKibanaCore.mockImplementation(() => ({
uiSettings: mockUiSettings,
savedObjects: {},
}));
describe('Header', () => {
const indexPattern = mockIndexPattern;

View file

@ -84,7 +84,11 @@ export const TimelineHeader = React.memo<Props>(
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
show={show}
/>
<StatefulSearchOrFilter timelineId={id} indexPattern={indexPattern} />
<StatefulSearchOrFilter
browserFields={browserFields}
indexPattern={indexPattern}
timelineId={id}
/>
</TimelineHeaderContainer>
)
);

View file

@ -6,6 +6,8 @@
import { cloneDeep } from 'lodash/fp';
import { esFilters } from '../../../../../../../src/plugins/data/public';
import { FilterStateStore } from '../../../../../../../src/plugins/data/common/es_query/filters';
import { mockIndexPattern } from '../../mock';
import { mockDataProviders } from './data_providers/mock/mock_data_providers';
@ -160,6 +162,52 @@ describe('Combined Queries', () => {
});
});
test('No Data Provider & No kqlQuery & with Filters', () => {
const isEventViewer = true;
expect(
combineQueries({
config,
dataProviders: [],
indexPattern: mockIndexPattern,
browserFields: mockBrowserFields,
filters: [
{
$state: { store: FilterStateStore.APP_STATE },
meta: {
alias: null,
disabled: false,
key: 'event.category',
negate: false,
params: { query: 'file' },
type: 'phrase',
},
query: { match_phrase: { 'event.category': 'file' } },
},
{
$state: { store: FilterStateStore.APP_STATE },
meta: {
alias: null,
disabled: false,
key: 'host.name',
negate: false,
type: 'exists',
value: 'exists',
},
exists: { field: 'host.name' },
} as esFilters.Filter,
],
kqlQuery: { query: '', language: 'kuery' },
kqlMode: 'search',
start: startDate,
end: endDate,
isEventViewer,
})
).toEqual({
filterQuery:
'{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}',
});
});
test('Only Data Provider', () => {
const dataProviders = mockDataProviders.slice(0, 1);
const { filterQuery } = combineQueries({

View file

@ -7,7 +7,7 @@
import { isEmpty, isNumber, get } from 'lodash/fp';
import memoizeOne from 'memoize-one';
import { StaticIndexPattern } from 'ui/index_patterns';
import { Query, esFilters } from 'src/plugins/data/public';
import { Query, esFilters } from '../../../../../../../src/plugins/data/public';
import { escapeQueryValue, convertToBuildEsQuery, EsQueryConfig } from '../../lib/keury';
@ -113,13 +113,18 @@ export const combineQueries = ({
isEventViewer?: boolean;
}): { filterQuery: string } | null => {
const kuery: Query = { query: '', language: kqlQuery.language };
if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEventViewer) {
if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) {
return null;
} else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) {
kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`;
return {
filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }),
};
} else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) {
kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`;
return {
filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }),
};
} else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) {
kuery.query = `(${kqlQuery.query}) and @timestamp >= ${start} and @timestamp <= ${end}`;
return {

View file

@ -9,6 +9,8 @@ import React, { useEffect, useCallback } from 'react';
import { connect } from 'react-redux';
import { ActionCreator } from 'typescript-fsa';
import { esFilters } from '../../../../../../../src/plugins/data/public';
import { WithSource } from '../../containers/source';
import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store';
import { timelineActions } from '../../store/actions';
@ -40,6 +42,7 @@ interface StateReduxProps {
columns: ColumnHeader[];
dataProviders?: DataProvider[];
end: number;
filters: esFilters.Filter[];
isLive: boolean;
itemsPerPage?: number;
itemsPerPageOptions?: number[];
@ -137,6 +140,7 @@ const StatefulTimelineComponent = React.memo<Props>(
createTimeline,
dataProviders,
end,
filters,
flyoutHeaderHeight,
flyoutHeight,
id,
@ -252,6 +256,7 @@ const StatefulTimelineComponent = React.memo<Props>(
columns={columns}
dataProviders={dataProviders!}
end={end}
filters={filters}
flyoutHeaderHeight={flyoutHeaderHeight}
flyoutHeight={flyoutHeight}
id={id}
@ -295,6 +300,7 @@ const StatefulTimelineComponent = React.memo<Props>(
prevProps.start === nextProps.start &&
isEqual(prevProps.columns, nextProps.columns) &&
isEqual(prevProps.dataProviders, nextProps.dataProviders) &&
isEqual(prevProps.filters, nextProps.filters) &&
isEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) &&
isEqual(prevProps.sort, nextProps.sort)
);
@ -314,6 +320,7 @@ const makeMapStateToProps = () => {
const {
columns,
dataProviders,
filters,
itemsPerPage,
itemsPerPageOptions,
kqlMode,
@ -322,10 +329,13 @@ const makeMapStateToProps = () => {
} = timeline;
const kqlQueryExpression = getKqlQueryTimeline(state, id);
const timelineFilter = kqlMode === 'filter' ? filters || [] : [];
return {
columns,
dataProviders,
end: input.timerange.to,
filters: timelineFilter,
id,
isLive: input.policy.kind === 'interval',
itemsPerPage,

View file

@ -8,11 +8,19 @@ import { mount } from 'enzyme';
import * as React from 'react';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { useKibanaCore } from '../../../lib/compose/kibana_core';
import { mockGlobalState, apolloClientObservable } from '../../../mock';
import { mockUiSettings } from '../../../mock/ui_settings';
import { createStore, State } from '../../../store';
import { Properties, showDescriptionThreshold, showNotesThreshold } from '.';
const mockUseKibanaCore = useKibanaCore as jest.Mock;
jest.mock('../../../lib/compose/kibana_core');
mockUseKibanaCore.mockImplementation(() => ({
uiSettings: mockUiSettings,
}));
describe('Properties', () => {
const usersViewing = ['elastic'];

View file

@ -0,0 +1,406 @@
/*
* 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 React from 'react';
import { DEFAULT_FROM, DEFAULT_TO } from '../../../../common/constants';
import { mockBrowserFields } from '../../../containers/source/mock';
import { useKibanaCore } from '../../../lib/compose/kibana_core';
import { convertKueryToElasticSearchQuery } from '../../../lib/keury';
import { mockIndexPattern, TestProviders } from '../../../mock';
import { mockUiSettings } from '../../../mock/ui_settings';
import { QueryBar } from '../../query_bar';
import { mockDataProviders } from '../data_providers/mock/mock_data_providers';
import { buildGlobalQuery } from '../helpers';
import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index';
const mockUseKibanaCore = useKibanaCore as jest.Mock;
jest.mock('../../../lib/compose/kibana_core');
mockUseKibanaCore.mockImplementation(() => ({
uiSettings: mockUiSettings,
savedObjects: {},
}));
describe('Timeline QueryBar ', () => {
// We are doing that because we need to wrapped this component with redux
// and redux does not like to be updated and since we need to update our
// child component (BODY) and we do not want to scare anyone with this error
// we are hiding it!!!
// eslint-disable-next-line no-console
const originalError = console.error;
beforeAll(() => {
// eslint-disable-next-line no-console
console.error = (...args: string[]) => {
if (/<Provider> does not support changing `store` on the fly/.test(args[0])) {
return;
}
originalError.call(console, ...args);
};
});
const mockApplyKqlFilterQuery = jest.fn();
const mockSetFilters = jest.fn();
const mockSetKqlFilterQueryDraft = jest.fn();
const mockSetSavedQueryId = jest.fn();
const mockUpdateReduxTime = jest.fn();
beforeEach(() => {
mockApplyKqlFilterQuery.mockClear();
mockSetFilters.mockClear();
mockSetKqlFilterQueryDraft.mockClear();
mockSetSavedQueryId.mockClear();
mockUpdateReduxTime.mockClear();
});
test('check if we format the appropriate props to QueryBar', () => {
const wrapper = mount(
<TestProviders>
<QueryBarTimeline
applyKqlFilterQuery={mockApplyKqlFilterQuery}
browserFields={mockBrowserFields}
dataProviders={mockDataProviders}
filters={[]}
filterQuery={{ expression: 'here: query', kind: 'kuery' }}
filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }}
from={0}
fromStr={DEFAULT_FROM}
to={1}
toStr={DEFAULT_TO}
kqlMode="search"
indexPattern={mockIndexPattern}
isRefreshPaused={true}
refreshInterval={3000}
savedQueryId={null}
setFilters={mockSetFilters}
setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft}
setSavedQueryId={mockSetSavedQueryId}
timelineId="timline-real-id"
updateReduxTime={mockUpdateReduxTime}
/>
</TestProviders>
);
const queryBarProps = wrapper.find(QueryBar).props();
expect(queryBarProps.dateRangeFrom).toEqual('now-24h');
expect(queryBarProps.dateRangeTo).toEqual('now');
expect(queryBarProps.filterQuery).toEqual({ query: 'here: query', language: 'kuery' });
expect(queryBarProps.savedQuery).toEqual(null);
});
describe('#onChangeQuery', () => {
test(' is the only reference that changed when filterQueryDraft props get updated', () => {
const Proxy = (props: QueryBarTimelineComponentProps) => (
<TestProviders>
<QueryBarTimeline {...props} />
</TestProviders>
);
const wrapper = mount(
<Proxy
applyKqlFilterQuery={mockApplyKqlFilterQuery}
browserFields={mockBrowserFields}
dataProviders={mockDataProviders}
filters={[]}
filterQuery={{ expression: 'here: query', kind: 'kuery' }}
filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }}
from={0}
fromStr={DEFAULT_FROM}
to={1}
toStr={DEFAULT_TO}
kqlMode="search"
indexPattern={mockIndexPattern}
isRefreshPaused={true}
refreshInterval={3000}
savedQueryId={null}
setFilters={mockSetFilters}
setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft}
setSavedQueryId={mockSetSavedQueryId}
timelineId="timeline-real-id"
updateReduxTime={mockUpdateReduxTime}
/>
);
const queryBarProps = wrapper.find(QueryBar).props();
const onChangedQueryRef = queryBarProps.onChangedQuery;
const onSubmitQueryRef = queryBarProps.onSubmitQuery;
const onSavedQueryRef = queryBarProps.onSavedQuery;
wrapper.setProps({ filterQueryDraft: { expression: 'new: one', kind: 'kuery' } });
wrapper.update();
expect(onChangedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onChangedQuery);
expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery);
expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery);
});
});
describe('#onSubmitQuery', () => {
test(' is the only reference that changed when filterQuery props get updated', () => {
const Proxy = (props: QueryBarTimelineComponentProps) => (
<TestProviders>
<QueryBarTimeline {...props} />
</TestProviders>
);
const wrapper = mount(
<Proxy
applyKqlFilterQuery={mockApplyKqlFilterQuery}
browserFields={mockBrowserFields}
dataProviders={mockDataProviders}
filters={[]}
filterQuery={{ expression: 'here: query', kind: 'kuery' }}
filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }}
from={0}
fromStr={DEFAULT_FROM}
to={1}
toStr={DEFAULT_TO}
kqlMode="search"
indexPattern={mockIndexPattern}
isRefreshPaused={true}
refreshInterval={3000}
savedQueryId={null}
setFilters={mockSetFilters}
setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft}
setSavedQueryId={mockSetSavedQueryId}
timelineId="timeline-real-id"
updateReduxTime={mockUpdateReduxTime}
/>
);
const queryBarProps = wrapper.find(QueryBar).props();
const onChangedQueryRef = queryBarProps.onChangedQuery;
const onSubmitQueryRef = queryBarProps.onSubmitQuery;
const onSavedQueryRef = queryBarProps.onSavedQuery;
wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } });
wrapper.update();
expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery);
expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery);
expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery);
});
test(' is only reference that changed when timelineId props get updated', () => {
const Proxy = (props: QueryBarTimelineComponentProps) => (
<TestProviders>
<QueryBarTimeline {...props} />
</TestProviders>
);
const wrapper = mount(
<Proxy
applyKqlFilterQuery={mockApplyKqlFilterQuery}
browserFields={mockBrowserFields}
dataProviders={mockDataProviders}
filters={[]}
filterQuery={{ expression: 'here: query', kind: 'kuery' }}
filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }}
from={0}
fromStr={DEFAULT_FROM}
to={1}
toStr={DEFAULT_TO}
kqlMode="search"
indexPattern={mockIndexPattern}
isRefreshPaused={true}
refreshInterval={3000}
savedQueryId={null}
setFilters={mockSetFilters}
setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft}
setSavedQueryId={mockSetSavedQueryId}
timelineId="timeline-real-id"
updateReduxTime={mockUpdateReduxTime}
/>
);
const queryBarProps = wrapper.find(QueryBar).props();
const onChangedQueryRef = queryBarProps.onChangedQuery;
const onSubmitQueryRef = queryBarProps.onSubmitQuery;
const onSavedQueryRef = queryBarProps.onSavedQuery;
wrapper.setProps({ timelineId: 'new-timeline' });
wrapper.update();
expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery);
expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery);
expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery);
});
});
describe('#onSavedQuery', () => {
test('is only reference that changed when dataProviders props get updated', () => {
const Proxy = (props: QueryBarTimelineComponentProps) => (
<TestProviders>
<QueryBarTimeline {...props} />
</TestProviders>
);
const wrapper = mount(
<Proxy
applyKqlFilterQuery={mockApplyKqlFilterQuery}
browserFields={mockBrowserFields}
dataProviders={mockDataProviders}
filters={[]}
filterQuery={{ expression: 'here: query', kind: 'kuery' }}
filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }}
from={0}
fromStr={DEFAULT_FROM}
to={1}
toStr={DEFAULT_TO}
kqlMode="search"
indexPattern={mockIndexPattern}
isRefreshPaused={true}
refreshInterval={3000}
savedQueryId={null}
setFilters={mockSetFilters}
setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft}
setSavedQueryId={mockSetSavedQueryId}
timelineId="timeline-real-id"
updateReduxTime={mockUpdateReduxTime}
/>
);
const queryBarProps = wrapper.find(QueryBar).props();
const onChangedQueryRef = queryBarProps.onChangedQuery;
const onSubmitQueryRef = queryBarProps.onSubmitQuery;
const onSavedQueryRef = queryBarProps.onSavedQuery;
wrapper.setProps({ dataProviders: mockDataProviders.slice(1, 0) });
wrapper.update();
expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery);
expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery);
expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery);
});
test('is only reference that changed when savedQueryId props get updated', () => {
const Proxy = (props: QueryBarTimelineComponentProps) => (
<TestProviders>
<QueryBarTimeline {...props} />
</TestProviders>
);
const wrapper = mount(
<Proxy
applyKqlFilterQuery={mockApplyKqlFilterQuery}
browserFields={mockBrowserFields}
dataProviders={mockDataProviders}
filters={[]}
filterQuery={{ expression: 'here: query', kind: 'kuery' }}
filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }}
from={0}
fromStr={DEFAULT_FROM}
to={1}
toStr={DEFAULT_TO}
kqlMode="search"
indexPattern={mockIndexPattern}
isRefreshPaused={true}
refreshInterval={3000}
savedQueryId={null}
setFilters={mockSetFilters}
setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft}
setSavedQueryId={mockSetSavedQueryId}
timelineId="timeline-real-id"
updateReduxTime={mockUpdateReduxTime}
/>
);
const queryBarProps = wrapper.find(QueryBar).props();
const onChangedQueryRef = queryBarProps.onChangedQuery;
const onSubmitQueryRef = queryBarProps.onSubmitQuery;
const onSavedQueryRef = queryBarProps.onSavedQuery;
wrapper.setProps({
savedQueryId: 'new',
});
wrapper.update();
expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery);
expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery);
expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery);
});
});
describe('#getDataProviderFilter', () => {
test('returns valid data provider filter with a simple bool data provider', () => {
const dataProvidersDsl = convertKueryToElasticSearchQuery(
buildGlobalQuery(mockDataProviders.slice(0, 1), mockBrowserFields),
mockIndexPattern
);
const filter = getDataProviderFilter(dataProvidersDsl);
expect(filter).toEqual({
$state: {
store: 'appState',
},
bool: {
minimum_should_match: 1,
should: [
{
match_phrase: {
name: 'Provider 1',
},
},
],
},
meta: {
alias: 'timeline-filter-drop-area',
controlledBy: 'timeline-filter-drop-area',
disabled: false,
key: 'bool',
negate: false,
type: 'custom',
value:
'{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}',
},
});
});
test('returns valid data provider filter with an exists operator', () => {
const dataProvidersDsl = convertKueryToElasticSearchQuery(
buildGlobalQuery(
[
{
id: `id-exists`,
name,
enabled: true,
excluded: false,
kqlQuery: '',
queryMatch: {
field: 'host.name',
value: '',
operator: ':*',
},
and: [],
},
],
mockBrowserFields
),
mockIndexPattern
);
const filter = getDataProviderFilter(dataProvidersDsl);
expect(filter).toEqual({
$state: {
store: 'appState',
},
bool: {
minimum_should_match: 1,
should: [
{
exists: {
field: 'host.name',
},
},
],
},
meta: {
alias: 'timeline-filter-drop-area',
controlledBy: 'timeline-filter-drop-area',
disabled: false,
key: 'bool',
negate: false,
type: 'custom',
value: '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}',
},
});
});
});
});

View file

@ -0,0 +1,310 @@
/*
* 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 { isEqual, isEmpty } from 'lodash/fp';
import React, { memo, useCallback, useState, useEffect } from 'react';
import { StaticIndexPattern } from 'ui/index_patterns';
import { Query } from 'src/plugins/data/common/types';
import { Subscription } from 'rxjs';
import { SavedQueryTimeFilter } from '../../../../../../../../src/legacy/core_plugins/data/public/search';
import { SavedQuery } from '../../../../../../../../src/legacy/core_plugins/data/public';
import { FilterManager } from '../../../../../../../../src/plugins/data/public';
import {
Filter,
FilterStateStore,
} from '../../../../../../../../src/plugins/data/common/es_query/filters';
import { BrowserFields } from '../../../containers/source';
import { convertKueryToElasticSearchQuery } from '../../../lib/keury';
import { useKibanaCore } from '../../../lib/compose/kibana_core';
import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store';
import { KqlMode } from '../../../store/timeline/model';
import { useSavedQueryServices } from '../../../utils/saved_query_services';
import { DispatchUpdateReduxTime } from '../../super_date_picker';
import { QueryBar } from '../../query_bar';
import { DataProvider } from '../data_providers/data_provider';
import { buildGlobalQuery } from '../helpers';
export interface QueryBarTimelineComponentProps {
applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void;
browserFields: BrowserFields;
dataProviders: DataProvider[];
filters: Filter[];
filterQuery: KueryFilterQuery;
filterQueryDraft: KueryFilterQuery;
from: number;
fromStr: string;
kqlMode: KqlMode;
indexPattern: StaticIndexPattern;
isRefreshPaused: boolean;
refreshInterval: number;
savedQueryId: string | null;
setFilters: (filters: Filter[]) => void;
setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void;
setSavedQueryId: (savedQueryId: string | null) => void;
timelineId: string;
to: number;
toStr: string;
updateReduxTime: DispatchUpdateReduxTime;
}
const timelineFilterDropArea = 'timeline-filter-drop-area';
export const QueryBarTimeline = memo<QueryBarTimelineComponentProps>(
({
applyKqlFilterQuery,
browserFields,
dataProviders,
filters,
filterQuery,
filterQueryDraft,
from,
fromStr,
kqlMode,
indexPattern,
isRefreshPaused,
savedQueryId,
setFilters,
setKqlFilterQueryDraft,
setSavedQueryId,
refreshInterval,
timelineId,
to,
toStr,
updateReduxTime,
}) => {
const [dateRangeFrom, setDateRangeFrom] = useState<string>(
fromStr != null ? fromStr : new Date(from).toISOString()
);
const [dateRangeTo, setDateRangTo] = useState<string>(
toStr != null ? toStr : new Date(to).toISOString()
);
const [savedQuery, setSavedQuery] = useState<SavedQuery | null>(null);
const [filterQueryConverted, setFilterQueryConverted] = useState<Query>({
query: filterQuery != null ? filterQuery.expression : '',
language: filterQuery != null ? filterQuery.kind : 'kuery',
});
const [queryBarFilters, setQueryBarFilters] = useState<Filter[]>([]);
const [dataProvidersDsl, setDataProvidersDsl] = useState<string>(
convertKueryToElasticSearchQuery(buildGlobalQuery(dataProviders, browserFields), indexPattern)
);
const core = useKibanaCore();
const [filterManager] = useState<FilterManager>(new FilterManager(core.uiSettings));
const savedQueryServices = useSavedQueryServices();
useEffect(() => {
let isSubscribed = true;
const subscriptions = new Subscription();
filterManager.setFilters(filters);
subscriptions.add(
filterManager.getUpdates$().subscribe({
next: () => {
if (isSubscribed) {
const filterWithoutDropArea = filterManager
.getFilters()
.filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea);
setFilters(filterWithoutDropArea);
setQueryBarFilters(filterWithoutDropArea);
}
},
})
);
return () => {
isSubscribed = false;
subscriptions.unsubscribe();
};
}, []);
useEffect(() => {
const filterWithoutDropArea = filterManager
.getFilters()
.filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea);
if (!isEqual(filters, filterWithoutDropArea)) {
filterManager.setFilters(filters);
}
}, [filters]);
useEffect(() => {
setFilterQueryConverted({
query: filterQuery != null ? filterQuery.expression : '',
language: filterQuery != null ? filterQuery.kind : 'kuery',
});
}, [filterQuery]);
useEffect(() => {
setDataProvidersDsl(
convertKueryToElasticSearchQuery(
buildGlobalQuery(dataProviders, browserFields),
indexPattern
)
);
}, [dataProviders, browserFields, indexPattern]);
useEffect(() => {
if (fromStr != null && toStr != null) {
setDateRangeFrom(fromStr);
setDateRangTo(toStr);
} else if (from != null && to != null) {
setDateRangeFrom(new Date(from).toISOString());
setDateRangTo(new Date(to).toISOString());
}
}, [from, fromStr, to, toStr]);
useEffect(() => {
let isSubscribed = true;
async function setSavedQueryByServices() {
if (savedQueryId != null && savedQueryServices != null) {
const mySavedQuery = await savedQueryServices.getSavedQuery(savedQueryId);
if (isSubscribed) {
setSavedQuery({
...mySavedQuery,
attributes: {
...mySavedQuery.attributes,
filters: filters.filter(f => f.meta.controlledBy !== timelineFilterDropArea),
},
});
}
} else if (isSubscribed) {
setSavedQuery(null);
}
}
setSavedQueryByServices();
return () => {
isSubscribed = false;
};
}, [savedQueryId]);
const onChangedQuery = useCallback(
(newQuery: Query) => {
if (
filterQueryDraft == null ||
(filterQueryDraft != null && filterQueryDraft.expression !== newQuery.query) ||
filterQueryDraft.kind !== newQuery.language
) {
setKqlFilterQueryDraft(
newQuery.query as string,
newQuery.language as KueryFilterQueryKind
);
}
},
[filterQueryDraft]
);
const onSubmitQuery = useCallback(
(newQuery: Query, timefilter?: SavedQueryTimeFilter) => {
if (
filterQuery == null ||
(filterQuery != null && filterQuery.expression !== newQuery.query) ||
filterQuery.kind !== newQuery.language
) {
setKqlFilterQueryDraft(
newQuery.query as string,
newQuery.language as KueryFilterQueryKind
);
applyKqlFilterQuery(newQuery.query as string, newQuery.language as KueryFilterQueryKind);
}
if (timefilter != null) {
const isQuickSelection = timefilter.from.includes('now') || timefilter.to.includes('now');
updateReduxTime({
id: 'timeline',
end: timefilter.to,
start: timefilter.from,
isInvalid: false,
isQuickSelection,
timelineId,
});
}
},
[filterQuery, timelineId]
);
const onSavedQuery = useCallback(
(newSavedQuery: SavedQuery | null) => {
if (newSavedQuery != null) {
if (newSavedQuery.id !== savedQueryId) {
setSavedQueryId(newSavedQuery.id);
}
if (savedQueryServices != null && dataProvidersDsl !== '') {
const dataProviderFilterExists =
newSavedQuery.attributes.filters != null
? newSavedQuery.attributes.filters.findIndex(
f => f.meta.controlledBy === timelineFilterDropArea
)
: -1;
savedQueryServices.saveQuery(
{
...newSavedQuery.attributes,
filters:
newSavedQuery.attributes.filters != null
? dataProviderFilterExists > -1
? [
...newSavedQuery.attributes.filters.slice(0, dataProviderFilterExists),
getDataProviderFilter(dataProvidersDsl),
...newSavedQuery.attributes.filters.slice(dataProviderFilterExists + 1),
]
: [
...newSavedQuery.attributes.filters,
getDataProviderFilter(dataProvidersDsl),
]
: [],
},
{
overwrite: true,
}
);
}
} else {
setSavedQueryId(null);
}
},
[dataProvidersDsl, savedQueryId, savedQueryServices]
);
return (
<QueryBar
dateRangeFrom={dateRangeFrom}
dateRangeTo={dateRangeTo}
hideSavedQuery={kqlMode === 'search'}
indexPattern={indexPattern}
isRefreshPaused={isRefreshPaused}
filterQuery={filterQueryConverted}
filterManager={filterManager}
filters={queryBarFilters}
onChangedQuery={onChangedQuery}
onSubmitQuery={onSubmitQuery}
refreshInterval={refreshInterval}
savedQuery={savedQuery}
onSavedQuery={onSavedQuery}
/>
);
}
);
export const getDataProviderFilter = (dataProviderDsl: string): Filter => {
const dslObject = JSON.parse(dataProviderDsl);
const key = Object.keys(dslObject);
return {
...dslObject,
meta: {
alias: timelineFilterDropArea,
controlledBy: timelineFilterDropArea,
negate: false,
disabled: false,
type: 'custom',
key: isEmpty(key) ? 'bool' : key[0],
value: dataProviderDsl,
},
$state: {
store: FilterStateStore.APP_STATE,
},
};
};

View file

@ -4,43 +4,69 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getOr } from 'lodash/fp';
import { getOr, isEqual } from 'lodash/fp';
import React, { useCallback } from 'react';
import { connect } from 'react-redux';
import { ActionCreator } from 'typescript-fsa';
import { Dispatch } from 'redux';
import { StaticIndexPattern } from 'ui/index_patterns';
import { esFilters } from '../../../../../../../../src/plugins/data/public';
import { BrowserFields } from '../../../containers/source';
import { convertKueryToElasticSearchQuery } from '../../../lib/keury';
import { KueryFilterQuery, SerializedFilterQuery, State, timelineSelectors } from '../../../store';
import { SearchOrFilter } from './search_or_filter';
import {
KueryFilterQuery,
SerializedFilterQuery,
State,
timelineSelectors,
inputsModel,
inputsSelectors,
} from '../../../store';
import { timelineActions } from '../../../store/actions';
import { KqlMode, TimelineModel } from '../../../store/timeline/model';
import { DispatchUpdateReduxTime, dispatchUpdateReduxTime } from '../../super_date_picker';
import { DataProvider } from '../data_providers/data_provider';
import { SearchOrFilter } from './search_or_filter';
interface OwnProps {
browserFields: BrowserFields;
indexPattern: StaticIndexPattern;
timelineId: string;
}
interface StateReduxProps {
dataProviders: DataProvider[];
filters: esFilters.Filter[];
filterQuery: KueryFilterQuery;
filterQueryDraft: KueryFilterQuery;
isFilterQueryDraftValid: boolean;
kqlMode?: KqlMode;
from: number;
fromStr: string;
isRefreshPaused: boolean;
kqlMode: KqlMode;
refreshInterval: number;
savedQueryId: string | null;
to: number;
toStr: string;
}
interface DispatchProps {
applyKqlFilterQuery: ActionCreator<{
applyKqlFilterQuery: ({
id,
filterQuery,
}: {
id: string;
filterQuery: SerializedFilterQuery;
}>;
updateKqlMode: ActionCreator<{
id: string;
kqlMode: KqlMode;
}>;
setKqlFilterQueryDraft: ActionCreator<{
}) => void;
updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => void;
setKqlFilterQueryDraft: ({
id,
filterQueryDraft,
}: {
id: string;
filterQueryDraft: KueryFilterQuery;
}>;
}) => void;
setSavedQueryId: ({ id, savedQueryId }: { id: string; savedQueryId: string | null }) => void;
setFilters: ({ id, filters }: { id: string; filters: esFilters.Filter[] }) => void;
updateReduxTime: DispatchUpdateReduxTime;
}
type Props = OwnProps & StateReduxProps & DispatchProps;
@ -48,21 +74,34 @@ type Props = OwnProps & StateReduxProps & DispatchProps;
const StatefulSearchOrFilterComponent = React.memo<Props>(
({
applyKqlFilterQuery,
browserFields,
dataProviders,
filters,
filterQuery,
filterQueryDraft,
from,
fromStr,
indexPattern,
isFilterQueryDraftValid,
isRefreshPaused,
kqlMode,
refreshInterval,
savedQueryId,
setFilters,
setKqlFilterQueryDraft,
setSavedQueryId,
timelineId,
to,
toStr,
updateKqlMode,
updateReduxTime,
}) => {
const applyFilterQueryFromKueryExpression = useCallback(
(expression: string) =>
(expression: string, kind) =>
applyKqlFilterQuery({
id: timelineId,
filterQuery: {
kuery: {
kind: 'kuery',
kind,
expression,
},
serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern),
@ -72,29 +111,80 @@ const StatefulSearchOrFilterComponent = React.memo<Props>(
);
const setFilterQueryDraftFromKueryExpression = useCallback(
(expression: string) =>
(expression: string, kind) =>
setKqlFilterQueryDraft({
id: timelineId,
filterQueryDraft: {
kind: 'kuery',
kind,
expression,
},
}),
[timelineId]
);
const setFiltersInTimeline = useCallback(
(newFilters: esFilters.Filter[]) =>
setFilters({
id: timelineId,
filters: newFilters,
}),
[timelineId]
);
const setSavedQueryInTimeline = useCallback(
(newSavedQueryId: string | null) =>
setSavedQueryId({
id: timelineId,
savedQueryId: newSavedQueryId,
}),
[timelineId]
);
return (
<SearchOrFilter
applyKqlFilterQuery={applyFilterQueryFromKueryExpression}
browserFields={browserFields}
dataProviders={dataProviders}
filters={filters}
filterQuery={filterQuery}
filterQueryDraft={filterQueryDraft}
from={from}
fromStr={fromStr}
indexPattern={indexPattern}
isFilterQueryDraftValid={isFilterQueryDraftValid}
isRefreshPaused={isRefreshPaused}
kqlMode={kqlMode!}
refreshInterval={refreshInterval}
savedQueryId={savedQueryId}
setFilters={setFiltersInTimeline}
setKqlFilterQueryDraft={setFilterQueryDraftFromKueryExpression!}
setSavedQueryId={setSavedQueryInTimeline}
timelineId={timelineId}
to={to}
toStr={toStr}
updateKqlMode={updateKqlMode!}
updateReduxTime={updateReduxTime}
/>
);
},
(prevProps, nextProps) => {
return (
prevProps.from === nextProps.from &&
prevProps.fromStr === nextProps.fromStr &&
prevProps.to === nextProps.to &&
prevProps.toStr === nextProps.toStr &&
prevProps.isRefreshPaused === nextProps.isRefreshPaused &&
prevProps.refreshInterval === nextProps.refreshInterval &&
prevProps.timelineId === nextProps.timelineId &&
isEqual(prevProps.browserFields, nextProps.browserFields) &&
isEqual(prevProps.dataProviders, nextProps.dataProviders) &&
isEqual(prevProps.filters, nextProps.filters) &&
isEqual(prevProps.filterQuery, nextProps.filterQuery) &&
isEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) &&
isEqual(prevProps.indexPattern, nextProps.indexPattern) &&
isEqual(prevProps.kqlMode, nextProps.kqlMode) &&
isEqual(prevProps.savedQueryId, nextProps.savedQueryId) &&
isEqual(prevProps.timelineId, nextProps.timelineId)
);
}
);
StatefulSearchOrFilterComponent.displayName = 'StatefulSearchOrFilterComponent';
@ -102,20 +192,62 @@ StatefulSearchOrFilterComponent.displayName = 'StatefulSearchOrFilterComponent';
const makeMapStateToProps = () => {
const getTimeline = timelineSelectors.getTimelineByIdSelector();
const getKqlFilterQueryDraft = timelineSelectors.getKqlFilterQueryDraftSelector();
const isFilterQueryDraftValid = timelineSelectors.isFilterQueryDraftValidSelector();
const getKqlFilterQuery = timelineSelectors.getKqlFilterKuerySelector();
const getInputsTimeline = inputsSelectors.getTimelineSelector();
const getInputsPolicy = inputsSelectors.getTimelinePolicySelector();
const mapStateToProps = (state: State, { timelineId }: OwnProps) => {
const timeline: TimelineModel | {} = getTimeline(state, timelineId);
const timeline: TimelineModel = getTimeline(state, timelineId);
const input: inputsModel.InputsRange = getInputsTimeline(state);
const policy: inputsModel.Policy = getInputsPolicy(state);
return {
dataProviders: timeline.dataProviders,
filterQuery: getKqlFilterQuery(state, timelineId),
filterQueryDraft: getKqlFilterQueryDraft(state, timelineId),
isFilterQueryDraftValid: isFilterQueryDraftValid(state, timelineId),
filters: timeline.filters,
from: input.timerange.from,
fromStr: input.timerange.fromStr,
isRefreshPaused: policy.kind === 'manual',
kqlMode: getOr('filter', 'kqlMode', timeline),
refreshInterval: policy.duration,
savedQueryId: getOr(null, 'savedQueryId', timeline),
to: input.timerange.to,
toStr: input.timerange.toStr,
};
};
return mapStateToProps;
};
export const StatefulSearchOrFilter = connect(makeMapStateToProps, {
applyKqlFilterQuery: timelineActions.applyKqlFilterQuery,
setKqlFilterQueryDraft: timelineActions.setKqlFilterQueryDraft,
updateKqlMode: timelineActions.updateKqlMode,
})(StatefulSearchOrFilterComponent);
const mapDispatchToProps = (dispatch: Dispatch) => ({
applyKqlFilterQuery: ({ id, filterQuery }: { id: string; filterQuery: SerializedFilterQuery }) =>
dispatch(
timelineActions.applyKqlFilterQuery({
id,
filterQuery,
})
),
updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) =>
dispatch(timelineActions.updateKqlMode({ id, kqlMode })),
setKqlFilterQueryDraft: ({
id,
filterQueryDraft,
}: {
id: string;
filterQueryDraft: KueryFilterQuery;
}) =>
dispatch(
timelineActions.setKqlFilterQueryDraft({
id,
filterQueryDraft,
})
),
setSavedQueryId: ({ id, savedQueryId }: { id: string; savedQueryId: string | null }) =>
dispatch(timelineActions.setSavedQueryId({ id, savedQueryId })),
setFilters: ({ id, filters }: { id: string; filters: esFilters.Filter[] }) =>
dispatch(timelineActions.setFilters({ id, filters })),
updateReduxTime: dispatchUpdateReduxTime(dispatch),
});
export const StatefulSearchOrFilter = connect(
makeMapStateToProps,
mapDispatchToProps
)(StatefulSearchOrFilterComponent);

View file

@ -10,13 +10,16 @@ import { pure } from 'recompose';
import styled, { injectGlobal } from 'styled-components';
import { StaticIndexPattern } from 'ui/index_patterns';
import { KueryAutocompletion } from '../../../containers/kuery_autocompletion';
import { KueryFilterQuery } from '../../../store';
import { AutocompleteField } from '../../autocomplete_field';
import { getPlaceholderText, modes, options } from './helpers';
import * as i18n from './translations';
import { esFilters } from '../../../../../../../../src/plugins/data/public';
import { BrowserFields } from '../../../containers/source';
import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store';
import { KqlMode } from '../../../store/timeline/model';
import { DataProvider } from '../data_providers/data_provider';
import { QueryBarTimeline } from '../query_bar';
import { options } from './helpers';
import * as i18n from './translations';
import { DispatchUpdateReduxTime } from '../../super_date_picker';
const timelineSelectModeItemsClassName = 'timelineSelectModeItemsClassName';
const searchOrFilterPopoverClassName = 'searchOrFilterPopover';
@ -39,19 +42,40 @@ injectGlobal`
`;
interface Props {
applyKqlFilterQuery: (expression: string) => void;
applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void;
browserFields: BrowserFields;
dataProviders: DataProvider[];
filterQuery: KueryFilterQuery;
filterQueryDraft: KueryFilterQuery;
from: number;
fromStr: string;
indexPattern: StaticIndexPattern;
isFilterQueryDraftValid: boolean;
isRefreshPaused: boolean;
kqlMode: KqlMode;
timelineId: string;
updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => void;
setKqlFilterQueryDraft: (expression: string) => void;
refreshInterval: number;
setFilters: (filters: esFilters.Filter[]) => void;
setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void;
setSavedQueryId: (savedQueryId: string | null) => void;
filters: esFilters.Filter[];
savedQueryId: string | null;
to: number;
toStr: string;
updateReduxTime: DispatchUpdateReduxTime;
}
const SearchOrFilterContainer = styled.div`
margin: 5px 0 10px 0;
user-select: none;
.globalQueryBar {
padding: 0px;
.kbnQueryBar {
div:first-child {
margin-right: 0px;
}
}
}
`;
SearchOrFilterContainer.displayName = 'SearchOrFilterContainer';
@ -65,13 +89,26 @@ ModeFlexItem.displayName = 'ModeFlexItem';
export const SearchOrFilter = pure<Props>(
({
applyKqlFilterQuery,
browserFields,
dataProviders,
indexPattern,
isFilterQueryDraftValid,
isRefreshPaused,
filters,
filterQuery,
filterQueryDraft,
from,
fromStr,
kqlMode,
timelineId,
refreshInterval,
savedQueryId,
setFilters,
setKqlFilterQueryDraft,
setSavedQueryId,
to,
toStr,
updateKqlMode,
updateReduxTime,
}) => (
<SearchOrFilterContainer>
<EuiFlexGroup data-test-subj="timeline-search-or-filter" gutterSize="xs">
@ -90,22 +127,28 @@ export const SearchOrFilter = pure<Props>(
</EuiToolTip>
</ModeFlexItem>
<EuiFlexItem data-test-subj="timeline-search-or-filter-search-container">
<EuiToolTip content={modes[kqlMode].kqlBarTooltip}>
<KueryAutocompletion indexPattern={indexPattern}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setKqlFilterQueryDraft}
onSubmit={applyKqlFilterQuery}
placeholder={getPlaceholderText(kqlMode)}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
/>
)}
</KueryAutocompletion>
</EuiToolTip>
<QueryBarTimeline
applyKqlFilterQuery={applyKqlFilterQuery}
browserFields={browserFields}
dataProviders={dataProviders}
filters={filters}
filterQuery={filterQuery}
filterQueryDraft={filterQueryDraft}
from={from}
fromStr={fromStr}
kqlMode={kqlMode}
indexPattern={indexPattern}
isRefreshPaused={isRefreshPaused}
refreshInterval={refreshInterval}
savedQueryId={savedQueryId}
setFilters={setFilters}
setKqlFilterQueryDraft={setKqlFilterQueryDraft}
setSavedQueryId={setSavedQueryId}
timelineId={timelineId}
to={to}
toStr={toStr}
updateReduxTime={updateReduxTime}
/>
</EuiFlexItem>
</EuiFlexGroup>
</SearchOrFilterContainer>

View file

@ -33,6 +33,7 @@ const mockUseKibanaCore = useKibanaCore as jest.Mock;
jest.mock('../../lib/compose/kibana_core');
mockUseKibanaCore.mockImplementation(() => ({
uiSettings: mockUiSettings,
savedObjects: {},
}));
describe('Timeline', () => {
@ -58,6 +59,7 @@ describe('Timeline', () => {
id="foo"
dataProviders={mockDataProviders}
end={endDate}
filters={[]}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
indexPattern={indexPattern}
@ -93,6 +95,7 @@ describe('Timeline', () => {
id="foo"
dataProviders={mockDataProviders}
end={endDate}
filters={[]}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
indexPattern={indexPattern}
@ -131,6 +134,7 @@ describe('Timeline', () => {
id="foo"
dataProviders={mockDataProviders}
end={endDate}
filters={[]}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
indexPattern={indexPattern}
@ -169,6 +173,7 @@ describe('Timeline', () => {
id="foo"
dataProviders={mockDataProviders}
end={endDate}
filters={[]}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
indexPattern={indexPattern}
@ -212,6 +217,7 @@ describe('Timeline', () => {
id="foo"
dataProviders={mockDataProviders}
end={endDate}
filters={[]}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
indexPattern={indexPattern}
@ -257,6 +263,7 @@ describe('Timeline', () => {
id="foo"
dataProviders={mockDataProviders}
end={endDate}
filters={[]}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
indexPattern={indexPattern}
@ -310,6 +317,7 @@ describe('Timeline', () => {
id="foo"
dataProviders={mockDataProviders}
end={endDate}
filters={[]}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
indexPattern={indexPattern}
@ -367,6 +375,7 @@ describe('Timeline', () => {
id="foo"
dataProviders={mockDataProviders}
end={endDate}
filters={[]}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
indexPattern={indexPattern}
@ -427,6 +436,7 @@ describe('Timeline', () => {
id="foo"
dataProviders={dataProviders}
end={endDate}
filters={[]}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
indexPattern={indexPattern}
@ -477,6 +487,7 @@ describe('Timeline', () => {
id="foo"
dataProviders={dataProviders}
end={endDate}
filters={[]}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
indexPattern={indexPattern}
@ -533,6 +544,7 @@ describe('Timeline', () => {
id="foo"
dataProviders={dataProviders}
end={endDate}
filters={[]}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
indexPattern={indexPattern}
@ -593,6 +605,7 @@ describe('Timeline', () => {
id="foo"
dataProviders={dataProviders}
end={endDate}
filters={[]}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
indexPattern={indexPattern}

View file

@ -11,6 +11,7 @@ import * as React from 'react';
import styled from 'styled-components';
import { StaticIndexPattern } from 'ui/index_patterns';
import { esFilters } from '../../../../../../../src/plugins/data/public';
import { BrowserFields } from '../../containers/source';
import { TimelineQuery } from '../../containers/timeline';
import { Direction } from '../../graphql/types';
@ -61,6 +62,7 @@ interface Props {
columns: ColumnHeader[];
dataProviders: DataProvider[];
end: number;
filters: esFilters.Filter[];
flyoutHeaderHeight: number;
flyoutHeight: number;
id: string;
@ -91,6 +93,7 @@ export const Timeline = React.memo<Props>(
columns,
dataProviders,
end,
filters,
flyoutHeaderHeight,
flyoutHeight,
id,
@ -119,7 +122,7 @@ export const Timeline = React.memo<Props>(
dataProviders,
indexPattern,
browserFields,
filters: [],
filters,
kqlQuery: { query: kqlQueryExpression, language: 'kuery' },
kqlMode,
start,

View file

@ -72,6 +72,27 @@ export const oneTimelineQuery = gql`
userName
favoriteDate
}
filters {
meta {
alias
controlledBy
disabled
field
formattedValue
index
key
negate
params
type
value
}
query
exists
match_all
missing
range
script
}
kqlMode
kqlQuery {
filterQuery {
@ -107,6 +128,7 @@ export const oneTimelineQuery = gql`
version
}
title
savedQueryId
sort {
columnId
sortDirection

View file

@ -60,6 +60,27 @@ export const persistTimelineMutation = gql`
userName
favoriteDate
}
filters {
meta {
alias
controlledBy
disabled
field
formattedValue
index
key
negate
params
type
value
}
query
exists
match_all
missing
range
script
}
kqlMode
kqlQuery {
filterQuery {
@ -75,6 +96,7 @@ export const persistTimelineMutation = gql`
start
end
}
savedQueryId
sort {
columnId
sortDirection

View file

@ -9044,18 +9044,6 @@
"name": "TimelineResult",
"description": "",
"fields": [
{
"name": "savedObjectId",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "columns",
"description": "",
@ -9072,6 +9060,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "created",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdBy",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dataProviders",
"description": "",
@ -9136,6 +9140,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "filters",
"description": "",
"args": [],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "OBJECT", "name": "FilterTimelineResult", "ofType": null }
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "kqlMode",
"description": "",
@ -9217,13 +9237,25 @@
"deprecationReason": null
},
{
"name": "title",
"name": "savedQueryId",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "savedObjectId",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sort",
"description": "",
@ -9233,15 +9265,7 @@
"deprecationReason": null
},
{
"name": "created",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdBy",
"name": "title",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
@ -9577,6 +9601,172 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "FilterTimelineResult",
"description": "",
"fields": [
{
"name": "exists",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "meta",
"description": "",
"args": [],
"type": { "kind": "OBJECT", "name": "FilterMetaTimelineResult", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "match_all",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "missing",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "query",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "range",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "script",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "FilterMetaTimelineResult",
"description": "",
"fields": [
{
"name": "alias",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "controlledBy",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "disabled",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Boolean", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "field",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "formattedValue",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "index",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "key",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "negate",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Boolean", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "params",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "value",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SerializedFilterQueryResult",
@ -10175,6 +10365,20 @@
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "filters",
"description": "",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "INPUT_OBJECT", "name": "FilterTimelineInput", "ofType": null }
}
},
"defaultValue": null
},
{
"name": "kqlMode",
"description": "",
@ -10203,6 +10407,12 @@
"type": { "kind": "INPUT_OBJECT", "name": "DateRangePickerInput", "ofType": null },
"defaultValue": null
},
{
"name": "savedQueryId",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "sort",
"description": "",
@ -10401,6 +10611,136 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "FilterTimelineInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "exists",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "meta",
"description": "",
"type": { "kind": "INPUT_OBJECT", "name": "FilterMetaTimelineInput", "ofType": null },
"defaultValue": null
},
{
"name": "match_all",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "missing",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "query",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "range",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "script",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "FilterMetaTimelineInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "alias",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "controlledBy",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "disabled",
"description": "",
"type": { "kind": "SCALAR", "name": "Boolean", "ofType": null },
"defaultValue": null
},
{
"name": "field",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "formattedValue",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "index",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "key",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "negate",
"description": "",
"type": { "kind": "SCALAR", "name": "Boolean", "ofType": null },
"defaultValue": null
},
{
"name": "params",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "type",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "value",
"description": "",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "SerializedFilterQueryInput",

View file

@ -122,6 +122,8 @@ export interface TimelineInput {
description?: Maybe<string>;
filters?: Maybe<FilterTimelineInput[]>;
kqlMode?: Maybe<string>;
kqlQuery?: Maybe<SerializedFilterQueryInput>;
@ -130,6 +132,8 @@ export interface TimelineInput {
dateRange?: Maybe<DateRangePickerInput>;
savedQueryId?: Maybe<string>;
sort?: Maybe<SortTimelineInput>;
}
@ -185,6 +189,46 @@ export interface QueryMatchInput {
operator?: Maybe<string>;
}
export interface FilterTimelineInput {
exists?: Maybe<string>;
meta?: Maybe<FilterMetaTimelineInput>;
match_all?: Maybe<string>;
missing?: Maybe<string>;
query?: Maybe<string>;
range?: Maybe<string>;
script?: Maybe<string>;
}
export interface FilterMetaTimelineInput {
alias?: Maybe<string>;
controlledBy?: Maybe<string>;
disabled?: Maybe<boolean>;
field?: Maybe<string>;
formattedValue?: Maybe<string>;
index?: Maybe<string>;
key?: Maybe<string>;
negate?: Maybe<boolean>;
params?: Maybe<string>;
type?: Maybe<string>;
value?: Maybe<string>;
}
export interface SerializedFilterQueryInput {
filterQuery?: Maybe<SerializedKueryQueryInput>;
}
@ -1760,10 +1804,12 @@ export interface SayMyName {
}
export interface TimelineResult {
savedObjectId: string;
columns?: Maybe<ColumnHeaderResult[]>;
created?: Maybe<number>;
createdBy?: Maybe<string>;
dataProviders?: Maybe<DataProviderResult[]>;
dateRange?: Maybe<DateRangePickerResult>;
@ -1774,6 +1820,8 @@ export interface TimelineResult {
favorite?: Maybe<FavoriteTimelineResult[]>;
filters?: Maybe<FilterTimelineResult[]>;
kqlMode?: Maybe<string>;
kqlQuery?: Maybe<SerializedFilterQueryResult>;
@ -1786,13 +1834,13 @@ export interface TimelineResult {
pinnedEventsSaveObject?: Maybe<PinnedEvent[]>;
title?: Maybe<string>;
savedQueryId?: Maybe<string>;
savedObjectId: string;
sort?: Maybe<SortTimelineResult>;
created?: Maybe<number>;
createdBy?: Maybe<string>;
title?: Maybe<string>;
updated?: Maybe<number>;
@ -1867,6 +1915,46 @@ export interface FavoriteTimelineResult {
favoriteDate?: Maybe<number>;
}
export interface FilterTimelineResult {
exists?: Maybe<string>;
meta?: Maybe<FilterMetaTimelineResult>;
match_all?: Maybe<string>;
missing?: Maybe<string>;
query?: Maybe<string>;
range?: Maybe<string>;
script?: Maybe<string>;
}
export interface FilterMetaTimelineResult {
alias?: Maybe<string>;
controlledBy?: Maybe<string>;
disabled?: Maybe<boolean>;
field?: Maybe<string>;
formattedValue?: Maybe<string>;
index?: Maybe<string>;
key?: Maybe<string>;
negate?: Maybe<boolean>;
params?: Maybe<string>;
type?: Maybe<string>;
value?: Maybe<string>;
}
export interface SerializedFilterQueryResult {
filterQuery?: Maybe<SerializedKueryQueryResult>;
}
@ -4874,6 +4962,8 @@ export namespace GetOneTimeline {
favorite: Maybe<Favorite[]>;
filters: Maybe<Filters[]>;
kqlMode: Maybe<string>;
kqlQuery: Maybe<KqlQuery>;
@ -4888,6 +4978,8 @@ export namespace GetOneTimeline {
title: Maybe<string>;
savedQueryId: Maybe<string>;
sort: Maybe<Sort>;
created: Maybe<number>;
@ -5029,6 +5121,50 @@ export namespace GetOneTimeline {
favoriteDate: Maybe<number>;
};
export type Filters = {
__typename?: 'FilterTimelineResult';
meta: Maybe<Meta>;
query: Maybe<string>;
exists: Maybe<string>;
match_all: Maybe<string>;
missing: Maybe<string>;
range: Maybe<string>;
script: Maybe<string>;
};
export type Meta = {
__typename?: 'FilterMetaTimelineResult';
alias: Maybe<string>;
controlledBy: Maybe<string>;
disabled: Maybe<boolean>;
field: Maybe<string>;
formattedValue: Maybe<string>;
index: Maybe<string>;
key: Maybe<string>;
negate: Maybe<boolean>;
params: Maybe<string>;
type: Maybe<string>;
value: Maybe<string>;
};
export type KqlQuery = {
__typename?: 'SerializedFilterQueryResult';
@ -5142,6 +5278,8 @@ export namespace PersistTimelineMutation {
favorite: Maybe<Favorite[]>;
filters: Maybe<Filters[]>;
kqlMode: Maybe<string>;
kqlQuery: Maybe<KqlQuery>;
@ -5150,6 +5288,8 @@ export namespace PersistTimelineMutation {
dateRange: Maybe<DateRange>;
savedQueryId: Maybe<string>;
sort: Maybe<Sort>;
created: Maybe<number>;
@ -5257,6 +5397,50 @@ export namespace PersistTimelineMutation {
favoriteDate: Maybe<number>;
};
export type Filters = {
__typename?: 'FilterTimelineResult';
meta: Maybe<Meta>;
query: Maybe<string>;
exists: Maybe<string>;
match_all: Maybe<string>;
missing: Maybe<string>;
range: Maybe<string>;
script: Maybe<string>;
};
export type Meta = {
__typename?: 'FilterMetaTimelineResult';
alias: Maybe<string>;
controlledBy: Maybe<string>;
disabled: Maybe<boolean>;
field: Maybe<string>;
formattedValue: Maybe<string>;
index: Maybe<string>;
key: Maybe<string>;
negate: Maybe<boolean>;
params: Maybe<string>;
type: Maybe<string>;
value: Maybe<string>;
};
export type KqlQuery = {
__typename?: 'SerializedFilterQueryResult';

View file

@ -4,10 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { buildEsQuery, fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { buildEsQuery, fromKueryExpression, toElasticsearchQuery, JsonObject } from '@kbn/es-query';
import { isEmpty, isString, flow } from 'lodash/fp';
import { StaticIndexPattern } from 'ui/index_patterns';
import { Query } from 'src/plugins/data/common';
import { esFilters } from '../../../../../../../src/plugins/data/public';
import { KueryFilterQuery } from '../../store';
@ -25,6 +26,19 @@ export const convertKueryToElasticSearchQuery = (
}
};
export const convertKueryToDslFilter = (
kueryExpression: string,
indexPattern: StaticIndexPattern
): JsonObject => {
try {
return kueryExpression
? toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern)
: {};
} catch (err) {
return {};
}
};
export const escapeQueryValue = (val: number | string = ''): string | number => {
if (isString(val)) {
if (isEmpty(val)) {

View file

@ -9,6 +9,7 @@ import {
DEFAULT_BYTES_FORMAT,
DEFAULT_KBN_VERSION,
DEFAULT_TIMEZONE_BROWSER,
DEFAULT_TIMEPICKER_QUICK_RANGES,
} from '../../common/constants';
export interface MockFrameworks {
@ -29,6 +30,61 @@ export const getMockKibanaUiSetting = (config: MockFrameworks) => (key: string)
return ['8.0.0'];
} else if (key === DEFAULT_TIMEZONE_BROWSER) {
return config && config.timezone ? [config.timezone] : ['America/New_York'];
} else if (key === DEFAULT_TIMEPICKER_QUICK_RANGES) {
return [
[
{
from: 'now/d',
to: 'now/d',
display: 'Today',
},
{
from: 'now/w',
to: 'now/w',
display: 'This week',
},
{
from: 'now-15m',
to: 'now',
display: 'Last 15 minutes',
},
{
from: 'now-30m',
to: 'now',
display: 'Last 30 minutes',
},
{
from: 'now-1h',
to: 'now',
display: 'Last 1 hour',
},
{
from: 'now-24h',
to: 'now',
display: 'Last 24 hours',
},
{
from: 'now-7d',
to: 'now',
display: 'Last 7 days',
},
{
from: 'now-30d',
to: 'now',
display: 'Last 30 days',
},
{
from: 'now-90d',
to: 'now',
display: 'Last 90 days',
},
{
from: 'now-1y',
to: 'now',
display: 'Last 1 year',
},
],
];
}
return [null];
};

View file

@ -18,8 +18,14 @@ import { Store } from 'redux';
import { BehaviorSubject } from 'rxjs';
import { ThemeProvider } from 'styled-components';
import { CoreStart } from 'src/core/public';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { createStore, State } from '../store';
import { mockGlobalState } from './global_state';
import { mockUiSettings } from './ui_settings';
jest.mock('ui/new_platform');
const state: State = mockGlobalState;
@ -36,17 +42,57 @@ export const apolloClient = new ApolloClient({
export const apolloClientObservable = new BehaviorSubject(apolloClient);
const services = {
uiSettings: mockUiSettings,
savedObjects: {} as CoreStart['savedObjects'],
notifications: {} as CoreStart['notifications'],
docLinks: {
links: {
query: {
kueryQuerySyntax: '',
},
},
} as CoreStart['docLinks'],
http: {} as CoreStart['http'],
overlays: {} as CoreStart['overlays'],
storage: {
get: () => {},
},
};
const localStorageMock = () => {
let store: Record<string, unknown> = {};
return {
getItem: (key: string) => {
return store[key] || null;
},
setItem: (key: string, value: unknown) => {
store[key] = value;
},
clear() {
store = {};
},
};
};
Object.defineProperty(window, 'localStorage', {
value: localStorageMock(),
});
/** A utility for wrapping children in the providers required to run most tests */
export const TestProviders = pure<Props>(
({ children, store = createStore(state, apolloClientObservable), onDragEnd = jest.fn() }) => (
<I18nProvider>
<ApolloProvider client={apolloClient}>
<ReduxStoreProvider store={store}>
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<DragDropContext onDragEnd={onDragEnd}>{children}</DragDropContext>
</ThemeProvider>
</ReduxStoreProvider>
</ApolloProvider>
<KibanaContextProvider services={services}>
<ApolloProvider client={apolloClient}>
<ReduxStoreProvider store={store}>
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<DragDropContext onDragEnd={onDragEnd}>{children}</DragDropContext>
</ThemeProvider>
</ReduxStoreProvider>
</ApolloProvider>
</KibanaContextProvider>
</I18nProvider>
)
);

View file

@ -9,6 +9,7 @@ import {
DEFAULT_SIEM_TIME_RANGE,
DEFAULT_SIEM_REFRESH_INTERVAL,
DEFAULT_INDEX_KEY,
DEFAULT_DATE_FORMAT,
DEFAULT_DATE_FORMAT_TZ,
DEFAULT_DARK_MODE,
DEFAULT_TIME_RANGE,
@ -40,6 +41,8 @@ chrome.getUiSettingsClient().get.mockImplementation((key: string) => {
return defaultIndexPattern;
case DEFAULT_DATE_FORMAT_TZ:
return 'Asia/Taipei';
case DEFAULT_DATE_FORMAT:
return 'MMM D, YYYY @ HH:mm:ss.SSS';
case DEFAULT_DARK_MODE:
return false;
default:
@ -62,6 +65,9 @@ export const mockUiSettings = {
get: (item: Config) => {
return mockUiSettings[item];
},
get$: () => ({
subscribe: jest.fn(),
}),
'query:allowLeadingWildcards': true,
'query:queryString:options': {},
'courier:ignoreFilterIfFieldNotInIndex': true,

View file

@ -39,10 +39,13 @@ jest.mock('../../../containers/source', () => ({
}));
// Test will fail because we will to need to mock some core services to make the test work
// For now let's forget about SiemSearchBar
// For now let's forget about SiemSearchBar and QueryBar
jest.mock('../../../components/search_bar', () => ({
SiemSearchBar: () => null,
}));
jest.mock('../../../components/query_bar', () => ({
QueryBar: () => null,
}));
describe('body', () => {
const scenariosMap = {

View file

@ -38,10 +38,13 @@ jest.mock('ui/documentation_links', () => ({
}));
// Test will fail because we will to need to mock some core services to make the test work
// For now let's forget about SiemSearchBar
// For now let's forget about SiemSearchBar and QueryBar
jest.mock('../../components/search_bar', () => ({
SiemSearchBar: () => null,
}));
jest.mock('../../components/query_bar', () => ({
QueryBar: () => null,
}));
let localSource: Array<{
request: {};

View file

@ -38,10 +38,13 @@ mockUseKibanaCore.mockImplementation(() => ({
}));
// Test will fail because we will to need to mock some core services to make the test work
// For now let's forget about SiemSearchBar
// For now let's forget about SiemSearchBar and QueryBar
jest.mock('../../../components/search_bar', () => ({
SiemSearchBar: () => null,
}));
jest.mock('../../../components/query_bar', () => ({
QueryBar: () => null,
}));
let localSource: Array<{
request: {};

View file

@ -33,10 +33,13 @@ mockUseKibanaCore.mockImplementation(() => ({
}));
// Test will fail because we will to need to mock some core services to make the test work
// For now let's forget about SiemSearchBar
// For now let's forget about SiemSearchBar and QueryBar
jest.mock('../../components/search_bar', () => ({
SiemSearchBar: () => null,
}));
jest.mock('../../components/query_bar', () => ({
QueryBar: () => null,
}));
let localSource: Array<{
request: {};

View file

@ -73,3 +73,6 @@ export const globalFiltersQuerySelector = () =>
createSelector(selectGlobal, global => global.filters || []);
export const getTimelineSelector = () => createSelector(selectTimeline, timeline => timeline);
export const getTimelinePolicySelector = () =>
createSelector(selectTimeline, timeline => timeline.policy);

View file

@ -10,8 +10,10 @@ export { hostsModel } from './hosts';
export { dragAndDropModel } from './drag_and_drop';
export { networkModel } from './network';
export type KueryFilterQueryKind = 'kuery' | 'lucene';
export interface KueryFilterQuery {
kind: 'kuery';
kind: KueryFilterQueryKind;
expression: string;
}

View file

@ -6,6 +6,7 @@
import actionCreatorFactory from 'typescript-fsa';
import { esFilters } from '../../../../../../../src/plugins/data/public';
import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header';
import { Sort } from '../../components/timeline/body/sort';
import {
@ -187,3 +188,13 @@ export const updateAutoSaveMsg = actionCreator<{
}>('UPDATE_AUTO_SAVE');
export const showCallOutUnauthorizedMsg = actionCreator('SHOW_CALL_OUT_UNAUTHORIZED_MSG');
export const setSavedQueryId = actionCreator<{
id: string;
savedQueryId: string | null;
}>('SET_TIMELINE_SAVED_QUERY');
export const setFilters = actionCreator<{
id: string;
filters: esFilters.Filter[];
}>('SET_TIMELINE_FILTERS');

View file

@ -0,0 +1,283 @@
/*
* 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 { TimelineModel } from './model';
import { Direction } from '../../graphql/types';
import { convertTimelineAsInput } from './epic';
import { esFilters } from '../../../../../../../src/plugins/data/public';
import { FilterStateStore } from '../../../../../../../src/plugins/data/common/es_query/filters';
describe('Epic Timeline', () => {
describe('#convertTimelineAsInput ', () => {
test('should return a TimelineInput instead of TimelineModel ', () => {
const timelineModel: TimelineModel = {
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
width: 190,
},
{
columnHeaderType: 'not-filtered',
id: 'message',
width: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.category',
width: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.action',
width: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
width: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
width: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
width: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
width: 180,
},
],
dataProviders: [
{
id: 'hosts-table-hostName-DESKTOP-QBBSCUT',
name: 'DESKTOP-QBBSCUT',
enabled: true,
excluded: false,
kqlQuery: '',
queryMatch: {
field: 'host.name',
value: 'DESKTOP-QBBSCUT',
operator: ':',
},
and: [
{
id:
'plain-column-renderer-data-provider-hosts-page-event_module-CQg7I24BHe9nqdOi_LYL-event_module-endgame',
name: 'event.module: endgame',
enabled: true,
excluded: false,
kqlQuery: '',
queryMatch: {
field: 'event.module',
value: 'endgame',
operator: ':',
},
},
],
},
],
description: '',
eventIdToNoteIds: {},
highlightedDropAndProviderId: '',
historyIds: [],
filters: [
{
$state: { store: FilterStateStore.APP_STATE },
meta: {
alias: null,
disabled: false,
key: 'event.category',
negate: false,
params: { query: 'file' },
type: 'phrase',
},
query: { match_phrase: { 'event.category': 'file' } },
},
{
$state: { store: FilterStateStore.APP_STATE },
meta: {
alias: null,
disabled: false,
key: '@timestamp',
negate: false,
type: 'exists',
value: 'exists',
},
exists: { field: '@timestamp' },
} as esFilters.Filter,
],
isFavorite: false,
isLive: false,
isLoading: false,
isSaving: false,
itemsPerPage: 25,
itemsPerPageOptions: [10, 25, 50, 100],
kqlMode: 'filter',
kqlQuery: {
filterQuery: {
kuery: { kind: 'kuery', expression: 'endgame.user_name : "zeus" ' },
serializedQuery:
'{"bool":{"should":[{"match_phrase":{"endgame.user_name":"zeus"}}],"minimum_should_match":1}}',
},
filterQueryDraft: { kind: 'kuery', expression: 'endgame.user_name : "zeus" ' },
},
title: 'saved',
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
dateRange: { start: 1572469587644, end: 1572555987644 },
savedObjectId: '11169110-fc22-11e9-8ca9-072f15ce2685',
show: true,
sort: { columnId: '@timestamp', sortDirection: Direction.desc },
width: 1100,
version: 'WzM4LDFd',
id: '11169110-fc22-11e9-8ca9-072f15ce2685',
savedQueryId: 'my endgame timeline query',
};
expect(
convertTimelineAsInput(timelineModel, {
kind: 'absolute',
from: 1572469587644,
fromStr: undefined,
to: 1572555987644,
toStr: undefined,
})
).toEqual({
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
},
{
columnHeaderType: 'not-filtered',
id: 'message',
},
{
columnHeaderType: 'not-filtered',
id: 'event.category',
},
{
columnHeaderType: 'not-filtered',
id: 'event.action',
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
},
],
dataProviders: [
{
and: [
{
enabled: true,
excluded: false,
id:
'plain-column-renderer-data-provider-hosts-page-event_module-CQg7I24BHe9nqdOi_LYL-event_module-endgame',
kqlQuery: '',
name: 'event.module: endgame',
queryMatch: {
field: 'event.module',
operator: ':',
value: 'endgame',
},
},
],
enabled: true,
excluded: false,
id: 'hosts-table-hostName-DESKTOP-QBBSCUT',
kqlQuery: '',
name: 'DESKTOP-QBBSCUT',
queryMatch: {
field: 'host.name',
operator: ':',
value: 'DESKTOP-QBBSCUT',
},
},
],
dateRange: {
end: 1572555987644,
start: 1572469587644,
},
description: '',
filters: [
{
exists: null,
match_all: null,
meta: {
alias: null,
disabled: false,
field: null,
key: 'event.category',
negate: false,
params: '{"query":"file"}',
type: 'phrase',
value: null,
},
missing: null,
query: '{"match_phrase":{"event.category":"file"}}',
range: null,
script: null,
},
{
exists: '{"field":"@timestamp"}',
match_all: null,
meta: {
alias: null,
disabled: false,
field: null,
key: '@timestamp',
negate: false,
params: null,
type: 'exists',
value: 'exists',
},
missing: null,
query: null,
range: null,
script: null,
},
],
kqlMode: 'filter',
kqlQuery: {
filterQuery: {
kuery: {
expression: 'endgame.user_name : "zeus" ',
kind: 'kuery',
},
serializedQuery:
'{"bool":{"should":[{"match_phrase":{"endgame.user_name":"zeus"}}],"minimum_should_match":1}}',
},
},
savedQueryId: 'my endgame timeline query',
sort: {
columnId: '@timestamp',
sortDirection: 'desc',
},
title: 'saved',
});
});
});
});

View file

@ -4,7 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { get, has, merge as mergeObject, set, omit } from 'lodash/fp';
import {
get,
has,
merge as mergeObject,
set,
omit,
isObject,
toString as fpToString,
} from 'lodash/fp';
import { Action } from 'redux';
import { Epic } from 'redux-observable';
import { from, Observable, empty, merge } from 'rxjs';
@ -20,6 +28,7 @@ import {
takeUntil,
} from 'rxjs/operators';
import { esFilters } from '../../../../../../../src/plugins/data/public';
import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header';
import { persistTimelineMutation } from '../../containers/timeline/persist.gql_query';
import {
@ -52,6 +61,8 @@ import {
updateTimeline,
updateTitle,
updateAutoSaveMsg,
setFilters,
setSavedQueryId,
startTimelineSaving,
endTimelineSaving,
createTimeline,
@ -81,6 +92,8 @@ const timelineActionsType = [
dataProviderEdited.type,
removeColumn.type,
removeProvider.type,
setFilters.type,
setSavedQueryId.type,
updateColumns.type,
updateDataProviderEnabled.type,
updateDataProviderExcluded.type,
@ -235,14 +248,16 @@ const timelineInput: TimelineInput = {
columns: null,
dataProviders: null,
description: null,
filters: null,
kqlMode: null,
kqlQuery: null,
title: null,
dateRange: null,
savedQueryId: null,
sort: null,
};
const convertTimelineAsInput = (
export const convertTimelineAsInput = (
timeline: TimelineModel,
timelineTimeRange: TimeRange
): TimelineInput =>
@ -258,6 +273,65 @@ const convertTimelineAsInput = (
get(key, timeline).map((col: ColumnHeader) => omit(['width', '__typename'], col)),
acc
);
} else if (key === 'filters' && get(key, timeline) != null) {
const filters = get(key, timeline);
return set(
key,
filters != null
? filters.map((myFilter: esFilters.Filter) => {
const basicFilter = omit(['$state'], myFilter);
return {
...basicFilter,
meta: {
...basicFilter.meta,
field:
(esFilters.isMatchAllFilter(basicFilter) ||
esFilters.isPhraseFilter(basicFilter) ||
esFilters.isPhrasesFilter(basicFilter) ||
esFilters.isRangeFilter(basicFilter)) &&
basicFilter.meta.field != null
? convertToString(basicFilter.meta.field)
: null,
value:
basicFilter.meta.value != null
? convertToString(basicFilter.meta.value)
: null,
params:
basicFilter.meta.params != null
? convertToString(basicFilter.meta.params)
: null,
},
...(esFilters.isMatchAllFilter(basicFilter)
? {
match_all: convertToString(
(basicFilter as esFilters.MatchAllFilter).match_all
),
}
: { match_all: null }),
...(esFilters.isMissingFilter(basicFilter) && basicFilter.missing != null
? { missing: convertToString(basicFilter.missing) }
: { missing: null }),
...(esFilters.isExistsFilter(basicFilter) && basicFilter.exists != null
? { exists: convertToString(basicFilter.exists) }
: { exists: null }),
...((esFilters.isQueryStringFilter(basicFilter) ||
get('query', basicFilter) != null) &&
basicFilter.query != null
? { query: convertToString(basicFilter.query) }
: { query: null }),
...(esFilters.isRangeFilter(basicFilter) && basicFilter.range != null
? { range: convertToString(basicFilter.range) }
: { range: null }),
...(esFilters.isRangeFilter(basicFilter) &&
basicFilter.script !=
null /* TODO remove it when PR50713 is merged || esFilters.isPhraseFilter(basicFilter) */
? { script: convertToString(basicFilter.script) }
: { script: null }),
};
})
: [],
acc
);
}
return set(key, get(key, timeline), acc);
}
@ -271,3 +345,14 @@ const omitTypenameInTimeline = (
oldTimeline: TimelineModel,
newTimeline: TimelineResult
): TimelineModel => JSON.parse(JSON.stringify(mergeObject(oldTimeline, newTimeline)), omitTypename);
const convertToString = (obj: unknown) => {
try {
if (isObject(obj)) {
return JSON.stringify(obj);
}
return fpToString(obj);
} catch {
return '';
}
};

View file

@ -3,8 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getOr, omit, uniq, isEmpty, isEqualWith } from 'lodash/fp';
import { esFilters } from '../../../../../../../src/plugins/data/public';
import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header';
import { getColumnWidthFromType } from '../../components/timeline/body/helpers';
import { Sort } from '../../components/timeline/body/sort';
@ -1135,3 +1137,43 @@ export const updateHighlightedDropAndProvider = ({
},
};
};
interface UpdateSavedQueryParams {
id: string;
savedQueryId: string | null;
timelineById: TimelineById;
}
export const updateSavedQuery = ({
id,
savedQueryId,
timelineById,
}: UpdateSavedQueryParams): TimelineById => {
const timeline = timelineById[id];
return {
...timelineById,
[id]: {
...timeline,
savedQueryId,
},
};
};
interface UpdateFiltersParams {
id: string;
filters: esFilters.Filter[];
timelineById: TimelineById;
}
export const updateFilters = ({ id, filters, timelineById }: UpdateFiltersParams): TimelineById => {
const timeline = timelineById[id];
return {
...timelineById,
[id]: {
...timeline,
filters,
},
};
};

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { esFilters } from '../../../../../../../src/plugins/data/public';
import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header';
import { DataProvider } from '../../components/timeline/data_providers/data_provider';
import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/helpers';
@ -25,6 +26,7 @@ export interface TimelineModel {
description: string;
/** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */
eventIdToNoteIds: Record<string, string[]>;
filters?: esFilters.Filter[];
/** The chronological history of actions related to this timeline */
historyIds: string[];
/** The chronological history of actions related to this timeline */
@ -59,6 +61,7 @@ export interface TimelineModel {
start: number;
end: number;
};
savedQueryId?: string | null;
/** When true, show the timeline flyover */
show: boolean;
/** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */
@ -77,6 +80,7 @@ export const timelineDefaults: Readonly<Pick<
| 'dataProviders'
| 'description'
| 'eventIdToNoteIds'
| 'filters'
| 'highlightedDropAndProviderId'
| 'historyIds'
| 'isFavorite'
@ -104,6 +108,7 @@ export const timelineDefaults: Readonly<Pick<
eventIdToNoteIds: {},
highlightedDropAndProviderId: '',
historyIds: [],
filters: [],
isFavorite: false,
isLive: false,
isLoading: false,

View file

@ -45,6 +45,8 @@ import {
updateTitle,
upsertColumn,
updateIsLoading,
setSavedQueryId,
setFilters,
} from './actions';
import {
addNewTimeline,
@ -80,6 +82,8 @@ import {
updateTimelineSort,
updateTimelineTitle,
upsertTimelineColumn,
updateSavedQuery,
updateFilters,
} from './helpers';
import { TimelineState, EMPTY_TIMELINE_BY_ID } from './types';
@ -132,7 +136,11 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
}))
.case(applyKqlFilterQuery, (state, { id, filterQuery }) => ({
...state,
timelineById: applyKqlFilterQueryDraft({ id, filterQuery, timelineById: state.timelineById }),
timelineById: applyKqlFilterQueryDraft({
id,
filterQuery,
timelineById: state.timelineById,
}),
}))
.case(setKqlFilterQueryDraft, (state, { id, filterQueryDraft }) => ({
...state,
@ -361,4 +369,20 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
showCallOutUnauthorizedMsg: true,
}))
.case(setSavedQueryId, (state, { id, savedQueryId }) => ({
...state,
timelineById: updateSavedQuery({
id,
savedQueryId,
timelineById: state.timelineById,
}),
}))
.case(setFilters, (state, { id, filters }) => ({
...state,
timelineById: updateFilters({
id,
filters,
timelineById: state.timelineById,
}),
}))
.build();

View file

@ -14,12 +14,6 @@ mockDispatch.mockImplementation(fn => fn);
const applyTimelineKqlMock: jest.Mock = (dispatchApplyTimelineFilterQuery as unknown) as jest.Mock;
jest.mock('../../store/hosts/actions', () => ({
applyHostsFilterQuery: jest.fn(),
}));
jest.mock('../../store/network/actions', () => ({
applyNetworkFilterQuery: jest.fn(),
}));
jest.mock('../../store/timeline/actions', () => ({
applyKqlFilterQuery: jest.fn(),
}));
@ -36,7 +30,6 @@ describe('#useUpdateKql', () => {
kueryFilterQuery: { expression: '', kind: 'kuery' },
kueryFilterQueryDraft: { expression: 'host.name: "myLove"', kind: 'kuery' },
storeType: 'timelineType',
type: null,
timelineId: 'myTimelineId',
})(mockDispatch);
expect(applyTimelineKqlMock).toHaveBeenCalledWith({

View file

@ -18,7 +18,6 @@ interface UseUpdateKqlProps {
kueryFilterQuery: KueryFilterQuery | null;
kueryFilterQueryDraft: KueryFilterQuery | null;
storeType: 'timelineType';
type: null;
timelineId?: string;
}
@ -28,7 +27,6 @@ export const useUpdateKql = ({
kueryFilterQueryDraft,
storeType,
timelineId,
type,
}: UseUpdateKqlProps): RefetchKql => {
const updateKql: RefetchKql = (dispatch: Dispatch) => {
if (kueryFilterQueryDraft != null && !isEqual(kueryFilterQuery, kueryFilterQueryDraft)) {
@ -37,10 +35,7 @@ export const useUpdateKql = ({
dispatchApplyTimelineFilterQuery({
id: timelineId,
filterQuery: {
kuery: {
kind: 'kuery',
expression: kueryFilterQueryDraft.expression,
},
kuery: kueryFilterQueryDraft,
serializedQuery: convertKueryToElasticSearchQuery(
kueryFilterQueryDraft.expression,
indexPattern
@ -49,7 +44,6 @@ export const useUpdateKql = ({
})
);
}
return true;
}
return false;

View file

@ -0,0 +1,25 @@
/*
* 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 { useState, useEffect } from 'react';
import {
SavedQueryService,
createSavedQueryService,
} from '../../../../../../../src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service';
import { useKibanaCore } from '../../lib/compose/kibana_core';
export const useSavedQueryServices = () => {
const core = useKibanaCore();
const [savedQueryService, setSavedQueryService] = useState<SavedQueryService>(
createSavedQueryService(core.savedObjects.client)
);
useEffect(() => {
setSavedQueryService(createSavedQueryService(core.savedObjects.client));
}, [core.savedObjects.client]);
return savedQueryService;
};

View file

@ -49,6 +49,20 @@ const sortTimeline = `
sortDirection: String
`;
const filtersMetaTimeline = `
alias: String
controlledBy: String
disabled: Boolean
field: String
formattedValue: String
index: String
key: String
negate: Boolean
params: String
type: String
value: String
`;
export const timelineSchema = gql`
###############
#### INPUT ####
@ -97,14 +111,30 @@ export const timelineSchema = gql`
${sortTimeline}
}
input FilterMetaTimelineInput {
${filtersMetaTimeline}
}
input FilterTimelineInput {
exists: String
meta: FilterMetaTimelineInput
match_all: String
missing: String
query: String
range: String
script: String
}
input TimelineInput {
columns: [ColumnHeaderInput!]
dataProviders: [DataProviderInput!]
description: String
filters: [FilterTimelineInput!]
kqlMode: String
kqlQuery: SerializedFilterQueryInput
title: String
dateRange: DateRangePickerInput
savedQueryId: String
sort: SortTimelineInput
}
@ -171,24 +201,40 @@ export const timelineSchema = gql`
${sortTimeline}
}
type FilterMetaTimelineResult {
${filtersMetaTimeline}
}
type FilterTimelineResult {
exists: String
meta: FilterMetaTimelineResult
match_all: String
missing: String
query: String
range: String
script: String
}
type TimelineResult {
savedObjectId: String!
columns: [ColumnHeaderResult!]
created: Float
createdBy: String
dataProviders: [DataProviderResult!]
dateRange: DateRangePickerResult
description: String
eventIdToNoteIds: [NoteResult!]
favorite: [FavoriteTimelineResult!]
filters: [FilterTimelineResult!]
kqlMode: String
kqlQuery: SerializedFilterQueryResult
notes: [NoteResult!]
noteIds: [String!]
pinnedEventIds: [String!]
pinnedEventsSaveObject: [PinnedEvent!]
title: String
savedQueryId: String
savedObjectId: String!
sort: SortTimelineResult
created: Float
createdBy: String
title: String
updated: Float
updatedBy: String
version: String!

View file

@ -124,6 +124,8 @@ export interface TimelineInput {
description?: Maybe<string>;
filters?: Maybe<FilterTimelineInput[]>;
kqlMode?: Maybe<string>;
kqlQuery?: Maybe<SerializedFilterQueryInput>;
@ -132,6 +134,8 @@ export interface TimelineInput {
dateRange?: Maybe<DateRangePickerInput>;
savedQueryId?: Maybe<string>;
sort?: Maybe<SortTimelineInput>;
}
@ -187,6 +191,46 @@ export interface QueryMatchInput {
operator?: Maybe<string>;
}
export interface FilterTimelineInput {
exists?: Maybe<string>;
meta?: Maybe<FilterMetaTimelineInput>;
match_all?: Maybe<string>;
missing?: Maybe<string>;
query?: Maybe<string>;
range?: Maybe<string>;
script?: Maybe<string>;
}
export interface FilterMetaTimelineInput {
alias?: Maybe<string>;
controlledBy?: Maybe<string>;
disabled?: Maybe<boolean>;
field?: Maybe<string>;
formattedValue?: Maybe<string>;
index?: Maybe<string>;
key?: Maybe<string>;
negate?: Maybe<boolean>;
params?: Maybe<string>;
type?: Maybe<string>;
value?: Maybe<string>;
}
export interface SerializedFilterQueryInput {
filterQuery?: Maybe<SerializedKueryQueryInput>;
}
@ -1762,10 +1806,12 @@ export interface SayMyName {
}
export interface TimelineResult {
savedObjectId: string;
columns?: Maybe<ColumnHeaderResult[]>;
created?: Maybe<number>;
createdBy?: Maybe<string>;
dataProviders?: Maybe<DataProviderResult[]>;
dateRange?: Maybe<DateRangePickerResult>;
@ -1776,6 +1822,8 @@ export interface TimelineResult {
favorite?: Maybe<FavoriteTimelineResult[]>;
filters?: Maybe<FilterTimelineResult[]>;
kqlMode?: Maybe<string>;
kqlQuery?: Maybe<SerializedFilterQueryResult>;
@ -1788,13 +1836,13 @@ export interface TimelineResult {
pinnedEventsSaveObject?: Maybe<PinnedEvent[]>;
title?: Maybe<string>;
savedQueryId?: Maybe<string>;
savedObjectId: string;
sort?: Maybe<SortTimelineResult>;
created?: Maybe<number>;
createdBy?: Maybe<string>;
title?: Maybe<string>;
updated?: Maybe<number>;
@ -1869,6 +1917,46 @@ export interface FavoriteTimelineResult {
favoriteDate?: Maybe<number>;
}
export interface FilterTimelineResult {
exists?: Maybe<string>;
meta?: Maybe<FilterMetaTimelineResult>;
match_all?: Maybe<string>;
missing?: Maybe<string>;
query?: Maybe<string>;
range?: Maybe<string>;
script?: Maybe<string>;
}
export interface FilterMetaTimelineResult {
alias?: Maybe<string>;
controlledBy?: Maybe<string>;
disabled?: Maybe<boolean>;
field?: Maybe<string>;
formattedValue?: Maybe<string>;
index?: Maybe<string>;
key?: Maybe<string>;
negate?: Maybe<boolean>;
params?: Maybe<string>;
type?: Maybe<string>;
value?: Maybe<string>;
}
export interface SerializedFilterQueryResult {
filterQuery?: Maybe<SerializedKueryQueryResult>;
}
@ -7469,10 +7557,12 @@ export namespace SayMyNameResolvers {
export namespace TimelineResultResolvers {
export interface Resolvers<TContext = SiemContext, TypeParent = TimelineResult> {
savedObjectId?: SavedObjectIdResolver<string, TypeParent, TContext>;
columns?: ColumnsResolver<Maybe<ColumnHeaderResult[]>, TypeParent, TContext>;
created?: CreatedResolver<Maybe<number>, TypeParent, TContext>;
createdBy?: CreatedByResolver<Maybe<string>, TypeParent, TContext>;
dataProviders?: DataProvidersResolver<Maybe<DataProviderResult[]>, TypeParent, TContext>;
dateRange?: DateRangeResolver<Maybe<DateRangePickerResult>, TypeParent, TContext>;
@ -7483,6 +7573,8 @@ export namespace TimelineResultResolvers {
favorite?: FavoriteResolver<Maybe<FavoriteTimelineResult[]>, TypeParent, TContext>;
filters?: FiltersResolver<Maybe<FilterTimelineResult[]>, TypeParent, TContext>;
kqlMode?: KqlModeResolver<Maybe<string>, TypeParent, TContext>;
kqlQuery?: KqlQueryResolver<Maybe<SerializedFilterQueryResult>, TypeParent, TContext>;
@ -7499,13 +7591,13 @@ export namespace TimelineResultResolvers {
TContext
>;
title?: TitleResolver<Maybe<string>, TypeParent, TContext>;
savedQueryId?: SavedQueryIdResolver<Maybe<string>, TypeParent, TContext>;
savedObjectId?: SavedObjectIdResolver<string, TypeParent, TContext>;
sort?: SortResolver<Maybe<SortTimelineResult>, TypeParent, TContext>;
created?: CreatedResolver<Maybe<number>, TypeParent, TContext>;
createdBy?: CreatedByResolver<Maybe<string>, TypeParent, TContext>;
title?: TitleResolver<Maybe<string>, TypeParent, TContext>;
updated?: UpdatedResolver<Maybe<number>, TypeParent, TContext>;
@ -7514,13 +7606,18 @@ export namespace TimelineResultResolvers {
version?: VersionResolver<string, TypeParent, TContext>;
}
export type SavedObjectIdResolver<
R = string,
export type ColumnsResolver<
R = Maybe<ColumnHeaderResult[]>,
Parent = TimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type ColumnsResolver<
R = Maybe<ColumnHeaderResult[]>,
export type CreatedResolver<
R = Maybe<number>,
Parent = TimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type CreatedByResolver<
R = Maybe<string>,
Parent = TimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
@ -7549,6 +7646,11 @@ export namespace TimelineResultResolvers {
Parent = TimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type FiltersResolver<
R = Maybe<FilterTimelineResult[]>,
Parent = TimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type KqlModeResolver<
R = Maybe<string>,
Parent = TimelineResult,
@ -7579,22 +7681,22 @@ export namespace TimelineResultResolvers {
Parent = TimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type TitleResolver<
export type SavedQueryIdResolver<
R = Maybe<string>,
Parent = TimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type SavedObjectIdResolver<
R = string,
Parent = TimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type SortResolver<
R = Maybe<SortTimelineResult>,
Parent = TimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type CreatedResolver<
R = Maybe<number>,
Parent = TimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type CreatedByResolver<
export type TitleResolver<
R = Maybe<string>,
Parent = TimelineResult,
TContext = SiemContext
@ -7837,6 +7939,142 @@ export namespace FavoriteTimelineResultResolvers {
> = Resolver<R, Parent, TContext>;
}
export namespace FilterTimelineResultResolvers {
export interface Resolvers<TContext = SiemContext, TypeParent = FilterTimelineResult> {
exists?: ExistsResolver<Maybe<string>, TypeParent, TContext>;
meta?: MetaResolver<Maybe<FilterMetaTimelineResult>, TypeParent, TContext>;
match_all?: MatchAllResolver<Maybe<string>, TypeParent, TContext>;
missing?: MissingResolver<Maybe<string>, TypeParent, TContext>;
query?: QueryResolver<Maybe<string>, TypeParent, TContext>;
range?: RangeResolver<Maybe<string>, TypeParent, TContext>;
script?: ScriptResolver<Maybe<string>, TypeParent, TContext>;
}
export type ExistsResolver<
R = Maybe<string>,
Parent = FilterTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type MetaResolver<
R = Maybe<FilterMetaTimelineResult>,
Parent = FilterTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type MatchAllResolver<
R = Maybe<string>,
Parent = FilterTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type MissingResolver<
R = Maybe<string>,
Parent = FilterTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type QueryResolver<
R = Maybe<string>,
Parent = FilterTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type RangeResolver<
R = Maybe<string>,
Parent = FilterTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type ScriptResolver<
R = Maybe<string>,
Parent = FilterTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
}
export namespace FilterMetaTimelineResultResolvers {
export interface Resolvers<TContext = SiemContext, TypeParent = FilterMetaTimelineResult> {
alias?: AliasResolver<Maybe<string>, TypeParent, TContext>;
controlledBy?: ControlledByResolver<Maybe<string>, TypeParent, TContext>;
disabled?: DisabledResolver<Maybe<boolean>, TypeParent, TContext>;
field?: FieldResolver<Maybe<string>, TypeParent, TContext>;
formattedValue?: FormattedValueResolver<Maybe<string>, TypeParent, TContext>;
index?: IndexResolver<Maybe<string>, TypeParent, TContext>;
key?: KeyResolver<Maybe<string>, TypeParent, TContext>;
negate?: NegateResolver<Maybe<boolean>, TypeParent, TContext>;
params?: ParamsResolver<Maybe<string>, TypeParent, TContext>;
type?: TypeResolver<Maybe<string>, TypeParent, TContext>;
value?: ValueResolver<Maybe<string>, TypeParent, TContext>;
}
export type AliasResolver<
R = Maybe<string>,
Parent = FilterMetaTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type ControlledByResolver<
R = Maybe<string>,
Parent = FilterMetaTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type DisabledResolver<
R = Maybe<boolean>,
Parent = FilterMetaTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type FieldResolver<
R = Maybe<string>,
Parent = FilterMetaTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type FormattedValueResolver<
R = Maybe<string>,
Parent = FilterMetaTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type IndexResolver<
R = Maybe<string>,
Parent = FilterMetaTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type KeyResolver<
R = Maybe<string>,
Parent = FilterMetaTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type NegateResolver<
R = Maybe<boolean>,
Parent = FilterMetaTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type ParamsResolver<
R = Maybe<string>,
Parent = FilterMetaTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type TypeResolver<
R = Maybe<string>,
Parent = FilterMetaTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type ValueResolver<
R = Maybe<string>,
Parent = FilterMetaTimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
}
export namespace SerializedFilterQueryResultResolvers {
export interface Resolvers<TContext = SiemContext, TypeParent = SerializedFilterQueryResult> {
filterQuery?: FilterQueryResolver<Maybe<SerializedKueryQueryResult>, TypeParent, TContext>;
@ -8484,6 +8722,8 @@ export type IResolvers<TContext = SiemContext> = {
QueryMatchResult?: QueryMatchResultResolvers.Resolvers<TContext>;
DateRangePickerResult?: DateRangePickerResultResolvers.Resolvers<TContext>;
FavoriteTimelineResult?: FavoriteTimelineResultResolvers.Resolvers<TContext>;
FilterTimelineResult?: FilterTimelineResultResolvers.Resolvers<TContext>;
FilterMetaTimelineResult?: FilterMetaTimelineResultResolvers.Resolvers<TContext>;
SerializedFilterQueryResult?: SerializedFilterQueryResultResolvers.Resolvers<TContext>;
SerializedKueryQueryResult?: SerializedKueryQueryResultResolvers.Resolvers<TContext>;
KueryFilterQueryResult?: KueryFilterQueryResultResolvers.Resolvers<TContext>;

View file

@ -146,6 +146,65 @@ export const timelineSavedObjectMappings: {
},
},
},
filters: {
properties: {
meta: {
properties: {
alias: {
type: 'text',
},
controlledBy: {
type: 'text',
},
disabled: {
type: 'boolean',
},
field: {
type: 'text',
},
formattedValue: {
type: 'text',
},
index: {
type: 'keyword',
},
key: {
type: 'keyword',
},
negate: {
type: 'boolean',
},
params: {
type: 'text',
},
type: {
type: 'keyword',
},
value: {
type: 'text',
},
},
},
exists: {
type: 'text',
},
match_all: {
type: 'text',
},
missing: {
type: 'text',
},
query: {
type: 'text',
},
range: {
type: 'text',
},
script: {
type: 'text',
},
},
},
kqlMode: {
type: 'keyword',
},
@ -183,6 +242,9 @@ export const timelineSavedObjectMappings: {
},
},
},
savedQueryId: {
type: 'keyword',
},
sort: {
properties: {
columnId: {

View file

@ -59,6 +59,33 @@ const SavedDataProviderRuntimeType = runtimeTypes.partial({
and: unionWithNullType(runtimeTypes.array(SavedDataProviderQueryMatchRuntimeType)),
});
/*
* Filters Types
*/
const SavedFilterMetaRuntimeType = runtimeTypes.partial({
alias: unionWithNullType(runtimeTypes.string),
controlledBy: unionWithNullType(runtimeTypes.string),
disabled: unionWithNullType(runtimeTypes.boolean),
field: unionWithNullType(runtimeTypes.string),
formattedValue: unionWithNullType(runtimeTypes.string),
index: unionWithNullType(runtimeTypes.string),
key: unionWithNullType(runtimeTypes.string),
negate: unionWithNullType(runtimeTypes.boolean),
params: unionWithNullType(runtimeTypes.string),
type: unionWithNullType(runtimeTypes.string),
value: unionWithNullType(runtimeTypes.string),
});
const SavedFilterRuntimeType = runtimeTypes.partial({
exists: unionWithNullType(runtimeTypes.string),
meta: unionWithNullType(SavedFilterMetaRuntimeType),
match_all: unionWithNullType(runtimeTypes.string),
missing: unionWithNullType(runtimeTypes.string),
query: unionWithNullType(runtimeTypes.string),
range: unionWithNullType(runtimeTypes.string),
script: unionWithNullType(runtimeTypes.string),
});
/*
* kqlQuery -> filterQuery Types
*/
@ -110,10 +137,12 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({
dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)),
description: unionWithNullType(runtimeTypes.string),
favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)),
filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)),
kqlMode: unionWithNullType(runtimeTypes.string),
kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType),
title: unionWithNullType(runtimeTypes.string),
dateRange: unionWithNullType(SavedDateRangePickerRuntimeType),
savedQueryId: unionWithNullType(runtimeTypes.string),
sort: unionWithNullType(SavedSortRuntimeType),
created: unionWithNullType(runtimeTypes.number),
createdBy: unionWithNullType(runtimeTypes.string),