From 8220c9bc4c30698b0044850091e8bba13c386d82 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Mon, 18 Sep 2023 10:17:17 -0400 Subject: [PATCH] [Security Solution] [Detections] Adds support for index patterns (DataViewBase) to be used for query bar filters (#166318) ## Summary Ref: https://github.com/elastic/kibana/issues/164265 --- .../components/query_bar/index.test.tsx | 202 ++++++------------ .../common/components/query_bar/index.tsx | 32 ++- 2 files changed, 89 insertions(+), 145 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index 118c78e290759..934e923d5724d 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -16,9 +16,45 @@ import { SearchBar } from '@kbn/unified-search-plugin/public'; import type { QueryBarComponentProps } from '.'; import { QueryBar } from '.'; +import type { DataViewFieldMap } from '@kbn/data-views-plugin/common'; +import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { fields } from '@kbn/data-views-plugin/common/mocks'; +import { useKibana } from '../../lib/kibana'; + +const getMockIndexPattern = () => ({ + ...createStubDataView({ + spec: { + id: '1234', + title: 'logstash-*', + fields: ((): DataViewFieldMap => { + const fieldMap: DataViewFieldMap = Object.create(null); + for (const field of fields) { + fieldMap[field.name] = { ...field }; + } + return fieldMap; + })(), + }, + }), +}); + const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; +jest.mock('../../lib/kibana'); describe('QueryBar ', () => { + const mockClearInstanceCache = jest.fn().mockImplementation(({ id }: { id: string }) => { + return id; + }); + + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + dataViews: { + create: jest.fn().mockResolvedValue(getMockIndexPattern()), + clearInstanceCache: mockClearInstanceCache, + }, + }, + }, + }); const mockOnChangeQuery = jest.fn(); const mockOnSubmitQuery = jest.fn(); const mockOnSavedQuery = jest.fn(); @@ -52,10 +88,10 @@ describe('QueryBar ', () => { mockOnSavedQuery.mockClear(); }); - test('check if we format the appropriate props to QueryBar', () => { - const wrapper = mount( - - { + await act(async () => { + const wrapper = await getWrapper( + { onSubmitQuery={mockOnSubmitQuery} onSavedQuery={mockOnSavedQuery} /> - - ); - const { - customSubmitButton, - timeHistory, - onClearSavedQuery, - onFiltersUpdated, - onQueryChange, - onQuerySubmit, - onSaved, - onSavedQueryUpdated, - ...searchBarProps - } = wrapper.find(SearchBar).props(); + ); - expect(searchBarProps).toEqual({ - dataTestSubj: undefined, - dateRangeFrom: 'now/d', - dateRangeTo: 'now/d', - displayStyle: undefined, - 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', - }, - { - aggregatable: false, - name: 'nestedField.firstAttributes', - searchable: true, - type: 'string', - }, - { - aggregatable: false, - name: 'nestedField.secondAttributes', - searchable: true, - type: 'string', - }, - ], - title: 'filebeat-*,auditbeat-*,packetbeat-*', - }, - ], - isLoading: false, - isRefreshPaused: true, - query: { - language: 'kuery', - query: 'here: query', - }, - refreshInterval: undefined, - savedQuery: undefined, - showAutoRefreshOnly: false, - showDatePicker: false, - showFilterBar: true, - showQueryInput: true, - showSaveQuery: true, - showSubmitButton: false, + await waitFor(() => { + wrapper.update(); + const { + customSubmitButton, + timeHistory, + onClearSavedQuery, + onFiltersUpdated, + onQueryChange, + onQuerySubmit, + onSaved, + onSavedQueryUpdated, + ...searchBarProps + } = wrapper.find(SearchBar).props(); + expect((searchBarProps?.indexPatterns ?? [{ id: 'unknown' }])[0].id).toEqual( + getMockIndexPattern().id + ); + }); + // ensure useEffect cleanup is called correctly after component unmounts + wrapper.unmount(); + expect(mockClearInstanceCache).toHaveBeenCalledWith(getMockIndexPattern().id); }); }); @@ -294,7 +215,6 @@ describe('QueryBar ', () => { 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); diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx index d86f3de10b549..9356956c23d56 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useMemo, useCallback } from 'react'; +import React, { memo, useMemo, useCallback, useState, useEffect } from 'react'; import deepEqual from 'fast-deep-equal'; import type { DataViewBase, Filter, Query, TimeRange } from '@kbn/es-query'; @@ -16,6 +16,8 @@ import type { SearchBarProps } from '@kbn/unified-search-plugin/public'; import { SearchBar } from '@kbn/unified-search-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { useKibana } from '../../lib/kibana'; + export interface QueryBarComponentProps { dataTestSubj?: string; dateRangeFrom?: string; @@ -36,6 +38,9 @@ export interface QueryBarComponentProps { isDisabled?: boolean; } +export const isDataView = (obj: unknown): obj is DataView => + obj != null && typeof obj === 'object' && Object.hasOwn(obj, 'getName'); + export const QueryBar = memo( ({ dateRangeFrom, @@ -56,6 +61,8 @@ export const QueryBar = memo( displayStyle, isDisabled, }) => { + const { data } = useKibana().services; + const [dataView, setDataView] = useState(); const onQuerySubmit = useCallback( (payload: { dateRange: TimeRange; query?: Query }) => { if (payload.query != null && !deepEqual(payload.query, filterQuery)) { @@ -102,16 +109,33 @@ export const QueryBar = memo( [filterManager] ); - const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); - const timeHistory = useMemo(() => new TimeHistory(new Storage(localStorage)), []); + useEffect(() => { + let dv: DataView; + if (isDataView(indexPattern)) { + setDataView(indexPattern); + } else { + const createDataView = async () => { + dv = await data.dataViews.create({ title: indexPattern.title }); + setDataView(dv); + }; + createDataView(); + } + return () => { + if (dv?.id) { + data.dataViews.clearInstanceCache(dv?.id); + } + }; + }, [data.dataViews, indexPattern]); + const timeHistory = useMemo(() => new TimeHistory(new Storage(localStorage)), []); + const arrDataView = useMemo(() => (dataView != null ? [dataView] : []), [dataView]); return (