diff --git a/src/platform/plugins/shared/data_views/common/data_views/data_view.ts b/src/platform/plugins/shared/data_views/common/data_views/data_view.ts index 5bb5cab83abb3..c877bb4609702 100644 --- a/src/platform/plugins/shared/data_views/common/data_views/data_view.ts +++ b/src/platform/plugins/shared/data_views/common/data_views/data_view.ts @@ -340,6 +340,7 @@ export class DataView extends AbstractDataView implements DataViewBase { if (existingField && existingField.isMapped) { // mapped field, remove runtimeField def existingField.runtimeField = undefined; + this.fields.clearSpecCache(); } else { Object.values(this.getFieldsByRuntimeFieldName(name) || {}).forEach((field) => { this.fields.remove(field); diff --git a/src/platform/plugins/shared/data_views/common/data_views/data_views.test.ts b/src/platform/plugins/shared/data_views/common/data_views/data_views.test.ts index 3980e66d16a6c..fe8d9003d3ba5 100644 --- a/src/platform/plugins/shared/data_views/common/data_views/data_views.test.ts +++ b/src/platform/plugins/shared/data_views/common/data_views/data_views.test.ts @@ -434,6 +434,25 @@ describe('IndexPatterns', () => { expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); }); + test('caches fields toSpec calls', async () => { + const indexPatternSpec: DataViewSpec = { + runtimeFieldMap: { + a: { + type: 'keyword', + script: { + source: "emit('a');", + }, + }, + }, + title: 'test', + }; + + const indexPattern = await indexPatterns.create(indexPatternSpec); + const firstSpec = indexPattern.fields.toSpec(); + const secondSpec = indexPattern.fields.toSpec(); + expect(firstSpec).toEqual(secondSpec); + }); + test('deletes the index pattern', async () => { const id = '1'; const indexPattern = await indexPatterns.get(id); @@ -936,5 +955,25 @@ describe('IndexPatterns', () => { // @ts-expect-error expect(apiClient.getFieldsForWildcard.mock.calls[0][0].allowNoIndex).toBe(true); }); + + test('refreshFields should return a new fields spec instance', async () => { + const indexPatternSpec: DataViewSpec = { + runtimeFieldMap: { + a: { + type: 'keyword', + script: { + source: "emit('a');", + }, + }, + }, + title: 'test', + }; + + const indexPattern = await indexPatterns.create(indexPatternSpec); + const originalFieldsSpec = indexPattern.fields.toSpec(); + + await indexPatterns.refreshFields(indexPattern); + expect(indexPattern.fields.toSpec()).not.toBe(originalFieldsSpec); + }); }); }); diff --git a/src/platform/plugins/shared/data_views/common/fields/field_list.test.ts b/src/platform/plugins/shared/data_views/common/fields/field_list.test.ts new file mode 100644 index 0000000000000..add5a2174b187 --- /dev/null +++ b/src/platform/plugins/shared/data_views/common/fields/field_list.test.ts @@ -0,0 +1,245 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { fieldList, IIndexPatternFieldList } from './field_list'; +import { DataViewField } from './data_view_field'; +import type { FieldSpec } from '../types'; + +const baseField: FieldSpec = { + name: 'foo', + type: 'string', + esTypes: ['keyword'], + aggregatable: true, + searchable: true, + scripted: false, +}; + +const anotherField: FieldSpec = { + name: 'bar', + type: 'number', + esTypes: ['long'], + aggregatable: false, + searchable: false, + scripted: false, + count: 5, + customLabel: 'Bar Label', + customDescription: 'Bar Description', + conflictDescriptions: { idx1: ['type1', 'type2'] }, + readFromDocValues: true, + indexed: true, + isMapped: true, +}; + +const scriptedField: FieldSpec = { + name: 'scripted', + type: 'number', + esTypes: ['long'], + aggregatable: false, + searchable: false, + scripted: true, + script: 'doc["bar"].value + 1', + lang: 'painless', +}; + +describe('fieldList', () => { + let list: IIndexPatternFieldList; + + beforeEach(() => { + list = fieldList([baseField], false); + }); + + it('creates a field list with initial fields', () => { + expect(list.length).toBe(1); + expect(list[0]).toBeInstanceOf(DataViewField); + expect(list[0].name).toBe('foo'); + expect(list[0].type).toBe('string'); + expect(list[0].esTypes).toEqual(['keyword']); + expect(list[0].aggregatable).toBe(true); + expect(list[0].searchable).toBe(true); + expect(list[0].scripted).toBe(false); + }); + + it('creates a DataViewField instance with create()', () => { + const field = list.create(anotherField); + expect(field).toBeInstanceOf(DataViewField); + expect(field.name).toBe('bar'); + expect(field.type).toBe('number'); + expect(field.customLabel).toBe('Bar Label'); + expect(field.customDescription).toBe('Bar Description'); + expect(field.conflictDescriptions).toEqual({ idx1: ['type1', 'type2'] }); + expect(field.count).toBe(5); + expect(field.readFromDocValues).toBe(true); + expect(field.isMapped).toBe(true); + }); + + it('adds a field with add()', () => { + const added = list.add(anotherField); + expect(list.length).toBe(2); + expect(list.getByName('bar')).toBe(added); + expect(list.getByType('number')).toContain(added); + expect(list.getByType('string')[0].name).toBe('foo'); + }); + + it('gets all fields with getAll()', () => { + list.add(anotherField); + const all = list.getAll(); + expect(all.length).toBe(2); + expect(all[0]).toBeInstanceOf(DataViewField); + expect(all[1]).toBeInstanceOf(DataViewField); + expect(all.map((f) => f.name)).toEqual(['foo', 'bar']); + }); + + it('gets a field by name with getByName()', () => { + expect(list.getByName('foo')).toBeInstanceOf(DataViewField); + expect(list.getByName('foo')!.name).toBe('foo'); + expect(list.getByName('notfound')).toBeUndefined(); + }); + + it('gets fields by type with getByType()', () => { + list.add(anotherField); + const stringFields = list.getByType('string'); + expect(stringFields.length).toBe(1); + expect(stringFields[0].name).toBe('foo'); + const numberFields = list.getByType('number'); + expect(numberFields.length).toBe(1); + expect(numberFields[0].name).toBe('bar'); + const notFound = list.getByType('boolean'); + expect(notFound.length).toBe(0); + }); + + it('removes a field with remove()', () => { + const field = list.getByName('foo')!; + list.remove(field); + expect(list.length).toBe(0); + expect(list.getByName('foo')).toBeUndefined(); + expect(list.getByType('string').length).toBe(0); + }); + + it('updates a field with update()', () => { + list.add(anotherField); + const updatedField: FieldSpec = { + ...anotherField, + aggregatable: true, + name: 'bar', + customLabel: 'Updated Label', + }; + list.update(updatedField); + const field = list.getByName('bar'); + expect(field).toBeInstanceOf(DataViewField); + expect(field!.aggregatable).toBe(true); + expect(field!.customLabel).toBe('Updated Label'); + }); + + it('removes all fields with removeAll()', () => { + list.add(anotherField); + list.removeAll(); + expect(list.length).toBe(0); + expect(list.getAll().length).toBe(0); + expect(list.getByName('foo')).toBeUndefined(); + expect(list.getByType('string').length).toBe(0); + }); + + it('replaces all fields with replaceAll()', () => { + list.replaceAll([anotherField, scriptedField]); + expect(list.length).toBe(2); + expect(list[0].name).toBe('bar'); + expect(list[1].name).toBe('scripted'); + expect(list.getByName('foo')).toBeUndefined(); + expect(list.getByName('bar')).toBeInstanceOf(DataViewField); + expect(list.getByName('scripted')).toBeInstanceOf(DataViewField); + expect(list.getByName('scripted')?.scripted).toBe(true); + expect(list.getByName('scripted')?.script).toBe('doc["bar"].value + 1'); + expect(list.getByName('scripted')?.lang).toBe('painless'); + }); + + it('caches and clears toSpec()', () => { + const spy = jest.spyOn(list[0], 'toSpec'); + // First call, not cached + const spec1 = list.toSpec(); + expect(spec1.foo).toBeDefined(); + expect(spy).toHaveBeenCalled(); + + // Second call, should be cached (no new calls to toSpec) + spy.mockClear(); + const spec2 = list.toSpec(); + expect(spec2.foo).toBeDefined(); + expect(spy).not.toHaveBeenCalled(); + + // After add, cache should be cleared + list.add(anotherField); + const spec3 = list.toSpec(); + expect(spec3.bar).toBeDefined(); + }); + + it('calls toSpec with getFormatterForField', () => { + const getFormatterForField = jest.fn().mockReturnValue({ toJSON: () => ({ id: 'test' }) }); + list.toSpec({ getFormatterForField }); + expect(getFormatterForField).toHaveBeenCalledWith(list[0]); + }); + + it('handles remove on non-existent field gracefully', () => { + expect(list.length).toBe(1); + const fakeField = new DataViewField({ ...baseField, name: 'notfound' }); + expect(() => list.remove(fakeField)).not.toThrow(); + expect(list.length).toBe(1); + }); + + it('handles update on non-existent field gracefully', () => { + const fakeField: FieldSpec = { ...baseField, name: 'notfound' }; + expect(() => list.update(fakeField)).not.toThrow(); + expect(list.length).toBe(1); + }); + + it('handles replaceAll with empty array', () => { + list.replaceAll([]); + expect(list.length).toBe(0); + }); + + it('supports adding and updating a scripted field', () => { + list.add(scriptedField); + const field = list.getByName('scripted'); + expect(field).toBeInstanceOf(DataViewField); + expect(field?.scripted).toBe(true); + expect(field?.script).toBe('doc["bar"].value + 1'); + expect(field?.lang).toBe('painless'); + + // Update scripted field + const updatedScripted: FieldSpec = { + ...scriptedField, + script: 'doc["bar"].value + 2', + lang: 'expression', + }; + list.update(updatedScripted); + const updated = list.getByName('scripted'); + expect(updated?.script).toBe('doc["bar"].value + 2'); + expect(updated?.lang).toBe('expression'); + }); + + it('does not throw if remove is called on a field not in the list', () => { + const field = new DataViewField({ ...baseField, name: 'notfound' }); + expect(() => list.remove(field)).not.toThrow(); + }); + + it('does not throw if update is called on a field not in the list', () => { + const field: FieldSpec = { ...baseField, name: 'notfound' }; + expect(() => list.update(field)).not.toThrow(); + }); + + it('preserves shortDotsEnable option', () => { + const shortDotsList = fieldList([baseField], true); + expect(shortDotsList[0].spec.shortDotsEnable).toBe(true); + }); + + it('does not fail if add is called with a field with the same name', () => { + list.add(baseField); + expect(list.length).toBe(2); + expect(list[0].name).toBe('foo'); + expect(list[1].name).toBe('foo'); + }); +}); diff --git a/src/platform/plugins/shared/data_views/common/fields/field_list.ts b/src/platform/plugins/shared/data_views/common/fields/field_list.ts index 7e7c6ab88fadb..53752303e63c7 100644 --- a/src/platform/plugins/shared/data_views/common/fields/field_list.ts +++ b/src/platform/plugins/shared/data_views/common/fields/field_list.ts @@ -28,6 +28,12 @@ export interface IIndexPatternFieldList extends Array { * @returns a new data view field instance */ create(field: FieldSpec): DataViewField; + + /** + * Clear the cached field spec map. + * This is used to avoid recalculating the spec every time `toSpec` is called. + */ + clearSpecCache(): void; /** * Add field to field list. * @param field field spec to add field to list @@ -97,6 +103,16 @@ export const fieldList = ( private removeByGroup = (field: DataViewField) => this.groups.get(field.type)?.delete(field.name); + /** + * @private + * @description + * This cache is used to store the result of the `toSpec` method, which converts the field list + * to a map of field specs by name. It is initialized to null and is cleared every time a field + * is added, removed, or updated. This is done to avoid recalculating the spec every time `toSpec` + * is called, which can be expensive if the field list is large. + */ + private cachedFieldSpec: DataViewFieldMap | null = null; + constructor() { super(); specs.map((field) => this.add(field)); @@ -112,11 +128,16 @@ export const fieldList = ( return new DataViewField({ ...field, shortDotsEnable }); }; + public clearSpecCache = () => { + this.cachedFieldSpec = null; + }; + public readonly add = (field: FieldSpec): DataViewField => { const newField = this.create(field); this.push(newField); this.setByName(newField); this.setByGroup(newField); + this.clearSpecCache(); return newField; }; @@ -125,7 +146,9 @@ export const fieldList = ( this.byName.delete(field.name); const fieldIndex = findIndex(this, { name: field.name }); + if (fieldIndex === -1) return; this.splice(fieldIndex, 1); + this.clearSpecCache(); }; public readonly update = (field: FieldSpec) => { @@ -135,12 +158,14 @@ export const fieldList = ( this.setByName(newField); this.removeByGroup(newField); this.setByGroup(newField); + this.clearSpecCache(); }; public readonly removeAll = () => { this.length = 0; this.byName.clear(); this.groups.clear(); + this.clearSpecCache(); }; public readonly replaceAll = (spcs: FieldSpec[] = []) => { @@ -153,12 +178,17 @@ export const fieldList = ( }: { getFormatterForField?: DataView['getFormatterForField']; } = {}) { - return { + if (this.cachedFieldSpec) { + return this.cachedFieldSpec; + } + const toSpecResult = { ...this.reduce((collector, field) => { collector[field.name] = field.toSpec({ getFormatterForField }); return collector; }, {}), }; + this.cachedFieldSpec = toSpecResult; + return toSpecResult; } } diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/send_to_timeline/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/send_to_timeline/index.tsx index 43cf7ef997811..03afa137225ff 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/send_to_timeline/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/send_to_timeline/index.tsx @@ -67,10 +67,10 @@ export const SendToTimelineButton: FC = ({ children, ...props }) => { + const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); + const { dataViewSpec } = useDataViewSpec(sourcererScopeId); + const experimentalDataViewId = dataViewSpec?.id ?? ''; const getFieldSpec = useGetFieldSpec(sourcererScopeId); - const dataViewId = useDataViewId(sourcererScopeId); + const oldDataViewId = useDataViewId(sourcererScopeId); + + const dataViewId = newDataViewPickerEnabled ? experimentalDataViewId : oldDataViewId; // Make a dependency key to prevent unnecessary re-renders when data object is defined inline // It is necessary because the data object is an array or an object and useMemo would always re-render const dependencyKey = JSON.stringify(data); @@ -74,14 +81,16 @@ export const SecurityCellActions: React.FC = ({ () => (Array.isArray(data) ? data : [data]) .map(({ field, value }) => ({ - field: getFieldSpec(field), + field: newDataViewPickerEnabled ? dataViewSpec.fields?.[field] : getFieldSpec(field), value, })) .filter((item): item is CellActionsData => !!item.field), // eslint-disable-next-line react-hooks/exhaustive-deps -- Use the dependencyKey to prevent unnecessary re-renders - [dependencyKey, getFieldSpec] + [dependencyKey, dataViewSpec, getFieldSpec, newDataViewPickerEnabled] ); + console.log('FIELD DATA', fieldData, dataViewSpec); + const metadataWithDataView = useMemo(() => ({ ...metadata, dataViewId }), [dataViewId, metadata]); return fieldData.length > 0 ? ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/events_viewer/index.tsx index 65ab197a0d7fb..55565b3994117 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -164,7 +164,13 @@ const StatefulEventsViewerComponent: React.FC + newDataViewPickerEnabled ? dataViewSpec?.fields?.[fieldName] : oldGetFieldSpec(fieldName), + [dataViewSpec?.fields, newDataViewPickerEnabled, oldGetFieldSpec] + ); const editorActionsRef = useRef(null); useEffect(() => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 91e790d9d1cf0..a17f9703762bf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -61,7 +61,6 @@ import { useLicense } from '../../../../hooks/use_license'; import { isProviderValid } from './helpers'; import * as i18n from './translations'; import { useGetScopedSourcererDataView } from '../../../../../sourcerer/components/use_get_sourcerer_data_view'; -import { useDataViewSpec } from '../../../../../data_view_manager/hooks/use_data_view_spec'; interface InsightComponentProps { label?: string; @@ -291,9 +290,6 @@ const InsightEditorComponent = ({ const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); - const { dataViewSpec } = useDataViewSpec(); - const sourcererDataView = newDataViewPickerEnabled ? dataViewSpec : oldSourcererDataView; - const { unifiedSearch: { ui: { FiltersBuilderLazy }, @@ -309,6 +305,8 @@ const InsightEditorComponent = ({ const dataView = newDataViewPickerEnabled ? experimentalDataView : oldDataView; + const sourcererDataView = newDataViewPickerEnabled ? dataView : oldSourcererDataView; + const [providers, setProviders] = useState([[]]); const dateRangeChoices = useMemo(() => { const settings: Array<{ from: string; to: string; display: string }> = uiSettings.get( @@ -416,7 +414,7 @@ const InsightEditorComponent = ({ ); }, [labelController.field.value, providers, dataView]); const filtersStub = useMemo(() => { - const index = sourcererDataView.name ?? '*'; + const index = sourcererDataView?.name ?? '*'; return [ { $state: { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/index.tsx index 384f1e88a0fbe..40cc9e0594472 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/index.tsx @@ -110,6 +110,7 @@ export const useFetchIndex = ( const indexFieldsSearch = useCallback( (iNames: string[]) => { const asyncSearch = async () => { + console.trace('useFetchIndex is deprecated, use useDataView instead: ', indexNames); try { setState({ ...state, loading: true }); abortCtrl.current = new AbortController(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/use_data_view.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/use_data_view.tsx index 1dd1bf2877438..f841431a09397 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/use_data_view.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/use_data_view.tsx @@ -9,6 +9,7 @@ import { useCallback, useRef } from 'react'; import type { Subscription } from 'rxjs'; import { useDispatch } from 'react-redux'; import memoizeOne from 'memoize-one'; +import deepEqual from 'fast-deep-equal'; import type { BrowserFields } from '@kbn/timelines-plugin/common'; import type { DataViewSpec } from '@kbn/data-views-plugin/public'; import type { FieldCategory } from '@kbn/timelines-plugin/common/search_strategy'; @@ -66,9 +67,14 @@ export const getDataViewStateFromIndexFields = memoizeOne( return { browserFields: browserFields as DangerCastForBrowserFieldsMutation }; } }, - (newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1]?.length === lastArgs[1]?.length + (newArgs, lastArgs) => deepEqual(newArgs, lastArgs) ); +// This is a utility function to get an instance of the getDataViewStateFromIndexFields function +// If the original function is called in a hook called in different places, the memoization becomes potential useless +// as each call overrides the previous one. This hook is used to ensure that the memoization is preserved for each hook instance. +export const getMemoizedGetDataViewStateFromIndexFields = () => getDataViewStateFromIndexFields; + export const useDataView = (): { indexFieldsSearch: IndexFieldSearch; } => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts index dfbe4b5fc3915..8d2e3ebf56596 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts @@ -73,6 +73,7 @@ export const useInvestigateInTimeline = () => { const setSelectedDataView = useSelectDataView(); + console.log('!!DEFAULT DATA VIEW', defaultDataView); const investigateInTimeline = useCallback( async ({ query, @@ -84,7 +85,7 @@ export const useInvestigateInTimeline = () => { const hasTemplateProviders = dataProviders && dataProviders.find((provider) => provider.type === 'template'); const clearTimeline = hasTemplateProviders ? clearTimelineTemplate : clearTimelineDefault; - + console.log('!!DEFAULT DATA VIEW', defaultDataView, keepDataView); if (dataProviders || filters || query) { // Reset the current timeline if (timeRange) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts b/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts index 4a50e197fab36..6bbbc7d702e59 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts @@ -153,16 +153,16 @@ const timelineActionsWithNonserializablePayloads = [ const actionSanitizer = (action: AnyAction) => { if (action.type === sourcererActions.setDataView.type) { - return { - ...action, - payload: { - ...action.payload, - dataView: 'dataView', - browserFields: 'browserFields', - indexFields: 'indexFields', - fields: 'fields', - }, - }; + // return { + // ...action, + // payload: { + // ...action.payload, + // dataView: 'dataView', + // browserFields: 'browserFields', + // indexFields: 'indexFields', + // fields: 'fields', + // }, + // }; } else if (timelineActionsWithNonserializablePayloads.includes(action.type)) { const { type, payload } = action; if (type === timelineActions.addTimeline.type || type === timelineActions.updateTimeline.type) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx index 1cb229926a3c9..1e9c28778455b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx @@ -55,7 +55,7 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie const closeDataViewEditor = useRef<() => void | undefined>(); const closeFieldEditor = useRef<() => void | undefined>(); - const { dataViewSpec, status } = useDataViewSpec(scope); + const { dataViewSpec, status } = useDataViewSpec(scope, false); const { adhocDataViews: adhocDataViewSpecs, defaultDataViewId } = useSelector(sharedStateSelector); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_data_view_spec.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_data_view_spec.ts index a89524559c25d..ffebc70bb43f6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_data_view_spec.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_data_view_spec.ts @@ -25,7 +25,8 @@ export interface UseDataViewSpecResult { * Returns an object with the dataViewSpec and status values for the given scopeName. */ export const useDataViewSpec = ( - scopeName: DataViewManagerScopeName = DataViewManagerScopeName.default + scopeName: DataViewManagerScopeName = DataViewManagerScopeName.default, + includeFields: boolean = true ): UseDataViewSpecResult => { const { dataView, status } = useDataView(scopeName); @@ -42,6 +43,6 @@ export const useDataViewSpec = ( }; } - return { dataViewSpec: dataView?.toSpec?.(), status }; - }, [dataView, status]); + return { dataViewSpec: dataView?.toSpec?.(includeFields), status }; + }, [dataView, includeFields, status]); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx index 768a80a9df6f1..37182637e6fe9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx @@ -14,7 +14,7 @@ import { ACTION_INVESTIGATE_IN_TIMELINE, ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL, } from '../translations'; -import { useInvestigateInTimeline } from './use_investigate_in_timeline'; +import { useInvestigateAlertInTimeline } from './use_investigate_alert_in_timeline'; interface InvestigateInTimelineActionProps { ecsRowData?: Ecs | null; @@ -29,7 +29,7 @@ const InvestigateInTimelineActionComponent: React.FC { - const { investigateInTimelineAlertClick } = useInvestigateInTimeline({ + const { investigateInTimelineAlertClick } = useInvestigateAlertInTimeline({ ecsRowData, onInvestigateInTimelineAlertClick, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline.test.tsx similarity index 95% rename from x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline.test.tsx index 868b390a3ceea..d3ae0625a259c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline.test.tsx @@ -10,7 +10,7 @@ import { of } from 'rxjs'; import { TestProviders } from '../../../../common/mock'; import { useKibana } from '../../../../common/lib/kibana'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; -import { useInvestigateInTimeline } from './use_investigate_in_timeline'; +import { useInvestigateAlertInTimeline } from './use_investigate_alert_in_timeline'; import * as actions from '../actions'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import type { AlertTableContextMenuItem } from '../types'; @@ -254,7 +254,7 @@ const renderContextMenu = (items: AlertTableContextMenuItem[]) => { ); }; -describe('useInvestigateInTimeline', () => { +describe('useInvestigateAlertInTimeline', () => { let mockSearchStrategyClient = { search: jest .fn() @@ -279,7 +279,7 @@ describe('useInvestigateInTimeline', () => { jest.clearAllMocks(); }); test('creates a component and click handler', () => { - const { result } = renderHook(() => useInvestigateInTimeline(props), { + const { result } = renderHook(() => useInvestigateAlertInTimeline(props), { wrapper: TestProviders, }); expect(result.current.investigateInTimelineActionItems).toBeTruthy(); @@ -288,7 +288,7 @@ describe('useInvestigateInTimeline', () => { describe('the click handler calls createTimeline once and only once', () => { test('runs 0 times on render, once on click', async () => { - const { result } = renderHook(() => useInvestigateInTimeline(props), { + const { result } = renderHook(() => useInvestigateAlertInTimeline(props), { wrapper: TestProviders, }); const actionItem = result.current.investigateInTimelineActionItems[0]; @@ -311,7 +311,7 @@ describe('useInvestigateInTimeline', () => { }; const ecsData = getEcsDataWithRuleTypeAndTimelineTemplate(ruleType); const { result } = renderHook( - () => useInvestigateInTimeline({ ...props, ecsRowData: ecsData }), + () => useInvestigateAlertInTimeline({ ...props, ecsRowData: ecsData }), { wrapper: TestProviders, } @@ -358,7 +358,7 @@ describe('useInvestigateInTimeline', () => { }; const ecsData: Ecs = getEcsDataWithRuleTypeAndTimelineTemplate(ruleType); const { result } = renderHook( - () => useInvestigateInTimeline({ ...props, ecsRowData: ecsData }), + () => useInvestigateAlertInTimeline({ ...props, ecsRowData: ecsData }), { wrapper: TestProviders, } @@ -404,7 +404,7 @@ describe('useInvestigateInTimeline', () => { timelinePrivileges: { read: false }, }); - const { result } = renderHook(() => useInvestigateInTimeline(props), { + const { result } = renderHook(() => useInvestigateAlertInTimeline(props), { wrapper: TestProviders, }); expect(result.current.investigateInTimelineActionItems).toHaveLength(0); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline.tsx similarity index 82% rename from x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline.tsx index 7a0d02456ad3a..e3c376a570bac 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline.tsx @@ -18,6 +18,7 @@ import { useApi } from '@kbn/securitysolution-list-hooks'; import type { Filter } from '@kbn/es-query'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { isEmpty } from 'lodash'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; import { createHistoryEntry } from '../../../../common/utils/global_query_string/helpers'; import { useKibana } from '../../../../common/lib/kibana'; import { TimelineId } from '../../../../../common/types/timeline'; @@ -33,8 +34,12 @@ import { useStartTransaction } from '../../../../common/lib/apm/use_start_transa import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { defaultUdtHeaders } from '../../../../timelines/components/timeline/body/column_headers/default_headers'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; +import { DataViewManagerScopeName } from '../../../../data_view_manager/constants'; +import { useDataView } from '../../../../data_view_manager/hooks/use_data_view'; +import { SourcererScopeName } from '../../../../sourcerer/store/model'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; -interface UseInvestigateInTimelineActionProps { +interface UseInvestigateAlertInTimelineActionProps { ecsRowData?: Ecs | Ecs[] | null; onInvestigateInTimelineAlertClick?: () => void; } @@ -89,10 +94,10 @@ const detectionExceptionList = (ecsData: Ecs): ExceptionListId[] => { return detectionExceptionsList; }; -export const useInvestigateInTimeline = ({ +export const useInvestigateAlertInTimeline = ({ ecsRowData, onInvestigateInTimelineAlertClick, -}: UseInvestigateInTimelineActionProps) => { +}: UseInvestigateAlertInTimelineActionProps) => { const { addError } = useAppToasts(); const { data: { search: searchStrategyClient }, @@ -138,12 +143,31 @@ export const useInvestigateInTimeline = ({ const updateTimeline = useUpdateTimeline(); + const { newDataViewPickerEnabled } = useEnableExperimental(); + const { dataView: experimentalDatView } = useDataView(DataViewManagerScopeName.detections); + const { dataViewId: oldTimelineDataViewId } = useSourcererDataView(SourcererScopeName.detections); + + const alertsDefaultDataViewId = useMemo( + () => (newDataViewPickerEnabled ? experimentalDatView?.id ?? null : oldTimelineDataViewId), + [experimentalDatView?.id, newDataViewPickerEnabled, oldTimelineDataViewId] + ); + + const alertsFallbackIndexNames = useMemo( + () => (newDataViewPickerEnabled ? experimentalDatView?.getIndexPattern().split(',') ?? [] : []), + [experimentalDatView, newDataViewPickerEnabled] + ); + const createTimeline = useCallback( async ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { const newColumns = timeline.columns; const newColumnsOverride = !newColumns || isEmpty(newColumns) ? defaultUdtHeaders : newColumns; + const dataViewId = timeline.dataViewId ?? alertsDefaultDataViewId; + const indexNames = isEmpty(timeline.indexNames) + ? alertsFallbackIndexNames + : timeline.indexNames; + await clearActiveTimeline(); updateTimeline({ duplicate: true, @@ -152,8 +176,9 @@ export const useInvestigateInTimeline = ({ notes: [], timeline: { ...timeline, + dataViewId, columns: newColumnsOverride, - indexNames: timeline.indexNames ?? [], + indexNames, show: true, excludedRowRendererIds: timeline.timelineType !== TimelineTypeEnum.template @@ -164,7 +189,7 @@ export const useInvestigateInTimeline = ({ ruleNote, }); }, - [updateTimeline, clearActiveTimeline] + [clearActiveTimeline, updateTimeline, alertsDefaultDataViewId, alertsFallbackIndexNames] ); const investigateInTimelineAlertClick = useCallback(async () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/cell_value_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/cell_value_context.tsx index 906bc4e77c1b0..3a35186c009db 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/cell_value_context.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/cell_value_context.tsx @@ -65,6 +65,7 @@ export const AlertTableCellContextProvider = ({ [browserFields, browserFieldsByName, columnHeaders] ); + console.log("CELL VALUE CONTEXT", cellValueContext); return ( {children} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx index f440de0a7d206..11318428ffabb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx @@ -9,6 +9,7 @@ import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; import { useCallback, useMemo } from 'react'; import { TableId } from '@kbn/securitysolution-data-table'; import type { RenderContext } from '@kbn/response-ops-alerts-table/types'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import type { UseDataGridColumnsSecurityCellActionsProps } from '../../../common/components/cell_actions'; import { useDataGridColumnsSecurityCellActions } from '../../../common/components/cell_actions'; import { SecurityCellActionsTrigger, SecurityCellActionType } from '../../../app/actions/constants'; @@ -19,6 +20,7 @@ import type { SecurityAlertsTableContext, GetSecurityAlertsTableProp, } from '../../components/alerts_table/types'; +import { useDataViewSpec } from '../../../data_view_manager/hooks/use_data_view_spec'; export const useCellActionsOptions = ( tableId: TableId, @@ -34,24 +36,36 @@ export const useCellActionsOptions = ( pageSize = 0, dataGridRef, } = context ?? {}; + const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); + const { dataViewSpec } = useDataViewSpec(SourcererScopeName.detections); + const experimentalDataViewId = dataViewSpec?.id ?? ''; const getFieldSpec = useGetFieldSpec(SourcererScopeName.detections); - const dataViewId = useDataViewId(SourcererScopeName.detections); + const oldDataViewId = useDataViewId(SourcererScopeName.detections); + + const dataViewId = newDataViewPickerEnabled ? experimentalDataViewId : oldDataViewId; + const cellActionsMetadata = useMemo( () => ({ scopeId: tableId, dataViewId }), [dataViewId, tableId] ); const cellActionsFields: UseDataGridColumnsSecurityCellActionsProps['fields'] = useMemo( () => - columns.map( - (column) => - getFieldSpec(column.id) ?? { - name: '', - type: '', // When type is an empty string all cell actions are incompatible - aggregatable: false, - searchable: false, - } + columns.map((column) => + newDataViewPickerEnabled + ? dataViewSpec?.fields?.[column.id] ?? { + name: '', + type: '', // When type is an empty string all cell actions are incompatible + aggregatable: false, + searchable: false, + } + : getFieldSpec(column.id) ?? { + name: '', + type: '', // When type is an empty string all cell actions are incompatible + aggregatable: false, + searchable: false, + } ), - [columns, getFieldSpec] + [columns, dataViewSpec?.fields, getFieldSpec, newDataViewPickerEnabled] ); /** diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.tsx index 60412efb4afa3..f041490c1d900 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.tsx @@ -106,6 +106,7 @@ const StyledFullHeightContainer = styled.div` type DetectionEngineComponentProps = PropsFromRedux; +// eslint-disable-next-line complexity const DetectionEnginePageComponent: React.FC = () => { const dispatch = useDispatch(); const containerElement = useRef(null); @@ -175,7 +176,7 @@ const DetectionEnginePageComponent: React.FC = () TableId.alertsOnAlertsPage ); - const loading = userInfoLoading || listsConfigLoading; + const loading = userInfoLoading || listsConfigLoading || isLoadingIndexPattern; const { application: { navigateToUrl }, data, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/preview/footer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/preview/footer.test.tsx index 1a87a950e2315..07097c2a866ae 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/preview/footer.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/preview/footer.test.tsx @@ -18,7 +18,7 @@ import { FLYOUT_FOOTER_DROPDOWN_BUTTON_TEST_ID } from '../shared/components/test import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock'; import { useKibana } from '../../../common/lib/kibana'; import { useAlertExceptionActions } from '../../../detections/components/alerts_table/timeline_actions/use_add_exception_actions'; -import { useInvestigateInTimeline } from '../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; +import { useInvestigateAlertInTimeline } from '../../../detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline'; import { useAddToCaseActions } from '../../../detections/components/alerts_table/timeline_actions/use_add_to_case_actions'; jest.mock('@kbn/expandable-flyout'); @@ -33,7 +33,7 @@ jest.mock('react-router-dom', () => { jest.mock('../../../common/lib/kibana'); jest.mock('../../../detections/components/alerts_table/timeline_actions/use_add_exception_actions'); jest.mock( - '../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline' + '../../../detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline' ); jest.mock('../../../detections/components/alerts_table/timeline_actions/use_add_to_case_actions'); @@ -50,7 +50,7 @@ describe('', () => { }, }); (useAlertExceptionActions as jest.Mock).mockReturnValue({ exceptionActionItems: [] }); - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + (useInvestigateAlertInTimeline as jest.Mock).mockReturnValue({ investigateInTimelineActionItems: [], }); (useAddToCaseActions as jest.Mock).mockReturnValue({ addToCaseActionItems: [] }); @@ -94,7 +94,7 @@ describe('', () => { }); it('should render the take action button', () => { - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + (useInvestigateAlertInTimeline as jest.Mock).mockReturnValue({ investigateInTimelineActionItems: [{ name: 'test', onClick: jest.fn() }], }); const { getByTestId } = render( diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx index 582a8b1fa4ca8..352abfe2a3033 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx @@ -23,14 +23,14 @@ import { EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID, } from '../../../shared/components/test_ids'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; -import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; +import { useInvestigateAlertInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline'; jest.mock( '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver' ); jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree'); jest.mock( - '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline' + '../../../../detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline' ); jest.mock('../../../../common/hooks/use_experimental_features'); @@ -79,7 +79,7 @@ describe('AnalyzerPreviewContainer', () => { alertIds: ['alertid'], statsNodes: mock.mockStatsNodes, }); - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + (useInvestigateAlertInTimeline as jest.Mock).mockReturnValue({ investigateInTimelineAlertClick: jest.fn(), }); @@ -108,7 +108,7 @@ describe('AnalyzerPreviewContainer', () => { it('should render error message and text in header', () => { (useIsInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(false); - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + (useInvestigateAlertInTimeline as jest.Mock).mockReturnValue({ investigateInTimelineAlertClick: jest.fn(), }); @@ -129,7 +129,7 @@ describe('AnalyzerPreviewContainer', () => { alertIds: ['alertid'], statsNodes: mock.mockStatsNodes, }); - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + (useInvestigateAlertInTimeline as jest.Mock).mockReturnValue({ investigateInTimelineAlertClick: jest.fn(), }); @@ -147,7 +147,7 @@ describe('AnalyzerPreviewContainer', () => { alertIds: ['alertid'], statsNodes: mock.mockStatsNodes, }); - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + (useInvestigateAlertInTimeline as jest.Mock).mockReturnValue({ investigateInTimelineAlertClick: jest.fn(), }); @@ -168,7 +168,7 @@ describe('AnalyzerPreviewContainer', () => { alertIds: ['alertid'], statsNodes: mock.mockStatsNodes, }); - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + (useInvestigateAlertInTimeline as jest.Mock).mockReturnValue({ investigateInTimelineAlertClick: jest.fn(), }); @@ -194,7 +194,7 @@ describe('AnalyzerPreviewContainer', () => { alertIds: ['alertid'], statsNodes: mock.mockStatsNodes, }); - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + (useInvestigateAlertInTimeline as jest.Mock).mockReturnValue({ investigateInTimelineAlertClick: jest.fn(), }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.test.tsx index ddecbdd2f5d06..8e9353fd20f15 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.test.tsx @@ -22,13 +22,13 @@ import { } from '../../../shared/components/test_ids'; import { mockContextValue } from '../../shared/mocks/mock_context'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; -import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; +import { useInvestigateAlertInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline'; jest.mock('../../shared/hooks/use_session_view_config'); jest.mock('../../../../common/hooks/use_license'); jest.mock('../../../../common/hooks/use_experimental_features'); jest.mock( - '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline' + '../../../../detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline' ); const mockNavigateToSessionView = jest.fn(); @@ -70,7 +70,7 @@ describe('SessionPreviewContainer', () => { beforeEach(() => { jest.clearAllMocks(); (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + (useInvestigateAlertInTimeline as jest.Mock).mockReturnValue({ investigateInTimelineAlertClick: jest.fn(), }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx index 31863b4404d90..32799202d8b99 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx @@ -29,7 +29,7 @@ import { useAlertPrevalenceFromProcessTree } from '../../shared/hooks/use_alert_ import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; import { useExpandSection } from '../hooks/use_expand_section'; -import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; +import { useInvestigateAlertInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline'; import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; @@ -150,7 +150,7 @@ describe('', () => { }); it('should render the component expanded if value is true in local storage', () => { - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + (useInvestigateAlertInTimeline as jest.Mock).mockReturnValue({ investigateInTimelineAlertClick: jest.fn(), }); (useIsInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.test.tsx index d6f92ed2c6aaa..47094514b4f46 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.test.tsx @@ -16,7 +16,7 @@ import { FLYOUT_FOOTER_DROPDOWN_BUTTON_TEST_ID } from '../shared/components/test import { useKibana } from '../../../common/lib/kibana'; import { useAssistant } from './hooks/use_assistant'; import { useAlertExceptionActions } from '../../../detections/components/alerts_table/timeline_actions/use_add_exception_actions'; -import { useInvestigateInTimeline } from '../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; +import { useInvestigateAlertInTimeline } from '../../../detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline'; import { useAddToCaseActions } from '../../../detections/components/alerts_table/timeline_actions/use_add_to_case_actions'; jest.mock('../../../common/lib/kibana'); @@ -66,7 +66,7 @@ describe('PanelFooter', () => { }, }); (useAlertExceptionActions as jest.Mock).mockReturnValue({ exceptionActionItems: [] }); - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + (useInvestigateAlertInTimeline as jest.Mock).mockReturnValue({ investigateInTimelineActionItems: [{ name: 'test', onClick: jest.fn() }], }); (useAddToCaseActions as jest.Mock).mockReturnValue({ addToCaseActionItems: [] }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/take_action_button.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/take_action_button.test.tsx index 4326a60c4a0cf..c2eb5aa989a57 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/take_action_button.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/take_action_button.test.tsx @@ -14,7 +14,7 @@ import { DocumentDetailsContext } from '../context'; import { FLYOUT_FOOTER_DROPDOWN_BUTTON_TEST_ID } from './test_ids'; import { useKibana } from '../../../../common/lib/kibana'; import { useAlertExceptionActions } from '../../../../detections/components/alerts_table/timeline_actions/use_add_exception_actions'; -import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; +import { useInvestigateAlertInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline'; import { useAddToCaseActions } from '../../../../detections/components/alerts_table/timeline_actions/use_add_to_case_actions'; jest.mock('../../../../common/lib/kibana'); @@ -29,7 +29,7 @@ jest.mock( '../../../../detections/components/alerts_table/timeline_actions/use_add_exception_actions' ); jest.mock( - '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline' + '../../../../detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline' ); jest.mock( '../../../../detections/components/alerts_table/timeline_actions/use_add_to_case_actions' @@ -44,7 +44,7 @@ describe('TakeActionButton', () => { }, }); (useAlertExceptionActions as jest.Mock).mockReturnValue({ exceptionActionItems: [] }); - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + (useInvestigateAlertInTimeline as jest.Mock).mockReturnValue({ investigateInTimelineActionItems: [{ name: 'test', onClick: jest.fn() }], }); (useAddToCaseActions as jest.Mock).mockReturnValue({ addToCaseActionItems: [] }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/take_action_dropdown.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/take_action_dropdown.tsx index da94ec6e02e99..6f023347f40c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/take_action_dropdown.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/take_action_dropdown.tsx @@ -22,7 +22,7 @@ import { import { isActiveTimeline } from '../../../../helpers'; import { useAlertExceptionActions } from '../../../../detections/components/alerts_table/timeline_actions/use_add_exception_actions'; import { useAlertsActions } from '../../../../detections/components/alerts_table/timeline_actions/use_alerts_actions'; -import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; +import { useInvestigateAlertInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_alert_in_timeline'; import { useEventFilterAction } from '../../../../detections/components/alerts_table/timeline_actions/use_event_filter_action'; import { useResponderActionItem } from '../../../../common/components/endpoint/responder'; import { useHostIsolationAction } from '../../../../common/components/endpoint/host_isolation'; @@ -246,7 +246,7 @@ export const TakeActionDropdown = memo( }); // timeline interaction - const { investigateInTimelineActionItems } = useInvestigateInTimeline({ + const { investigateInTimelineActionItems } = useInvestigateAlertInTimeline({ ecsRowData: dataAsNestedObject, onInvestigateInTimelineAlertClick: closePopoverHandler, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/index.tsx index ba4e0eced4dd3..0b52f726589cc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/index.tsx @@ -16,10 +16,12 @@ import { getDataViewStateFromIndexFields } from '../../common/containers/source/ import { useFetchIndex } from '../../common/containers/source'; import type { State } from '../../common/store/types'; import { sortWithExcludesAtEnd } from '../../../common/utils/sourcerer'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; export const useSourcererDataView = ( scopeId: SourcererScopeName = SourcererScopeName.default ): SelectedDataView => { + const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); const kibanaDataViews = useSelector(sourcererSelectors.kibanaDataViews); const signalIndexName = useSelector(sourcererSelectors.signalIndexName); const defaultDataView = useSelector(sourcererSelectors.defaultDataView); @@ -64,6 +66,9 @@ export const useSourcererDataView = ( ); useEffect(() => { + if (newDataViewPickerEnabled) { + return; + } if (selectedDataView == null || missingPatterns.length > 0) { // old way of fetching indices, legacy timeline setLegacyPatterns(selectedPatterns); @@ -71,7 +76,13 @@ export const useSourcererDataView = ( // Only create a new array reference if legacyPatterns is not empty setLegacyPatterns([]); } - }, [legacyPatterns.length, missingPatterns, selectedDataView, selectedPatterns]); + }, [ + legacyPatterns.length, + missingPatterns, + newDataViewPickerEnabled, + selectedDataView, + selectedPatterns, + ]); const sourcererDataView = useMemo(() => { const _dv = diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index c9acc48051911..360e738f2b862 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -14,7 +14,9 @@ import type { GetFieldTableColumns, } from '@kbn/response-ops-alerts-fields-browser/types'; import type { ColumnHeaderOptions } from '../../../../common/types'; -import { useDataView } from '../../../common/containers/source/use_data_view'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; +import { useDataView } from '../../../data_view_manager/hooks/use_data_view'; +import { useDataView as useDataViewOld } from '../../../common/containers/source/use_data_view'; import { useKibana } from '../../../common/lib/kibana'; import { sourcererSelectors } from '../../../common/store'; import type { State } from '../../../common/store'; @@ -50,10 +52,12 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ removeColumn, upsertColumn, }) => { + const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); const [dataView, setDataView] = useState(null); + const { dataView: experimentalDataView } = useDataView(sourcererScope); const { startTransaction } = useStartTransaction(); - const { indexFieldsSearch } = useDataView(); + const { indexFieldsSearch } = useDataViewOld(); const { dataViewFieldEditor, data: { dataViews }, @@ -61,12 +65,17 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ const missingPatterns = useSelector((state: State) => { return sourcererSelectors.sourcererScopeMissingPatterns(state, sourcererScope); }); - const selectedDataViewId = useSelector((state: State) => { - return sourcererSelectors.sourcererScopeSelectedDataViewId(state, sourcererScope); - }); + const selectedDataViewId = useMemo( + () => (newDataViewPickerEnabled ? experimentalDataView?.id : dataView?.id), + [dataView?.id, experimentalDataView?.id, newDataViewPickerEnabled] + ); useEffect(() => { let ignore = false; const fetchAndSetDataView = async (dataViewId: string) => { + if (newDataViewPickerEnabled) { + if (experimentalDataView) setDataView(experimentalDataView); + return; + } const aDatView = await dataViews.get(dataViewId); if (ignore) return; setDataView(aDatView); @@ -78,7 +87,13 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ return () => { ignore = true; }; - }, [selectedDataViewId, missingPatterns, dataViews]); + }, [ + selectedDataViewId, + missingPatterns, + dataViews, + newDataViewPickerEnabled, + experimentalDataView, + ]); const openFieldEditor = useCallback( async (fieldName) => { @@ -90,7 +105,11 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ startTransaction({ name: FIELD_BROWSER_ACTIONS.FIELD_SAVED }); // Fetch the updated list of fields // Using cleanCache since the number of fields might have not changed, but we need to update the state anyway - await indexFieldsSearch({ dataViewId: selectedDataViewId, cleanCache: true }); + if (newDataViewPickerEnabled) { + await dataViews.clearInstanceCache(selectedDataViewId); + } else { + await indexFieldsSearch({ dataViewId: selectedDataViewId, cleanCache: true }); + } for (const savedField of savedFields) { if (fieldName && fieldName !== savedField.name) { @@ -129,10 +148,12 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ selectedDataViewId, dataViewFieldEditor, editorActionsRef, + startTransaction, + newDataViewPickerEnabled, + dataViews, indexFieldsSearch, - removeColumn, upsertColumn, - startTransaction, + removeColumn, ] ); @@ -145,9 +166,11 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ onDelete: async () => { startTransaction({ name: FIELD_BROWSER_ACTIONS.FIELD_DELETED }); - // Fetch the updated list of fields - await indexFieldsSearch({ dataViewId: selectedDataViewId }); - + if (newDataViewPickerEnabled) { + await dataViews.clearInstanceCache(selectedDataViewId); + } else { + await indexFieldsSearch({ dataViewId: selectedDataViewId, cleanCache: true }); + } removeColumn(fieldName); }, }); @@ -157,9 +180,11 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ dataView, selectedDataViewId, dataViewFieldEditor, - indexFieldsSearch, - removeColumn, startTransaction, + newDataViewPickerEnabled, + removeColumn, + dataViews, + indexFieldsSearch, ] ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 0fdbb03484b54..c8dfb1d965c5c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -58,7 +58,7 @@ import { useStartTransaction } from '../../../common/lib/apm/use_start_transacti import { TIMELINE_ACTIONS } from '../../../common/lib/apm/user_actions'; import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers'; import { timelineDefaults } from '../../store/defaults'; -import { useDataViewSpec } from '../../../data_view_manager/hooks/use_data_view_spec'; +import { useDataView } from '../../../data_view_manager/hooks/use_data_view'; interface OwnProps { /** Displays open timeline in modal */ @@ -164,12 +164,12 @@ export const StatefulOpenTimelineComponent = React.memo( useSourcererDataView(SourcererScopeName.timeline); const { newDataViewPickerEnabled } = useEnableExperimental(); - const { dataViewSpec: experimentalDataViewSpec } = useDataViewSpec(SourcererScopeName.timeline); + const { dataView: experimentalDataView } = useDataView(SourcererScopeName.timeline); const experimentalSelectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); const dataViewId = useMemo( - () => (newDataViewPickerEnabled ? experimentalDataViewSpec?.id || '' : oldDataViewId), - [experimentalDataViewSpec?.id, newDataViewPickerEnabled, oldDataViewId] + () => (newDataViewPickerEnabled ? experimentalDataView?.id || '' : oldDataViewId), + [experimentalDataView?.id, newDataViewPickerEnabled, oldDataViewId] ); const selectedPatterns = useMemo( () => (newDataViewPickerEnabled ? experimentalSelectedPatterns : oldSelectedPatterns), diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx index 1bc33046044bd..0fe042ae55372 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx @@ -8,7 +8,7 @@ import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { isEmpty } from 'lodash/fp'; -import { useEnableExperimental } from '../../../common/hooks/use_experimental_features'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { DataViewManagerScopeName } from '../../../data_view_manager/constants'; import { useSelectDataView } from '../../../data_view_manager/hooks/use_select_data_view'; import type { Note } from '../../../../common/api/timeline'; @@ -40,7 +40,7 @@ import type { UpdateTimeline } from './types'; export const useUpdateTimeline = () => { const dispatch = useDispatch(); const selectDataView = useSelectDataView(); - const { newDataViewPickerEnabled } = useEnableExperimental(); + const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); return useCallback( // NOTE: this is only enabled for the data view picker test diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx index fe037890f126b..176789e8efb85 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx @@ -8,6 +8,7 @@ import type { ComponentProps, ReactElement } from 'react'; import React, { useMemo } from 'react'; import { RootDragDropProvider } from '@kbn/dom-drag-drop'; +import { EuiSkeletonText } from '@elastic/eui'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { useDataView } from '../../../../data_view_manager/hooks/use_data_view'; import { useGetScopedSourcererDataView } from '../../../../sourcerer/components/use_get_sourcerer_data_view'; @@ -48,11 +49,16 @@ export const UnifiedTimelineBody = (props: UnifiedTimelineBodyProps) => { }); const columnsHeader = useMemo(() => columns ?? defaultUdtHeaders, [columns]); - const { dataView: experimentalDataView } = useDataView(SourcererScopeName.timeline); + const { dataView: experimentalDataView, status: dataViewStatus } = useDataView( + SourcererScopeName.timeline + ); const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); const dataView = newDataViewPickerEnabled ? experimentalDataView : oldDataView; + if (dataViewStatus === 'loading') { + return ; + } return ( {header} diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/index.tsx index af02a1b6691a6..4e7251e687af4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -102,7 +102,10 @@ const StatefulTimelineComponent: React.FC = ({ const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); const experimentalSelectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); - const { dataViewSpec: experimentalDataViewSpec } = useDataViewSpec(SourcererScopeName.timeline); + const { dataViewSpec: experimentalDataViewSpec } = useDataViewSpec( + SourcererScopeName.timeline, + false + ); const selectedDataViewId = useMemo( () => diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx index 653c3943efd85..e67657c834b19 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx @@ -96,19 +96,21 @@ export const useCreateTimeline = ({ setTimelineFullScreen(false); } - setSelectedDataView({ - id: dataViewId, - fallbackPatterns: selectedPatterns, - scope: DataViewManagerScopeName.timeline, - }); - - dispatch( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.timeline, - selectedDataViewId: dataViewId, - selectedPatterns, - }) - ); + if (newDataViewPickerEnabled) { + setSelectedDataView({ + id: dataViewId, + fallbackPatterns: selectedPatterns, + scope: DataViewManagerScopeName.timeline, + }); + } else { + dispatch( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: dataViewId, + selectedPatterns, + }) + ); + } dispatch( timelineActions.createTimeline({