diff --git a/packages/kbn-cell-actions/src/__stories__/cell_actions.stories.tsx b/packages/kbn-cell-actions/src/__stories__/cell_actions.stories.tsx index 49f812cd9005a..5dfcdd1065a61 100644 --- a/packages/kbn-cell-actions/src/__stories__/cell_actions.stories.tsx +++ b/packages/kbn-cell-actions/src/__stories__/cell_actions.stories.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { ComponentStory } from '@storybook/react'; +import { FieldSpec } from '@kbn/data-views-plugin/common'; import { CellActionsProvider } from '../context/cell_actions_context'; import { makeAction } from '../mocks/helpers'; import { CellActions } from '../components/cell_actions'; @@ -16,7 +17,13 @@ import type { CellActionsProps } from '../types'; const TRIGGER_ID = 'testTriggerId'; -const FIELD = { name: 'name', value: '123', type: 'text' }; +const VALUE = '123'; +const FIELD: FieldSpec = { + name: 'name', + type: 'text', + searchable: true, + aggregatable: true, +}; const getCompatibleActions = () => Promise.resolve([ @@ -62,24 +69,56 @@ DefaultWithControls.args = { showActionTooltips: true, mode: CellActionsMode.INLINE, triggerId: TRIGGER_ID, - field: FIELD, + data: [ + { + field: FIELD, + value: '', + }, + ], visibleCellActions: 3, }; export const CellActionInline = ({}: {}) => ( - + Field value ); export const CellActionHoverPopoverDown = ({}: {}) => ( - + Hover me ); export const CellActionHoverPopoverRight = ({}: {}) => ( - + Hover me ); diff --git a/packages/kbn-cell-actions/src/actions/copy_to_clipboard/copy_to_clipboard.test.ts b/packages/kbn-cell-actions/src/actions/copy_to_clipboard/copy_to_clipboard.test.ts index 5037e1cd529f5..d636b64379d43 100644 --- a/packages/kbn-cell-actions/src/actions/copy_to_clipboard/copy_to_clipboard.test.ts +++ b/packages/kbn-cell-actions/src/actions/copy_to_clipboard/copy_to_clipboard.test.ts @@ -21,7 +21,12 @@ describe('Default createCopyToClipboardActionFactory', () => { }); const copyToClipboardAction = copyToClipboardActionFactory({ id: 'testAction' }); const context = { - field: { name: 'user.name', value: 'the value', type: 'text' }, + data: [ + { + field: { name: 'user.name', type: 'text' }, + value: 'the value', + }, + ], } as CellActionExecutionContext; beforeEach(() => { @@ -52,7 +57,12 @@ describe('Default createCopyToClipboardActionFactory', () => { it('should escape value', async () => { await copyToClipboardAction.execute({ ...context, - field: { ...context.field, value: 'the "value"' }, + data: [ + { + ...context.data[0], + value: 'the "value"', + }, + ], }); expect(mockCopy).toHaveBeenCalledWith('user.name: "the \\"value\\""'); expect(mockSuccessToast).toHaveBeenCalled(); @@ -61,7 +71,12 @@ describe('Default createCopyToClipboardActionFactory', () => { it('should suport multiple values', async () => { await copyToClipboardAction.execute({ ...context, - field: { ...context.field, value: ['the "value"', 'another value', 'last value'] }, + data: [ + { + ...context.data[0], + value: ['the "value"', 'another value', 'last value'], + }, + ], }); expect(mockCopy).toHaveBeenCalledWith( 'user.name: "the \\"value\\"" AND "another value" AND "last value"' diff --git a/packages/kbn-cell-actions/src/actions/copy_to_clipboard/copy_to_clipboard.ts b/packages/kbn-cell-actions/src/actions/copy_to_clipboard/copy_to_clipboard.ts index cafbd29d91455..dd945e986ddbc 100644 --- a/packages/kbn-cell-actions/src/actions/copy_to_clipboard/copy_to_clipboard.ts +++ b/packages/kbn-cell-actions/src/actions/copy_to_clipboard/copy_to_clipboard.ts @@ -31,13 +31,23 @@ export const createCopyToClipboardActionFactory = createCellActionFactory( getIconType: () => ICON, getDisplayName: () => COPY_TO_CLIPBOARD, getDisplayNameTooltip: () => COPY_TO_CLIPBOARD, - isCompatible: async ({ field }) => field.name != null, - execute: async ({ field }) => { + isCompatible: async ({ data }) => { + const field = data[0]?.field; + + return ( + data.length === 1 && // TODO Add support for multiple values + field.name != null + ); + }, + execute: async ({ data }) => { + const field = data[0]?.field; + const value = data[0]?.value; + let textValue: undefined | string; - if (field.value != null) { - textValue = Array.isArray(field.value) - ? field.value.map((value) => `"${escapeValue(value)}"`).join(' AND ') - : `"${escapeValue(field.value)}"`; + if (value != null) { + textValue = Array.isArray(value) + ? value.map((v) => `"${escapeValue(v)}"`).join(' AND ') + : `"${escapeValue(value)}"`; } const text = textValue ? `${field.name}: ${textValue}` : field.name; const isSuccess = copy(text, { debug: true }); diff --git a/packages/kbn-cell-actions/src/actions/filter/filter_in.test.ts b/packages/kbn-cell-actions/src/actions/filter/filter_in.test.ts index f75447fe7458b..546881072aed9 100644 --- a/packages/kbn-cell-actions/src/actions/filter/filter_in.test.ts +++ b/packages/kbn-cell-actions/src/actions/filter/filter_in.test.ts @@ -26,7 +26,12 @@ describe('createFilterInActionFactory', () => { }); const filterInAction = filterInActionFactory({ id: 'testAction' }); const context = makeActionContext({ - field: { name: fieldName, value, type: 'text' }, + data: [ + { + field: { name: fieldName, type: 'text', searchable: true, aggregatable: true }, + value, + }, + ], }); beforeEach(() => { @@ -50,7 +55,11 @@ describe('createFilterInActionFactory', () => { expect( await filterInAction.isCompatible({ ...context, - field: { ...context.field, name: '' }, + data: [ + { + field: { ...context.data[0].field, name: '' }, + }, + ], }) ).toEqual(false); }); @@ -74,7 +83,12 @@ describe('createFilterInActionFactory', () => { it('should create filter query with array value', async () => { await filterInAction.execute({ ...context, - field: { ...context.field, value: [value] }, + data: [ + { + ...context.data[0], + value: [value], + }, + ], }); expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, @@ -86,7 +100,12 @@ describe('createFilterInActionFactory', () => { it('should create negate filter query with null value', async () => { await filterInAction.execute({ ...context, - field: { ...context.field, value: null }, + data: [ + { + ...context.data[0], + value: null, + }, + ], }); expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: null, negate: true }); }); @@ -94,7 +113,12 @@ describe('createFilterInActionFactory', () => { it('should create negate filter query with undefined value', async () => { await filterInAction.execute({ ...context, - field: { ...context.field, value: undefined }, + data: [ + { + ...context.data[0], + value: undefined, + }, + ], }); expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, @@ -106,7 +130,12 @@ describe('createFilterInActionFactory', () => { it('should create negate filter query with empty string value', async () => { await filterInAction.execute({ ...context, - field: { ...context.field, value: '' }, + data: [ + { + ...context.data[0], + value: '', + }, + ], }); expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: '', negate: true }); }); @@ -114,7 +143,12 @@ describe('createFilterInActionFactory', () => { it('should create negate filter query with empty array value', async () => { await filterInAction.execute({ ...context, - field: { ...context.field, value: [] }, + data: [ + { + ...context.data[0], + value: [], + }, + ], }); expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: [], negate: true }); }); diff --git a/packages/kbn-cell-actions/src/actions/filter/filter_in.ts b/packages/kbn-cell-actions/src/actions/filter/filter_in.ts index 524318d9722d1..b113986425fba 100644 --- a/packages/kbn-cell-actions/src/actions/filter/filter_in.ts +++ b/packages/kbn-cell-actions/src/actions/filter/filter_in.ts @@ -22,9 +22,18 @@ export const createFilterInActionFactory = createCellActionFactory( getIconType: () => ICON, getDisplayName: () => FILTER_IN, getDisplayNameTooltip: () => FILTER_IN, - isCompatible: async ({ field }) => !!field.name, - execute: async ({ field }) => { - addFilterIn({ filterManager, fieldName: field.name, value: field.value }); + isCompatible: async ({ data }) => { + const field = data[0]?.field; + + return ( + data.length === 1 && // TODO Add support for multiple values + !!field.name + ); + }, + execute: async ({ data }) => { + const field = data[0]?.field; + const value = data[0]?.value; + addFilterIn({ filterManager, fieldName: field.name, value }); }, }) ); diff --git a/packages/kbn-cell-actions/src/actions/filter/filter_out.test.ts b/packages/kbn-cell-actions/src/actions/filter/filter_out.test.ts index 72564d64fc8bf..d42b8bc7a26a4 100644 --- a/packages/kbn-cell-actions/src/actions/filter/filter_out.test.ts +++ b/packages/kbn-cell-actions/src/actions/filter/filter_out.test.ts @@ -24,7 +24,12 @@ describe('createFilterOutAction', () => { const filterOutActionFactory = createFilterOutActionFactory({ filterManager: mockFilterManager }); const filterOutAction = filterOutActionFactory({ id: 'testAction' }); const context = makeActionContext({ - field: { name: fieldName, value, type: 'text' }, + data: [ + { + field: { name: fieldName, type: 'text', searchable: true, aggregatable: true }, + value, + }, + ], }); beforeEach(() => { @@ -48,7 +53,11 @@ describe('createFilterOutAction', () => { expect( await filterOutAction.isCompatible({ ...context, - field: { ...context.field, name: '' }, + data: [ + { + field: { ...context.data[0].field, name: '' }, + }, + ], }) ).toEqual(false); }); @@ -68,7 +77,12 @@ describe('createFilterOutAction', () => { it('should create negate filter query with array value', async () => { await filterOutAction.execute({ ...context, - field: { ...context.field, value: [value] }, + data: [ + { + field: { ...context.data[0].field }, + value: [value], + }, + ], }); expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, @@ -80,7 +94,12 @@ describe('createFilterOutAction', () => { it('should create filter query with null value', async () => { await filterOutAction.execute({ ...context, - field: { ...context.field, value: null }, + data: [ + { + field: { ...context.data[0].field }, + value: null, + }, + ], }); expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: null, negate: false }); }); @@ -88,7 +107,12 @@ describe('createFilterOutAction', () => { it('should create filter query with undefined value', async () => { await filterOutAction.execute({ ...context, - field: { ...context.field, value: undefined }, + data: [ + { + field: { ...context.data[0].field }, + value: undefined, + }, + ], }); expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, @@ -100,7 +124,12 @@ describe('createFilterOutAction', () => { it('should create negate filter query with empty string value', async () => { await filterOutAction.execute({ ...context, - field: { ...context.field, value: '' }, + data: [ + { + field: { ...context.data[0].field }, + value: '', + }, + ], }); expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: '', negate: false }); }); @@ -108,7 +137,12 @@ describe('createFilterOutAction', () => { it('should create negate filter query with empty array value', async () => { await filterOutAction.execute({ ...context, - field: { ...context.field, value: [] }, + data: [ + { + field: { ...context.data[0].field }, + value: [], + }, + ], }); expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: [], negate: false }); }); diff --git a/packages/kbn-cell-actions/src/actions/filter/filter_out.ts b/packages/kbn-cell-actions/src/actions/filter/filter_out.ts index bbb1b7ee70a61..99f737daa1bac 100644 --- a/packages/kbn-cell-actions/src/actions/filter/filter_out.ts +++ b/packages/kbn-cell-actions/src/actions/filter/filter_out.ts @@ -22,12 +22,22 @@ export const createFilterOutActionFactory = createCellActionFactory( getIconType: () => ICON, getDisplayName: () => FILTER_OUT, getDisplayNameTooltip: () => FILTER_OUT, - isCompatible: async ({ field }) => !!field.name, - execute: async ({ field }) => { + isCompatible: async ({ data }) => { + const field = data[0]?.field; + + return ( + data.length === 1 && // TODO Add support for multiple values + !!field.name + ); + }, + execute: async ({ data }) => { + const field = data[0]?.field; + const value = data[0]?.value; + addFilterOut({ filterManager, fieldName: field.name, - value: field.value, + value, }); }, }) diff --git a/packages/kbn-cell-actions/src/components/cell_actions.test.tsx b/packages/kbn-cell-actions/src/components/cell_actions.test.tsx index 36d0482ebf84d..7f2eb2505b8a2 100644 --- a/packages/kbn-cell-actions/src/components/cell_actions.test.tsx +++ b/packages/kbn-cell-actions/src/components/cell_actions.test.tsx @@ -11,9 +11,20 @@ import React from 'react'; import { CellActions } from './cell_actions'; import { CellActionsMode } from '../constants'; import { CellActionsProvider } from '../context/cell_actions_context'; +import type { FieldSpec } from '@kbn/data-views-plugin/common'; const TRIGGER_ID = 'test-trigger-id'; -const FIELD = { name: 'name', value: '123', type: 'text' }; +const VALUE = '123'; +const FIELD: FieldSpec = { + name: 'name', + type: 'text', + searchable: true, + aggregatable: true, +}; +const DATA = { + field: FIELD, + value: VALUE, +}; jest.mock('./hover_actions_popover', () => ({ HoverActionsPopover: jest.fn((props) => ( @@ -27,7 +38,7 @@ describe('CellActions', () => { const { queryByTestId } = render( - + Field value @@ -46,7 +57,7 @@ describe('CellActions', () => { const { queryByTestId } = render( - + Field value @@ -65,7 +76,7 @@ describe('CellActions', () => { const { getByTestId } = render( - + Field value @@ -85,7 +96,7 @@ describe('CellActions', () => { const { getByTestId } = render( - + Field value diff --git a/packages/kbn-cell-actions/src/components/cell_actions.tsx b/packages/kbn-cell-actions/src/components/cell_actions.tsx index df6f957575c20..cf9d7d27a68e5 100644 --- a/packages/kbn-cell-actions/src/components/cell_actions.tsx +++ b/packages/kbn-cell-actions/src/components/cell_actions.tsx @@ -8,13 +8,14 @@ import React, { useMemo, useRef } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { isArray } from 'lodash/fp'; import { InlineActions } from './inline_actions'; import { HoverActionsPopover } from './hover_actions_popover'; import { CellActionsMode } from '../constants'; import type { CellActionsProps, CellActionExecutionContext } from '../types'; export const CellActions: React.FC = ({ - field, + data, triggerId, children, mode, @@ -26,14 +27,16 @@ export const CellActions: React.FC = ({ }) => { const nodeRef = useRef(null); + const dataArray = useMemo(() => (isArray(data) ? data : [data]), [data]); + const actionContext: CellActionExecutionContext = useMemo( () => ({ - field, + data: dataArray, trigger: { id: triggerId }, nodeRef, metadata, }), - [field, triggerId, metadata] + [dataArray, triggerId, metadata] ); const anchorPosition = useMemo( @@ -41,7 +44,10 @@ export const CellActions: React.FC = ({ [mode] ); - const dataTestSubj = `cellActions-renderContent-${field.name}`; + const dataTestSubj = `cellActions-renderContent-${dataArray + .map(({ field }) => field.name) + .join('-')}`; + if (mode === CellActionsMode.HOVER_DOWN || mode === CellActionsMode.HOVER_RIGHT) { return (
diff --git a/packages/kbn-cell-actions/src/components/extra_actions_popover.tsx b/packages/kbn-cell-actions/src/components/extra_actions_popover.tsx index b210fa5bfbdc8..b0fdf416fae28 100644 --- a/packages/kbn-cell-actions/src/components/extra_actions_popover.tsx +++ b/packages/kbn-cell-actions/src/components/extra_actions_popover.tsx @@ -128,10 +128,15 @@ const ExtraActionsPopOverContent: React.FC = ({ )), [actionContext, actions, closePopOver] ); + return ( <> -

{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(actionContext.field.name)}

+

+ {YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS( + actionContext.data.map(({ field }) => field.name).join(', ') + )} +

diff --git a/packages/kbn-cell-actions/src/components/hover_actions_popover.test.tsx b/packages/kbn-cell-actions/src/components/hover_actions_popover.test.tsx index de68ccd6fca51..de524edfac9fe 100644 --- a/packages/kbn-cell-actions/src/components/hover_actions_popover.test.tsx +++ b/packages/kbn-cell-actions/src/components/hover_actions_popover.test.tsx @@ -19,7 +19,11 @@ const defaultProps = { visibleCellActions: 4, actionContext: { trigger: { id: 'triggerId' }, - field: { name: 'fieldName' }, + data: [ + { + field: { name: 'fieldName' }, + }, + ], } as CellActionExecutionContext, showActionTooltips: false, }; diff --git a/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx b/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx index 0487c27b06e52..8ba273f98c6a1 100644 --- a/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx +++ b/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx @@ -141,7 +141,11 @@ export const HoverActionsPopover: React.FC = ({ {showHoverContent && (
-

{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(actionContext.field.name)}

+

+ {YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS( + actionContext.data.map(({ field }) => field.name).join(', ') + )} +

{visibleActions.map((action) => ( actions); jest.mock('../context/cell_actions_context', () => ({ useCellActionsContext: () => ({ getActions: mockGetActions }), })); +const values1 = ['0.0', '0.1', '0.2', '0.3']; +const field1 = { + name: 'column1', + type: 'string', + searchable: true, + aggregatable: true, +}; + +const values2 = ['1.0', '1.1', '1.2', '1.3']; +const field2 = { + name: 'column2', -const field1 = { name: 'column1', values: ['0.0', '0.1', '0.2', '0.3'], type: 'text' }; -const field2 = { name: 'column2', values: ['1.0', '1.1', '1.2', '1.3'], type: 'keyword' }; + type: 'string', + searchable: true, + aggregatable: true, +}; const columns = [{ id: field1.name }, { id: field2.name }]; const mockCloseCellPopover = jest.fn(); const useDataGridColumnsCellActionsProps: UseDataGridColumnsCellActionsProps = { - fields: [field1, field2], + data: [ + { field: field1, values: values1 }, + { field: field2, values: values2 }, + ], triggerId: 'testTriggerId', metadata: { some: 'value' }, dataGridRef: { @@ -138,7 +154,17 @@ describe('useDataGridColumnsCellActions', () => { await waitFor(() => { expect(action1.execute).toHaveBeenCalledWith( expect.objectContaining({ - field: { name: field1.name, type: field1.type, value: field1.values[1] }, + data: [ + { + value: values1[1], + field: { + name: field1.name, + type: field1.type, + aggregatable: true, + searchable: true, + }, + }, + ], trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, }) ); @@ -151,7 +177,17 @@ describe('useDataGridColumnsCellActions', () => { await waitFor(() => { expect(action2.execute).toHaveBeenCalledWith( expect.objectContaining({ - field: { name: field2.name, type: field2.type, value: field2.values[2] }, + data: [ + { + value: values2[2], + field: { + name: field2.name, + type: field2.type, + aggregatable: true, + searchable: true, + }, + }, + ], trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, }) ); @@ -171,7 +207,17 @@ describe('useDataGridColumnsCellActions', () => { await waitFor(() => { expect(action1.execute).toHaveBeenCalledWith( expect.objectContaining({ - field: { name: field1.name, type: field1.type, value: field1.values[1] }, + data: [ + { + value: values1[1], + field: { + name: field1.name, + type: field1.type, + aggregatable: true, + searchable: true, + }, + }, + ], }) ); }); @@ -196,7 +242,7 @@ describe('useDataGridColumnsCellActions', () => { const { result, waitForNextUpdate } = renderHook(useDataGridColumnsCellActions, { initialProps: { ...useDataGridColumnsCellActionsProps, - fields: [], + data: [], }, }); @@ -210,7 +256,7 @@ describe('useDataGridColumnsCellActions', () => { const { result, waitForNextUpdate } = renderHook(useDataGridColumnsCellActions, { initialProps: { ...useDataGridColumnsCellActionsProps, - fields: undefined, + data: undefined, }, }); diff --git a/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.tsx b/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.tsx index 2913004218032..3a0de0cc90eb1 100644 --- a/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.tsx +++ b/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.tsx @@ -16,12 +16,12 @@ import type { CellAction, CellActionCompatibilityContext, CellActionExecutionContext, - CellActionField, + CellActionsData, CellActionsProps, } from '../types'; import { useBulkLoadActions } from './use_load_actions'; -interface BulkField extends Pick { +interface BulkData extends Omit { /** * Array containing all the values of the field in the visible page, indexed by rowIndex */ @@ -30,7 +30,7 @@ interface BulkField extends Pick { export interface UseDataGridColumnsCellActionsProps extends Pick { - fields?: BulkField[]; + data?: BulkData[]; dataGridRef: MutableRefObject; } export type UseDataGridColumnsCellActions< @@ -38,7 +38,7 @@ export type UseDataGridColumnsCellActions< > = (props: P) => EuiDataGridColumnCellAction[][]; export const useDataGridColumnsCellActions: UseDataGridColumnsCellActions = ({ - fields, + data, triggerId, metadata, dataGridRef, @@ -46,12 +46,12 @@ export const useDataGridColumnsCellActions: UseDataGridColumnsCellActions = ({ }) => { const bulkContexts: CellActionCompatibilityContext[] = useMemo( () => - fields?.map(({ values, ...field }) => ({ - field, // we are getting the actions for the whole column field, so the compatibility check will be done without the value + data?.map(({ field }) => ({ + data: [{ field }], // we are getting the actions for the whole column field, so the compatibility check will be done without the value trigger: { id: triggerId }, metadata, })) ?? [], - [fields, triggerId, metadata] + [triggerId, metadata, data] ); const { loading, value: columnsActions } = useBulkLoadActions(bulkContexts, { @@ -61,37 +61,44 @@ export const useDataGridColumnsCellActions: UseDataGridColumnsCellActions = ({ const columnsCellActions = useMemo(() => { if (loading) { return ( - fields?.map(() => [ + data?.map(() => [ () => , ]) ?? [] ); } - if (!columnsActions || !fields || fields.length === 0) { + if (!columnsActions || !data || data.length === 0) { return []; } + + // Check for a temporary inconsistency because `useBulkLoadActions` takes one render loop before setting `loading` to true. + // It will eventually update to a consistent state + if (columnsActions.length !== data.length) { + return []; + } + return columnsActions.map((actions, columnIndex) => actions.map((action) => createColumnCellAction({ action, metadata, triggerId, - field: fields[columnIndex], + data: data[columnIndex], dataGridRef, }) ) ); - }, [columnsActions, fields, loading, metadata, triggerId, dataGridRef]); + }, [loading, columnsActions, data, metadata, triggerId, dataGridRef]); return columnsCellActions; }; interface CreateColumnCellActionParams extends Pick { - field: BulkField; + data: BulkData; action: CellAction; } const createColumnCellAction = ({ - field, + data: { field, values }, action, metadata, triggerId, @@ -102,11 +109,15 @@ const createColumnCellAction = ({ const buttonRef = useRef(null); const actionContext: CellActionExecutionContext = useMemo(() => { - const { name, type, values } = field; // rowIndex refers to all pages, we need to use the row index relative to the page to get the value const value = values[rowIndex % values.length]; return { - field: { name, type, value }, + data: [ + { + field, + value, + }, + ], trigger: { id: triggerId }, nodeRef, metadata, diff --git a/packages/kbn-cell-actions/src/mocks/helpers.ts b/packages/kbn-cell-actions/src/mocks/helpers.ts index 46fd53448f512..091f671acfc2d 100644 --- a/packages/kbn-cell-actions/src/mocks/helpers.ts +++ b/packages/kbn-cell-actions/src/mocks/helpers.ts @@ -27,11 +27,17 @@ export const makeActionContext = ( override: Partial = {} ): CellActionExecutionContext => ({ trigger: { id: 'triggerId' }, - field: { - name: 'fieldName', - type: 'keyword', - value: 'some value', - }, + data: [ + { + field: { + name: 'fieldName', + type: 'keyword', + searchable: true, + aggregatable: true, + }, + value: 'some value', + }, + ], nodeRef: {} as MutableRefObject, metadata: undefined, ...override, diff --git a/packages/kbn-cell-actions/src/types.ts b/packages/kbn-cell-actions/src/types.ts index 1efc4239514f7..a23ff76549017 100644 --- a/packages/kbn-cell-actions/src/types.ts +++ b/packages/kbn-cell-actions/src/types.ts @@ -10,6 +10,7 @@ import type { ActionExecutionContext, UiActionsService, } from '@kbn/ui-actions-plugin/public'; +import type { FieldSpec } from '@kbn/data-views-plugin/common'; import type { CellActionsMode } from './constants'; export interface CellActionsProviderProps { @@ -20,39 +21,25 @@ export interface CellActionsProviderProps { getTriggerCompatibleActions: UiActionsService['getTriggerCompatibleActions']; } -export interface CellActionField { - /** - * Field name. - * Example: 'host.name' - */ - name: string; - /** - * Field type. - * Example: 'keyword' - */ - type: string; +type Metadata = Record; + +export type CellActionFieldValue = string | string[] | null | undefined; + +export interface CellActionsData { /** - * Field value. - * Example: 'My-Laptop' + * The field specification */ - value: string | string[] | null | undefined; + field: FieldSpec; + /** - * When true the field supports aggregations. - * - * It defaults to false. - * - * You can verify if a field is aggregatable on kibana/management/kibana/dataViews. + * Common set of properties used by most actions. */ - aggregatable?: boolean; + value: CellActionFieldValue; } -type Metadata = Record; - export interface CellActionsProps { - /** - * Common set of properties used by most actions. - */ - field: CellActionField; + data: CellActionsData | CellActionsData[]; + /** * The trigger in which the actions are registered. */ @@ -89,7 +76,8 @@ export interface CellActionsProps { } export interface CellActionExecutionContext extends ActionExecutionContext { - field: CellActionField; + data: CellActionsData[]; + /** * Ref to the node where the cell action are rendered. */ @@ -104,13 +92,15 @@ export interface CellActionExecutionContext extends ActionExecutionContext { * Subset of `CellActionExecutionContext` used only for the compatibility check in the `isCompatible` function. * It omits the references and the `field.value`. */ + export interface CellActionCompatibilityContext< C extends CellActionExecutionContext = CellActionExecutionContext > extends ActionExecutionContext { /** - * The object containing the field name and type, needed for the compatibility check + * CellActionsData containing the field spec but not the value for the compatibility check */ - field: Omit; + data: Array>; + /** * Extra configurations for actions. */ diff --git a/packages/kbn-cell-actions/tsconfig.json b/packages/kbn-cell-actions/tsconfig.json index cc40d4c80f3b0..b2d41295e44e6 100644 --- a/packages/kbn-cell-actions/tsconfig.json +++ b/packages/kbn-cell-actions/tsconfig.json @@ -19,6 +19,7 @@ "@kbn/data-plugin", "@kbn/es-query", "@kbn/ui-actions-plugin", + "@kbn/data-views-plugin", ], "exclude": ["target/**/*"] } diff --git a/x-pack/packages/security-solution/data_table/common/types/header_actions/index.ts b/x-pack/packages/security-solution/data_table/common/types/header_actions/index.ts index f8dea6bf3612b..0b13d3bf3baba 100644 --- a/x-pack/packages/security-solution/data_table/common/types/header_actions/index.ts +++ b/x-pack/packages/security-solution/data_table/common/types/header_actions/index.ts @@ -36,6 +36,7 @@ export type ColumnHeaderOptions = Pick< | 'isResizable' > & { aggregatable?: boolean; + searchable?: boolean; category?: string; columnHeaderType: ColumnHeaderType; description?: string | null; diff --git a/x-pack/packages/security-solution/data_table/components/data_table/index.test.tsx b/x-pack/packages/security-solution/data_table/components/data_table/index.test.tsx index e44798c3ffa97..64140f71a62ea 100644 --- a/x-pack/packages/security-solution/data_table/components/data_table/index.test.tsx +++ b/x-pack/packages/security-solution/data_table/components/data_table/index.test.tsx @@ -171,14 +171,20 @@ describe('DataTable', () => { expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith({ triggerId: 'mockCellActionsTrigger', - fields: [ + data: [ { - name: '@timestamp', values: [data[0]?.data[0]?.value], - type: 'date', - aggregatable: true, + field: { + name: '@timestamp', + type: 'date', + aggregatable: true, + esTypes: ['date'], + searchable: true, + subType: undefined, + }, }, ], + metadata: { scopeId: 'table-test', }, @@ -196,7 +202,7 @@ describe('DataTable', () => { expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith( expect.objectContaining({ - fields: [], + data: [], }) ); }); diff --git a/x-pack/packages/security-solution/data_table/components/data_table/index.tsx b/x-pack/packages/security-solution/data_table/components/data_table/index.tsx index 8078a4f3cb489..7f21e8e3bca6a 100644 --- a/x-pack/packages/security-solution/data_table/components/data_table/index.tsx +++ b/x-pack/packages/security-solution/data_table/components/data_table/index.tsx @@ -328,21 +328,27 @@ export const DataTableComponent = React.memo( ); const columnsCellActionsProps = useMemo(() => { - const fields = !cellActionsTriggerId + const columnsCellActionData = !cellActionsTriggerId ? [] : columnHeaders.map((column) => ({ - name: column.id, - type: column.type ?? 'keyword', + // TODO use FieldSpec object instead of column + field: { + name: column.id, + type: column.type ?? 'keyword', + aggregatable: column.aggregatable ?? false, + searchable: column.searchable ?? false, + esTypes: column.esTypes ?? [], + subType: column.subType, + }, values: data.map( ({ data: columnData }) => columnData.find((rowData) => rowData.field === column.id)?.value ), - aggregatable: column.aggregatable, })); return { triggerId: cellActionsTriggerId || '', - fields, + data: columnsCellActionData, metadata: { scopeId: id, }, diff --git a/x-pack/packages/security-solution/data_table/mock/header.ts b/x-pack/packages/security-solution/data_table/mock/header.ts index ce7ac9a08d031..4edc3452500da 100644 --- a/x-pack/packages/security-solution/data_table/mock/header.ts +++ b/x-pack/packages/security-solution/data_table/mock/header.ts @@ -23,6 +23,7 @@ export const defaultHeaders: ColumnHeaderOptions[] = [ type: 'date', esTypes: ['date'], aggregatable: true, + searchable: true, initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, }, { diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_timeline.test.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_timeline.test.ts index 5f9cb8b29b506..72777a61b021d 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_timeline.test.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_timeline.test.ts @@ -25,7 +25,12 @@ const store = { const value = 'the-value'; const context = { - field: { name: 'user.name', value, type: 'text' }, + data: [ + { + field: { name: 'user.name', type: 'text' }, + value, + }, + ], } as CellActionExecutionContext; const defaultDataProvider = { @@ -74,7 +79,12 @@ describe('createAddToTimelineCellAction', () => { expect( await addToTimelineAction.isCompatible({ ...context, - field: { ...context.field, name: 'signal.reason' }, + data: [ + { + ...context.data[0], + field: { ...context.data[0].field, name: 'signal.reason' }, + }, + ], }) ).toEqual(false); }); @@ -89,7 +99,7 @@ describe('createAddToTimelineCellAction', () => { it('should execute with number value', async () => { await addToTimelineAction.execute({ - field: { name: 'process.parent.pid', value: 12345, type: 'number' }, + data: [{ field: { name: 'process.parent.pid', type: 'number' }, value: 12345 }], } as unknown as CellActionExecutionContext); // TODO: remove `as unknown` when number value type is supported expect(mockDispatch).toHaveBeenCalledWith( set( @@ -112,8 +122,8 @@ describe('createAddToTimelineCellAction', () => { it('should execute with null value', async () => { await addToTimelineAction.execute({ - field: { name: 'user.name', value: null, type: 'text' }, - } as CellActionExecutionContext); + data: [{ field: { name: 'user.name', type: 'text' }, value: null }], + } as unknown as CellActionExecutionContext); expect(mockDispatch).toHaveBeenCalledWith( set( 'payload.providers[0]', @@ -137,8 +147,8 @@ describe('createAddToTimelineCellAction', () => { const value2 = 'value2'; const value3 = 'value3'; await addToTimelineAction.execute({ - field: { name: 'user.name', value: [value, value2, value3], type: 'text' }, - } as CellActionExecutionContext); + data: [{ field: { name: 'user.name', type: 'text' }, value: [value, value2, value3] }], + } as unknown as CellActionExecutionContext); expect(mockDispatch).toHaveBeenCalledWith( set( 'payload.providers[0]', @@ -166,10 +176,15 @@ describe('createAddToTimelineCellAction', () => { it('should show warning if no provider added', async () => { await addToTimelineAction.execute({ ...context, - field: { - ...context.field, - type: GEO_FIELD_TYPE, - }, + data: [ + { + ...context.data[0], + field: { + ...context.data[0].field, + type: GEO_FIELD_TYPE, + }, + }, + ], }); expect(mockDispatch).not.toHaveBeenCalled(); expect(mockWarningToast).toHaveBeenCalled(); diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_timeline.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_timeline.ts index 961825d9bacd9..b46ceeb5dd313 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_timeline.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_timeline.ts @@ -38,9 +38,19 @@ export const createAddToTimelineCellActionFactory = createCellActionFactory( getIconType: () => ADD_TO_TIMELINE_ICON, getDisplayName: () => ADD_TO_TIMELINE, getDisplayNameTooltip: () => ADD_TO_TIMELINE, - isCompatible: async ({ field }) => - fieldHasCellActions(field.name) && isValidDataProviderField(field.name, field.type), - execute: async ({ field: { value, type, name }, metadata }) => { + isCompatible: async ({ data }) => { + const field = data[0]?.field; + + return ( + data.length === 1 && // TODO Add support for multiple values + fieldHasCellActions(field.name) && + isValidDataProviderField(field.name, field.type) + ); + }, + execute: async ({ data, metadata }) => { + const { name, type } = data[0]?.field; + const value = data[0]?.value; + const values = Array.isArray(value) ? value : [value]; const [firstValue, ...andValues] = values; const [dataProvider] = diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.test.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.test.ts index 74b59b74ce5a5..97edad91e5a01 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.test.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.test.ts @@ -25,7 +25,12 @@ const store = { const value = 'the-value'; const context = { - field: { name: 'user.name', value, type: 'text' }, + data: [ + { + field: { name: 'user.name', type: 'text' }, + value, + }, + ], } as CellActionExecutionContext; const defaultAddProviderAction = { @@ -77,7 +82,12 @@ describe('createAddToNewTimelineCellAction', () => { expect( await addToTimelineAction.isCompatible({ ...context, - field: { ...context.field, name: 'signal.reason' }, + data: [ + { + ...context.data[0], + field: { ...context.data[0].field, name: 'signal.reason' }, + }, + ], }) ).toEqual(false); }); @@ -93,10 +103,12 @@ describe('createAddToNewTimelineCellAction', () => { it('should show warning if no provider added', async () => { await addToTimelineAction.execute({ ...context, - field: { - ...context.field, - type: GEO_FIELD_TYPE, - }, + data: [ + { + ...context.data[0], + field: { ...context.data[0].field, type: GEO_FIELD_TYPE }, + }, + ], }); expect(mockDispatch).not.toHaveBeenCalled(); expect(mockWarningToast).toHaveBeenCalled(); diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.ts index e1be2a374a6b3..b24c0c39ce365 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.ts @@ -38,14 +38,24 @@ export const createInvestigateInNewTimelineCellActionFactory = createCellActionF getIconType: () => ADD_TO_TIMELINE_ICON, getDisplayName: () => INVESTIGATE_IN_TIMELINE, getDisplayNameTooltip: () => INVESTIGATE_IN_TIMELINE, - isCompatible: async ({ field }) => - fieldHasCellActions(field.name) && isValidDataProviderField(field.name, field.type), - execute: async ({ field, metadata }) => { + isCompatible: async ({ data }) => { + const field = data[0]?.field; + + return ( + data.length === 1 && // TODO Add support for multiple values + fieldHasCellActions(field.name) && + isValidDataProviderField(field.name, field.type) + ); + }, + execute: async ({ data, metadata }) => { + const field = data[0]?.field; + const value = data[0]?.value; + const dataProviders = createDataProviders({ contextId: TimelineId.active, fieldType: field.type, - values: field.value, + values: value, field: field.name, negate: metadata?.negateFilters === true, }) ?? []; diff --git a/x-pack/plugins/security_solution/public/actions/copy_to_clipboard/cell_action/copy_to_clipboard.ts b/x-pack/plugins/security_solution/public/actions/copy_to_clipboard/cell_action/copy_to_clipboard.ts index 90ab55a5b0fd0..39d39ea8b4910 100644 --- a/x-pack/plugins/security_solution/public/actions/copy_to_clipboard/cell_action/copy_to_clipboard.ts +++ b/x-pack/plugins/security_solution/public/actions/copy_to_clipboard/cell_action/copy_to_clipboard.ts @@ -22,6 +22,13 @@ export const createCopyToClipboardCellActionFactory = ({ }); return genericCopyToClipboardActionFactory.combine({ type: SecurityCellActionType.COPY, - isCompatible: async ({ field }) => fieldHasCellActions(field.name), + isCompatible: async ({ data }) => { + const field = data[0]?.field; + + return ( + data.length === 1 && // TODO Add support for multiple values + fieldHasCellActions(field.name) + ); + }, }); }; diff --git a/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_in.test.ts b/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_in.test.ts index 9cb5855db5f99..61ba26cfd5086 100644 --- a/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_in.test.ts +++ b/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_in.test.ts @@ -54,7 +54,12 @@ describe('createFilterInCellActionFactory', () => { }); const context = { - field: { name: 'user.name', value: 'the value', type: 'text' }, + data: [ + { + field: { name: 'user.name', type: 'text' }, + value: 'the value', + }, + ], } as SecurityCellActionExecutionContext; it('should return display name', () => { @@ -73,7 +78,11 @@ describe('createFilterInCellActionFactory', () => { expect( await filterInAction.isCompatible({ ...context, - field: { ...context.field, name: 'signal.reason' }, + data: [ + { + field: { ...context.data[0].field, name: 'signal.reason' }, + }, + ], }) ).toEqual(false); }); diff --git a/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_in.ts b/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_in.ts index 7b206b1f41cd1..6285d662fff93 100644 --- a/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_in.ts +++ b/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_in.ts @@ -29,8 +29,20 @@ export const createFilterInCellActionFactory = ({ return genericFilterInActionFactory.combine({ type: SecurityCellActionType.FILTER, - isCompatible: async ({ field }) => fieldHasCellActions(field.name), - execute: async ({ field, metadata }) => { + isCompatible: async ({ data }) => { + const field = data[0]?.field; + + return ( + data.length === 1 && // TODO Add support for multiple values + fieldHasCellActions(field.name) + ); + }, + execute: async ({ data, metadata }) => { + const field = data[0]?.field; + const value = data[0]?.value; + + if (!field) return; + // if negateFilters is true we have to perform the opposite operation, we can just execute filterOut with the same params const addFilter = metadata?.negateFilters === true ? addFilterOut : addFilterIn; @@ -43,13 +55,13 @@ export const createFilterInCellActionFactory = ({ addFilter({ filterManager: timelineFilterManager, fieldName: field.name, - value: field.value, + value, }); } else { addFilter({ filterManager, fieldName: field.name, - value: field.value, + value, }); } }, diff --git a/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_out.test.ts b/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_out.test.ts index 6a6c3e107fd5a..09f7c9ffbecde 100644 --- a/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_out.test.ts +++ b/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_out.test.ts @@ -48,7 +48,12 @@ describe('createFilterOutCellActionFactory', () => { }); const context = { - field: { name: 'user.name', value: 'the value', type: 'text' }, + data: [ + { + field: { name: 'user.name', type: 'text' }, + value: 'the value', + }, + ], } as SecurityCellActionExecutionContext; it('should return display name', () => { @@ -67,7 +72,11 @@ describe('createFilterOutCellActionFactory', () => { expect( await filterOutAction.isCompatible({ ...context, - field: { ...context.field, name: 'signal.reason' }, + data: [ + { + field: { ...context.data[0].field, name: 'signal.reason' }, + }, + ], }) ).toEqual(false); }); diff --git a/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_out.ts b/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_out.ts index dfe5b6cf46330..faa591b2c1617 100644 --- a/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_out.ts +++ b/x-pack/plugins/security_solution/public/actions/filter/cell_action/filter_out.ts @@ -29,8 +29,19 @@ export const createFilterOutCellActionFactory = ({ return genericFilterOutActionFactory.combine({ type: SecurityCellActionType.FILTER, - isCompatible: async ({ field }) => fieldHasCellActions(field.name), - execute: async ({ field, metadata }) => { + isCompatible: async ({ data }) => { + const field = data[0]?.field; + + return ( + data.length === 1 && // TODO Add support for multiple values + fieldHasCellActions(field.name) + ); + }, + execute: async ({ data, metadata }) => { + const field = data[0]?.field; + const value = data[0]?.value; + + if (!field) return; // if negateFilters is true we have to perform the opposite operation, we can just execute filterIn with the same params const addFilter = metadata?.negateFilters === true ? addFilterIn : addFilterOut; @@ -43,13 +54,13 @@ export const createFilterOutCellActionFactory = ({ addFilter({ filterManager: timelineFilterManager, fieldName: field.name, - value: field.value, + value, }); } else { addFilter({ filterManager, fieldName: field.name, - value: field.value, + value, }); } }, diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/cell_action/show_top_n.test.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/cell_action/show_top_n.test.tsx index 58754bdf8bbbd..bb64a2a2bdac6 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/cell_action/show_top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/cell_action/show_top_n.test.tsx @@ -40,7 +40,17 @@ describe('createShowTopNCellActionFactory', () => { const showTopNAction = showTopNActionFactory({ id: 'testAction' }); const context = { - field: { name: 'user.name', value: 'the-value', type: 'keyword', aggregatable: true }, + data: [ + { + value: 'the-value', + field: { + name: 'user.name', + type: 'keyword', + aggregatable: true, + searchable: true, + }, + }, + ], trigger: { id: 'trigger' }, nodeRef: { current: element, @@ -65,9 +75,16 @@ describe('createShowTopNCellActionFactory', () => { expect(await showTopNAction.isCompatible(context)).toEqual(true); }); - it('should return false if field type does not support aggregations', async () => { + it('should return false if field esType does not support aggregations', async () => { expect( - await showTopNAction.isCompatible({ ...context, field: { ...context.field, type: 'text' } }) + await showTopNAction.isCompatible({ + ...context, + data: [ + { + field: { ...context.data[0].field, esTypes: ['text'] }, + }, + ], + }) ).toEqual(false); }); @@ -75,7 +92,11 @@ describe('createShowTopNCellActionFactory', () => { expect( await showTopNAction.isCompatible({ ...context, - field: { ...context.field, aggregatable: false }, + data: [ + { + field: { ...context.data[0].field, aggregatable: false }, + }, + ], }) ).toEqual(false); }); diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/cell_action/show_top_n.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/cell_action/show_top_n.tsx index 557210286678e..fa67739ef8f4b 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/cell_action/show_top_n.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/cell_action/show_top_n.tsx @@ -12,6 +12,7 @@ import { Router } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { createCellActionFactory, type CellActionTemplate } from '@kbn/cell-actions'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import { KibanaContextProvider } from '../../../common/lib/kibana'; import { APP_NAME, DEFAULT_DARK_MODE } from '../../../../common/constants'; import type { SecurityAppStore } from '../../../common/store'; @@ -28,7 +29,7 @@ const SHOW_TOP = (fieldName: string) => }); const ICON = 'visBarVertical'; -const UNSUPPORTED_FIELD_TYPES = ['date', 'text']; +const UNSUPPORTED_FIELD_TYPES = [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.TEXT]; export const createShowTopNCellActionFactory = createCellActionFactory( ({ @@ -42,12 +43,20 @@ export const createShowTopNCellActionFactory = createCellActionFactory( }): CellActionTemplate => ({ type: SecurityCellActionType.SHOW_TOP_N, getIconType: () => ICON, - getDisplayName: ({ field }) => SHOW_TOP(field.name), - getDisplayNameTooltip: ({ field }) => SHOW_TOP(field.name), - isCompatible: async ({ field }) => - fieldHasCellActions(field.name) && - !UNSUPPORTED_FIELD_TYPES.includes(field.type) && - !!field.aggregatable, + getDisplayName: ({ data }) => SHOW_TOP(data[0]?.field.name), + getDisplayNameTooltip: ({ data }) => SHOW_TOP(data[0]?.field.name), + isCompatible: async ({ data }) => { + const field = data[0]?.field; + + return ( + data.length === 1 && + fieldHasCellActions(field.name) && + (field.esTypes ?? []).every( + (esType) => !UNSUPPORTED_FIELD_TYPES.includes(esType as ES_FIELD_TYPES) + ) && + !!field.aggregatable + ); + }, execute: async (context) => { if (!context.nodeRef.current) return; diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx index 56ba68117646a..dc2dd253d18cf 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx @@ -31,7 +31,17 @@ const element = document.createElement('div'); document.body.appendChild(element); const context = { - field: { name: 'user.name', value: 'the-value', type: 'keyword' }, + data: [ + { + value: 'the-value', + field: { + name: 'user.name', + type: 'keyword', + searchable: true, + aggregatable: true, + }, + }, + ], trigger: { id: 'trigger' }, nodeRef: { current: element, diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.tsx index bc231ec106975..c7807bb79987d 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.tsx @@ -10,6 +10,7 @@ import { EuiWrappingPopover } from '@elastic/eui'; import { useLocation } from 'react-router-dom'; import type { CasesUiStart } from '@kbn/cases-plugin/public'; +import { first } from 'lodash/fp'; import { StatefulTopN } from '../../common/components/top_n'; import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { APP_ID } from '../../../common/constants'; @@ -29,9 +30,10 @@ export const TopNAction = ({ const { browserFields, indexPattern } = useSourcererDataView(getScopeFromPath(pathname)); const userCasesPermissions = useGetUserCasesPermissions(); const CasesContext = casesService.ui.getCasesContext(); - const { field, nodeRef, metadata } = context; + const { data, nodeRef, metadata } = context; + const firstItem = first(data); - if (!nodeRef?.current) return null; + if (!nodeRef?.current || !firstItem) return null; return ( @@ -46,11 +48,11 @@ export const TopNAction = ({ attachToAnchor={false} > diff --git a/x-pack/plugins/security_solution/public/actions/telemetry.test.ts b/x-pack/plugins/security_solution/public/actions/telemetry.test.ts index 82a691348a206..2cfdf603b4998 100644 --- a/x-pack/plugins/security_solution/public/actions/telemetry.test.ts +++ b/x-pack/plugins/security_solution/public/actions/telemetry.test.ts @@ -23,7 +23,7 @@ const action = createAction({ getDisplayName: () => displayName, }); const context = { - field: { name: fieldName, value: fieldValue, type: 'text' }, + data: [{ field: { name: fieldName, type: 'text' }, value: fieldValue }], metadata, } as CellActionExecutionContext; diff --git a/x-pack/plugins/security_solution/public/actions/telemetry.ts b/x-pack/plugins/security_solution/public/actions/telemetry.ts index c051f11a81271..3179e5143a34f 100644 --- a/x-pack/plugins/security_solution/public/actions/telemetry.ts +++ b/x-pack/plugins/security_solution/public/actions/telemetry.ts @@ -22,7 +22,7 @@ export const enhanceActionWithTelemetry = ( telemetry.reportCellActionClicked({ actionId: rest.id, displayName: rest.getDisplayName(context), - fieldName: context.field.name, + fieldName: context.data.map(({ field }) => field.name).join(', '), metadata: context.metadata, }); diff --git a/x-pack/plugins/security_solution/public/actions/toggle_column/cell_action/toggle_column.test.ts b/x-pack/plugins/security_solution/public/actions/toggle_column/cell_action/toggle_column.test.ts index abe25695fa4f0..a7ab0d0769b10 100644 --- a/x-pack/plugins/security_solution/public/actions/toggle_column/cell_action/toggle_column.test.ts +++ b/x-pack/plugins/security_solution/public/actions/toggle_column/cell_action/toggle_column.test.ts @@ -22,7 +22,12 @@ const store = { const value = 'the-value'; const fieldName = 'user.name'; const context = { - field: { name: fieldName, value, type: 'text' }, + data: [ + { + field: { name: fieldName, type: 'text', searchable: true, aggregatable: true }, + value, + }, + ], metadata: { scopeId: TableId.test, }, @@ -78,7 +83,10 @@ describe('createToggleColumnCellActionFactory', () => { it('should add column', async () => { const name = 'fake-field-name'; - await toggleColumnAction.execute({ ...context, field: { ...context.field, name } }); + await toggleColumnAction.execute({ + ...context, + data: [{ ...context.data[0], field: { ...context.data[0].field, name } }], + }); expect(mockDispatch).toHaveBeenCalledWith( dataTableActions.upsertColumn({ column: { diff --git a/x-pack/plugins/security_solution/public/actions/toggle_column/cell_action/toggle_column.ts b/x-pack/plugins/security_solution/public/actions/toggle_column/cell_action/toggle_column.ts index b12b1a405c50a..a522488cfa715 100644 --- a/x-pack/plugins/security_solution/public/actions/toggle_column/cell_action/toggle_column.ts +++ b/x-pack/plugins/security_solution/public/actions/toggle_column/cell_action/toggle_column.ts @@ -37,16 +37,21 @@ export const createToggleColumnCellActionFactory = createCellActionFactory( type: SecurityCellActionType.TOGGLE_COLUMN, getIconType: () => ICON, getDisplayName: () => COLUMN_TOGGLE, - getDisplayNameTooltip: ({ field, metadata }) => - metadata?.isObjectArray ? NESTED_COLUMN(field.name) : COLUMN_TOGGLE, - isCompatible: async ({ field, metadata }) => { + getDisplayNameTooltip: ({ data, metadata }) => + metadata?.isObjectArray ? NESTED_COLUMN(data[0]?.field.name) : COLUMN_TOGGLE, + isCompatible: async ({ data, metadata }) => { + const field = data[0]?.field; + return ( + data.length === 1 && fieldHasCellActions(field.name) && !!metadata?.scopeId && (isTimelineScope(metadata.scopeId) || isInTableScope(metadata.scopeId)) ); }, - execute: async ({ metadata, field }) => { + + execute: async ({ metadata, data }) => { + const field = data[0]?.field; const scopeId = metadata?.scopeId; if (!scopeId) return; diff --git a/x-pack/plugins/security_solution/public/common/components/cell_actions/index.ts b/x-pack/plugins/security_solution/public/common/components/cell_actions/index.ts deleted file mode 100644 index 23a11aa738443..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/cell_actions/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CellActions, useDataGridColumnsCellActions } from '@kbn/cell-actions'; -import type { - CellActionsProps, - UseDataGridColumnsCellActions, - UseDataGridColumnsCellActionsProps, -} from '@kbn/cell-actions'; -import type { SecurityMetadata } from '../../../actions/types'; -import { SecurityCellActionsTrigger, SecurityCellActionType } from '../../../actions/constants'; - -// bridge exports for convenience -export * from '@kbn/cell-actions'; -export { SecurityCellActionsTrigger, SecurityCellActionType }; - -export interface SecurityCellActionsProps extends CellActionsProps { - triggerId: string; // can not use SecurityCellActionsTrigger, React.FC Validation throws error for some reason - disabledActionTypes?: string[]; // can not use SecurityCellActionType[], React.FC Validation throws error for some reason - metadata?: SecurityMetadata; -} -export interface UseDataGridColumnsSecurityCellActionsProps - extends UseDataGridColumnsCellActionsProps { - triggerId: SecurityCellActionsTrigger; - disabledActionTypes?: SecurityCellActionType[]; - metadata?: SecurityMetadata; -} - -// same components with security cell actions types -export const SecurityCellActions: React.FC = CellActions; -export const useDataGridColumnsSecurityCellActions: UseDataGridColumnsCellActions = - useDataGridColumnsCellActions; diff --git a/x-pack/plugins/security_solution/public/common/components/cell_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/cell_actions/index.tsx new file mode 100644 index 0000000000000..71318f51dd89b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/cell_actions/index.tsx @@ -0,0 +1,84 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CellActions, useDataGridColumnsCellActions } from '@kbn/cell-actions'; +import type { + CellActionsProps, + UseDataGridColumnsCellActions, + UseDataGridColumnsCellActionsProps, +} from '@kbn/cell-actions'; +import React, { useMemo } from 'react'; +import type { CellActionFieldValue, CellActionsData } from '@kbn/cell-actions/src/types'; +import type { SecurityMetadata } from '../../../actions/types'; +import { SecurityCellActionsTrigger, SecurityCellActionType } from '../../../actions/constants'; +import { SourcererScopeName } from '../../store/sourcerer/model'; +import { useGetFieldSpec } from '../../hooks/use_get_field_spec'; + +// bridge exports for convenience +export * from '@kbn/cell-actions'; +export { SecurityCellActionsTrigger, SecurityCellActionType }; + +export interface SecurityCellActionsData { + /** + * The field name is necessary to fetch the FieldSpec from the Dataview. + * Ex: `event.category` + */ + field: string; + + value: CellActionFieldValue; +} + +export interface SecurityCellActionsProps + extends Omit { + sourcererScopeId?: SourcererScopeName; + data: SecurityCellActionsData | SecurityCellActionsData[]; + triggerId: SecurityCellActionsTrigger; + disabledActionTypes?: SecurityCellActionType[]; + metadata?: SecurityMetadata; +} + +export interface UseDataGridColumnsSecurityCellActionsProps + extends UseDataGridColumnsCellActionsProps { + triggerId: SecurityCellActionsTrigger; + disabledActionTypes?: SecurityCellActionType[]; + metadata?: SecurityMetadata; +} + +export const useDataGridColumnsSecurityCellActions: UseDataGridColumnsCellActions = + useDataGridColumnsCellActions; + +export const SecurityCellActions: React.FC = ({ + sourcererScopeId = SourcererScopeName.default, + data, + children, + ...props +}) => { + const getFieldSpec = useGetFieldSpec(sourcererScopeId); + // 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); + + const fieldData: CellActionsData[] = useMemo( + () => + (Array.isArray(data) ? data : [data]) + .map(({ field, value }) => ({ + 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] + ); + + return fieldData.length > 0 ? ( + + {children} + + ) : ( + <>{children} + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx index 4d55b7ab61cdc..c6a022ff0998f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx @@ -44,6 +44,8 @@ jest.mock('@kbn/cell-actions/src/hooks/use_load_actions', () => { }; }); +jest.mock('../../hooks/use_get_field_spec'); + const props = { data: mockAlertDetailsData as TimelineEventsDetailsItem[], browserFields: mockBrowserFields, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx index 0e30a803b546e..c2b044b35cae1 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx @@ -26,6 +26,8 @@ jest.mock('@kbn/cell-actions/src/hooks/use_load_actions', () => { }; }); +jest.mock('../../hooks/use_get_field_spec'); + interface Column { field: string; name: string | JSX.Element; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index 7f68eb73a2355..662f39e72b5ab 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -17,6 +17,7 @@ import type { EventFieldsData } from './types'; import type { BrowserField } from '../../../../common/search_strategy'; import { FieldValueCell } from './table/field_value_cell'; import { FieldNameCell } from './table/field_name_cell'; +import { getSourcererScopeId } from '../../../helpers'; const HoverActionsContainer = styled(EuiPanel)` align-items: center; @@ -37,6 +38,7 @@ export const getFieldFromBrowserField = memoizeOne( get(browserFields, keys), (newArgs, lastArgs) => newArgs[0].join() === lastArgs[0].join() ); + export const getColumns = ({ browserFields, eventId, @@ -67,22 +69,16 @@ export const getColumns = ({ truncateText: false, width: '132px', render: (values: string[] | null | undefined, data: EventFieldsData) => { - const fieldFromBrowserField = getFieldFromBrowserField( - [data.category, 'fields', data.field], - browserFields - ); - return ( ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx index 43fca97e57475..c8e8f2bfb24db 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx @@ -27,6 +27,7 @@ import type { } from '../../../../../common/search_strategy'; import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; import { EnrichedDataRow, ThreatSummaryPanelHeader } from './threat_summary_view'; +import { getSourcererScopeId } from '../../../../helpers'; export interface ThreatSummaryDescription { browserField: BrowserField; @@ -73,20 +74,8 @@ const EnrichmentDescription: React.FC = ({ isReadOnly, }) => { const metadata = useMemo(() => ({ scopeId }), [scopeId]); - const field = useMemo( - () => - !data - ? null - : { - name: data.field, - value, - type: data.type, - aggregatable: browserField?.aggregatable, - }, - [browserField, data, value] - ); - if (!data || !value || !field) return null; + if (!data || !value) return null; const key = `alert-details-value-formatted-field-value-${scopeId}-${eventId}-${data.field}-${value}-${index}-${feedName}`; return ( @@ -115,9 +104,13 @@ const EnrichmentDescription: React.FC = ({ {value && !isReadOnly && ( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx index bbff20aef925e..0756992878518 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx @@ -25,6 +25,8 @@ jest.mock('@elastic/eui', () => { }; }); +jest.mock('../../hooks/use_get_field_spec'); + jest.mock('@kbn/cell-actions/src/hooks/use_load_actions', () => { const actual = jest.requireActual('@kbn/cell-actions/src/hooks/use_load_actions'); return { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap index b6e9ea7a2ea0f..86c89bf79eda9 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap @@ -113,24 +113,7 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = `
-
-
-
-
-
-
-
+ />
@@ -159,24 +142,7 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = `
-
-
-
-
-
-
-
+ />
@@ -217,24 +183,7 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = `
-
-
-
-
-
-
-
+ />
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx index 5f547a4690ea3..3d6808847de0f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx @@ -75,6 +75,8 @@ const props = { jest.mock('../../../lib/kibana'); +jest.mock('../../../hooks/use_get_field_spec'); + const mockAction = createAction({ id: 'test_action', execute: async () => {}, @@ -82,8 +84,6 @@ const mockAction = createAction({ getDisplayName: () => 'test-actions', }); -// jest.useFakeTimers(); - describe('OverviewCardWithActions', () => { test('it renders correctly', async () => { await act(async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx index cfe4247d4b091..0501031f9ad68 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx @@ -15,6 +15,7 @@ import { SecurityCellActionsTrigger, } from '../../cell_actions'; import type { EnrichedFieldInfo } from '../types'; +import { getSourcererScopeId } from '../../../../helpers'; const ActionWrapper = euiStyled.div` margin-left: ${({ theme }) => theme.eui.euiSizeS}; @@ -92,14 +93,13 @@ export const OverviewCardWithActions: React.FC = ( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.test.tsx index afaaafc440138..ace35265885f8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.test.tsx @@ -18,6 +18,8 @@ import { AGENT_STATUS_FIELD_NAME } from '../../../../timelines/components/timeli jest.mock('../../../lib/kibana'); +jest.mock('../../../hooks/use_get_field_spec'); + const eventId = 'TUWyf3wBFCFU0qRJTauW'; const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::']; const hostIpFieldFromBrowserField: BrowserField = { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx index eb6bf0700d9ff..e90367b0f7af1 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx @@ -16,6 +16,7 @@ import { FieldValueCell } from './field_value_cell'; import type { AlertSummaryRow } from '../helpers'; import { hasHoverOrRowActions } from '../helpers'; import { TimelineId } from '../../../../../common/types'; +import { getSourcererScopeId } from '../../../../helpers'; const style = { flexGrow: 0 }; @@ -45,15 +46,14 @@ export const SummaryValueCell: React.FC = ({ /> {scopeId !== TimelineId.active && !isReadOnly && hoverActionsEnabled && ( )} diff --git a/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap index 4cc7aad784cc8..38707a49a12b0 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap @@ -1,12 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`entity_draggable renders correctly against snapshot 1`] = ` - entity-name: "entity-value" - + `; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx b/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx index a50ea1e1f47f5..8d74452ac26fc 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx @@ -16,11 +16,9 @@ interface Props { export const EntityComponent: React.FC = ({ entityName, entityValue }) => { return ( 17 -
+ `; exports[`draggable_score renders correctly against snapshot when the index is not included 1`] = ` - 17 - + `; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx index 4e5fc8b29190b..3c5030706da05 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx @@ -27,11 +27,9 @@ export const ScoreComponent = ({ return ( , }), }, diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx index 6ce43c54392c7..a593847b519ab 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx @@ -12,7 +12,6 @@ import type { Anomaly, AnomaliesByNetwork } from '../types'; import { getRowItemsWithActions } from '../../tables/helpers'; import { createCompoundAnomalyKey } from './create_compound_key'; import { NetworkDetailsLink } from '../../links'; - import * as i18n from './translations'; import { NetworkType } from '../../../../explore/network/store/model'; import type { FlowTarget } from '../../../../../common/search_strategy'; @@ -41,8 +40,6 @@ export const getAnomaliesNetworkTableColumns = ( idPrefix: `anomalies-network-table-ip-${createCompoundAnomalyKey( anomaliesByNetwork.anomaly )}`, - aggregatable: true, - fieldType: 'ip', render: (item) => , }), }, diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.tsx index 4c687e4373787..04a221f54ac66 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.tsx @@ -38,8 +38,6 @@ export const getAnomaliesUserTableColumns = ( idPrefix: `anomalies-user-table-userName-${createCompoundAnomalyKey( anomaliesByUser.anomaly )}-userName`, - aggregatable: true, - fieldType: 'keyword', render: (item) => , }), }, diff --git a/x-pack/plugins/security_solution/public/common/components/tables/__snapshots__/helpers.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/tables/__snapshots__/helpers.test.tsx.snap index a88b00c573b2e..6205ff5b379c9 100644 --- a/x-pack/plugins/security_solution/public/common/components/tables/__snapshots__/helpers.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/tables/__snapshots__/helpers.test.tsx.snap @@ -11,7 +11,6 @@ exports[`Table Helpers #RowItemOverflow it returns correctly against snapshot 1` > { const items = ['item1', 'item2', 'item3']; const mount = useMountAppended(); @@ -31,7 +33,6 @@ describe('Table Helpers', () => { values: undefined, fieldName: 'attrName', idPrefix: 'idPrefix', - aggregatable: false, }); const { container } = render({rowItem}); @@ -44,7 +45,6 @@ describe('Table Helpers', () => { values: [''], fieldName: 'attrName', idPrefix: 'idPrefix', - aggregatable: false, }); const { container } = render({rowItem}); @@ -56,7 +56,6 @@ describe('Table Helpers', () => { values: null, fieldName: 'attrName', idPrefix: 'idPrefix', - aggregatable: false, displayCount: 0, }); const { container } = render({rowItem}); @@ -69,7 +68,6 @@ describe('Table Helpers', () => { values: ['item1'], fieldName: 'attrName', idPrefix: 'idPrefix', - aggregatable: false, render: renderer, }); const { container } = render({rowItem}); @@ -82,7 +80,6 @@ describe('Table Helpers', () => { values: [], fieldName: 'attrName', idPrefix: 'idPrefix', - aggregatable: false, }); const { container } = render({rowItems}); expect(container.textContent).toBe(getEmptyValue()); @@ -93,11 +90,12 @@ describe('Table Helpers', () => { values: items, fieldName: 'attrName', idPrefix: 'idPrefix', - aggregatable: false, displayCount: 2, }); - const { queryAllByTestId, queryByTestId } = render({rowItems}); - + const { queryAllByTestId, queryByTestId, debug } = render( + {rowItems} + ); + debug(); expect(queryAllByTestId('cellActions-renderContent-attrName').length).toBe(2); expect(queryByTestId('overflow-button')).toBeInTheDocument(); }); @@ -112,7 +110,6 @@ describe('Table Helpers', () => { idPrefix="idPrefix" maxOverflowItems={1} overflowIndexStart={1} - fieldType="keyword" /> ); expect(wrapper).toMatchSnapshot(); @@ -126,7 +123,6 @@ describe('Table Helpers', () => { idPrefix="idPrefix" maxOverflowItems={5} overflowIndexStart={1} - fieldType="keyword" /> ); expect(wrapper.find('[data-test-subj="popover-additional-overflow"]').length).toBe(0); @@ -141,7 +137,6 @@ describe('Table Helpers', () => { idPrefix="idPrefix" maxOverflowItems={5} overflowIndexStart={1} - fieldType="keyword" /> ); @@ -161,7 +156,6 @@ describe('Table Helpers', () => { idPrefix="idPrefix" maxOverflowItems={1} overflowIndexStart={1} - fieldType="keyword" /> ); expect(wrapper.find('[data-test-subj="popover-additional-overflow"]').length).toBe(1); diff --git a/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx index 94348668e89e3..4d8dd866e3075 100644 --- a/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx @@ -21,23 +21,19 @@ const Subtext = styled.div` interface GetRowItemsWithActionsParams { values: string[] | null | undefined; fieldName: string; - fieldType?: string; idPrefix: string; render?: (item: string) => JSX.Element; displayCount?: number; maxOverflow?: number; - aggregatable: boolean; } export const getRowItemsWithActions = ({ values, fieldName, - fieldType = 'keyword', idPrefix, render, displayCount = 5, maxOverflow = 5, - aggregatable, }: GetRowItemsWithActionsParams): JSX.Element => { if (values != null && values.length > 0) { const visibleItems = values.slice(0, displayCount).map((value, index) => { @@ -49,11 +45,9 @@ export const getRowItemsWithActions = ({ visibleCellActions={5} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: fieldName, + data={{ value, - type: fieldType, - aggregatable, + field: fieldName, }} > <>{render ? render(value) : defaultToEmptyTag(value)} @@ -67,11 +61,9 @@ export const getRowItemsWithActions = ({ ) : ( @@ -84,8 +76,6 @@ export const getRowItemsWithActions = ({ interface RowItemOverflowProps { fieldName: string; - fieldType: string; - isAggregatable?: boolean; values: string[]; idPrefix: string; maxOverflowItems: number; @@ -95,8 +85,6 @@ interface RowItemOverflowProps { export const RowItemOverflowComponent: React.FC = ({ fieldName, values, - fieldType, - isAggregatable, idPrefix, maxOverflowItems = 5, overflowIndexStart = 5, @@ -109,8 +97,6 @@ export const RowItemOverflowComponent: React.FC = ({ { + return (name: string) => ({ + name, + type: 'string', + }); +}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_get_field_spec.ts b/x-pack/plugins/security_solution/public/common/hooks/use_get_field_spec.ts new file mode 100644 index 0000000000000..2330ee26b7bc8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_get_field_spec.ts @@ -0,0 +1,26 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import type { SourcererScopeName } from '../store/sourcerer/model'; +import { getSelectedDataviewSelector } from '../store/sourcerer/selectors'; +import { useDeepEqualSelector } from './use_selector'; + +// Calls it from the module scope due to non memoized selectors https://github.com/elastic/kibana/issues/159315 +const selectedDataviewSelector = getSelectedDataviewSelector(); + +export const useGetFieldSpec = (scopeId: SourcererScopeName) => { + const dataView = useDeepEqualSelector((state) => selectedDataviewSelector(state, scopeId)); + + return useCallback( + (fieldName: string) => { + const fields = dataView?.fields; + return fields && fields[fieldName]; + }, + [dataView?.fields] + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts index 9a229f16e08e3..8900e07efb011 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts @@ -6,6 +6,7 @@ */ import { createSelector } from 'reselect'; +import type { DataViewSpec } from '@kbn/data-views-plugin/common'; import type { State } from '../types'; import type { SourcererDataView, @@ -13,7 +14,6 @@ import type { SourcererScope, SourcererScopeName, } from './model'; - export const sourcererKibanaDataViewsSelector = ({ sourcerer, }: State): SourcererModel['kibanaDataViews'] => sourcerer.kibanaDataViews; @@ -97,3 +97,15 @@ export const getSourcererScopeSelector = () => { }; }; }; + +export const getSelectedDataviewSelector = () => { + const getSourcererDataViewSelector = sourcererDataViewSelector(); + const getScopeSelector = scopeIdSelector(); + + return (state: State, scopeId: SourcererScopeName): DataViewSpec | undefined => { + const scope = getScopeSelector(state, scopeId); + const selectedDataView = getSourcererDataViewSelector(state, scope.selectedDataViewId); + + return selectedDataView?.dataView; + }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/columns.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/columns.tsx index f68f8d54a5b19..150a11d8f9888 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/columns.tsx @@ -20,6 +20,7 @@ import { ALERTS_HEADERS_RULE_NAME } from '../../alerts_table/translations'; import { ALERT_TYPE_COLOR, ALERT_TYPE_LABEL } from './helpers'; import { COUNT_TABLE_TITLE } from '../alerts_count_panel/translations'; import * as i18n from './translations'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; export const getAlertsTypeTableColumns = ( isAlertTypeEnabled: boolean @@ -60,11 +61,11 @@ export const getAlertsTypeTableColumns = ( visibleCellActions={4} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: 'event.type', + data={{ value: 'denied', - type: 'keyword', + field: 'event.type', }} + sourcererScopeId={SourcererScopeName.detections} metadata={{ negateFilters: type === 'Detection' }} // Detection: event.type != denied > {ALERT_TYPE_LABEL[type as AlertType]} diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx index 9cdf540326ae2..eb3aa7f714f40 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx @@ -62,24 +62,30 @@ export const getUseCellActionsHook = (tableId: TableId) => { tableDefaults.viewMode; const cellActionProps = useMemo(() => { - const fields = + const cellActionsData = viewMode === VIEW_SELECTION.eventRenderedView ? [] : columns.map((col) => { const fieldMeta: Partial | undefined = browserFieldsByName[col.id]; return { - name: col.id, - type: fieldMeta?.type ?? 'keyword', + // TODO use FieldSpec object instead of browserField + field: { + name: col.id, + type: fieldMeta?.type ?? 'keyword', + esTypes: fieldMeta?.esTypes ?? [], + aggregatable: fieldMeta?.aggregatable ?? false, + searchable: fieldMeta?.searchable ?? false, + subType: fieldMeta?.subType, + }, values: (finalData as TimelineNonEcsData[][]).map( (row) => row.find((rowData) => rowData.field === col.id)?.value ?? [] ), - aggregatable: fieldMeta?.aggregatable ?? false, }; }); return { triggerId: SecurityCellActionsTrigger.DEFAULT, - fields, + data: cellActionsData, metadata: { // cell actions scope scopeId: tableId, diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/index.tsx index 7e5f51f4215b5..c6b0f2160095b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/index.tsx @@ -24,6 +24,7 @@ import { import { DefaultFieldRenderer } from '../../../../../../timelines/components/field_renderers/field_renderers'; import { HostDetailsLink, NetworkDetailsLink } from '../../../../../../common/components/links'; import type { SelectedDataView } from '../../../../../../common/store/sourcerer/model'; +import { SourcererScopeName } from '../../../../../../common/store/sourcerer/model'; import { getEnrichedFieldInfo } from '../../../../../../common/components/event_details/helpers'; import { getTimelineEventData } from '../../../utils/get_timeline_event_data'; import { @@ -170,6 +171,7 @@ export const HostPanel = React.memo( attrName={'host.ip'} idPrefix="alert-details-page-user" render={renderHostIp} + sourcererScopeId={SourcererScopeName.detections} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/index.tsx index e6999a14c274e..044427d46212b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/index.tsx @@ -21,6 +21,7 @@ import { import { DefaultFieldRenderer } from '../../../../../../timelines/components/field_renderers/field_renderers'; import { NetworkDetailsLink, UserDetailsLink } from '../../../../../../common/components/links'; import type { SelectedDataView } from '../../../../../../common/store/sourcerer/model'; +import { SourcererScopeName } from '../../../../../../common/store/sourcerer/model'; import { getTimelineEventData } from '../../../utils/get_timeline_event_data'; import { IP_ADDRESSES_TITLE, @@ -135,6 +136,7 @@ export const UserPanel = React.memo( attrName={'source.ip'} idPrefix="alert-details-page-user" render={renderSourceIp} + sourcererScopeId={SourcererScopeName.detections} /> diff --git a/x-pack/plugins/security_solution/public/explore/components/authentication/helpers.tsx b/x-pack/plugins/security_solution/public/explore/components/authentication/helpers.tsx index 4069d1c3635f4..9ca08cc8cd1e4 100644 --- a/x-pack/plugins/security_solution/public/explore/components/authentication/helpers.tsx +++ b/x-pack/plugins/security_solution/public/explore/components/authentication/helpers.tsx @@ -12,7 +12,6 @@ import { getEmptyTagValue } from '../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; import type { Columns, ItemsPerRow } from '../paginated_table'; import { getRowItemsWithActions } from '../../../common/components/tables/helpers'; - import * as i18n from './translations'; import { HostDetailsLink, @@ -102,8 +101,6 @@ const LAST_SUCCESSFUL_SOURCE_COLUMN: Columns , }), @@ -116,8 +113,6 @@ const LAST_SUCCESSFUL_DESTINATION_COLUMN: Columns , }), @@ -141,8 +136,6 @@ const LAST_FAILED_SOURCE_COLUMN: Columns , }), @@ -155,8 +148,6 @@ const LAST_FAILED_DESTINATION_COLUMN: Columns , }), @@ -171,8 +162,6 @@ const USER_COLUMN: Columns = { values: node.stackedValue, fieldName: 'user.name', idPrefix: `authentications-table-${node._id}-userName`, - fieldType: 'keyword', - aggregatable: true, render: (item) => , }), }; @@ -186,8 +175,6 @@ const HOST_COLUMN: Columns = { values: node.stackedValue, fieldName: 'host.name', idPrefix: `authentications-table-${node._id}-hostName`, - fieldType: 'keyword', - aggregatable: true, render: (item) => , }), }; diff --git a/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx index d9b57d3da7c29..ae2969a0d1116 100644 --- a/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx @@ -42,11 +42,9 @@ export const getHostRiskScoreColumns = ({ visibleCellActions={5} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: 'host.name', + data={{ value: hostName, - type: 'keyword', - aggregatable: true, + field: 'host.name', }} metadata={{ telemetry: CELL_ACTIONS_TELEMETRY, diff --git a/x-pack/plugins/security_solution/public/explore/hosts/components/hosts_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/hosts/components/hosts_table/columns.tsx index 07b458c14760f..3848b6dafe6ba 100644 --- a/x-pack/plugins/security_solution/public/explore/hosts/components/hosts_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/hosts/components/hosts_table/columns.tsx @@ -42,11 +42,9 @@ export const getHostsColumns = ( visibleCellActions={5} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: 'host.name', + data={{ value: hostName[0], - type: 'keyword', - aggregatable: true, + field: 'host.name', }} > @@ -100,10 +98,9 @@ export const getHostsColumns = ( visibleCellActions={5} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: 'host.os.name', + data={{ value: hostOsName[0], - type: 'keyword', + field: 'host.os.name', }} > {hostOsName} @@ -127,10 +124,9 @@ export const getHostsColumns = ( visibleCellActions={5} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: 'host.os.version', + data={{ value: hostOsVersion[0], - type: 'keyword', + field: 'host.os.version', }} > {hostOsVersion} diff --git a/x-pack/plugins/security_solution/public/explore/hosts/components/uncommon_process_table/index.tsx b/x-pack/plugins/security_solution/public/explore/hosts/components/uncommon_process_table/index.tsx index f46b009643f81..67aa381bfbe5f 100644 --- a/x-pack/plugins/security_solution/public/explore/hosts/components/uncommon_process_table/index.tsx +++ b/x-pack/plugins/security_solution/public/explore/hosts/components/uncommon_process_table/index.tsx @@ -151,8 +151,6 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [ getRowItemsWithActions({ values: node.process.name, fieldName: 'process.name', - fieldType: 'keyword', - aggregatable: true, idPrefix: `uncommon-process-table-${node._id}-processName`, }), }, @@ -181,8 +179,6 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [ getRowItemsWithActions({ values: getHostNames(node.hosts), fieldName: 'host.name', - fieldType: 'keyword', - aggregatable: true, idPrefix: `uncommon-process-table-${node._id}-processHost`, render: (item) => , }), @@ -196,8 +192,6 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [ getRowItemsWithActions({ values: node.process != null ? node.process.args : null, fieldName: 'process.args', - fieldType: 'keyword', - aggregatable: true, idPrefix: `uncommon-process-table-${node._id}-processArgs`, render: (item) => , displayCount: 1, @@ -211,8 +205,6 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [ getRowItemsWithActions({ values: node.user != null ? node.user.name : null, fieldName: 'user.name', - fieldType: 'keyword', - aggregatable: true, idPrefix: `uncommon-process-table-${node._id}-processUser`, }), }, diff --git a/x-pack/plugins/security_solution/public/explore/network/components/network_dns_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/network_dns_table/columns.tsx index 170d7642b1151..91377920f2e0e 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/network_dns_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/network_dns_table/columns.tsx @@ -46,11 +46,9 @@ export const getNetworkDnsColumns = (): NetworkDnsColumns => [ visibleCellActions={5} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: 'dns.question.registered_domain', + data={{ value: dnsName, - type: 'keyword', - aggregatable: true, + field: 'dns.question.registered_domain', }} > {defaultToEmptyTag(dnsName)} diff --git a/x-pack/plugins/security_solution/public/explore/network/components/network_http_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/network_http_table/columns.tsx index 8556dc5298cc3..46020d8a7807c 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/network_http_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/network_http_table/columns.tsx @@ -38,8 +38,6 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ fieldName: 'http.request.method', values: methods, idPrefix: escapeDataProviderId(`${tableId}-table-methods-${path}`), - fieldType: 'keyword', - aggregatable: true, displayCount: 3, }) : getEmptyTagValue(); @@ -53,8 +51,6 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ values: domains, fieldName: 'url.domain', idPrefix: escapeDataProviderId(`${tableId}-table-domains-${path}`), - fieldType: 'keyword', - aggregatable: true, }) : getEmptyTagValue(), }, @@ -67,8 +63,6 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ values: [path], fieldName: 'url.path', idPrefix: escapeDataProviderId(`${tableId}-table-path-${path}`), - fieldType: 'keyword', - aggregatable: true, }) : getEmptyTagValue(), }, @@ -80,8 +74,6 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ values: statuses, fieldName: 'http.response.status_code', idPrefix: escapeDataProviderId(`${tableId}-table-statuses-${path}`), - fieldType: 'keyword', - aggregatable: true, displayCount: 3, }) : getEmptyTagValue(), @@ -94,8 +86,6 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ values: [lastHost], fieldName: 'host.name', idPrefix: escapeDataProviderId(`${tableId}-table-lastHost-${path}`), - fieldType: 'keyword', - aggregatable: true, }) : getEmptyTagValue(), }, @@ -107,8 +97,6 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ values: [lastSourceIp], fieldName: 'source.ip', idPrefix: escapeDataProviderId(`${tableId}-table-lastSourceIp-${path}`), - fieldType: 'keyword', - aggregatable: true, render: () => , }) : getEmptyTagValue(), diff --git a/x-pack/plugins/security_solution/public/explore/network/components/network_top_countries_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/network_top_countries_table/columns.tsx index a2e84f3386d7a..5530b1eb0a1c7 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/network_top_countries_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/network_top_countries_table/columns.tsx @@ -64,11 +64,9 @@ export const getNetworkTopCountriesColumns = ( visibleCellActions={5} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: geoAttr, + data={{ value: geo, - type: 'keyword', - aggregatable: true, + field: geoAttr, }} > diff --git a/x-pack/plugins/security_solution/public/explore/network/components/network_top_n_flow_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/network_top_n_flow_table/columns.tsx index addce53c36596..f6f80747b8690 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/network_top_n_flow_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/network_top_n_flow_table/columns.tsx @@ -72,11 +72,9 @@ export const getNetworkTopNFlowColumns = ( visibleCellActions={5} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: ipAttr, + data={{ value: ip, - type: 'keyword', - aggregatable: true, + field: ipAttr, }} > @@ -89,11 +87,9 @@ export const getNetworkTopNFlowColumns = ( visibleCellActions={5} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: geoAttrName, + data={{ value: geo, - type: 'keyword', - aggregatable: true, + field: geoAttrName, }} > {' '} @@ -121,8 +117,6 @@ export const getNetworkTopNFlowColumns = ( return getRowItemsWithActions({ values: domains, fieldName: domainAttr, - fieldType: 'keyword', - aggregatable: true, idPrefix: id, displayCount: 1, }); @@ -145,8 +139,6 @@ export const getNetworkTopNFlowColumns = ( getRowItemsWithActions({ values: [as.name], fieldName: `${flowTarget}.as.organization.name`, - fieldType: 'keyword', - aggregatable: true, idPrefix: `${id}-name`, })} @@ -157,8 +149,6 @@ export const getNetworkTopNFlowColumns = ( values: [`${as.number}`], fieldName: `${flowTarget}.as.number`, idPrefix: `${id}-number`, - fieldType: 'keyword', - aggregatable: true, })} )} diff --git a/x-pack/plugins/security_solution/public/explore/network/components/tls_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/tls_table/columns.tsx index f86c152c569d9..07a8336b98ea6 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/tls_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/tls_table/columns.tsx @@ -35,8 +35,6 @@ export const getTlsColumns = (tableId: string): TlsColumns => [ getRowItemsWithActions({ values: issuers, fieldName: 'tls.server.issuer', - fieldType: 'keyword', - aggregatable: true, idPrefix: `${tableId}-${_id}-table-issuers`, }), }, @@ -50,8 +48,6 @@ export const getTlsColumns = (tableId: string): TlsColumns => [ getRowItemsWithActions({ values: subjects, fieldName: 'tls.server.subject', - fieldType: 'keyword', - aggregatable: true, idPrefix: `${tableId}-${_id}-table-subjects`, }), }, @@ -65,8 +61,6 @@ export const getTlsColumns = (tableId: string): TlsColumns => [ getRowItemsWithActions({ values: sha1 ? [sha1] : undefined, fieldName: 'tls.server.hash.sha1', - fieldType: 'keyword', - aggregatable: true, idPrefix: `${tableId}-${sha1}-table-sha1`, }), }, @@ -80,8 +74,6 @@ export const getTlsColumns = (tableId: string): TlsColumns => [ getRowItemsWithActions({ values: ja3, fieldName: 'tls.server.ja3s', - fieldType: 'keyword', - aggregatable: true, idPrefix: `${tableId}-${_id}-table-ja3`, }), }, @@ -95,8 +87,6 @@ export const getTlsColumns = (tableId: string): TlsColumns => [ getRowItemsWithActions({ values: notAfter, fieldName: 'tls.server.not_after', - fieldType: 'date', - aggregatable: false, idPrefix: `${tableId}-${_id}-table-notAfter`, render: (validUntil) => ( diff --git a/x-pack/plugins/security_solution/public/explore/network/components/users_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/users_table/columns.tsx index ec5f1b204821b..a15b46cd44f5b 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/users_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/users_table/columns.tsx @@ -34,8 +34,6 @@ export const getUsersColumns = ( getRowItemsWithActions({ values: userName ? [userName] : undefined, fieldName: 'user.name', - fieldType: 'keyword', - aggregatable: true, idPrefix: `${tableId}-table-${flowTarget}-user`, }), }, @@ -49,8 +47,6 @@ export const getUsersColumns = ( getRowItemsWithActions({ values: userIds, fieldName: 'user.id', - fieldType: 'keyword', - aggregatable: true, idPrefix: `${tableId}-table-${flowTarget}`, }), }, @@ -64,8 +60,6 @@ export const getUsersColumns = ( getRowItemsWithActions({ values: groupNames, fieldName: 'user.group.name', - fieldType: 'keyword', - aggregatable: true, idPrefix: `${tableId}-table-${flowTarget}`, }), }, @@ -79,8 +73,6 @@ export const getUsersColumns = ( getRowItemsWithActions({ values: groupId, fieldName: 'user.group.id', - fieldType: 'keyword', - aggregatable: true, idPrefix: `${tableId}-table-${flowTarget}`, }), }, diff --git a/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx b/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx index 2139a140ec523..b3582262a691a 100644 --- a/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx @@ -182,7 +182,10 @@ const NetworkDetailsComponent: React.FC = () => { } title={ , - aggregatable: true, - fieldType: 'keyword', }) : getOrEmptyTagFromValue(name), }, @@ -110,8 +108,6 @@ const getUsersColumns = ( fieldName: 'user.domain', values: [domain], idPrefix: `users-table-${domain}-domain`, - aggregatable: true, - fieldType: 'keyword', }) : getOrEmptyTagFromValue(domain), }, diff --git a/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx index e6af3c9a873f7..a41f97f4e81d1 100644 --- a/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx @@ -45,11 +45,9 @@ export const getUserRiskScoreColumns = ({ visibleCellActions={5} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: 'user.name', + data={{ value: userName, - type: 'keyword', - aggregatable: true, + field: 'user.name', }} metadata={{ telemetry: CELL_ACTIONS_TELEMETRY, diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx index 2cd503caf5583..c1a399af45aea 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx @@ -126,10 +126,9 @@ export const HostDetails: React.FC = ({ hostName, timestamp }) visibleCellActions={5} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: 'user.name', + data={{ + field: 'user.name', value: user, - type: 'keyword', }} > {user} diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx index 0a77ddad023e3..cee8189859154 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx @@ -126,10 +126,9 @@ export const UserDetails: React.FC = ({ userName, timestamp }) visibleCellActions={5} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: 'host.name', + data={{ value: host, - type: 'keyword', + field: 'host.name', }} > {host} @@ -284,5 +283,3 @@ export const UserDetails: React.FC = ({ userName, timestamp }) ); }; - -UserDetails.displayName = 'UserDetails'; diff --git a/x-pack/plugins/security_solution/public/helpers.tsx b/x-pack/plugins/security_solution/public/helpers.tsx index dd7739513114e..2dffcdc87beb0 100644 --- a/x-pack/plugins/security_solution/public/helpers.tsx +++ b/x-pack/plugins/security_solution/public/helpers.tsx @@ -37,6 +37,7 @@ import type { InspectResponse, StartedSubPlugins, StartServices } from './types' import { CASES_SUB_PLUGIN_KEY } from './types'; import { timelineActions } from './timelines/store/timeline'; import { TimelineId } from '../common/types'; +import { SourcererScopeName } from './common/store/sourcerer/model'; export const parseRoute = (location: Pick) => { if (!isEmpty(location.hash)) { @@ -308,6 +309,11 @@ export const isTimelineScope = (scopeId: string) => export const isInTableScope = (scopeId: string) => Object.values(TableId).includes(scopeId as unknown as TableId); +export const isAlertsPageScope = (scopeId: string) => + [TableId.alertsOnAlertsPage, TableId.alertsOnRuleDetailsPage, TableId.alertsOnCasePage].includes( + scopeId as TableId + ); + export const getScopedActions = (scopeId: string) => { if (isTimelineScope(scopeId)) { return timelineActions; @@ -325,3 +331,13 @@ export const getScopedSelectors = (scopeId: string) => { }; export const isActiveTimeline = (timelineId: string) => timelineId === TimelineId.active; + +export const getSourcererScopeId = (scopeId: string): SourcererScopeName => { + if (isTimelineScope(scopeId)) { + return SourcererScopeName.timeline; + } else if (isAlertsPageScope(scopeId)) { + return SourcererScopeName.detections; + } else { + return SourcererScopeName.default; + } +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx index fb18a3a6a4459..0a959da38d423 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx @@ -27,6 +27,8 @@ jest.mock('../../../../common/hooks/use_global_filter_query', () => { }; }); +jest.mock('../../../../common/hooks/use_get_field_spec'); + type UseHostAlertsItemsReturn = ReturnType; const defaultUseHostAlertsItemsReturn: UseHostAlertsItemsReturn = { items: [], diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx index 8ac908bad945a..0799b0ea3c610 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx @@ -39,6 +39,7 @@ import { SecurityCellActionsTrigger, } from '../../../../common/components/cell_actions'; import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; interface HostAlertsTableProps { signalIndexName: string | null; @@ -152,14 +153,13 @@ const getTableColumns: GetTableColumns = (handleClick) => [ 'data-test-subj': 'hostSeverityAlertsTable-totalAlerts', render: (totalAlerts: number, { hostName }) => ( [ {count > 0 ? ( [ {count > 0 ? ( [ {count > 0 ? ( [ {count > 0 ? ( ( getTableColumns(openUserInAlerts), [openUserInAlerts]); return ( - + [ 'data-test-subj': 'userSeverityAlertsTable-totalAlerts', render: (totalAlerts: number, { userName }) => ( [ {count > 0 ? ( [ {count > 0 ? ( [ {count > 0 ? ( [ {count > 0 ? ( (({ contextID, data }) => { +export const EndpointOverview = React.memo(({ contextID, data, sourcererScopeId }) => { const getDefaultRenderer = useCallback( (fieldName: string, fieldData: EndpointFields, attrName: string) => ( ), - [contextID] + [contextID, sourcererScopeId] ); const descriptionLists: Readonly = useMemo(() => { const appliedPolicy = data?.hostInfo?.metadata.Endpoint.policy.applied; diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index 954dc51572ea2..8b0e973f3c274 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -39,9 +39,11 @@ import { OverviewDescriptionList } from '../../../common/components/overview_des import { useRiskScore } from '../../../explore/containers/risk_score'; import { RiskScore } from '../../../explore/components/risk_score/severity/common'; import { RiskScoreHeaderTitle } from '../../../explore/components/risk_score/risk_score_onboarding/risk_score_header_title'; +import type { SourcererScopeName } from '../../../common/store/sourcerer/model'; interface HostSummaryProps { contextID?: string; // used to provide unique draggable context when viewing in the side panel + sourcererScopeId?: SourcererScopeName; data: HostItem; id: string; isDraggable?: boolean; @@ -66,6 +68,7 @@ export const HostOverview = React.memo( ({ anomaliesData, contextID, + sourcererScopeId, data, endDate, id, @@ -109,9 +112,10 @@ export const HostOverview = React.memo( attrName={fieldName} idPrefix={contextID ? `host-overview-${contextID}` : 'host-overview'} isDraggable={isDraggable} + sourcererScopeId={sourcererScopeId} /> ), - [contextID, isDraggable] + [contextID, isDraggable, sourcererScopeId] ); const [hostRiskScore, hostRiskLevel] = useMemo(() => { @@ -229,6 +233,7 @@ export const HostOverview = React.memo( rowItems={getOr([], 'host.ip', data)} attrName={'host.ip'} idPrefix={contextID ? `host-overview-${contextID}` : 'host-overview'} + sourcererScopeId={sourcererScopeId} isDraggable={isDraggable} render={(ip) => (ip != null ? : getEmptyTagValue())} /> @@ -265,7 +270,7 @@ export const HostOverview = React.memo( }, ], ], - [contextID, data, firstColumn, getDefaultRenderer, isDraggable] + [contextID, sourcererScopeId, data, firstColumn, getDefaultRenderer, isDraggable] ); return ( <> @@ -312,7 +317,11 @@ export const HostOverview = React.memo( <> - + {loading && ( ( ({ anomaliesData, contextID, + sourcererScopeId, data, id, isDraggable = false, @@ -109,9 +112,10 @@ export const UserOverview = React.memo( attrName={fieldName} idPrefix={contextID ? `user-overview-${contextID}` : 'user-overview'} isDraggable={isDraggable} + sourcererScopeId={sourcererScopeId} /> ), - [contextID, isDraggable] + [contextID, isDraggable, sourcererScopeId] ); const [userRiskScore, userRiskLevel] = useMemo(() => { @@ -243,6 +247,7 @@ export const UserOverview = React.memo( rowItems={getOr([], 'host.ip', data)} attrName={'host.ip'} idPrefix={contextID ? `user-overview-${contextID}` : 'user-overview'} + sourcererScopeId={sourcererScopeId} isDraggable={isDraggable} render={(ip) => (ip != null ? : getEmptyTagValue())} /> @@ -250,7 +255,16 @@ export const UserOverview = React.memo( }, ], ], - [data, indexPatterns, getDefaultRenderer, contextID, isDraggable, userName, firstColumn] + [ + data, + indexPatterns, + getDefaultRenderer, + contextID, + sourcererScopeId, + isDraggable, + userName, + firstColumn, + ] ); return ( <> diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx index b08988b275094..47ea68dcf75a9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx @@ -44,6 +44,8 @@ jest.mock('../../../common/lib/kibana/kibana_react', () => { }; }); +jest.mock('../../../common/hooks/use_get_field_spec'); + describe('Field Renderers', () => { describe('#locationRenderer', () => { test('it renders correctly against snapshot', () => { @@ -276,7 +278,6 @@ describe('Field Renderers', () => { render( { render( { render( { render( { render( { render( { render( { render( React.ReactNode; rowItems: string[] | null | undefined; + sourcererScopeId?: SourcererScopeName; } export const DefaultFieldRendererComponent: React.FC = ({ @@ -209,6 +211,7 @@ export const DefaultFieldRendererComponent: React.FC moreMaxHeight = DEFAULT_MORE_MAX_HEIGHT, render, rowItems, + sourcererScopeId, }) => { if (rowItems != null && rowItems.length > 0) { const draggables = rowItems.slice(0, displayCount).map((rowItem, index) => { @@ -250,13 +253,12 @@ export const DefaultFieldRendererComponent: React.FC @@ -274,36 +276,33 @@ DefaultFieldRenderer.displayName = 'DefaultFieldRenderer'; interface DefaultFieldRendererOverflowProps { attrName: string; - fieldType: string; rowItems: string[]; idPrefix: string; - isAggregatable?: boolean; render?: (item: string) => React.ReactNode; overflowIndexStart?: number; moreMaxHeight: string; + sourcererScopeId?: SourcererScopeName; } interface MoreContainerProps { fieldName: string; - fieldType: string; values: string[]; idPrefix: string; - isAggregatable?: boolean; moreMaxHeight: string; overflowIndexStart: number; render?: (item: string) => React.ReactNode; + sourcererScopeId?: SourcererScopeName; } export const MoreContainer = React.memo( ({ fieldName, - fieldType, idPrefix, - isAggregatable, moreMaxHeight, overflowIndexStart, render, values, + sourcererScopeId, }) => { const { timelineId } = useContext(TimelineContext); @@ -321,12 +320,11 @@ export const MoreContainer = React.memo( visibleCellActions={5} showActionTooltips triggerId={SecurityCellActionsTrigger.DEFAULT} - field={{ - name: fieldName, + data={{ value, - type: fieldType, - aggregatable: isAggregatable, + field: fieldName, }} + sourcererScopeId={sourcererScopeId ?? SourcererScopeName.default} metadata={{ scopeId: timelineId ?? undefined, }} @@ -339,16 +337,7 @@ export const MoreContainer = React.memo( return acc; }, []), - [ - fieldName, - fieldType, - idPrefix, - overflowIndexStart, - render, - values, - timelineId, - isAggregatable, - ] + [values, overflowIndexStart, idPrefix, fieldName, timelineId, render, sourcererScopeId] ); return ( @@ -377,8 +366,7 @@ export const DefaultFieldRendererOverflow = React.memo { const [isOpen, setIsOpen] = useState(false); const togglePopover = useCallback(() => setIsOpen((currentIsOpen) => !currentIsOpen), []); @@ -420,8 +408,7 @@ export const DefaultFieldRendererOverflow = React.memo )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx index da3c66e415fb0..e1e20845211af 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx @@ -48,6 +48,7 @@ describe('Expandable Host Component', () => { const mockProps = { contextID: 'text-context', hostName: 'testHostName', + scopeId: 'testScopeId', }; describe('ExpandableHostDetails: rendering', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx index bdbd6b28ffee2..d307b9e8273ea 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx @@ -21,6 +21,7 @@ import { AnomalyTableProvider } from '../../../../common/components/ml/anomaly/a import { hostToCriteria } from '../../../../common/components/ml/criteria/host_to_criteria'; import { scoreIntervalToDateTime } from '../../../../common/components/ml/score/score_interval_to_datetime'; import { useHostDetails, ID } from '../../../../explore/hosts/containers/hosts/details'; +import { getSourcererScopeId } from '../../../../helpers'; interface ExpandableHostProps { hostName: string; @@ -53,9 +54,10 @@ export const ExpandableHostDetailsPageLink = ({ hostName }: ExpandableHostProps) export const ExpandableHostDetails = ({ contextID, + scopeId, hostName, isDraggable = false, -}: ExpandableHostProps & { contextID: string; isDraggable?: boolean }) => { +}: ExpandableHostProps & { contextID: string; scopeId: string; isDraggable?: boolean }) => { const { to, from, isInitializing } = useGlobalTime(); /* Normally `selectedPatterns` from useSourcererDataView would be where we obtain the indices, @@ -98,6 +100,7 @@ export const ExpandableHostDetails = ({ {({ isLoadingAnomaliesData, anomaliesData, jobNameById }) => ( void; isFlyoutView?: boolean; @@ -65,7 +66,7 @@ interface HostDetailsProps { } export const HostDetailsPanel: React.FC = React.memo( - ({ contextID, expandedHost, handleOnHostClosed, isDraggable, isFlyoutView }) => { + ({ contextID, scopeId, expandedHost, handleOnHostClosed, isDraggable, isFlyoutView }) => { const { hostName } = expandedHost; if (!hostName) { @@ -81,7 +82,7 @@ export const HostDetailsPanel: React.FC = React.memo( - + ) : ( @@ -111,6 +112,7 @@ export const HostDetailsPanel: React.FC = React.memo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx index 3831188540781..bf590a5d55cdf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx @@ -135,6 +135,7 @@ export const DetailsPanel = React.memo( handleOnHostClosed={closePanel} isDraggable={isDraggable} isFlyoutView={isFlyoutView} + scopeId={scopeId} /> ); } @@ -152,6 +153,7 @@ export const DetailsPanel = React.memo( isDraggable={isDraggable} isFlyoutView={isFlyoutView} isNewUserDetailsFlyoutEnable={isNewUserDetailsFlyoutEnable} + scopeId={scopeId} /> ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/columns.tsx index a876a380c8aea..10283f5f1027e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/columns.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/columns.tsx @@ -27,6 +27,7 @@ import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { scoreIntervalToDateTime } from '../../../../common/components/ml/score/score_interval_to_datetime'; import { InputsModelId } from '../../../../common/store/inputs/constants'; import { setAbsoluteRangeDatePicker } from '../../../../common/store/inputs/actions'; +import { getSourcererScopeId } from '../../../../helpers'; const fieldColumn: EuiBasicTableColumn = { name: i18n.FIELD_COLUMN_TITLE, @@ -45,6 +46,7 @@ const fieldColumn: EuiBasicTableColumn = { export const getManagedUserTableColumns = ( contextID: string, + scopeId: string, isDraggable: boolean ): ManagedUsersTableColumns => [ fieldColumn, @@ -58,6 +60,7 @@ export const getManagedUserTableColumns = ( attrName={field} idPrefix={contextID ? `managedUser-${contextID}` : 'managedUser'} isDraggable={isDraggable} + sourcererScopeId={getSourcererScopeId(scopeId)} /> ) : ( defaultToEmptyTag(value) @@ -75,6 +78,7 @@ function isAnomalies( export const getObservedUserTableColumns = ( contextID: string, + scopeId: string, isDraggable: boolean ): ObservedUsersTableColumns => [ fieldColumn, @@ -96,6 +100,7 @@ export const getObservedUserTableColumns = ( attrName={field} idPrefix={contextID ? `observedUser-${contextID}` : 'observedUser'} isDraggable={isDraggable} + sourcererScopeId={getSourcererScopeId(scopeId)} /> ); }, diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user.test.tsx index 442252e494800..9e0e60caf80de 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user.test.tsx @@ -15,6 +15,7 @@ describe('ManagedUser', () => { const mockProps = { managedUser: mockManagedUser, contextID: '', + scopeId: '', isDraggable: false, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user.tsx index fd05fb8290509..a802b49e85cae 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user.tsx @@ -34,10 +34,12 @@ import { useAppUrl } from '../../../../common/lib/kibana'; export const ManagedUser = ({ managedUser, contextID, + scopeId, isDraggable, }: { managedUser: ManagedUserData; contextID: string; + scopeId: string; isDraggable: boolean; }) => { const { euiTheme } = useEuiTheme(); @@ -47,8 +49,8 @@ export const ManagedUser = ({ setManagedDataToggleOpen((isOpen) => !isOpen); }, [setManagedDataToggleOpen]); const managedUserTableColumns = useMemo( - () => getManagedUserTableColumns(contextID, isDraggable), - [isDraggable, contextID] + () => getManagedUserTableColumns(contextID, scopeId, isDraggable), + [isDraggable, contextID, scopeId] ); const { getAppUrl } = useAppUrl(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.test.tsx index d7493ed3a8921..f57eda9b1fb26 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.test.tsx @@ -15,6 +15,7 @@ describe('ObservedUser', () => { const mockProps = { observedUser: mockObservedUser, contextID: '', + scopeId: '', isDraggable: false, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.tsx index c67975d50d238..01335997813a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.tsx @@ -23,10 +23,12 @@ import { OBSERVED_USER_QUERY_ID } from '../../../../explore/users/containers/use export const ObservedUser = ({ observedUser, contextID, + scopeId, isDraggable, }: { observedUser: ObservedUserData; contextID: string; + scopeId: string; isDraggable: boolean; }) => { const { euiTheme } = useEuiTheme(); @@ -36,8 +38,8 @@ export const ObservedUser = ({ setObservedDataToggleOpen((isOpen) => !isOpen); }, [setObservedDataToggleOpen]); const observedUserTableColumns = useMemo( - () => getObservedUserTableColumns(contextID, isDraggable), - [contextID, isDraggable] + () => getObservedUserTableColumns(contextID, scopeId, isDraggable), + [contextID, scopeId, isDraggable] ); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/user_details_content.stories.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/user_details_content.stories.tsx index 685315c66cf9d..1f82d21c65321 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/user_details_content.stories.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/user_details_content.stories.tsx @@ -27,6 +27,7 @@ storiesOf('UserDetailsContent', module) observedUser={mockObservedUser} riskScoreState={mockRiskScoreState} contextID={'test-user-details'} + scopeId={'test-scopeId'} isDraggable={false} /> )) @@ -49,6 +50,7 @@ storiesOf('UserDetailsContent', module) observedUser={mockObservedUser} riskScoreState={mockRiskScoreState} contextID={'test-user-details'} + scopeId={'test-scopeId'} isDraggable={false} /> )) @@ -71,6 +73,7 @@ storiesOf('UserDetailsContent', module) observedUser={mockObservedUser} riskScoreState={mockRiskScoreState} contextID={'test-user-details'} + scopeId={'test-scopeId'} isDraggable={false} /> )) @@ -105,6 +108,7 @@ storiesOf('UserDetailsContent', module) }} riskScoreState={mockRiskScoreState} contextID={'test-user-details'} + scopeId={'test-scopeId'} isDraggable={false} /> )) @@ -151,6 +155,7 @@ storiesOf('UserDetailsContent', module) }} riskScoreState={mockRiskScoreState} contextID={'test-user-details'} + scopeId={'test-scopeId'} isDraggable={false} /> )); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/user_details_content.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/user_details_content.test.tsx index 6ac3f1a0975f7..2d984cc926e4f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/user_details_content.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/user_details_content.test.tsx @@ -17,6 +17,7 @@ const mockProps = { observedUser: mockObservedUser, riskScoreState: mockRiskScoreState, contextID: 'test-user-details', + scopeId: 'test-scope-id', isDraggable: false, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/user_details_content.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/user_details_content.tsx index baab7d365c99d..95ffb05a16889 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/user_details_content.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/user_details_content.tsx @@ -48,6 +48,7 @@ interface UserDetailsContentComponentProps { managedUser: ManagedUserData; riskScoreState: RiskScoreState; contextID: string; + scopeId: string; isDraggable: boolean; } @@ -61,6 +62,7 @@ export const UserDetailsContentComponent = ({ managedUser, riskScoreState, contextID, + scopeId, isDraggable, }: UserDetailsContentComponentProps) => { const { euiTheme } = useEuiTheme(); @@ -130,9 +132,19 @@ export const UserDetailsContentComponent = ({ - + - + ); }; @@ -140,10 +152,12 @@ export const UserDetailsContentComponent = ({ export const UserDetailsContent = ({ userName, contextID, + scopeId, isDraggable = false, }: { userName: string; contextID: string; + scopeId: string; isDraggable?: boolean; }) => { const { to, from, isInitializing } = useGlobalTime(); @@ -174,6 +188,7 @@ export const UserDetailsContent = ({ }} riskScoreState={riskScoreState} contextID={contextID} + scopeId={scopeId} isDraggable={isDraggable} /> )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/expandable_user.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/expandable_user.test.tsx index 48d15017349e9..8d66538a242a8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/expandable_user.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/expandable_user.test.tsx @@ -47,6 +47,7 @@ describe('Expandable Host Component', () => { }); const mockProps = { contextID: 'text-context', + scopeId: 'testScopeId', userName: 'testUserName', isDraggable: true, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/expandable_user.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/expandable_user.tsx index f12ea6b0ada14..b85b31c2185c5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/expandable_user.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/expandable_user.tsx @@ -21,6 +21,7 @@ import { getCriteriaFromUsersType } from '../../../../common/components/ml/crite import { scoreIntervalToDateTime } from '../../../../common/components/ml/score/score_interval_to_datetime'; import { AnomalyTableProvider } from '../../../../common/components/ml/anomaly/anomaly_table_provider'; import { UsersType } from '../../../../explore/users/store/model'; +import { getSourcererScopeId } from '../../../../helpers'; export const QUERY_ID = 'usersDetailsQuery'; export interface ExpandableUserProps { @@ -54,9 +55,10 @@ export const ExpandableUserDetailsPageLink = ({ userName }: ExpandableUserProps) export const ExpandableUserDetails = ({ contextID, + scopeId, userName, isDraggable, -}: ExpandableUserProps & { contextID: string; isDraggable?: boolean }) => { +}: ExpandableUserProps & { contextID: string; scopeId: string; isDraggable?: boolean }) => { const { to, from, isInitializing } = useGlobalTime(); const { selectedPatterns } = useSourcererDataView(); const dispatch = useDispatch(); @@ -97,6 +99,7 @@ export const ExpandableUserDetails = ({ data={userDetails} loading={loading} contextID={contextID} + sourcererScopeId={getSourcererScopeId(scopeId)} isDraggable={isDraggable} id={QUERY_ID} anomaliesData={anomaliesData} diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/index.tsx index 2041b1e00bef4..17a1e21ceebd0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/index.tsx @@ -16,6 +16,7 @@ import * as i18n from './translations'; const UserDetailsPanelComponent = ({ contextID, + scopeId, userName, handleOnClose, isFlyoutView, @@ -25,7 +26,12 @@ const UserDetailsPanelComponent = ({ if (isNewUserDetailsFlyoutEnable) { return isFlyoutView ? ( - + ) : (
@@ -39,19 +45,25 @@ const UserDetailsPanelComponent = ({ `} /> - +
); } return isFlyoutView ? ( - + ) : ( ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/types.ts b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/types.ts index 4c22322888cc1..96d17c57c08f4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/types.ts @@ -7,6 +7,7 @@ export interface UserDetailsProps { contextID: string; + scopeId: string; userName: string; handleOnClose: () => void; isFlyoutView?: boolean; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_flyout.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_flyout.tsx index 9ffbeabf6609f..9bc87bdc49751 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_flyout.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_flyout.tsx @@ -33,8 +33,9 @@ const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` export const UserDetailsFlyout = ({ contextID, + scopeId, userName, -}: Pick) => ( +}: Pick) => ( <> @@ -43,7 +44,7 @@ export const UserDetailsFlyout = ({ - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_side_panel.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_side_panel.tsx index 03cb2fbe785a7..cf3ea34b8f925 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_side_panel.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_side_panel.tsx @@ -34,10 +34,14 @@ const StyledPanelContent = styled.div` export const UserDetailsSidePanel = ({ contextID, + scopeId, userName, isDraggable, handleOnClose, -}: Pick) => ( +}: Pick< + UserDetailsProps, + 'scopeId' | 'contextID' | 'userName' | 'isDraggable' | 'handleOnClose' +>) => ( <> @@ -62,7 +66,12 @@ export const UserDetailsSidePanel = ({ - + ); diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index b2edb9f439ef3..e3cdfea281f98 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -72,6 +72,7 @@ export type ColumnHeaderOptions = Pick< | 'schema' > & { aggregatable?: boolean; + searchable?: boolean; dataTableCellActions?: DataTableCellAction[]; category?: string; columnHeaderType: ColumnHeaderType;