diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx index ca0ac3c37184..cd64b1ecf254 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx @@ -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) { diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index ea0f6775e483..d713139366ee 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -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 { ); } + /* + * 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 { 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 { 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 { onSave={savedQueryMeta => this.onSave(savedQueryMeta, true)} onClose={() => this.setState({ showSaveNewQueryModal: false })} showFilterOption={this.props.showFilterBar} - showTimeFilterOption={this.props.showDatePicker} + showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} /> ) : null} diff --git a/src/plugins/data/common/es_query/filters/meta_filter.ts b/src/plugins/data/common/es_query/filters/meta_filter.ts index 9adfdc4eedcb..ff6dff9d8b74 100644 --- a/src/plugins/data/common/es_query/filters/meta_filter.ts +++ b/src/plugins/data/common/es_query/filters/meta_filter.ts @@ -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 { diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 6845648ee921..2b4b4b78db62 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -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 diff --git a/x-pack/legacy/plugins/siem/public/apps/index.ts b/x-pack/legacy/plugins/siem/public/apps/index.ts index 468e72c8a2e5..b71c4fe69986 100644 --- a/x-pack/legacy/plugins/siem/public/apps/index.ts +++ b/x-pack/legacy/plugins/siem/public/apps/index.ts @@ -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 +); diff --git a/x-pack/legacy/plugins/siem/public/apps/plugin.tsx b/x-pack/legacy/plugins/siem/public/apps/plugin.tsx index f3cbd44f34cd..1f19841788dd 100644 --- a/x-pack/legacy/plugins/siem/public/apps/plugin.tsx +++ b/x-pack/legacy/plugins/siem/public/apps/plugin.tsx @@ -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 = () => { diff --git a/x-pack/legacy/plugins/siem/public/apps/start_app.tsx b/x-pack/legacy/plugins/siem/public/apps/start_app.tsx index 47c8b6eee645..4549db946b81 100644 --- a/x-pack/legacy/plugins/siem/public/apps/start_app.tsx +++ b/x-pack/legacy/plugins/siem/public/apps/start_app.tsx @@ -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 = memo(libs => { const history = createHashHistory(); @@ -74,10 +77,21 @@ const StartApp: FC = memo(libs => { export const ROOT_ELEMENT_ID = 'react-siem-root'; -export const SiemApp = memo(({ core, plugins }) => ( - - - - - -)); +export const SiemApp = memo<{ core: LegacyCoreStart; plugins: PluginsStart }>( + ({ core, plugins }) => ( + + + + + + + + ) +); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx index a7603762f424..ddc3e4f15938 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx @@ -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; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx index 6f0f7a45e5c4..6681e5a90b1a 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx @@ -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( diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts index fbd3f3004964..840d8c0a4812 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts @@ -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, diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts index 1d2508fcfaf1..91480f20d8b0 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts @@ -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 diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx index 44898e7a307f..cdeaba0f067f 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx @@ -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(); diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx new file mode 100644 index 000000000000..d619b515ccc7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx @@ -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 (/ 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( + + + + ); + 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) => ( + + + + ); + + const wrapper = mount( + + ); + 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) => ( + + + + ); + + const wrapper = mount( + + ); + 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) => ( + + + + ); + + const wrapper = mount( + + ); + 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) => ( + + + + ); + + const wrapper = mount( + + ); + 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); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx new file mode 100644 index 000000000000..c7e58532fc7e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx @@ -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( + ({ + 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 ( + + ); + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx index f5a99c631131..850d78e1c342 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx @@ -234,7 +234,7 @@ const SearchBarComponent = memo { let isSubscribed = true; @@ -258,13 +258,13 @@ const SearchBarComponent = memo [indexPattern as IndexPattern], [indexPattern]); + const indexPatterns = useMemo(() => [indexPattern as IndexPattern], [indexPattern]); return ( { 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"]') diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx index caeb29fc6de7..a2e190da0f7b 100644 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx @@ -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( 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 ( ( kueryFilterQuery, kueryFilterQueryDraft, storeType: 'timelineType', - type: null, timelineId: id, }), }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap index a2d20ff0b8d1..048ca080772f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -150,6 +150,7 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` show={true} /> ({ + uiSettings: mockUiSettings, + savedObjects: {}, +})); + describe('Header', () => { const indexPattern = mockIndexPattern; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx index f7802203d125..9377668b4fda 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx @@ -84,7 +84,11 @@ export const TimelineHeader = React.memo( onToggleDataProviderExcluded={onToggleDataProviderExcluded} show={show} /> - + ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx index b30771760bad..7664814f7114 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx @@ -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({ diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx index 6182fca6e2e9..e31f5aac137b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx @@ -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 { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx index e4afef9a351e..8c911b4ab06c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx @@ -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( createTimeline, dataProviders, end, + filters, flyoutHeaderHeight, flyoutHeight, id, @@ -252,6 +256,7 @@ const StatefulTimelineComponent = React.memo( columns={columns} dataProviders={dataProviders!} end={end} + filters={filters} flyoutHeaderHeight={flyoutHeaderHeight} flyoutHeight={flyoutHeight} id={id} @@ -295,6 +300,7 @@ const StatefulTimelineComponent = React.memo( 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, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx index 3b42ead64ad0..8c586cf95841 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx @@ -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']; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx new file mode 100644 index 000000000000..b78691fabdcb --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx @@ -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 (/ 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( + + + + ); + 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) => ( + + + + ); + + const wrapper = mount( + + ); + 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) => ( + + + + ); + + const wrapper = mount( + + ); + 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) => ( + + + + ); + + const wrapper = mount( + + ); + 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) => ( + + + + ); + + const wrapper = mount( + + ); + 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) => ( + + + + ); + + const wrapper = mount( + + ); + 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}}', + }, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx new file mode 100644 index 000000000000..cb352059aaca --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx @@ -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( + ({ + applyKqlFilterQuery, + browserFields, + dataProviders, + filters, + filterQuery, + filterQueryDraft, + from, + fromStr, + kqlMode, + indexPattern, + isRefreshPaused, + savedQueryId, + setFilters, + setKqlFilterQueryDraft, + setSavedQueryId, + refreshInterval, + timelineId, + to, + toStr, + updateReduxTime, + }) => { + const [dateRangeFrom, setDateRangeFrom] = useState( + fromStr != null ? fromStr : new Date(from).toISOString() + ); + const [dateRangeTo, setDateRangTo] = useState( + toStr != null ? toStr : new Date(to).toISOString() + ); + + const [savedQuery, setSavedQuery] = useState(null); + const [filterQueryConverted, setFilterQueryConverted] = useState({ + query: filterQuery != null ? filterQuery.expression : '', + language: filterQuery != null ? filterQuery.kind : 'kuery', + }); + const [queryBarFilters, setQueryBarFilters] = useState([]); + const [dataProvidersDsl, setDataProvidersDsl] = useState( + convertKueryToElasticSearchQuery(buildGlobalQuery(dataProviders, browserFields), indexPattern) + ); + const core = useKibanaCore(); + const [filterManager] = useState(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 ( + + ); + } +); + +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, + }, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx index ec491fe50407..4af6178a7223 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx @@ -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( ({ 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( ); 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 ( ); + }, + (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); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index 08ab44317e82..db8909adda23 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -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( ({ applyKqlFilterQuery, + browserFields, + dataProviders, indexPattern, - isFilterQueryDraftValid, + isRefreshPaused, + filters, + filterQuery, filterQueryDraft, + from, + fromStr, kqlMode, timelineId, + refreshInterval, + savedQueryId, + setFilters, setKqlFilterQueryDraft, + setSavedQueryId, + to, + toStr, updateKqlMode, + updateReduxTime, }) => ( @@ -90,22 +127,28 @@ export const SearchOrFilter = pure( - - - {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( - - )} - - + diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index 700489f47d0c..a52d4ce38ccb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -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} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index fb62b636398c..d3a6c77db64f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -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( columns, dataProviders, end, + filters, flyoutHeaderHeight, flyoutHeight, id, @@ -119,7 +122,7 @@ export const Timeline = React.memo( dataProviders, indexPattern, browserFields, - filters: [], + filters, kqlQuery: { query: kqlQueryExpression, language: 'kuery' }, kqlMode, start, diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts index f8d615ee9a7d..eebb4a349dab 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts @@ -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 diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts index a170e4d53fd7..68b749064dc0 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts @@ -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 diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 8732b61e5c0e..9bde4bf47fff 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -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", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index a1b259b876a1..833102a0d00b 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -122,6 +122,8 @@ export interface TimelineInput { description?: Maybe; + filters?: Maybe; + kqlMode?: Maybe; kqlQuery?: Maybe; @@ -130,6 +132,8 @@ export interface TimelineInput { dateRange?: Maybe; + savedQueryId?: Maybe; + sort?: Maybe; } @@ -185,6 +189,46 @@ export interface QueryMatchInput { operator?: Maybe; } +export interface FilterTimelineInput { + exists?: Maybe; + + meta?: Maybe; + + match_all?: Maybe; + + missing?: Maybe; + + query?: Maybe; + + range?: Maybe; + + script?: Maybe; +} + +export interface FilterMetaTimelineInput { + alias?: Maybe; + + controlledBy?: Maybe; + + disabled?: Maybe; + + field?: Maybe; + + formattedValue?: Maybe; + + index?: Maybe; + + key?: Maybe; + + negate?: Maybe; + + params?: Maybe; + + type?: Maybe; + + value?: Maybe; +} + export interface SerializedFilterQueryInput { filterQuery?: Maybe; } @@ -1760,10 +1804,12 @@ export interface SayMyName { } export interface TimelineResult { - savedObjectId: string; - columns?: Maybe; + created?: Maybe; + + createdBy?: Maybe; + dataProviders?: Maybe; dateRange?: Maybe; @@ -1774,6 +1820,8 @@ export interface TimelineResult { favorite?: Maybe; + filters?: Maybe; + kqlMode?: Maybe; kqlQuery?: Maybe; @@ -1786,13 +1834,13 @@ export interface TimelineResult { pinnedEventsSaveObject?: Maybe; - title?: Maybe; + savedQueryId?: Maybe; + + savedObjectId: string; sort?: Maybe; - created?: Maybe; - - createdBy?: Maybe; + title?: Maybe; updated?: Maybe; @@ -1867,6 +1915,46 @@ export interface FavoriteTimelineResult { favoriteDate?: Maybe; } +export interface FilterTimelineResult { + exists?: Maybe; + + meta?: Maybe; + + match_all?: Maybe; + + missing?: Maybe; + + query?: Maybe; + + range?: Maybe; + + script?: Maybe; +} + +export interface FilterMetaTimelineResult { + alias?: Maybe; + + controlledBy?: Maybe; + + disabled?: Maybe; + + field?: Maybe; + + formattedValue?: Maybe; + + index?: Maybe; + + key?: Maybe; + + negate?: Maybe; + + params?: Maybe; + + type?: Maybe; + + value?: Maybe; +} + export interface SerializedFilterQueryResult { filterQuery?: Maybe; } @@ -4874,6 +4962,8 @@ export namespace GetOneTimeline { favorite: Maybe; + filters: Maybe; + kqlMode: Maybe; kqlQuery: Maybe; @@ -4888,6 +4978,8 @@ export namespace GetOneTimeline { title: Maybe; + savedQueryId: Maybe; + sort: Maybe; created: Maybe; @@ -5029,6 +5121,50 @@ export namespace GetOneTimeline { favoriteDate: Maybe; }; + export type Filters = { + __typename?: 'FilterTimelineResult'; + + meta: Maybe; + + query: Maybe; + + exists: Maybe; + + match_all: Maybe; + + missing: Maybe; + + range: Maybe; + + script: Maybe; + }; + + export type Meta = { + __typename?: 'FilterMetaTimelineResult'; + + alias: Maybe; + + controlledBy: Maybe; + + disabled: Maybe; + + field: Maybe; + + formattedValue: Maybe; + + index: Maybe; + + key: Maybe; + + negate: Maybe; + + params: Maybe; + + type: Maybe; + + value: Maybe; + }; + export type KqlQuery = { __typename?: 'SerializedFilterQueryResult'; @@ -5142,6 +5278,8 @@ export namespace PersistTimelineMutation { favorite: Maybe; + filters: Maybe; + kqlMode: Maybe; kqlQuery: Maybe; @@ -5150,6 +5288,8 @@ export namespace PersistTimelineMutation { dateRange: Maybe; + savedQueryId: Maybe; + sort: Maybe; created: Maybe; @@ -5257,6 +5397,50 @@ export namespace PersistTimelineMutation { favoriteDate: Maybe; }; + export type Filters = { + __typename?: 'FilterTimelineResult'; + + meta: Maybe; + + query: Maybe; + + exists: Maybe; + + match_all: Maybe; + + missing: Maybe; + + range: Maybe; + + script: Maybe; + }; + + export type Meta = { + __typename?: 'FilterMetaTimelineResult'; + + alias: Maybe; + + controlledBy: Maybe; + + disabled: Maybe; + + field: Maybe; + + formattedValue: Maybe; + + index: Maybe; + + key: Maybe; + + negate: Maybe; + + params: Maybe; + + type: Maybe; + + value: Maybe; + }; + export type KqlQuery = { __typename?: 'SerializedFilterQueryResult'; diff --git a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts index e9f4c95a80b7..81ceae68c5fb 100644 --- a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts +++ b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts @@ -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)) { diff --git a/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts b/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts index 9ba569cfec22..23f1f0e86dd6 100644 --- a/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts +++ b/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts @@ -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]; }; diff --git a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx index 75df1df59187..d4a7bb1f425d 100644 --- a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx +++ b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx @@ -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 = {}; + + 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( ({ children, store = createStore(state, apolloClientObservable), onDragEnd = jest.fn() }) => ( - - - ({ eui: euiDarkVars, darkMode: true })}> - {children} - - - + + + + ({ eui: euiDarkVars, darkMode: true })}> + {children} + + + + ) ); diff --git a/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts b/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts index c7803e99f385..6c6411c6bda5 100644 --- a/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts +++ b/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts @@ -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, diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx index 6ceebc1708b1..f136ff72c906 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx @@ -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 = { diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx index d2c9822889c2..2d0df0b6e003 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx @@ -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: {}; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx index b56b9d931af4..9e599bcfedff 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx @@ -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: {}; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx index a10118dc6ca6..a374b0082f28 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx @@ -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: {}; diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts b/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts index cb2d357b7400..a2f061dc648d 100644 --- a/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts +++ b/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts @@ -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); diff --git a/x-pack/legacy/plugins/siem/public/store/model.ts b/x-pack/legacy/plugins/siem/public/store/model.ts index 6f5bd327a101..6f04f22866be 100644 --- a/x-pack/legacy/plugins/siem/public/store/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/model.ts @@ -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; } diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts b/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts index 9729505c4e94..931d3e26172c 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts @@ -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'); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts new file mode 100644 index 000000000000..85d2e624c280 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts @@ -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', + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts index 6957db5578af..a9cf7cff812a 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts @@ -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 ''; + } +}; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts index eee467cd9d6d..16ae53ade796 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts @@ -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, + }, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts index 3b10314f7253..405564a4b5b0 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts @@ -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; + 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 ({ ...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(); diff --git a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx index de6fedb687d9..b70a5432e47f 100644 --- a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx @@ -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({ diff --git a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx index 1a4ca656ba0f..b5843d149d24 100644 --- a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx @@ -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; diff --git a/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx b/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx new file mode 100644 index 000000000000..f1e4cf341139 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx @@ -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( + createSavedQueryService(core.savedObjects.client) + ); + + useEffect(() => { + setSavedQueryService(createSavedQueryService(core.savedObjects.client)); + }, [core.savedObjects.client]); + return savedQueryService; +}; diff --git a/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts index a417ecb82b2d..f05c26de7f75 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts @@ -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! diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index cf7ce3ad02fa..d6a4d204124a 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -124,6 +124,8 @@ export interface TimelineInput { description?: Maybe; + filters?: Maybe; + kqlMode?: Maybe; kqlQuery?: Maybe; @@ -132,6 +134,8 @@ export interface TimelineInput { dateRange?: Maybe; + savedQueryId?: Maybe; + sort?: Maybe; } @@ -187,6 +191,46 @@ export interface QueryMatchInput { operator?: Maybe; } +export interface FilterTimelineInput { + exists?: Maybe; + + meta?: Maybe; + + match_all?: Maybe; + + missing?: Maybe; + + query?: Maybe; + + range?: Maybe; + + script?: Maybe; +} + +export interface FilterMetaTimelineInput { + alias?: Maybe; + + controlledBy?: Maybe; + + disabled?: Maybe; + + field?: Maybe; + + formattedValue?: Maybe; + + index?: Maybe; + + key?: Maybe; + + negate?: Maybe; + + params?: Maybe; + + type?: Maybe; + + value?: Maybe; +} + export interface SerializedFilterQueryInput { filterQuery?: Maybe; } @@ -1762,10 +1806,12 @@ export interface SayMyName { } export interface TimelineResult { - savedObjectId: string; - columns?: Maybe; + created?: Maybe; + + createdBy?: Maybe; + dataProviders?: Maybe; dateRange?: Maybe; @@ -1776,6 +1822,8 @@ export interface TimelineResult { favorite?: Maybe; + filters?: Maybe; + kqlMode?: Maybe; kqlQuery?: Maybe; @@ -1788,13 +1836,13 @@ export interface TimelineResult { pinnedEventsSaveObject?: Maybe; - title?: Maybe; + savedQueryId?: Maybe; + + savedObjectId: string; sort?: Maybe; - created?: Maybe; - - createdBy?: Maybe; + title?: Maybe; updated?: Maybe; @@ -1869,6 +1917,46 @@ export interface FavoriteTimelineResult { favoriteDate?: Maybe; } +export interface FilterTimelineResult { + exists?: Maybe; + + meta?: Maybe; + + match_all?: Maybe; + + missing?: Maybe; + + query?: Maybe; + + range?: Maybe; + + script?: Maybe; +} + +export interface FilterMetaTimelineResult { + alias?: Maybe; + + controlledBy?: Maybe; + + disabled?: Maybe; + + field?: Maybe; + + formattedValue?: Maybe; + + index?: Maybe; + + key?: Maybe; + + negate?: Maybe; + + params?: Maybe; + + type?: Maybe; + + value?: Maybe; +} + export interface SerializedFilterQueryResult { filterQuery?: Maybe; } @@ -7469,10 +7557,12 @@ export namespace SayMyNameResolvers { export namespace TimelineResultResolvers { export interface Resolvers { - savedObjectId?: SavedObjectIdResolver; - columns?: ColumnsResolver, TypeParent, TContext>; + created?: CreatedResolver, TypeParent, TContext>; + + createdBy?: CreatedByResolver, TypeParent, TContext>; + dataProviders?: DataProvidersResolver, TypeParent, TContext>; dateRange?: DateRangeResolver, TypeParent, TContext>; @@ -7483,6 +7573,8 @@ export namespace TimelineResultResolvers { favorite?: FavoriteResolver, TypeParent, TContext>; + filters?: FiltersResolver, TypeParent, TContext>; + kqlMode?: KqlModeResolver, TypeParent, TContext>; kqlQuery?: KqlQueryResolver, TypeParent, TContext>; @@ -7499,13 +7591,13 @@ export namespace TimelineResultResolvers { TContext >; - title?: TitleResolver, TypeParent, TContext>; + savedQueryId?: SavedQueryIdResolver, TypeParent, TContext>; + + savedObjectId?: SavedObjectIdResolver; sort?: SortResolver, TypeParent, TContext>; - created?: CreatedResolver, TypeParent, TContext>; - - createdBy?: CreatedByResolver, TypeParent, TContext>; + title?: TitleResolver, TypeParent, TContext>; updated?: UpdatedResolver, TypeParent, TContext>; @@ -7514,13 +7606,18 @@ export namespace TimelineResultResolvers { version?: VersionResolver; } - export type SavedObjectIdResolver< - R = string, + export type ColumnsResolver< + R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; - export type ColumnsResolver< - R = Maybe, + export type CreatedResolver< + R = Maybe, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; + export type CreatedByResolver< + R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; @@ -7549,6 +7646,11 @@ export namespace TimelineResultResolvers { Parent = TimelineResult, TContext = SiemContext > = Resolver; + export type FiltersResolver< + R = Maybe, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; export type KqlModeResolver< R = Maybe, Parent = TimelineResult, @@ -7579,22 +7681,22 @@ export namespace TimelineResultResolvers { Parent = TimelineResult, TContext = SiemContext > = Resolver; - export type TitleResolver< + export type SavedQueryIdResolver< R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; + export type SavedObjectIdResolver< + R = string, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; export type SortResolver< R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; - export type CreatedResolver< - R = Maybe, - Parent = TimelineResult, - TContext = SiemContext - > = Resolver; - export type CreatedByResolver< + export type TitleResolver< R = Maybe, Parent = TimelineResult, TContext = SiemContext @@ -7837,6 +7939,142 @@ export namespace FavoriteTimelineResultResolvers { > = Resolver; } +export namespace FilterTimelineResultResolvers { + export interface Resolvers { + exists?: ExistsResolver, TypeParent, TContext>; + + meta?: MetaResolver, TypeParent, TContext>; + + match_all?: MatchAllResolver, TypeParent, TContext>; + + missing?: MissingResolver, TypeParent, TContext>; + + query?: QueryResolver, TypeParent, TContext>; + + range?: RangeResolver, TypeParent, TContext>; + + script?: ScriptResolver, TypeParent, TContext>; + } + + export type ExistsResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type MetaResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type MatchAllResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type MissingResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type QueryResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type RangeResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type ScriptResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; +} + +export namespace FilterMetaTimelineResultResolvers { + export interface Resolvers { + alias?: AliasResolver, TypeParent, TContext>; + + controlledBy?: ControlledByResolver, TypeParent, TContext>; + + disabled?: DisabledResolver, TypeParent, TContext>; + + field?: FieldResolver, TypeParent, TContext>; + + formattedValue?: FormattedValueResolver, TypeParent, TContext>; + + index?: IndexResolver, TypeParent, TContext>; + + key?: KeyResolver, TypeParent, TContext>; + + negate?: NegateResolver, TypeParent, TContext>; + + params?: ParamsResolver, TypeParent, TContext>; + + type?: TypeResolver, TypeParent, TContext>; + + value?: ValueResolver, TypeParent, TContext>; + } + + export type AliasResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type ControlledByResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type DisabledResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type FieldResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type FormattedValueResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type IndexResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type KeyResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type NegateResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type ParamsResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type TypeResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type ValueResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; +} + export namespace SerializedFilterQueryResultResolvers { export interface Resolvers { filterQuery?: FilterQueryResolver, TypeParent, TContext>; @@ -8484,6 +8722,8 @@ export type IResolvers = { QueryMatchResult?: QueryMatchResultResolvers.Resolvers; DateRangePickerResult?: DateRangePickerResultResolvers.Resolvers; FavoriteTimelineResult?: FavoriteTimelineResultResolvers.Resolvers; + FilterTimelineResult?: FilterTimelineResultResolvers.Resolvers; + FilterMetaTimelineResult?: FilterMetaTimelineResultResolvers.Resolvers; SerializedFilterQueryResult?: SerializedFilterQueryResultResolvers.Resolvers; SerializedKueryQueryResult?: SerializedKueryQueryResultResolvers.Resolvers; KueryFilterQueryResult?: KueryFilterQueryResultResolvers.Resolvers; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts index f9175eb8ffb9..8c7275a86911 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts @@ -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: { diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts index 0ffdf73f4c74..72e5cd50af39 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts @@ -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),