From 2c91688d288911a903e7148c1ac7726992bacf4e Mon Sep 17 00:00:00 2001 From: semd Date: Wed, 25 Jan 2023 14:13:52 +0100 Subject: [PATCH 01/18] cell actions type and fix add to timeline empty value --- .../src/components/hover_actions_popover.tsx | 36 ++--- .../src/components/inline_actions.tsx | 10 +- .../use_data_grid_column_cell_actions.tsx | 9 +- .../src/hooks/use_load_actions.test.ts | 125 ++++++++++++------ .../src/hooks/use_load_actions.ts | 38 ++++-- packages/kbn-cell-actions/src/index.ts | 6 +- packages/kbn-cell-actions/src/types.ts | 74 +++++++---- .../actions/add_to_timeline/data_provider.ts | 123 ++++++++++------- .../default/add_to_timeline.tsx | 14 +- .../lens/add_to_timeline.test.ts | 42 +++++- .../default/copy_to_clipboard.tsx | 66 +++++---- .../actions/filter/default/filter_in.tsx | 45 +++---- .../actions/filter/default/filter_out.tsx | 46 +++---- .../actions/filter/timeline/filter_in.tsx | 15 +-- .../actions/filter/timeline/filter_out.tsx | 13 +- .../actions/show_top_n/default/show_top_n.tsx | 15 +-- .../show_top_n/show_top_n_component.tsx | 11 +- 17 files changed, 403 insertions(+), 285 deletions(-) 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 c908ad450b1f9..2ac83991a83b6 100644 --- a/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx +++ b/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx @@ -51,9 +51,8 @@ export const HoverActionsPopover: React.FC = ({ const contentRef = useRef(null); const [isExtraActionsPopoverOpen, setIsExtraActionsPopoverOpen] = useState(false); const [showHoverContent, setShowHoverContent] = useState(false); - const popoverRef = useRef(null); - const [{ value: actions, error }, loadActions] = useLoadActionsFn(); + const [{ value: actions }, loadActions] = useLoadActionsFn(); const { visibleActions, extraActions } = useMemo( () => partitionActions(actions ?? [], visibleCellActions), @@ -69,6 +68,11 @@ export const HoverActionsPopover: React.FC = ({ }, HOVER_INTENT_DELAY), [] ); + useEffect(() => { + return () => { + openPopOverDebounced.cancel(); + }; + }, [openPopOverDebounced]); const closePopover = useCallback(() => { openPopOverDebounced.cancel(); @@ -85,13 +89,6 @@ export const HoverActionsPopover: React.FC = ({ closePopover(); }, [closePopover, setIsExtraActionsPopoverOpen]); - // prevent setState on an unMounted component - useEffect(() => { - return () => { - openPopOverDebounced.cancel(); - }; - }, [openPopOverDebounced]); - const onMouseEnter = useCallback(async () => { // Do not open actions with extra action popover is open if (isExtraActionsPopoverOpen) return; @@ -104,10 +101,6 @@ export const HoverActionsPopover: React.FC = ({ openPopOverDebounced(); }, [isExtraActionsPopoverOpen, actions, openPopOverDebounced, loadActions, actionContext]); - const onMouseLeave = useCallback(() => { - closePopover(); - }, [closePopover]); - const content = useMemo(() => { return ( // Hack - Forces extra actions popover to close when hover content is clicked. @@ -120,18 +113,11 @@ export const HoverActionsPopover: React.FC = ({ ); }, [onMouseEnter, closeExtraActions, children]); - useEffect(() => { - if (error) { - throw error; - } - }, [error]); - return ( <> -
+
= ({ data-test-subj={'hoverActionsPopover'} aria-label={ACTIONS_AREA_LABEL} > - {showHoverContent ? ( + {showHoverContent && (

{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(actionContext.field.name)}

@@ -156,14 +142,14 @@ export const HoverActionsPopover: React.FC = ({ showTooltip={showActionTooltips} /> ))} - {extraActions.length > 0 ? ( + {extraActions.length > 0 && ( - ) : null} + )}
- ) : null} + )}
= ({ showActionTooltips, visibleCellActions, }) => { - const { value: allActions, error } = useLoadActions(actionContext); + const { value: allActions } = useLoadActions(actionContext); const { extraActions, visibleActions } = usePartitionActions( allActions ?? [], visibleCellActions @@ -38,12 +38,6 @@ export const InlineActions: React.FC = ({ [togglePopOver, showActionTooltips] ); - useEffect(() => { - if (error) { - throw error; - } - }, [error]); - return ( {visibleActions.map((action, index) => ( 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 6212ad2c30776..f8ccabd1bcd41 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 @@ -10,6 +10,7 @@ import React, { useMemo, useRef } from 'react'; import { EuiLoadingSpinner, type EuiDataGridColumnCellAction } from '@elastic/eui'; import type { CellAction, + CellActionCompatibilityContext, CellActionExecutionContext, CellActionField, CellActionsProps, @@ -32,7 +33,7 @@ export const useDataGridColumnsCellActions = ({ triggerId, metadata, }: UseDataGridColumnsCellActionsProps): EuiDataGridColumnCellAction[][] => { - const bulkContexts: CellActionExecutionContext[] = useMemo( + 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 @@ -74,7 +75,7 @@ const createColumnCellAction = ({ triggerId, }: CreateColumnCellActionParams): EuiDataGridColumnCellAction => function ColumnCellAction({ Component, rowIndex }) { - const nodeRef = useRef(null); + const nodeRef = useRef(null); const extraContentNodeRef = useRef(null); const { name, type, values } = field; @@ -91,7 +92,7 @@ const createColumnCellAction = ({ return ( nodeRef} + buttonRef={nodeRef} aria-label={action.getDisplayName(actionContext)} title={action.getDisplayName(actionContext)} data-test-subj={`dataGridColumnCellAction-${action.id}`} @@ -101,7 +102,7 @@ const createColumnCellAction = ({ }} > {action.getDisplayName(actionContext)} -
extraContentNodeRef} /> +
); }; diff --git a/packages/kbn-cell-actions/src/hooks/use_load_actions.test.ts b/packages/kbn-cell-actions/src/hooks/use_load_actions.test.ts index 74cb5091a5efb..deff7dd9c9d88 100644 --- a/packages/kbn-cell-actions/src/hooks/use_load_actions.test.ts +++ b/packages/kbn-cell-actions/src/hooks/use_load_actions.test.ts @@ -16,70 +16,117 @@ jest.mock('../context/cell_actions_context', () => ({ useCellActionsContext: () => ({ getActions: mockGetActions }), })); -describe('useLoadActions', () => { +describe('loadActions hooks', () => { const actionContext = makeActionContext(); beforeEach(() => { jest.clearAllMocks(); }); + describe('useLoadActions', () => { + it('should load actions when called', async () => { + const { result, waitForNextUpdate } = renderHook(useLoadActions, { + initialProps: actionContext, + }); - it('loads actions when useLoadActions called', async () => { - const { result, waitForNextUpdate } = renderHook(useLoadActions, { - initialProps: actionContext, + expect(result.current.value).toBeUndefined(); + expect(result.current.loading).toEqual(true); + expect(mockGetActions).toHaveBeenCalledTimes(1); + expect(mockGetActions).toHaveBeenCalledWith(actionContext); + + await waitForNextUpdate(); + + expect(result.current.value).toEqual([action]); + expect(result.current.loading).toEqual(false); }); - expect(result.current.value).toBeUndefined(); - expect(result.current.loading).toEqual(true); - expect(mockGetActions).toHaveBeenCalledTimes(1); - expect(mockGetActions).toHaveBeenCalledWith(actionContext); + it('should throw error when getAction is rejected', async () => { + const message = 'some division by 0'; + mockGetActions.mockRejectedValueOnce(Error(message)); - await waitForNextUpdate(); + const { result, waitForNextUpdate } = renderHook(useLoadActions, { + initialProps: actionContext, + }); + await waitForNextUpdate(); - expect(result.current.value).toEqual([action]); - expect(result.current.loading).toEqual(false); + expect(result.error?.message).toEqual(message); + }); }); - it('loads actions when useLoadActionsFn function is called', async () => { - const { result, waitForNextUpdate } = renderHook(useLoadActionsFn); - const [{ value: valueBeforeCall, loading: loadingBeforeCall }, loadActions] = result.current; + describe('useLoadActionsFn', () => { + it('should load actions when returned function is called', async () => { + const { result, waitForNextUpdate } = renderHook(useLoadActionsFn); + const [{ value: valueBeforeCall, loading: loadingBeforeCall }, loadActions] = result.current; + + expect(valueBeforeCall).toBeUndefined(); + expect(loadingBeforeCall).toEqual(false); + expect(mockGetActions).not.toHaveBeenCalled(); - expect(valueBeforeCall).toBeUndefined(); - expect(loadingBeforeCall).toEqual(false); - expect(mockGetActions).not.toHaveBeenCalled(); + act(() => { + loadActions(actionContext); + }); - act(() => { - loadActions(actionContext); + const [{ value: valueAfterCall, loading: loadingAfterCall }] = result.current; + expect(valueAfterCall).toBeUndefined(); + expect(loadingAfterCall).toEqual(true); + expect(mockGetActions).toHaveBeenCalledTimes(1); + expect(mockGetActions).toHaveBeenCalledWith(actionContext); + + await waitForNextUpdate(); + + const [{ value: valueAfterUpdate, loading: loadingAfterUpdate }] = result.current; + expect(valueAfterUpdate).toEqual([action]); + expect(loadingAfterUpdate).toEqual(false); }); - const [{ value: valueAfterCall, loading: loadingAfterCall }] = result.current; - expect(valueAfterCall).toBeUndefined(); - expect(loadingAfterCall).toEqual(true); - expect(mockGetActions).toHaveBeenCalledTimes(1); - expect(mockGetActions).toHaveBeenCalledWith(actionContext); + it('should throw error when getAction is rejected', async () => { + const message = 'some division by 0'; + mockGetActions.mockRejectedValueOnce(Error(message)); + + const { result, waitForNextUpdate } = renderHook(useLoadActionsFn); + const [_, loadActions] = result.current; - await waitForNextUpdate(); + expect(result.error).toBeUndefined(); - const [{ value: valueAfterUpdate, loading: loadingAfterUpdate }] = result.current; - expect(valueAfterUpdate).toEqual([action]); - expect(loadingAfterUpdate).toEqual(false); + act(() => { + loadActions(actionContext); + }); + await waitForNextUpdate(); + + expect(result.error?.message).toEqual(message); + }); }); - it('loads bulk actions array when useBulkLoadActions is called', async () => { + describe('useBulkLoadActions', () => { const actionContext2 = makeActionContext({ trigger: { id: 'triggerId2' } }); const actionContexts = [actionContext, actionContext2]; - const { result, waitForNextUpdate } = renderHook(useBulkLoadActions, { - initialProps: actionContexts, + + it('should load bulk actions array when called', async () => { + const { result, waitForNextUpdate } = renderHook(useBulkLoadActions, { + initialProps: actionContexts, + }); + + expect(result.current.value).toBeUndefined(); + expect(result.current.loading).toEqual(true); + expect(mockGetActions).toHaveBeenCalledTimes(2); + expect(mockGetActions).toHaveBeenCalledWith(actionContext); + expect(mockGetActions).toHaveBeenCalledWith(actionContext2); + + await waitForNextUpdate(); + + expect(result.current.value).toEqual([[action], [action]]); + expect(result.current.loading).toEqual(false); }); - expect(result.current.value).toBeUndefined(); - expect(result.current.loading).toEqual(true); - expect(mockGetActions).toHaveBeenCalledTimes(2); - expect(mockGetActions).toHaveBeenCalledWith(actionContext); - expect(mockGetActions).toHaveBeenCalledWith(actionContext2); + it('should throw error when getAction is rejected', async () => { + const message = 'some division by 0'; + mockGetActions.mockRejectedValueOnce(Error(message)); - await waitForNextUpdate(); + const { result, waitForNextUpdate } = renderHook(useBulkLoadActions, { + initialProps: actionContexts, + }); + await waitForNextUpdate(); - expect(result.current.value).toEqual([[action], [action]]); - expect(result.current.loading).toEqual(false); + expect(result.error?.message).toEqual(message); + }); }); }); diff --git a/packages/kbn-cell-actions/src/hooks/use_load_actions.ts b/packages/kbn-cell-actions/src/hooks/use_load_actions.ts index 85d8f64042f93..23443c881e629 100644 --- a/packages/kbn-cell-actions/src/hooks/use_load_actions.ts +++ b/packages/kbn-cell-actions/src/hooks/use_load_actions.ts @@ -6,31 +6,53 @@ * Side Public License, v 1. */ +import { useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; -import useAsyncFn from 'react-use/lib/useAsyncFn'; +import useAsyncFn, { type AsyncState } from 'react-use/lib/useAsyncFn'; import { useCellActionsContext } from '../context/cell_actions_context'; -import { CellActionExecutionContext } from '../types'; +import type { CellAction, CellActionCompatibilityContext, GetActions } from '../types'; + +type AsyncActions = Omit, 'error'>; + +const useThrowError = (error?: Error) => { + useMemo(() => { + if (error) { + throw error; + } + }, [error]); +}; /** * Performs the getActions async call and returns its value */ -export const useLoadActions = (context: CellActionExecutionContext) => { +export const useLoadActions = (context: CellActionCompatibilityContext): AsyncActions => { const { getActions } = useCellActionsContext(); - return useAsync(() => getActions(context), []); + const { error, ...actionsState } = useAsync(() => getActions(context), []); + useThrowError(error); + return actionsState; }; /** * Returns a function to perform the getActions async call */ -export const useLoadActionsFn = () => { +export const useLoadActionsFn = (): [AsyncActions, GetActions] => { const { getActions } = useCellActionsContext(); - return useAsyncFn(getActions, []); + const [{ error, ...actionsState }, loadActions] = useAsyncFn(getActions, []); + useThrowError(error); + return [actionsState, loadActions]; }; /** * Groups getActions calls for an array of contexts in one async bulk operation */ -export const useBulkLoadActions = (contexts: CellActionExecutionContext[]) => { +export const useBulkLoadActions = ( + contexts: CellActionCompatibilityContext[] +): AsyncActions => { const { getActions } = useCellActionsContext(); - return useAsync(() => Promise.all(contexts.map((context) => getActions(context))), []); + const { error, ...actionsState } = useAsync( + () => Promise.all(contexts.map((context) => getActions(context))), + [] + ); + useThrowError(error); + return actionsState; }; diff --git a/packages/kbn-cell-actions/src/index.ts b/packages/kbn-cell-actions/src/index.ts index 69c4b0fbc6e13..61c74c7c92621 100644 --- a/packages/kbn-cell-actions/src/index.ts +++ b/packages/kbn-cell-actions/src/index.ts @@ -10,4 +10,8 @@ export { CellActions } from './components'; export { CellActionsProvider } from './context'; export { useDataGridColumnsCellActions, type UseDataGridColumnsCellActionsProps } from './hooks'; export { CellActionsMode } from './types'; -export type { CellAction, CellActionExecutionContext } from './types'; +export type { + CellAction, + CellActionExecutionContext, + CellActionCompatibilityContext, +} from './types'; diff --git a/packages/kbn-cell-actions/src/types.ts b/packages/kbn-cell-actions/src/types.ts index 592a412e7fb68..978aed54b770a 100644 --- a/packages/kbn-cell-actions/src/types.ts +++ b/packages/kbn-cell-actions/src/types.ts @@ -12,8 +12,6 @@ import type { UiActionsService, } from '@kbn/ui-actions-plugin/public'; -export type CellAction = Action; - export interface CellActionsProviderProps { /** * Please assign `uiActions.getTriggerCompatibleActions` function. @@ -22,8 +20,6 @@ export interface CellActionsProviderProps { getTriggerCompatibleActions: UiActionsService['getTriggerCompatibleActions']; } -export type GetActions = (context: CellActionExecutionContext) => Promise; - export interface CellActionField { /** * Field name. @@ -39,30 +35,7 @@ export interface CellActionField { * Field value. * Example: 'My-Laptop' */ - value?: string | string[] | null; -} - -export interface PartitionedActions { - extraActions: CellAction[]; - visibleActions: CellAction[]; -} - -export interface CellActionExecutionContext extends ActionExecutionContext { - field: CellActionField; - /** - * Ref to a DOM node where the action can add custom HTML. - */ - extraContentNodeRef?: React.MutableRefObject; - - /** - * Ref to the node where the cell action are rendered. - */ - nodeRef?: React.MutableRefObject; - - /** - * Extra configurations for actions. - */ - metadata?: Record; + value: string | string[] | null | undefined; } export enum CellActionsMode { @@ -103,3 +76,48 @@ export interface CellActionsProps { */ metadata?: Record; } + +export interface CellActionExecutionContext extends ActionExecutionContext { + field: CellActionField; + /** + * Ref to a DOM node where the action can add custom HTML. + */ + extraContentNodeRef: React.MutableRefObject; + + /** + * Ref to the node where the cell action are rendered. + */ + nodeRef: React.MutableRefObject; + + /** + * Extra configurations for actions. + */ + metadata?: Record; +} + +export interface CellActionCompatibilityContext extends ActionExecutionContext { + /** + * The object containing the field name and type, needed for the compatibility check + */ + field: Pick; + /** + * Extra configurations for actions. + */ + metadata?: Record; +} + +export interface CellAction + extends Omit, 'isCompatible'> { + /** + * Returns a promise that resolves to true if this action is compatible given the context, + * otherwise resolves to false. + */ + isCompatible(context: CellActionCompatibilityContext): Promise; +} + +export type GetActions = (context: CellActionCompatibilityContext) => Promise; + +export interface PartitionedActions { + extraActions: CellAction[]; + visibleActions: CellAction[]; +} diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/data_provider.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/data_provider.ts index 3b51e8bed8439..46420a8fccf57 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/data_provider.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/data_provider.ts @@ -8,9 +8,8 @@ import { escapeDataProviderId } from '@kbn/securitysolution-t-grid'; import { isArray, isString, isEmpty } from 'lodash/fp'; import { INDICATOR_REFERENCE } from '../../../common/cti/constants'; -import type { DataProvider } from '../../../common/types'; -import { IS_OPERATOR } from '../../../common/types'; -import type { BrowserField } from '../../common/containers/source'; +import type { DataProvider, QueryOperator } from '../../../common/types'; +import { EXISTS_OPERATOR, IS_OPERATOR } from '../../../common/types'; import { IP_FIELD_TYPE } from '../../explore/network/components/ip'; import { PORT_NAMES } from '../../explore/network/components/port/helpers'; import { EVENT_DURATION_FIELD_NAME } from '../../timelines/components/duration'; @@ -18,27 +17,37 @@ import { BYTES_FORMAT } from '../../timelines/components/timeline/body/renderers import { GEO_FIELD_TYPE, MESSAGE_FIELD_NAME, - HOST_NAME_FIELD_NAME, SIGNAL_RULE_NAME_FIELD_NAME, EVENT_MODULE_FIELD_NAME, SIGNAL_STATUS_FIELD_NAME, - AGENT_STATUS_FIELD_NAME, RULE_REFERENCE_FIELD_NAME, REFERENCE_URL_FIELD_NAME, EVENT_URL_FIELD_NAME, } from '../../timelines/components/timeline/body/renderers/constants'; -export const getDataProvider = (field: string, id: string, value: string): DataProvider => ({ +export const getDataProvider = ({ + field, + id, + value, + operator = IS_OPERATOR, + excluded = false, +}: { + field: string; + id: string; + value: string; + operator?: QueryOperator; + excluded?: boolean; +}): DataProvider => ({ and: [], enabled: true, id: escapeDataProviderId(id), name: field, - excluded: false, + excluded, kqlQuery: '', queryMatch: { field, value, - operator: IS_OPERATOR, + operator, }, }); @@ -47,10 +56,7 @@ export interface CreateDataProviderParams { eventId?: string; field?: string; fieldFormat?: string; - fieldFromBrowserField?: BrowserField; fieldType?: string; - isObjectArray?: boolean; - linkValue?: string | null; values: string | string[] | null | undefined; } @@ -60,20 +66,31 @@ export const createDataProviders = ({ field, fieldFormat, fieldType, - linkValue, values, }: CreateDataProviderParams) => { - if (field == null || values === null || values === undefined) return null; + if (field == null) return null; + const arrayValues = Array.isArray(values) ? values : [values]; + return arrayValues.reduce((dataProviders, value, index) => { let id: string = ''; - const appendedUniqueId = `${contextId}${ - eventId ? `-${eventId}` : '' - }-${field}-${index}-${value}`; + const appendedUniqueId = `${contextId}${eventId ? `-${eventId}` : ''}-${field}-${index}${ + value ? `-${value}` : '' + }`; if (fieldType === GEO_FIELD_TYPE || field === MESSAGE_FIELD_NAME) { return dataProviders; - } else if (fieldType === IP_FIELD_TYPE) { + } + + if (value == null) { + id = `empty-value-${appendedUniqueId}`; + dataProviders.push( + getDataProvider({ field, id, value: '', excluded: true, operator: EXISTS_OPERATOR }) + ); + return dataProviders; + } + + if (fieldType === IP_FIELD_TYPE) { id = `formatted-ip-data-provider-${contextId}-${field}-${value}${ eventId ? `-${eventId}` : '' }`; @@ -85,41 +102,53 @@ export const createDataProviders = ({ // Default to keeping the existing string value } if (isArray(addresses)) { - addresses.forEach((ip) => dataProviders.push(getDataProvider(field, id, ip))); + addresses.forEach((ip) => dataProviders.push(getDataProvider({ field, id, value: ip }))); } else { - dataProviders.push(getDataProvider(field, id, addresses)); + dataProviders.push(getDataProvider({ field, id, value: addresses })); } return dataProviders; } - } else if (PORT_NAMES.some((portName) => field === portName)) { - id = `port-default-draggable-${appendedUniqueId}`; - } else if (field === EVENT_DURATION_FIELD_NAME) { - id = `duration-default-draggable-${appendedUniqueId}`; - } else if (field === HOST_NAME_FIELD_NAME) { - id = `event-details-value-default-draggable-${appendedUniqueId}`; - } else if (fieldFormat === BYTES_FORMAT) { - id = `bytes-default-draggable-${appendedUniqueId}`; - } else if (field === SIGNAL_RULE_NAME_FIELD_NAME) { - id = `event-details-value-default-draggable-${appendedUniqueId}-${linkValue}`; - } else if (field === EVENT_MODULE_FIELD_NAME) { - id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`; - } else if (field === SIGNAL_STATUS_FIELD_NAME) { - id = `alert-details-value-default-draggable-${appendedUniqueId}`; - } else if (field === AGENT_STATUS_FIELD_NAME) { - id = `event-details-value-default-draggable-${appendedUniqueId}`; - } else if ( - [ - RULE_REFERENCE_FIELD_NAME, - REFERENCE_URL_FIELD_NAME, - EVENT_URL_FIELD_NAME, - INDICATOR_REFERENCE, - ].includes(field) - ) { - id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`; - } else { - id = `event-details-value-default-draggable-${appendedUniqueId}`; } - dataProviders.push(getDataProvider(field, id, value)); + + id = getIdForField({ field, fieldFormat, appendedUniqueId, value }); + dataProviders.push(getDataProvider({ field, id, value })); return dataProviders; }, []); }; + +const getIdForField = ({ + field, + fieldFormat, + appendedUniqueId, + value, +}: { + field: string; + fieldFormat?: string; + appendedUniqueId: string; + value: string; +}) => { + let id: string; + if (PORT_NAMES.some((portName) => field === portName)) { + id = `port-default-${appendedUniqueId}`; + } else if (field === EVENT_DURATION_FIELD_NAME) { + id = `duration-default-${appendedUniqueId}`; + } else if (fieldFormat === BYTES_FORMAT) { + id = `bytes-default-${appendedUniqueId}`; + } else if (field === SIGNAL_STATUS_FIELD_NAME) { + id = `alert-field-default-${appendedUniqueId}`; + } else if ( + [ + RULE_REFERENCE_FIELD_NAME, + REFERENCE_URL_FIELD_NAME, + EVENT_URL_FIELD_NAME, + INDICATOR_REFERENCE, + SIGNAL_RULE_NAME_FIELD_NAME, + EVENT_MODULE_FIELD_NAME, + ].includes(field) + ) { + id = `event-field-default-${appendedUniqueId}-${value}`; + } else { + id = `event-field-default-${appendedUniqueId}`; + } + return id; +}; diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/default/add_to_timeline.tsx b/x-pack/plugins/security_solution/public/actions/add_to_timeline/default/add_to_timeline.tsx index f0710983da2da..dd132d2f95892 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/default/add_to_timeline.tsx +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/default/add_to_timeline.tsx @@ -5,13 +5,12 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/cell-actions'; -import { createAction } from '@kbn/ui-actions-plugin/public'; +import type { CellAction } from '@kbn/cell-actions'; import { addProvider } from '../../../timelines/store/timeline/actions'; import { TimelineId } from '../../../../common/types'; import { KibanaServices } from '../../../common/lib/kibana'; import type { SecurityAppStore } from '../../../common/store'; -import { isInSecurityApp } from '../../utils'; +import { fieldHasCellActions, isInSecurityApp } from '../../utils'; import { ADD_TO_TIMELINE, ADD_TO_TIMELINE_FAILED_TEXT, @@ -29,7 +28,7 @@ export const createAddToTimelineAction = ({ }: { store: SecurityAppStore; order?: number; -}) => { +}): CellAction => { const { application: applicationService, notifications: notificationsService } = KibanaServices.get(); let currentAppId: string | undefined; @@ -37,7 +36,7 @@ export const createAddToTimelineAction = ({ currentAppId = appId; }); - return createAction({ + return { id: ACTION_ID, type: ACTION_ID, order, @@ -45,7 +44,7 @@ export const createAddToTimelineAction = ({ getDisplayName: () => ADD_TO_TIMELINE, getDisplayNameTooltip: () => ADD_TO_TIMELINE, isCompatible: async ({ field }) => - isInSecurityApp(currentAppId) && field.name != null && field.value != null, + isInSecurityApp(currentAppId) && fieldHasCellActions(field.name), execute: async ({ field }) => { const dataProviders = createDataProviders({ @@ -62,7 +61,6 @@ export const createAddToTimelineAction = ({ if (field.value != null) { messageValue = Array.isArray(field.value) ? field.value.join(', ') : field.value; } - notificationsService.toasts.addSuccess({ title: ADD_TO_TIMELINE_SUCCESS_TITLE(messageValue), }); @@ -73,5 +71,5 @@ export const createAddToTimelineAction = ({ }); } }, - }); + }; }; diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/lens/add_to_timeline.test.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/lens/add_to_timeline.test.ts index 64cb526498e04..939e0d99948c3 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/lens/add_to_timeline.test.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/lens/add_to_timeline.test.ts @@ -168,7 +168,7 @@ describe('Lens createAddToTimelineAction', () => { and: [], enabled: true, excluded: false, - id: 'event-details-value-default-draggable-timeline-1-event_1-user_name-0-the value', + id: 'event-field-default-timeline-1-event_1-user_name-0-the value', kqlQuery: '', name: 'user.name', queryMatch: { @@ -190,19 +190,47 @@ describe('Lens createAddToTimelineAction', () => { expect(mockWarningToast).not.toHaveBeenCalled(); }); - it('should show warning if no provider added', async () => { + it('should add exclusive provider for empty values', async () => { await addToTimelineAction.execute({ ...context, - data: [], + data: [{ columnMeta }], }); - expect(mockDispatch).not.toHaveBeenCalled(); - expect(mockWarningToast).toHaveBeenCalled(); + expect(mockDispatch).toHaveBeenCalledTimes(2); + expect(mockDispatch).toHaveBeenCalledWith({ + type: addProvider.type, + payload: { + id: TimelineId.active, + providers: [ + { + and: [], + enabled: true, + excluded: true, + id: 'empty-value-timeline-1-user_name-0', + kqlQuery: '', + name: 'user.name', + queryMatch: { + field: 'user.name', + operator: ':*', + value: '', + }, + }, + ], + }, + }); + expect(mockDispatch).toHaveBeenCalledWith({ + type: showTimeline.type, + payload: { + id: TimelineId.active, + show: true, + }, + }); + expect(mockWarningToast).not.toHaveBeenCalled(); }); - it('should show warning if no value in the data', async () => { + it('should show warning if no provider added', async () => { await addToTimelineAction.execute({ ...context, - data: [{ columnMeta }], + data: [], }); expect(mockDispatch).not.toHaveBeenCalled(); expect(mockWarningToast).toHaveBeenCalled(); diff --git a/x-pack/plugins/security_solution/public/actions/copy_to_clipboard/default/copy_to_clipboard.tsx b/x-pack/plugins/security_solution/public/actions/copy_to_clipboard/default/copy_to_clipboard.tsx index 0cc7a34efb39c..9c0b65c42ff21 100644 --- a/x-pack/plugins/security_solution/public/actions/copy_to_clipboard/default/copy_to_clipboard.tsx +++ b/x-pack/plugins/security_solution/public/actions/copy_to_clipboard/default/copy_to_clipboard.tsx @@ -5,45 +5,43 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/cell-actions'; +import type { CellAction } from '@kbn/cell-actions'; import copy from 'copy-to-clipboard'; -import { createAction } from '@kbn/ui-actions-plugin/public'; import { COPY_TO_CLIPBOARD, COPY_TO_CLIPBOARD_ICON, COPY_TO_CLIPBOARD_SUCCESS } from '../constants'; import { KibanaServices } from '../../../common/lib/kibana'; +import { fieldHasCellActions } from '../../utils'; const ID = 'security_copyToClipboard'; -export const createCopyToClipboardAction = ({ order }: { order?: number }) => - createAction({ - id: ID, - type: ID, - order, - getIconType: (): string => COPY_TO_CLIPBOARD_ICON, - getDisplayName: () => COPY_TO_CLIPBOARD, - getDisplayNameTooltip: () => COPY_TO_CLIPBOARD, - isCompatible: async (context) => context.field.name != null && context.field.value != null, - execute: async ({ field }) => { - const { notifications } = KibanaServices.get(); +export const createCopyToClipboardAction = ({ order }: { order?: number }): CellAction => ({ + id: ID, + type: ID, + order, + getIconType: (): string => COPY_TO_CLIPBOARD_ICON, + getDisplayName: () => COPY_TO_CLIPBOARD, + getDisplayNameTooltip: () => COPY_TO_CLIPBOARD, + isCompatible: async ({ field }) => fieldHasCellActions(field.name), + execute: async ({ field }) => { + const { notifications } = KibanaServices.get(); - let textValue: undefined | string; - if (field.value != null) { - textValue = Array.isArray(field.value) - ? field.value.map((value) => `"${value}"`).join(', ') - : `"${field.value}"`; - } - const text = textValue ? `${field.name}: ${textValue}` : field.name; + let textValue: undefined | string; + if (field.value != null) { + textValue = Array.isArray(field.value) + ? field.value.map((value) => `"${value}"`).join(', ') + : `"${field.value}"`; + } + const text = textValue ? `${field.name}: ${textValue}` : field.name; + const isSuccess = copy(text, { debug: true }); - const isSuccess = copy(text, { debug: true }); - - if (isSuccess) { - notifications.toasts.addSuccess( - { - title: COPY_TO_CLIPBOARD_SUCCESS, - }, - { - toastLifeTimeMs: 800, - } - ); - } - }, - }); + if (isSuccess) { + notifications.toasts.addSuccess( + { + title: COPY_TO_CLIPBOARD_SUCCESS, + }, + { + toastLifeTimeMs: 800, + } + ); + } + }, +}); diff --git a/x-pack/plugins/security_solution/public/actions/filter/default/filter_in.tsx b/x-pack/plugins/security_solution/public/actions/filter/default/filter_in.tsx index 7beea5154bd9d..3afa93ab5d567 100644 --- a/x-pack/plugins/security_solution/public/actions/filter/default/filter_in.tsx +++ b/x-pack/plugins/security_solution/public/actions/filter/default/filter_in.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/cell-actions'; -import { createAction } from '@kbn/ui-actions-plugin/public'; +import type { CellAction } from '@kbn/cell-actions'; import { i18n } from '@kbn/i18n'; import { createFilter } from '../helpers'; import { KibanaServices } from '../../../common/lib/kibana'; +import { fieldHasCellActions } from '../../utils'; export const FILTER_IN = i18n.translate('xpack.securitySolution.actions.filterIn', { defaultMessage: 'Filter In', @@ -17,26 +17,25 @@ export const FILTER_IN = i18n.translate('xpack.securitySolution.actions.filterIn const ID = 'security_filterIn'; const ICON = 'plusInCircle'; -export const createFilterInAction = ({ order }: { order?: number }) => - createAction({ - id: ID, - type: ID, - order, - getIconType: (): string => ICON, - getDisplayName: () => FILTER_IN, - getDisplayNameTooltip: () => FILTER_IN, - isCompatible: async ({ field }) => field.name != null && field.value != null, - execute: async ({ field }) => { - const services = KibanaServices.get(); - const filterManager = services.data.query.filterManager; +export const createFilterInAction = ({ order }: { order?: number }): CellAction => ({ + id: ID, + type: ID, + order, + getIconType: (): string => ICON, + getDisplayName: () => FILTER_IN, + getDisplayNameTooltip: () => FILTER_IN, + isCompatible: async ({ field }) => fieldHasCellActions(field.name), + execute: async ({ field }) => { + const services = KibanaServices.get(); + const filterManager = services.data.query.filterManager; - const makeFilter = (currentVal: string | string[] | null | undefined) => - currentVal?.length === 0 - ? createFilter(field.name, undefined) - : createFilter(field.name, currentVal); + const makeFilter = (currentVal: string | string[] | null | undefined) => + currentVal?.length === 0 + ? createFilter(field.name, null) + : createFilter(field.name, currentVal); - if (filterManager != null) { - filterManager.addFilters(makeFilter(field.value)); - } - }, - }); + if (filterManager != null) { + filterManager.addFilters(makeFilter(field.value)); + } + }, +}); diff --git a/x-pack/plugins/security_solution/public/actions/filter/default/filter_out.tsx b/x-pack/plugins/security_solution/public/actions/filter/default/filter_out.tsx index 0b2560e53563f..8dc4387206daa 100644 --- a/x-pack/plugins/security_solution/public/actions/filter/default/filter_out.tsx +++ b/x-pack/plugins/security_solution/public/actions/filter/default/filter_out.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/cell-actions'; -import { createAction } from '@kbn/ui-actions-plugin/public'; import { i18n } from '@kbn/i18n'; +import type { CellAction } from '@kbn/cell-actions'; import { createFilter } from '../helpers'; import { KibanaServices } from '../../../common/lib/kibana'; +import { fieldHasCellActions } from '../../utils'; export const FILTER_OUT = i18n.translate('xpack.securitySolution.actions.filterOut', { defaultMessage: 'Filter Out', @@ -17,27 +17,25 @@ export const FILTER_OUT = i18n.translate('xpack.securitySolution.actions.filterO const ID = 'security_filterOut'; const ICON = 'minusInCircle'; -export const createFilterOutAction = ({ order }: { order?: number }) => - createAction({ - id: ID, - type: ID, - order, - getIconType: (): string => ICON, - getDisplayName: () => FILTER_OUT, - getDisplayNameTooltip: () => FILTER_OUT, - isCompatible: async ({ field }: CellActionExecutionContext) => - field.name != null && field.value != null, - execute: async ({ field }: CellActionExecutionContext) => { - const services = KibanaServices.get(); - const filterManager = services.data.query.filterManager; +export const createFilterOutAction = ({ order }: { order?: number }): CellAction => ({ + id: ID, + type: ID, + order, + getIconType: (): string => ICON, + getDisplayName: () => FILTER_OUT, + getDisplayNameTooltip: () => FILTER_OUT, + isCompatible: async ({ field }) => fieldHasCellActions(field.name), + execute: async ({ field }) => { + const services = KibanaServices.get(); + const filterManager = services.data.query.filterManager; - const makeFilter = (currentVal: string | string[] | null | undefined) => - currentVal == null || currentVal?.length === 0 - ? createFilter(field.name, null, false) - : createFilter(field.name, currentVal, true); + const makeFilter = (currentVal: string | string[] | null | undefined) => + currentVal == null || currentVal?.length === 0 + ? createFilter(field.name, null, false) + : createFilter(field.name, currentVal, true); - if (filterManager != null) { - filterManager.addFilters(makeFilter(field.value)); - } - }, - }); + if (filterManager != null) { + filterManager.addFilters(makeFilter(field.value)); + } + }, +}); diff --git a/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_in.tsx b/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_in.tsx index d22fc7d312605..650c4f8b8b9d3 100644 --- a/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_in.tsx +++ b/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_in.tsx @@ -5,13 +5,12 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/cell-actions'; -import { createAction } from '@kbn/ui-actions-plugin/public'; import { i18n } from '@kbn/i18n'; +import type { CellAction } from '@kbn/cell-actions'; import { createFilter } from '../helpers'; import type { SecurityAppStore } from '../../../common/store'; import { timelineSelectors } from '../../../timelines/store/timeline'; -import { isInSecurityApp } from '../../utils'; +import { fieldHasCellActions, isInSecurityApp } from '../../utils'; import { KibanaServices } from '../../../common/lib/kibana'; import { TimelineId } from '../../../../common/types'; @@ -27,14 +26,14 @@ export const createFilterInAction = ({ }: { store: SecurityAppStore; order?: number; -}) => { +}): CellAction => { const { application: applicationService } = KibanaServices.get(); let currentAppId: string | undefined; applicationService.currentAppId$.subscribe((appId) => { currentAppId = appId; }); - return createAction({ + return { id: ID, type: ID, order, @@ -42,11 +41,11 @@ export const createFilterInAction = ({ getDisplayName: () => FILTER_IN, getDisplayNameTooltip: () => FILTER_IN, isCompatible: async ({ field }) => - isInSecurityApp(currentAppId) && field.name != null && field.value != null, + isInSecurityApp(currentAppId) && fieldHasCellActions(field.name), execute: async ({ field }) => { const makeFilter = (currentVal?: string[] | string | null) => currentVal?.length === 0 - ? createFilter(field.name, undefined) + ? createFilter(field.name, null) : createFilter(field.name, currentVal); const state = store.getState(); @@ -57,5 +56,5 @@ export const createFilterInAction = ({ filterManager.addFilters(makeFilter(field.value)); } }, - }); + }; }; diff --git a/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_out.tsx b/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_out.tsx index 2efc3d8614203..e6c342bc861b0 100644 --- a/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_out.tsx +++ b/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_out.tsx @@ -5,12 +5,11 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/cell-actions'; -import { createAction } from '@kbn/ui-actions-plugin/public'; import { i18n } from '@kbn/i18n'; +import type { CellAction } from '@kbn/cell-actions'; import { createFilter } from '../helpers'; import { KibanaServices } from '../../../common/lib/kibana'; -import { isInSecurityApp } from '../../utils'; +import { fieldHasCellActions, isInSecurityApp } from '../../utils'; import type { SecurityAppStore } from '../../../common/store'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { TimelineId } from '../../../../common/types'; @@ -27,14 +26,14 @@ export const createFilterOutAction = ({ }: { store: SecurityAppStore; order?: number; -}) => { +}): CellAction => { const { application: applicationService } = KibanaServices.get(); let currentAppId: string | undefined; applicationService.currentAppId$.subscribe((appId) => { currentAppId = appId; }); - return createAction({ + return { id: ID, type: ID, order, @@ -42,7 +41,7 @@ export const createFilterOutAction = ({ getDisplayName: () => FILTER_OUT, getDisplayNameTooltip: () => FILTER_OUT, isCompatible: async ({ field }) => - isInSecurityApp(currentAppId) && field.name != null && field.value != null, + isInSecurityApp(currentAppId) && fieldHasCellActions(field.name), execute: async ({ field }) => { const makeFilter = (currentVal?: string[] | string | null) => currentVal == null || currentVal?.length === 0 @@ -57,5 +56,5 @@ export const createFilterOutAction = ({ filterManager.addFilters(makeFilter(field.value)); } }, - }); + }; }; diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx index ae612dd6d6ae2..5a42d09943c6c 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/cell-actions'; -import { createAction } from '@kbn/ui-actions-plugin/public'; import { i18n } from '@kbn/i18n'; +import type { CellAction, CellActionExecutionContext } from '@kbn/cell-actions'; import ReactDOM, { unmountComponentAtNode } from 'react-dom'; import React from 'react'; @@ -19,7 +18,7 @@ import { Router } from 'react-router-dom'; import { KibanaContextProvider } from '../../../common/lib/kibana'; import { APP_NAME, DEFAULT_DARK_MODE } from '../../../../common/constants'; import type { SecurityAppStore } from '../../../common/store'; -import { isInSecurityApp } from '../../utils'; +import { fieldHasCellActions, isInSecurityApp } from '../../utils'; import { TopNAction } from '../show_top_n_component'; import type { StartServices } from '../../../types'; @@ -49,14 +48,14 @@ export const createShowTopNAction = ({ history: H.History; services: StartServices; order?: number; -}) => { +}): CellAction => { let currentAppId: string | undefined; services.application.currentAppId$.subscribe((appId) => { currentAppId = appId; }); - return createAction({ + return { id: ID, type: ID, order, @@ -65,12 +64,10 @@ export const createShowTopNAction = ({ getDisplayNameTooltip: ({ field }) => SHOW_TOP(field.name), isCompatible: async ({ field }) => isInSecurityApp(currentAppId) && - field.name != null && - field.value != null && + fieldHasCellActions(field.name) && !UNSUPPORTED_FIELD_TYPES.includes(field.type), execute: async (context) => { const node = context.extraContentNodeRef?.current; - if (!node) return; const onClose = () => { @@ -96,5 +93,5 @@ export const createShowTopNAction = ({ ReactDOM.render(element, node); }, - }); + }; }; 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 47c9a2615930e..7d426b03ec605 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 @@ -29,13 +29,14 @@ export const TopNAction = ({ const { browserFields, indexPattern } = useSourcererDataView(getScopeFromPath(pathname)); const userCasesPermissions = useGetUserCasesPermissions(); const CasesContext = casesService.ui.getCasesContext(); + const { field, nodeRef, metadata } = context; - if (!context.nodeRef?.current) return null; + if (!nodeRef?.current) return null; return ( From 79820f50e89e83234419d65a9b0378ff99c1a3b0 Mon Sep 17 00:00:00 2001 From: semd Date: Wed, 25 Jan 2023 18:25:41 +0100 Subject: [PATCH 02/18] fix types and test --- packages/kbn-cell-actions/src/mocks/helpers.ts | 4 ++++ .../actions/add_to_timeline/default/add_to_timeline.test.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/kbn-cell-actions/src/mocks/helpers.ts b/packages/kbn-cell-actions/src/mocks/helpers.ts index 21f1047f384ad..75e4399199815 100644 --- a/packages/kbn-cell-actions/src/mocks/helpers.ts +++ b/packages/kbn-cell-actions/src/mocks/helpers.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { MutableRefObject } from 'react'; import { CellActionExecutionContext } from '../types'; export const makeAction = (actionsName: string, icon: string = 'icon', order?: number) => ({ @@ -29,6 +30,9 @@ export const makeActionContext = ( field: { name: 'fieldName', type: 'keyword', + value: 'some value', }, + extraContentNodeRef: {} as MutableRefObject, + nodeRef: {} as MutableRefObject, ...override, }); diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/default/add_to_timeline.test.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/default/add_to_timeline.test.ts index 32aeb870521d4..616306c5750c0 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/default/add_to_timeline.test.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/default/add_to_timeline.test.ts @@ -71,7 +71,7 @@ describe('Default createAddToTimelineAction', () => { and: [], enabled: true, excluded: false, - id: 'event-details-value-default-draggable-timeline-1-user_name-0-the-value', + id: 'event-field-default-timeline-1-user_name-0-the-value', kqlQuery: '', name: 'user.name', queryMatch: { From a24762024c48dac6052fcf9d255cb8a7036f6df0 Mon Sep 17 00:00:00 2001 From: semd Date: Wed, 25 Jan 2023 18:28:38 +0100 Subject: [PATCH 03/18] async chunk size improvement --- x-pack/plugins/security_solution/public/actions/utils.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/actions/utils.ts b/x-pack/plugins/security_solution/public/actions/utils.ts index 1753cd607554e..57d0494b8ca33 100644 --- a/x-pack/plugins/security_solution/public/actions/utils.ts +++ b/x-pack/plugins/security_solution/public/actions/utils.ts @@ -7,7 +7,14 @@ import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; import { LENS_EMBEDDABLE_TYPE, type Embeddable as LensEmbeddable } from '@kbn/lens-plugin/public'; import { APP_UI_ID } from '../../common/constants'; -import { FIELDS_WITHOUT_CELL_ACTIONS } from '../common/lib/cell_actions/constants'; + +/** actions are disabled for these fields in tables and popovers */ +export const FIELDS_WITHOUT_CELL_ACTIONS = [ + 'signal.rule.risk_score', + 'signal.reason', + 'kibana.alert.risk_score', + 'kibana.alert.reason', +]; export const isInSecurityApp = (currentAppId?: string): boolean => { return !!currentAppId && currentAppId === APP_UI_ID; From da88c20159ca62c37c38116a4cabe0fd1a42f6bc Mon Sep 17 00:00:00 2001 From: semd Date: Thu, 26 Jan 2023 11:18:29 +0100 Subject: [PATCH 04/18] fix type --- .../public/actions/show_top_n/show_top_n_component.test.tsx | 3 +++ x-pack/plugins/security_solution/public/actions/utils.ts | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) 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 01514308fbc63..4edbc9fe85a32 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 @@ -34,6 +34,9 @@ const context = { nodeRef: { current: element, }, + extraContentNodeRef: { + current: null, + }, } as CellActionExecutionContext; describe('TopNAction', () => { diff --git a/x-pack/plugins/security_solution/public/actions/utils.ts b/x-pack/plugins/security_solution/public/actions/utils.ts index 57d0494b8ca33..b00326b5ee82c 100644 --- a/x-pack/plugins/security_solution/public/actions/utils.ts +++ b/x-pack/plugins/security_solution/public/actions/utils.ts @@ -8,11 +8,11 @@ import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; import { LENS_EMBEDDABLE_TYPE, type Embeddable as LensEmbeddable } from '@kbn/lens-plugin/public'; import { APP_UI_ID } from '../../common/constants'; -/** actions are disabled for these fields in tables and popovers */ -export const FIELDS_WITHOUT_CELL_ACTIONS = [ +/** all cell actions are disabled for these fields */ +const FIELDS_WITHOUT_CELL_ACTIONS = [ 'signal.rule.risk_score', - 'signal.reason', 'kibana.alert.risk_score', + 'signal.reason', 'kibana.alert.reason', ]; From a5a082aee5640382cd4abfa931b4560c9f2d045b Mon Sep 17 00:00:00 2001 From: semd Date: Thu, 26 Jan 2023 15:31:47 +0100 Subject: [PATCH 05/18] remove useMemo --- packages/kbn-cell-actions/src/hooks/use_load_actions.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/kbn-cell-actions/src/hooks/use_load_actions.ts b/packages/kbn-cell-actions/src/hooks/use_load_actions.ts index 23443c881e629..4d57ec92c623b 100644 --- a/packages/kbn-cell-actions/src/hooks/use_load_actions.ts +++ b/packages/kbn-cell-actions/src/hooks/use_load_actions.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; import useAsyncFn, { type AsyncState } from 'react-use/lib/useAsyncFn'; import { useCellActionsContext } from '../context/cell_actions_context'; @@ -15,11 +14,9 @@ import type { CellAction, CellActionCompatibilityContext, GetActions } from '../ type AsyncActions = Omit, 'error'>; const useThrowError = (error?: Error) => { - useMemo(() => { - if (error) { - throw error; - } - }, [error]); + if (error) { + throw error; + } }; /** From 085b7193d2f42e94e2de9cfa001f2a83d3c4d250 Mon Sep 17 00:00:00 2001 From: semd Date: Tue, 31 Jan 2023 14:05:31 +0100 Subject: [PATCH 06/18] integrate cellActions in Security dataTables --- .../common/types/header_actions/index.ts | 1 - .../common/types/timeline/cells/index.ts | 3 - .../transform_control_columns.tsx | 1 - .../components/data_table/index.test.tsx | 149 ++++++------------ .../common/components/data_table/index.tsx | 83 ++++------ .../events_tab/events_query_tab_body.tsx | 2 - .../components/events_viewer/index.test.tsx | 6 +- .../common/components/events_viewer/index.tsx | 13 +- .../public/common/components/page/index.tsx | 11 +- .../data_table/epic_local_storage.test.tsx | 2 - .../components/alerts_table/index.tsx | 2 - .../rules/rule_preview/preview_histogram.tsx | 1 + .../preview_table_cell_renderer.tsx | 2 +- .../render_cell_value.tsx | 2 - .../cell_rendering/default_cell_renderer.tsx | 50 ++---- 15 files changed, 118 insertions(+), 210 deletions(-) diff --git a/x-pack/plugins/security_solution/common/types/header_actions/index.ts b/x-pack/plugins/security_solution/common/types/header_actions/index.ts index 6c533736bf150..0cb080ee23db1 100644 --- a/x-pack/plugins/security_solution/common/types/header_actions/index.ts +++ b/x-pack/plugins/security_solution/common/types/header_actions/index.ts @@ -83,7 +83,6 @@ export type ColumnHeaderOptions = Pick< | 'isResizable' > & { aggregatable?: boolean; - dataTableCellActions?: DataTableCellAction[]; category?: string; columnHeaderType: ColumnHeaderType; description?: string | null; diff --git a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts index f5ca9b28628e1..134b659116ee0 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts @@ -6,7 +6,6 @@ */ import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; -import type { Filter } from '@kbn/es-query'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import type { ColumnHeaderOptions, RowRenderer } from '../..'; import type { BrowserFields, TimelineNonEcsData } from '../../../search_strategy'; @@ -18,7 +17,6 @@ export type CellValueElementProps = EuiDataGridCellValueElementProps & { data: TimelineNonEcsData[]; ecsData?: Ecs; eventId: string; // _id - globalFilters?: Filter[]; header: ColumnHeaderOptions; isDraggable: boolean; isTimeline?: boolean; // Default cell renderer is used for both the alert table and timeline. This allows us to cheaply separate concerns @@ -30,5 +28,4 @@ export type CellValueElementProps = EuiDataGridCellValueElementProps & { truncate?: boolean; key?: string; closeCellPopover?: () => void; - enableActions?: boolean; }; diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx index 94980890d1530..651310755b183 100644 --- a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx @@ -34,7 +34,6 @@ export interface TransformColumnsProps { columnHeaders: ColumnHeaderOptions[]; controlColumns: ControlColumnProps[]; data: TimelineItem[]; - disabledCellActions: string[]; fieldBrowserOptions?: FieldBrowserOptions; loadingEventIds: string[]; onRowSelected: OnRowSelected; diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx index b4110c1e78340..893a7e78c46ba 100644 --- a/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx @@ -14,21 +14,32 @@ import { REMOVE_COLUMN } from './column_headers/translations'; import { useMountAppended } from '../../utils/use_mount_appended'; import type { EuiDataGridColumn } from '@elastic/eui'; import { defaultHeaders, mockGlobalState, mockTimelineData, TestProviders } from '../../mock'; -import { defaultColumnHeaderType } from '../../store/data_table/defaults'; import { mockBrowserFields } from '../../containers/source/mock'; import { getMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns'; import type { CellValueElementProps } from '../../../../common/types'; import { TableId } from '../../../../common/types'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../common/constants'; const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => mockDispatch, +})); - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); +const mockUseDataGridColumnsCellActions = jest.fn( + (_: object): Array JSX.Element>> => [] +); +jest.mock('@kbn/cell-actions', () => ({ + ...jest.requireActual('@kbn/cell-actions'), + useDataGridColumnsCellActions: (params: object) => mockUseDataGridColumnsCellActions(params), +})); + +const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); +const mockGetColumnHeaders = jest.fn(() => headersJustTimestamp); +jest.mock('./column_headers/helpers', () => ({ + ...jest.requireActual('./column_headers/helpers'), + getColumnHeaders: () => mockGetColumnHeaders(), +})); jest.mock('@kbn/kibana-react-plugin/public', () => { const originalModule = jest.requireActual('@kbn/kibana-react-plugin/public'); @@ -80,8 +91,6 @@ describe('DataTable', () => { const props: DataTableProps = { browserFields: mockBrowserFields, data: mockTimelineData, - defaultCellActions: [], - disabledCellActions: ['signal.rule.risk_score', 'signal.reason'], id: TableId.test, loadPage: jest.fn(), renderCellValue: TestCellRenderer, @@ -142,10 +151,8 @@ describe('DataTable', () => { }); test('it renders cell value', () => { - const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); const testProps = { ...props, - columnHeaders: headersJustTimestamp, data: mockTimelineData.slice(0, 1), }; const wrapper = mount( @@ -163,49 +170,43 @@ describe('DataTable', () => { .text() ).toEqual(mockTimelineData[0].ecs.timestamp); }); + }); - test('timestamp column renders cell actions', () => { - const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); - const testProps = { - ...props, - columnHeaders: headersJustTimestamp, - data: mockTimelineData.slice(0, 1), - }; + describe('cellActions', () => { + test('calls useDataGridColumnsCellActions properly', () => { + const data = mockTimelineData.slice(0, 1); const wrapper = mount( - + ); wrapper.update(); - expect( - wrapper - .find('[data-test-subj="body-data-grid"]') - .first() - .prop('columns') - .find((c) => c.id === '@timestamp')?.cellActions - ).toBeDefined(); + expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith({ + triggerId: CELL_ACTIONS_DEFAULT_TRIGGER, + fields: [{ name: '@timestamp', values: [data[0]?.data[0]?.value], type: 'date' }], + }); }); - test("signal.rule.risk_score column doesn't render cell actions", () => { - const columnHeaders = [ - { - category: 'signal', - columnHeaderType: defaultColumnHeaderType, - id: 'signal.rule.risk_score', - type: 'number', - aggregatable: true, - initialWidth: 105, - }, - ]; - const testProps = { - ...props, - columnHeaders, - data: mockTimelineData.slice(0, 1), - }; + test('does not render cell actions if disableCellActions is true', () => { const wrapper = mount( - + + + ); + wrapper.update(); + + expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith({ + triggerId: CELL_ACTIONS_DEFAULT_TRIGGER, + fields: [], + }); + }); + + test('does not render cell actions if empty actions returned', () => { + mockUseDataGridColumnsCellActions.mockReturnValueOnce([]); + const wrapper = mount( + + ); wrapper.update(); @@ -215,29 +216,15 @@ describe('DataTable', () => { .find('[data-test-subj="body-data-grid"]') .first() .prop('columns') - .find((c) => c.id === 'signal.rule.risk_score')?.cellActions - ).toBeUndefined(); + .find((c) => c.id === '@timestamp')?.cellActions + ).toHaveLength(0); }); - test("signal.reason column doesn't render cell actions", () => { - const columnHeaders = [ - { - category: 'signal', - columnHeaderType: defaultColumnHeaderType, - id: 'signal.reason', - type: 'string', - aggregatable: true, - initialWidth: 450, - }, - ]; - const testProps = { - ...props, - columnHeaders, - data: mockTimelineData.slice(0, 1), - }; + test('renders returned cell actions', () => { + mockUseDataGridColumnsCellActions.mockReturnValueOnce([[() =>
]]); const wrapper = mount( - + ); wrapper.update(); @@ -247,43 +234,11 @@ describe('DataTable', () => { .find('[data-test-subj="body-data-grid"]') .first() .prop('columns') - .find((c) => c.id === 'signal.reason')?.cellActions - ).toBeUndefined(); + .find((c) => c.id === '@timestamp')?.cellActions + ).toHaveLength(1); }); }); - test("signal.rule.risk_score column doesn't render cell actions", () => { - const columnHeaders = [ - { - category: 'signal', - columnHeaderType: defaultColumnHeaderType, - id: 'signal.rule.risk_score', - type: 'number', - aggregatable: true, - initialWidth: 105, - }, - ]; - const testProps = { - ...props, - columnHeaders, - data: mockTimelineData.slice(0, 1), - }; - const wrapper = mount( - - - - ); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="body-data-grid"]') - .first() - .prop('columns') - .find((c) => c.id === 'signal.rule.risk_score')?.cellActions - ).toBeUndefined(); - }); - test('it does NOT render switches for hiding columns in the `EuiDataGrid` `Columns` popover', async () => { render( diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx index 85189cd221b2f..da2c5591c97bd 100644 --- a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx @@ -22,11 +22,13 @@ import React, { useCallback, useEffect, useMemo, useContext, useRef } from 'reac import { useDispatch } from 'react-redux'; import styled, { ThemeContext } from 'styled-components'; -import type { Filter } from '@kbn/es-query'; import type { EuiTheme } from '@kbn/kibana-react-plugin/common'; import type { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public'; import { i18n } from '@kbn/i18n'; -import type { DataTableCellAction } from '../../../../common/types'; +import { + useDataGridColumnsCellActions, + type UseDataGridColumnsCellActionsProps, +} from '@kbn/cell-actions'; import type { CellValueElementProps, ColumnHeaderOptions, @@ -36,12 +38,7 @@ import type { import type { TimelineItem } from '../../../../common/search_strategy/timeline'; import { getColumnHeader, getColumnHeaders } from './column_headers/helpers'; -import { - addBuildingBlockStyle, - hasCellActions, - mapSortDirectionToDirection, - mapSortingColumns, -} from './helpers'; +import { addBuildingBlockStyle, mapSortDirectionToDirection, mapSortingColumns } from './helpers'; import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; import { REMOVE_COLUMN } from './column_headers/translations'; @@ -52,6 +49,7 @@ import { getPageRowIndex } from './pagination'; import { UnitCount } from '../toolbar/unit'; import { useShallowEqualSelector } from '../../hooks/use_selector'; import { tableDefaults } from '../../store/data_table/defaults'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../common/constants'; const DATA_TABLE_ARIA_LABEL = i18n.translate('xpack.securitySolution.dataTable.ariaLabel', { defaultMessage: 'Alerts', @@ -62,10 +60,8 @@ export interface DataTableProps { browserFields: BrowserFields; bulkActions?: BulkActionsProp; data: TimelineItem[]; - defaultCellActions?: DataTableCellAction[]; - disabledCellActions: string[]; + disableCellActions?: boolean; fieldBrowserOptions?: FieldBrowserOptions; - filters?: Filter[]; id: string; leadingControlColumns: EuiDataGridControlColumn[]; loadPage: (newActivePage: number) => void; @@ -107,7 +103,7 @@ const EuiDataGridContainer = styled.div<{ hideLastPage: boolean }>` } `; -const memoizedColumnHeaders: ( +const memoizedGetColumnHeaders: ( headers: ColumnHeaderOptions[], browserFields: BrowserFields, isEventRenderedView: boolean @@ -119,10 +115,8 @@ export const DataTableComponent = React.memo( browserFields, bulkActions = true, data, - defaultCellActions, - disabledCellActions, + disableCellActions = false, fieldBrowserOptions, - filters, hasCrudPermissions, id, leadingControlColumns, @@ -143,7 +137,7 @@ export const DataTableComponent = React.memo( const { columns, selectedEventIds, showCheckboxes, sort, isLoading, defaultColumns } = dataTable; - const columnHeaders = memoizedColumnHeaders(columns, browserFields, isEventRenderedView); + const columnHeaders = memoizedGetColumnHeaders(columns, browserFields, isEventRenderedView); const dataGridRef = useRef(null); @@ -309,19 +303,28 @@ export const DataTableComponent = React.memo( [dispatch, id] ); + const columnsCellActionsProps = useMemo(() => { + const fields: UseDataGridColumnsCellActionsProps['fields'] = disableCellActions + ? [] + : columnHeaders.map((column) => ({ + name: column.id, + type: column.type ?? 'keyword', + values: data.map( + ({ data: columnData }) => + columnData.find((rowData) => rowData.field === column.id)?.value + ), + })); + + return { + triggerId: CELL_ACTIONS_DEFAULT_TRIGGER, + fields, + }; + }, [disableCellActions, columnHeaders, data]); + + const columnsCellActions = useDataGridColumnsCellActions(columnsCellActionsProps); const columnsWithCellActions: EuiDataGridColumn[] = useMemo( () => - columnHeaders.map((header) => { - const buildAction = (dataTableCellAction: DataTableCellAction) => - dataTableCellAction({ - browserFields, - data: data.map((row) => row.data), - ecsData: data.map((row) => row.ecs), - header: columnHeaders.find((h) => h.id === header.id), - pageSize: pagination.pageSize, - scopeId: id, - closeCellPopover: dataGridRef.current?.closeCellPopover, - }); + columnHeaders.map((header, columnIndex) => { return { ...header, actions: { @@ -337,29 +340,11 @@ export const DataTableComponent = React.memo( }, ], }, - ...(hasCellActions({ - columnId: header.id, - disabledCellActions, - }) - ? { - cellActions: - header.dataTableCellActions?.map(buildAction) ?? - defaultCellActions?.map(buildAction), - visibleCellActions: 3, - } - : {}), + cellActions: columnsCellActions[columnIndex] ?? [], + visibleCellActions: 3, }; }), - [ - browserFields, - columnHeaders, - data, - defaultCellActions, - disabledCellActions, - dispatch, - id, - pagination.pageSize, - ] + [columnHeaders, columnsCellActions, dispatch, id] ); const renderTableCellValue = useMemo(() => { @@ -397,7 +382,6 @@ export const DataTableComponent = React.memo( data: rowData, ecsData: ecs, eventId, - globalFilters: filters, header, isDetails, isDraggable: false, @@ -417,7 +401,6 @@ export const DataTableComponent = React.memo( browserFields, columnHeaders, data, - filters, id, pagination.pageSize, renderCellValue, diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx index a72e0aa35f3ad..af8b54f6f5c90 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx @@ -30,7 +30,6 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell import { SourcererScopeName } from '../../store/sourcerer/model'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; -import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; import type { GlobalTimeArgs } from '../../containers/use_global_time'; import type { QueryTabBodyProps as UserQueryTabBodyProps } from '../../../explore/users/pages/navigation/types'; import type { QueryTabBodyProps as HostQueryTabBodyProps } from '../../../explore/hosts/pages/navigation/types'; @@ -182,7 +181,6 @@ const EventsQueryTabBodyComponent: React.FC = )} void; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; + renderCellValue: React.FC; rowRenderers: RowRenderer[]; additionalFilters?: React.ReactNode; hasCrudPermissions?: boolean; @@ -104,8 +102,8 @@ export interface EventsViewerProps { * NOTE: As of writting, it is not used in the Case_View component */ const StatefulEventsViewerComponent: React.FC = ({ - defaultCellActions, defaultModel, + disableCellActions, end, entityType = 'events', tableId, @@ -439,7 +437,6 @@ const StatefulEventsViewerComponent: React.FC css` SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly and `EuiPopover`, `EuiToolTip` global styles */ -export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimary: string } } }>` +export const AppGlobalStyle = createGlobalStyle<{ + theme: { eui: { euiColorPrimary: string; euiColorLightShade: string; euiSizeS: string } }; +}>` ${TIMELINE_OVERRIDES_CSS_STYLESHEET} @@ -103,11 +105,16 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar .euiPopoverFooter { border: 0; - margin-top: 0 !important; + margin-top: 0; .euiFlexGroup { flex-direction: column; } } + + .euiText + .euiPopoverFooter { + border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; + margin-top: ${({ theme }) => theme.eui.euiSizeS}; + } } /* overrides default styling in angular code that was not theme-friendly */ diff --git a/x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.test.tsx index 4eb160b644904..0ffd6c33aa3ed 100644 --- a/x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.test.tsx @@ -40,7 +40,6 @@ import { addTableInStorage } from '../../../timelines/containers/local_storage'; import { Direction } from '../../../../common/search_strategy'; import { StatefulEventsViewer } from '../../components/events_viewer'; import { eventsDefaultModel } from '../../components/events_viewer/default_model'; -import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; import { EntityType } from '@kbn/timelines-plugin/common'; import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; import { SourcererScopeName } from '../sourcerer/model'; @@ -64,7 +63,6 @@ describe('epicLocalStorage', () => { const ACTION_BUTTON_COUNT = 4; testProps = { - defaultCellActions, defaultModel: eventsDefaultModel, end: to, entityType: EntityType.ALERTS, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 8b601880fc64b..9b1c26a80b42e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -21,7 +21,6 @@ import { StatefulEventsViewer } from '../../../common/components/events_viewer'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; -import { defaultCellActions } from '../../../common/lib/cell_actions/default_cell_actions'; import { useKibana } from '../../../common/lib/kibana'; import type { inputsModel, State } from '../../../common/store'; import { inputsSelectors } from '../../../common/store'; @@ -219,7 +218,6 @@ export const AlertsTableComponent: React.FC = ({ = (props) => RenderCellValue({ ...props, enableActions: false, asPlainText: true }); +> = (props) => RenderCellValue({ ...props, asPlainText: true }); diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx index 21d0d3a199f55..476e15fce02a3 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx @@ -91,7 +91,6 @@ export const useRenderCellValue = ({ data, ecsData, eventId, - globalFilters, header, isDetails = false, isDraggable = false, @@ -120,7 +119,6 @@ export const useRenderCellValue = ({ data={data} ecsData={ecsData} eventId={eventId} - globalFilters={globalFilters} header={myHeader} isDetails={isDetails} isDraggable={isDraggable} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx index 1fafa85162ea1..8056a07fb39de 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx @@ -13,12 +13,6 @@ import { columnRenderers } from '../body/renderers'; import { getColumnRenderer } from '../body/renderers/get_column_renderer'; import type { CellValueElementProps } from '.'; import { getLinkColumnDefinition } from '../../../../common/lib/cell_actions/helpers'; -import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../../../common/lib/cell_actions/constants'; -import { ExpandedCellValueActions } from '../../../../common/lib/cell_actions/expanded_cell_value_actions'; - -const hasCellActions = (columnId?: string) => { - return columnId && !FIELDS_WITHOUT_CELL_ACTIONS.includes(columnId); -}; const StyledContent = styled.div<{ $isDetails: boolean }>` padding: ${({ $isDetails }) => ($isDetails ? '0 8px' : undefined)}; @@ -28,7 +22,6 @@ export const DefaultCellRenderer: React.FC = ({ data, ecsData, eventId, - globalFilters, header, isDetails, isDraggable, @@ -37,7 +30,6 @@ export const DefaultCellRenderer: React.FC = ({ rowRenderers, scopeId, truncate, - enableActions = true, asPlainText, }) => { const asPlainTextDefault = useMemo(() => { @@ -54,31 +46,21 @@ export const DefaultCellRenderer: React.FC = ({ ? 'eui-textBreakWord' : 'eui-displayInlineBlock eui-textTruncate'; return ( - <> - - {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ - asPlainText: asPlainText ?? asPlainTextDefault, // we want to render value with links as plain text but keep other formatters like badge. Except rule name for non preview tables - columnName: header.id, - ecsData, - eventId, - field: header, - isDetails, - isDraggable, - linkValues, - rowRenderers, - scopeId, - truncate, - values, - })} - - {enableActions && isDetails && hasCellActions(header.id) && ( - - )} - + + {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ + asPlainText: asPlainText ?? asPlainTextDefault, // we want to render value with links as plain text but keep other formatters like badge. Except rule name for non preview tables + columnName: header.id, + ecsData, + eventId, + field: header, + isDetails, + isDraggable, + linkValues, + rowRenderers, + scopeId, + truncate, + values, + })} + ); }; From fd20e2b9ecbb852849e948786b0763ed3739abf6 Mon Sep 17 00:00:00 2001 From: semd Date: Tue, 31 Jan 2023 14:06:45 +0100 Subject: [PATCH 07/18] remove legacy cell actions --- .../expanded_cell_value_actions.test.tsx | 33 ------- .../expanded_cell_value_actions.tsx | 85 ------------------- 2 files changed, 118 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.tsx diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.test.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.test.tsx deleted file mode 100644 index e20c4887c0df9..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.test.tsx +++ /dev/null @@ -1,33 +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 { shallow } from 'enzyme'; -import React from 'react'; -import { ExpandedCellValueActions } from './expanded_cell_value_actions'; -import type { ColumnHeaderType } from '@kbn/timelines-plugin/common/types'; - -jest.mock('../kibana'); - -describe('ExpandedCellValueActions', () => { - const props = { - field: { - id: 'host.name', - type: 'keyword', - columnHeaderType: 'not-filtered' as ColumnHeaderType, - aggregatable: true, - }, - globalFilters: [], - onFilterAdded: () => {}, - scopeId: 'mockTimelineId', - value: ['mock value'], - }; - const wrapper = shallow(); - - test('renders show topN button', () => { - expect(wrapper.find('[data-test-subj="data-grid-expanded-show-top-n"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.tsx deleted file mode 100644 index 4d36c450fd177..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.tsx +++ /dev/null @@ -1,85 +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 { EuiButtonEmpty } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { useMemo, useState, useCallback } from 'react'; -import styled from 'styled-components'; -import type { Filter } from '@kbn/es-query'; -import type { ColumnHeaderOptions } from '../../../../common/types'; -import { allowTopN } from '../../components/drag_and_drop/helpers'; -import { ShowTopNButton } from '../../components/hover_actions/actions/show_top_n'; -import { SHOW_TOP_VALUES, HIDE_TOP_VALUES } from './translations'; - -interface Props { - field: ColumnHeaderOptions; - globalFilters?: Filter[]; - scopeId: string; - value: string[] | undefined; - onFilterAdded?: () => void; -} - -const StyledContent = styled.div<{ $isDetails: boolean }>` - border-bottom: 1px solid #d3dae6; - padding: ${({ $isDetails }) => ($isDetails ? '0 8px' : undefined)}; -`; - -const ExpandedCellValueActionsComponent: React.FC = ({ - field, - globalFilters, - onFilterAdded, - scopeId, - value, -}) => { - const showButton = useMemo( - () => - allowTopN({ - fieldName: field.id, - fieldType: field.type ?? '', - isAggregatable: field.aggregatable ?? false, - hideTopN: false, - }), - [field] - ); - - const [showTopN, setShowTopN] = useState(false); - const onClick = useCallback(() => setShowTopN(!showTopN), [showTopN]); - - return ( - <> - - {showButton ? ( - - ) : null} - - - ); -}; - -ExpandedCellValueActionsComponent.displayName = 'ExpandedCellValueActionsComponent'; - -export const ExpandedCellValueActions = React.memo(ExpandedCellValueActionsComponent); From bb0845bf58c8ce74938a54e8834e514b741389da Mon Sep 17 00:00:00 2001 From: semd Date: Tue, 31 Jan 2023 16:22:29 +0100 Subject: [PATCH 08/18] fix tests --- .../cypress/screens/timeline.ts | 2 +- .../transform_control_columns.test.tsx | 1 - .../default_cell_renderer.test.tsx | 36 ------------------- 3 files changed, 1 insertion(+), 38 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index ca938164fb2a6..7a17187750391 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -294,7 +294,7 @@ export const ALERT_TABLE_FILE_NAME_HEADER = '[data-gridcell-column-id="file.name export const ALERT_TABLE_FILE_NAME_VALUES = '[data-gridcell-column-id="file.name"][data-test-subj="dataGridRowCell"]'; // empty column for the test data -export const ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE = '[data-test-subj="add-to-timeline"]'; +export const ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE = '[data-test-subj="security_addToTimeline"]'; export const ACTIVE_TIMELINE_BOTTOM_BAR = '[data-test-subj="flyoutBottomBar"] .active-timeline-button'; diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx index eb0c2379ff609..e4596cd1fb7f3 100644 --- a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx @@ -19,7 +19,6 @@ describe('transformControlColumns', () => { setEventsDeleted: jest.fn(), columnHeaders: [], controlColumns: [], - disabledCellActions: [], selectedEventIds: {}, tabType: '', isSelectAllChecked: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx index 34f57d567951c..3487e2770ff45 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx @@ -127,42 +127,6 @@ describe('DefaultCellRenderer', () => { values: ['2018-11-05T19:03:25.937Z'], }); }); - - test('if in tgrid expanded value, it renders ExpandedCellValueActions', () => { - const data = cloneDeep(mockTimelineData[0].data); - const header = cloneDeep(defaultHeaders[1]); - const isDetails = true; - const id = 'event.severity'; - const wrapper = mount( - - - - - - - - ); - - expect( - wrapper.find('[data-test-subj="data-grid-expanded-cell-value-actions"]').exists() - ).toBeTruthy(); - }); }); describe('host link rendering', () => { From b897806782c99312291f45cd0f9b0f562a0e4090 Mon Sep 17 00:00:00 2001 From: semd Date: Tue, 31 Jan 2023 18:03:41 +0100 Subject: [PATCH 09/18] fix cypress selector --- x-pack/plugins/security_solution/cypress/screens/timeline.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 7a17187750391..0f2410b2efecd 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -294,7 +294,8 @@ export const ALERT_TABLE_FILE_NAME_HEADER = '[data-gridcell-column-id="file.name export const ALERT_TABLE_FILE_NAME_VALUES = '[data-gridcell-column-id="file.name"][data-test-subj="dataGridRowCell"]'; // empty column for the test data -export const ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE = '[data-test-subj="security_addToTimeline"]'; +export const ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE = + '[data-test-subj="dataGridColumnCellAction-security_addToTimeline]'; export const ACTIVE_TIMELINE_BOTTOM_BAR = '[data-test-subj="flyoutBottomBar"] .active-timeline-button'; From 3c9fc4f469600bb8f5ad509292fa5a8e8d219d68 Mon Sep 17 00:00:00 2001 From: semd Date: Wed, 1 Feb 2023 14:58:49 +0100 Subject: [PATCH 10/18] fix cypress tests --- x-pack/plugins/security_solution/cypress/screens/timeline.ts | 2 +- x-pack/plugins/security_solution/cypress/tasks/search_bar.ts | 2 +- .../public/common/components/data_table/index.tsx | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 0f2410b2efecd..188948117312f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -295,7 +295,7 @@ export const ALERT_TABLE_FILE_NAME_VALUES = '[data-gridcell-column-id="file.name"][data-test-subj="dataGridRowCell"]'; // empty column for the test data export const ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE = - '[data-test-subj="dataGridColumnCellAction-security_addToTimeline]'; + '[data-test-subj="dataGridColumnCellAction-security_addToTimeline"]'; export const ACTIVE_TIMELINE_BOTTOM_BAR = '[data-test-subj="flyoutBottomBar"] .active-timeline-button'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts b/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts index fb5e978befdda..a5d66a0eb6499 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts @@ -40,7 +40,7 @@ export const fillAddFilterForm = ({ key, value, operator }: SearchBarFilter) => cy.get(ADD_FILTER_FORM_FIELD_INPUT).should('be.visible'); cy.get(ADD_FILTER_FORM_FIELD_INPUT).type(`${key}{downarrow}`); cy.get(ADD_FILTER_FORM_FIELD_INPUT).click(); - cy.get(ADD_FILTER_FORM_FIELD_OPTION(key)).click({ force: true }); + cy.get(ADD_FILTER_FORM_FIELD_OPTION(key)).click(); if (!operator) { cy.get(ADD_FILTER_FORM_OPERATOR_FIELD).click(); cy.get(ADD_FILTER_FORM_OPERATOR_OPTION_IS).click(); diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx index da2c5591c97bd..6a80b212905c2 100644 --- a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx @@ -318,8 +318,11 @@ export const DataTableComponent = React.memo( return { triggerId: CELL_ACTIONS_DEFAULT_TRIGGER, fields, + metadata: { + scopeId: id, + }, }; - }, [disableCellActions, columnHeaders, data]); + }, [disableCellActions, columnHeaders, data, id]); const columnsCellActions = useDataGridColumnsCellActions(columnsCellActionsProps); const columnsWithCellActions: EuiDataGridColumn[] = useMemo( From 465d1f863c9dc564a3d2d13e2df9de604c6db1ba Mon Sep 17 00:00:00 2001 From: semd Date: Mon, 6 Feb 2023 16:58:03 +0100 Subject: [PATCH 11/18] link entities in dataTables --- .../components/data_table/index.test.tsx | 22 ++++++++++++++----- .../common/components/data_table/index.tsx | 2 ++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx index 893a7e78c46ba..a0d282a24736d 100644 --- a/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx @@ -107,10 +107,14 @@ describe('DataTable', () => { }; beforeEach(() => { - mockDispatch.mockReset(); + mockDispatch.mockClear(); }); describe('rendering', () => { + beforeEach(() => { + mockDispatch.mockClear(); + }); + test('it renders the body data grid', () => { const wrapper = mount( @@ -173,6 +177,10 @@ describe('DataTable', () => { }); describe('cellActions', () => { + beforeEach(() => { + mockDispatch.mockClear(); + }); + test('calls useDataGridColumnsCellActions properly', () => { const data = mockTimelineData.slice(0, 1); const wrapper = mount( @@ -185,6 +193,9 @@ describe('DataTable', () => { expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith({ triggerId: CELL_ACTIONS_DEFAULT_TRIGGER, fields: [{ name: '@timestamp', values: [data[0]?.data[0]?.value], type: 'date' }], + metadata: { + scopeId: 'table-test', + }, }); }); @@ -196,10 +207,11 @@ describe('DataTable', () => { ); wrapper.update(); - expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith({ - triggerId: CELL_ACTIONS_DEFAULT_TRIGGER, - fields: [], - }); + expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith( + expect.objectContaining({ + fields: [], + }) + ); }); test('does not render cell actions if empty actions returned', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx index 6a80b212905c2..4b935619cf7c5 100644 --- a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx @@ -325,6 +325,7 @@ export const DataTableComponent = React.memo( }, [disableCellActions, columnHeaders, data, id]); const columnsCellActions = useDataGridColumnsCellActions(columnsCellActionsProps); + const columnsWithCellActions: EuiDataGridColumn[] = useMemo( () => columnHeaders.map((header, columnIndex) => { @@ -380,6 +381,7 @@ export const DataTableComponent = React.memo( } return renderCellValue({ + asPlainText: false, browserFields, columnId: header.id, data: rowData, From 1596b8e3e4e4d450d03fc06c78a97c867e7ebdf3 Mon Sep 17 00:00:00 2001 From: semd Date: Mon, 6 Feb 2023 17:01:13 +0100 Subject: [PATCH 12/18] fix user link cypress test --- .../plugins/security_solution/cypress/screens/alerts_details.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts index 9a1ac0b8d08f1..23b15524305ed 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts @@ -50,7 +50,7 @@ export const CELL_EXPAND_VALUE = '[data-test-subj="euiDataGridCellExpandButton"] export const CELL_EXPANSION_POPOVER = '[data-test-subj="euiDataGridExpansionPopover"]'; -export const USER_DETAILS_LINK = '[data-test-subj="data-grid-user-details"]'; +export const USER_DETAILS_LINK = '[data-test-subj="users-link-anchor"]'; export const TABLE_TAB = '[data-test-subj="tableTab"]'; From 2e21c855ceaa23c7fe85424a7f4df2ba63e9edcf Mon Sep 17 00:00:00 2001 From: semd Date: Wed, 8 Feb 2023 18:30:05 +0100 Subject: [PATCH 13/18] fix type --- .../public/detections/components/alerts_table/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 5b70212713d3d..c191dcf20e98e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -447,7 +447,6 @@ export const AlertsTableComponent: React.FC = ({ Date: Thu, 9 Feb 2023 12:26:20 +0100 Subject: [PATCH 14/18] fix functional test --- .../test/security_solution_ftr/page_objects/detections/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_ftr/page_objects/detections/index.ts b/x-pack/test/security_solution_ftr/page_objects/detections/index.ts index 1b2b6628afe69..e633bd9b62dda 100644 --- a/x-pack/test/security_solution_ftr/page_objects/detections/index.ts +++ b/x-pack/test/security_solution_ftr/page_objects/detections/index.ts @@ -172,7 +172,7 @@ export class DetectionsPageObject extends FtrService { for (const eventRow of allEvents) { const hostNameButton = await this.testSubjects.findDescendant( - 'formatted-field-host.name', + 'host-details-button', eventRow ); const eventRowHostName = (await hostNameButton.getVisibleText()).trim(); From 78fd1ec0a11ac5d6ef6b925cc3ac635794418086 Mon Sep 17 00:00:00 2001 From: semd Date: Fri, 10 Feb 2023 18:58:26 +0100 Subject: [PATCH 15/18] close popover on action clicked and cypress test --- .../src/components/cell_actions.tsx | 5 - ...use_data_grid_column_cell_actions.test.tsx | 68 +++++++---- .../use_data_grid_column_cell_actions.tsx | 98 ++++++++++++---- .../kbn-cell-actions/src/mocks/helpers.ts | 1 - packages/kbn-cell-actions/src/types.ts | 6 - .../alerts_cell_actions.cy.ts | 111 ++++++++++++++++++ .../investigate_in_timeline.cy.ts | 39 +----- .../cypress/screens/alerts.ts | 18 +++ .../security_solution/cypress/tasks/alerts.ts | 28 ++++- .../cypress/tasks/search_bar.ts | 5 +- .../show_top_n/default/show_top_n.test.tsx | 3 - .../actions/show_top_n/default/show_top_n.tsx | 7 +- .../show_top_n/show_top_n_component.test.tsx | 3 - .../common/components/data_table/index.tsx | 39 +++--- .../common/lib/cell_actions/constants.ts | 16 --- 15 files changed, 301 insertions(+), 146 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts delete mode 100644 x-pack/plugins/security_solution/public/common/lib/cell_actions/constants.ts diff --git a/packages/kbn-cell-actions/src/components/cell_actions.tsx b/packages/kbn-cell-actions/src/components/cell_actions.tsx index 8dd6f875f19e3..83de00c0a08a4 100644 --- a/packages/kbn-cell-actions/src/components/cell_actions.tsx +++ b/packages/kbn-cell-actions/src/components/cell_actions.tsx @@ -20,14 +20,12 @@ export const CellActions: React.FC = ({ visibleCellActions = 3, metadata, }) => { - const extraContentNodeRef = useRef(null); const nodeRef = useRef(null); const actionContext: CellActionExecutionContext = useMemo( () => ({ field, trigger: { id: triggerId }, - extraContentNodeRef, nodeRef, metadata, }), @@ -46,8 +44,6 @@ export const CellActions: React.FC = ({ > {children} - -
); } @@ -60,7 +56,6 @@ export const CellActions: React.FC = ({ showActionTooltips={showActionTooltips} visibleCellActions={visibleCellActions} /> -
); }; diff --git a/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.test.tsx b/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.test.tsx index db6a02b918ca1..7cb321a6a3f67 100644 --- a/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.test.tsx +++ b/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.test.tsx @@ -6,13 +6,14 @@ * Side Public License, v 1. */ -import React, { JSXElementConstructor } from 'react'; +import React, { JSXElementConstructor, MutableRefObject } from 'react'; import { EuiButtonEmpty, EuiDataGridColumnCellActionProps, + EuiDataGridRefProps, type EuiDataGridColumnCellAction, } from '@elastic/eui'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react-hooks'; import { makeAction } from '../mocks/helpers'; import { @@ -35,10 +36,14 @@ const field1 = { name: 'column1', values: ['0.0', '0.1', '0.2', '0.3'], type: 't const field2 = { name: 'column2', values: ['1.0', '1.1', '1.2', '1.3'], type: 'keyword' }; const columns = [{ id: field1.name }, { id: field2.name }]; +const mockCloseCellPopover = jest.fn(); const useDataGridColumnsCellActionsProps: UseDataGridColumnsCellActionsProps = { fields: [field1, field2], triggerId: 'testTriggerId', metadata: { some: 'value' }, + dataGridRef: { + current: { closeCellPopover: mockCloseCellPopover }, + } as unknown as MutableRefObject, }; const renderCellAction = ( @@ -115,7 +120,9 @@ describe('useDataGridColumnsCellActions', () => { cellAction.getByTestId(`dataGridColumnCellAction-${action1.id}`).click(); - expect(action1.execute).toHaveBeenCalled(); + waitFor(() => { + expect(action1.execute).toHaveBeenCalled(); + }); }); it('should execute the action with correct context', async () => { @@ -128,23 +135,27 @@ describe('useDataGridColumnsCellActions', () => { cellAction1.getByTestId(`dataGridColumnCellAction-${action1.id}`).click(); - expect(action1.execute).toHaveBeenCalledWith( - expect.objectContaining({ - field: { name: field1.name, type: field1.type, value: field1.values[1] }, - trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, - }) - ); + await waitFor(() => { + expect(action1.execute).toHaveBeenCalledWith( + expect.objectContaining({ + field: { name: field1.name, type: field1.type, value: field1.values[1] }, + trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, + }) + ); + }); const cellAction2 = renderCellAction(result.current[1][1], { rowIndex: 2 }); cellAction2.getByTestId(`dataGridColumnCellAction-${action2.id}`).click(); - expect(action2.execute).toHaveBeenCalledWith( - expect.objectContaining({ - field: { name: field2.name, type: field2.type, value: field2.values[2] }, - trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, - }) - ); + await waitFor(() => { + expect(action2.execute).toHaveBeenCalledWith( + expect.objectContaining({ + field: { name: field2.name, type: field2.type, value: field2.values[2] }, + trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, + }) + ); + }); }); it('should execute the action with correct page value', async () => { @@ -157,10 +168,27 @@ describe('useDataGridColumnsCellActions', () => { cellAction.getByTestId(`dataGridColumnCellAction-${action1.id}`).click(); - expect(action1.execute).toHaveBeenCalledWith( - expect.objectContaining({ - field: { name: field1.name, type: field1.type, value: field1.values[1] }, - }) - ); + await waitFor(() => { + expect(action1.execute).toHaveBeenCalledWith( + expect.objectContaining({ + field: { name: field1.name, type: field1.type, value: field1.values[1] }, + }) + ); + }); + }); + + it('should close popover then action executed', async () => { + const { result, waitForNextUpdate } = renderHook(useDataGridColumnsCellActions, { + initialProps: useDataGridColumnsCellActionsProps, + }); + await waitForNextUpdate(); + + const cellAction = renderCellAction(result.current[0][0], { rowIndex: 25 }); + + cellAction.getByTestId(`dataGridColumnCellAction-${action1.id}`).click(); + + await waitFor(() => { + expect(mockCloseCellPopover).toHaveBeenCalled(); + }); }); }); 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 f8ccabd1bcd41..c55c8bea5a314 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 @@ -6,8 +6,12 @@ * Side Public License, v 1. */ -import React, { useMemo, useRef } from 'react'; -import { EuiLoadingSpinner, type EuiDataGridColumnCellAction } from '@elastic/eui'; +import React, { MutableRefObject, useCallback, useMemo, useRef } from 'react'; +import { + EuiDataGridRefProps, + EuiLoadingSpinner, + type EuiDataGridColumnCellAction, +} from '@elastic/eui'; import type { CellAction, CellActionCompatibilityContext, @@ -27,11 +31,13 @@ interface BulkField extends Pick { export interface UseDataGridColumnsCellActionsProps extends Pick { fields: BulkField[]; + dataGridRef: MutableRefObject; } export const useDataGridColumnsCellActions = ({ fields, triggerId, metadata, + dataGridRef, }: UseDataGridColumnsCellActionsProps): EuiDataGridColumnCellAction[][] => { const bulkContexts: CellActionCompatibilityContext[] = useMemo( () => @@ -42,7 +48,6 @@ export const useDataGridColumnsCellActions = ({ })), [fields, triggerId, metadata] ); - const { loading, value: columnsActions } = useBulkLoadActions(bulkContexts); const columnsCellActions = useMemo(() => { @@ -56,15 +61,22 @@ export const useDataGridColumnsCellActions = ({ } return columnsActions.map((actions, columnIndex) => actions.map((action) => - createColumnCellAction({ action, metadata, triggerId, field: fields[columnIndex] }) + createColumnCellAction({ + action, + metadata, + triggerId, + field: fields[columnIndex], + dataGridRef, + }) ) ); - }, [columnsActions, fields, loading, metadata, triggerId]); + }, [columnsActions, fields, loading, metadata, triggerId, dataGridRef]); return columnsCellActions; }; -interface CreateColumnCellActionParams extends Pick { +interface CreateColumnCellActionParams + extends Pick { field: BulkField; action: CellAction; } @@ -73,36 +85,76 @@ const createColumnCellAction = ({ action, metadata, triggerId, + dataGridRef, }: CreateColumnCellActionParams): EuiDataGridColumnCellAction => - function ColumnCellAction({ Component, rowIndex }) { + function ColumnCellAction({ Component, rowIndex, isExpanded }) { const nodeRef = useRef(null); - const extraContentNodeRef = useRef(null); + const buttonRef = useRef(null); - 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]; + 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 }, + trigger: { id: triggerId }, + nodeRef, + metadata, + }; + }, [rowIndex]); - const actionContext: CellActionExecutionContext = { - field: { name, type, value }, - trigger: { id: triggerId }, - extraContentNodeRef, - nodeRef, - metadata, - }; + const onClick = useCallback(async () => { + actionContext.nodeRef.current = await closeAndGetCellElement({ + dataGrid: dataGridRef.current, + isExpanded, + buttonRef, + }); + action.execute(actionContext); + }, [actionContext, isExpanded]); return ( { - action.execute(actionContext); - }} + onClick={onClick} > {action.getDisplayName(actionContext)} -
); }; + +const closeAndGetCellElement = ({ + dataGrid, + isExpanded, + buttonRef, +}: { + dataGrid?: EuiDataGridRefProps | null; + isExpanded: boolean; + buttonRef: MutableRefObject; +}): Promise => + new Promise((resolve) => { + const gridCellElement = isExpanded + ? // if actions popover is expanded the button is outside dataGrid, using euiDataGridRowCell--open class + document.querySelector('div[role="gridcell"].euiDataGridRowCell--open') + : // if not expanded the button is inside the cell, get the parent cell from the button + getParentCellElement(buttonRef.current); + // close the popover if needed + dataGrid?.closeCellPopover(); + // closing the popover updates the cell content, get the first child after all updates + setTimeout(() => { + resolve((gridCellElement?.firstElementChild as HTMLElement) ?? null); + }); + }); + +const getParentCellElement = (element?: HTMLElement | null): HTMLElement | null => { + if (element == null) { + return null; + } + if (element.nodeName === 'div' && element.getAttribute('role') === 'gridcell') { + return element; + } + return getParentCellElement(element.parentElement); +}; diff --git a/packages/kbn-cell-actions/src/mocks/helpers.ts b/packages/kbn-cell-actions/src/mocks/helpers.ts index 75e4399199815..acb1afd1bc21e 100644 --- a/packages/kbn-cell-actions/src/mocks/helpers.ts +++ b/packages/kbn-cell-actions/src/mocks/helpers.ts @@ -32,7 +32,6 @@ export const makeActionContext = ( type: 'keyword', value: 'some value', }, - extraContentNodeRef: {} as MutableRefObject, nodeRef: {} as MutableRefObject, ...override, }); diff --git a/packages/kbn-cell-actions/src/types.ts b/packages/kbn-cell-actions/src/types.ts index 978aed54b770a..18290dc12b086 100644 --- a/packages/kbn-cell-actions/src/types.ts +++ b/packages/kbn-cell-actions/src/types.ts @@ -79,16 +79,10 @@ export interface CellActionsProps { export interface CellActionExecutionContext extends ActionExecutionContext { field: CellActionField; - /** - * Ref to a DOM node where the action can add custom HTML. - */ - extraContentNodeRef: React.MutableRefObject; - /** * Ref to the node where the cell action are rendered. */ nodeRef: React.MutableRefObject; - /** * Extra configurations for actions. */ diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts new file mode 100644 index 0000000000000..b3ff2c52964d2 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts @@ -0,0 +1,111 @@ +/* + * 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 { getNewRule } from '../../objects/rule'; +import { FILTER_BADGE, SHOW_TOP_N_HEADER } from '../../screens/alerts'; +import { + ALERT_TABLE_FILE_NAME_HEADER, + ALERT_TABLE_FILE_NAME_VALUES, + ALERT_TABLE_SEVERITY_VALUES, + PROVIDER_BADGE, +} from '../../screens/timeline'; + +import { + scrollAlertTableColumnIntoView, + addAlertPropertyToTimeline, + filterForAlertProperty, + showTopNAlertProperty, + copyToClipboardAlertProperty, +} from '../../tasks/alerts'; +import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; +import { cleanKibana } from '../../tasks/common'; +import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; +import { login, visit } from '../../tasks/login'; +import { fillAddFilterForm, openAddFilterPopover } from '../../tasks/search_bar'; +import { openActiveTimeline } from '../../tasks/timeline'; + +import { ALERTS_URL } from '../../urls/navigation'; +describe('Alerts cell actions', () => { + before(() => { + cleanKibana(); + login(); + }); + + context('Opening alerts', () => { + before(() => { + createCustomRuleEnabled(getNewRule()); + }); + + beforeEach(() => { + visit(ALERTS_URL); + waitForAlertsToPopulate(); + }); + + describe('Filter', () => { + it('should filter for a non-empty property', () => { + cy.get(ALERT_TABLE_SEVERITY_VALUES) + .first() + .invoke('text') + .then((severityVal) => { + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + filterForAlertProperty(ALERT_TABLE_SEVERITY_VALUES, 0); + cy.get(FILTER_BADGE) + .first() + .should('have.text', `kibana.alert.severity: ${severityVal}`); + }); + }); + + it('should filter for an empty property', () => { + // add condition to make sure the field is empty + openAddFilterPopover(); + fillAddFilterForm({ key: 'file.name', operator: 'does not exist' }); + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + filterForAlertProperty(ALERT_TABLE_FILE_NAME_VALUES, 0); + cy.get(FILTER_BADGE).first().should('have.text', 'NOT file.name: exists'); + }); + }); + + describe('Add to timeline', () => { + it('should add a non-empty property to default timeline', () => { + cy.get(ALERT_TABLE_SEVERITY_VALUES) + .first() + .invoke('text') + .then((severityVal) => { + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + addAlertPropertyToTimeline(ALERT_TABLE_SEVERITY_VALUES, 0); + openActiveTimeline(); + cy.get(PROVIDER_BADGE) + .first() + .should('have.text', `kibana.alert.severity: "${severityVal}"`); + }); + }); + + it('should add an empty property to default timeline', () => { + // add condition to make sure the field is empty + openAddFilterPopover(); + fillAddFilterForm({ key: 'file.name', operator: 'does not exist' }); + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + addAlertPropertyToTimeline(ALERT_TABLE_FILE_NAME_VALUES, 0); + openActiveTimeline(); + cy.get(PROVIDER_BADGE).first().should('have.text', 'NOT file.name exists'); + }); + }); + + describe('Show Top N', () => { + it('should show top for a property', () => { + cy.get(ALERT_TABLE_SEVERITY_VALUES) + .first() + .invoke('text') + .then((severityVal) => { + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + showTopNAlertProperty(ALERT_TABLE_SEVERITY_VALUES, 0); + cy.get(SHOW_TOP_N_HEADER).first().should('have.text', `Top kibana.alert.severity`); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts index 9095b5d83f4ff..5f52041e75d17 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts @@ -6,26 +6,15 @@ */ import { getNewRule } from '../../objects/rule'; -import { - ALERT_TABLE_FILE_NAME_HEADER, - ALERT_TABLE_FILE_NAME_VALUES, - ALERT_TABLE_SEVERITY_VALUES, - PROVIDER_BADGE, -} from '../../screens/timeline'; +import { PROVIDER_BADGE } from '../../screens/timeline'; -import { - addAlertPropertyToTimeline, - investigateFirstAlertInTimeline, - scrollAlertTableColumnIntoView, -} from '../../tasks/alerts'; +import { investigateFirstAlertInTimeline } from '../../tasks/alerts'; import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; import { cleanKibana } from '../../tasks/common'; import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; import { login, visit } from '../../tasks/login'; -import { openActiveTimeline } from '../../tasks/timeline'; import { ALERTS_URL } from '../../urls/navigation'; -import { fillAddFilterForm, openAddFilterPopover } from '../../tasks/search_bar'; describe('Alerts timeline', () => { before(() => { @@ -48,28 +37,4 @@ describe('Alerts timeline', () => { cy.get(PROVIDER_BADGE).filter(':visible').should('have.text', eventId); }); }); - - it('Add a non-empty property to default timeline', () => { - cy.get(ALERT_TABLE_SEVERITY_VALUES) - .first() - .invoke('text') - .then((severityVal) => { - scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); - addAlertPropertyToTimeline(ALERT_TABLE_SEVERITY_VALUES, 0); - openActiveTimeline(); - cy.get(PROVIDER_BADGE) - .first() - .should('have.text', `kibana.alert.severity: "${severityVal}"`); - }); - }); - - it('Add an empty property to default timeline', () => { - // add condition to make sure the field is empty - openAddFilterPopover(); - fillAddFilterForm({ key: 'file.name', operator: 'does not exist' }); - scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); - addAlertPropertyToTimeline(ALERT_TABLE_FILE_NAME_VALUES, 0); - openActiveTimeline(); - cy.get(PROVIDER_BADGE).first().should('have.text', 'NOT file.name exists'); - }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index 046962a5d9685..1b5ca92e1eb1c 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -134,3 +134,21 @@ export const EVENT_CONTAINER_TABLE_LOADING = '[data-test-subj="events-container- export const EVENT_CONTAINER_TABLE_NOT_LOADING = '[data-test-subj="events-container-loading-false"]'; + +export const FILTER_BADGE = '[data-test-subj^="filter-badge"]'; + +export const CELL_FILTER_IN_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_filterIn"]'; +export const CELL_FILTER_OUT_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_filterOut"]'; +export const CELL_ADD_TO_TIMELINE_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_addToTimeline"]'; +export const CELL_SHOW_TOP_FIELD_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_showTopN"]'; +export const CELL_COPY_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_copyToClipboard"]'; + +export const ACTIONS_EXPAND_BUTTON = '[data-test-subj="euiDataGridCellExpandButton"]'; + +export const SHOW_TOP_N_HEADER = + '[data-test-subj="topN-container"] [data-test-subj="header-section-title"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index c7810db6ae21d..ff84768f8a8c6 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -32,12 +32,14 @@ import { CLOSED_ALERTS_FILTER_BTN, OPENED_ALERTS_FILTER_BTN, ACKNOWLEDGED_ALERTS_FILTER_BTN, + CELL_ADD_TO_TIMELINE_BUTTON, + CELL_FILTER_IN_BUTTON, + CELL_SHOW_TOP_FIELD_BUTTON, + CELL_COPY_BUTTON, + ACTIONS_EXPAND_BUTTON, } from '../screens/alerts'; import { LOADING_INDICATOR, REFRESH_BUTTON } from '../screens/security_header'; -import { - ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE, - TIMELINE_COLUMN_SPINNER, -} from '../screens/timeline'; +import { TIMELINE_COLUMN_SPINNER } from '../screens/timeline'; import { UPDATE_ENRICHMENT_RANGE_BUTTON, ENRICHMENT_QUERY_END_INPUT, @@ -299,9 +301,23 @@ export const openAnalyzerForFirstAlertInTimeline = () => { cy.get(OPEN_ANALYZER_BTN).first().click({ force: true }); }; -export const addAlertPropertyToTimeline = (propertySelector: string, rowIndex: number) => { +const clickAction = (propertySelector: string, rowIndex: number, actionSelector: string) => { cy.get(propertySelector).eq(rowIndex).trigger('mouseover'); - cy.get(ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE).first().click({ force: true }); + cy.get(actionSelector).first().click({ force: true }); +}; +export const addAlertPropertyToTimeline = (propertySelector: string, rowIndex: number) => { + clickAction(propertySelector, rowIndex, CELL_ADD_TO_TIMELINE_BUTTON); +}; +export const filterForAlertProperty = (propertySelector: string, rowIndex: number) => { + clickAction(propertySelector, rowIndex, CELL_FILTER_IN_BUTTON); +}; +export const showTopNAlertProperty = (propertySelector: string, rowIndex: number) => { + clickAction(propertySelector, rowIndex, ACTIONS_EXPAND_BUTTON); + cy.get(CELL_SHOW_TOP_FIELD_BUTTON).first().click({ force: true }); +}; +export const copyToClipboardAlertProperty = (propertySelector: string, rowIndex: number) => { + clickAction(propertySelector, rowIndex, ACTIONS_EXPAND_BUTTON); + cy.get(CELL_COPY_BUTTON).first().click({ force: true }); }; export const waitForAlerts = () => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts b/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts index a5d66a0eb6499..fa3dbbc2589e5 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts @@ -14,7 +14,6 @@ import { ADD_FILTER_FORM_FIELD_INPUT, ADD_FILTER_FORM_OPERATOR_OPTION_IS, ADD_FILTER_FORM_OPERATOR_FIELD, - ADD_FILTER_FORM_FIELD_OPTION, ADD_FILTER_FORM_FILTER_VALUE_INPUT, GLOBAL_KQL_INPUT, } from '../screens/search_bar'; @@ -38,9 +37,7 @@ export const fillKqlQueryBar = (query: string) => { export const fillAddFilterForm = ({ key, value, operator }: SearchBarFilter) => { cy.get(ADD_FILTER_FORM_FIELD_INPUT).should('exist'); cy.get(ADD_FILTER_FORM_FIELD_INPUT).should('be.visible'); - cy.get(ADD_FILTER_FORM_FIELD_INPUT).type(`${key}{downarrow}`); - cy.get(ADD_FILTER_FORM_FIELD_INPUT).click(); - cy.get(ADD_FILTER_FORM_FIELD_OPTION(key)).click(); + cy.get(ADD_FILTER_FORM_FIELD_INPUT).type(`${key}{downarrow}{enter}`); if (!operator) { cy.get(ADD_FILTER_FORM_OPERATOR_FIELD).click(); cy.get(ADD_FILTER_FORM_OPERATOR_OPTION_IS).click(); diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.test.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.test.tsx index 29430cfa2371a..3b0787f74c505 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.test.tsx @@ -53,9 +53,6 @@ describe('createShowTopNAction', () => { const context = { field: { name: 'user.name', value: 'the-value', type: 'keyword' }, trigger: { id: 'trigger' }, - extraContentNodeRef: { - current: element, - }, nodeRef: { current: element, }, diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx index 5a42d09943c6c..3701273735c2e 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx @@ -67,11 +67,14 @@ export const createShowTopNAction = ({ fieldHasCellActions(field.name) && !UNSUPPORTED_FIELD_TYPES.includes(field.type), execute: async (context) => { - const node = context.extraContentNodeRef?.current; - if (!node) return; + if (!context.nodeRef.current) return; + + const node = document.createElement('div'); + document.body.appendChild(node); const onClose = () => { unmountComponentAtNode(node); + document.body.removeChild(node); }; const element = ( 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 49ddc83563fd7..1737a65596a78 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 @@ -35,9 +35,6 @@ const context = { nodeRef: { current: element, }, - extraContentNodeRef: { - current: null, - }, } as CellActionExecutionContext; describe('TopNAction', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx index 4b935619cf7c5..b0e9ea161a295 100644 --- a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx @@ -303,7 +303,7 @@ export const DataTableComponent = React.memo( [dispatch, id] ); - const columnsCellActionsProps = useMemo(() => { + const columnsCellActionsProps = useMemo((): UseDataGridColumnsCellActionsProps => { const fields: UseDataGridColumnsCellActionsProps['fields'] = disableCellActions ? [] : columnHeaders.map((column) => ({ @@ -321,6 +321,7 @@ export const DataTableComponent = React.memo( metadata: { scopeId: id, }, + dataGridRef, }; }, [disableCellActions, columnHeaders, data, id]); @@ -328,26 +329,24 @@ export const DataTableComponent = React.memo( const columnsWithCellActions: EuiDataGridColumn[] = useMemo( () => - columnHeaders.map((header, columnIndex) => { - return { - ...header, - actions: { - ...header.actions, - additional: [ - { - iconType: 'cross', - label: REMOVE_COLUMN, - onClick: () => { - dispatch(dataTableActions.removeColumn({ id, columnId: header.id })); - }, - size: 'xs', + columnHeaders.map((header, columnIndex) => ({ + ...header, + actions: { + ...header.actions, + additional: [ + { + iconType: 'cross', + label: REMOVE_COLUMN, + onClick: () => { + dispatch(dataTableActions.removeColumn({ id, columnId: header.id })); }, - ], - }, - cellActions: columnsCellActions[columnIndex] ?? [], - visibleCellActions: 3, - }; - }), + size: 'xs', + }, + ], + }, + cellActions: columnsCellActions[columnIndex] ?? [], + visibleCellActions: 3, + })), [columnHeaders, columnsCellActions, dispatch, id] ); diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/constants.ts b/x-pack/plugins/security_solution/public/common/lib/cell_actions/constants.ts deleted file mode 100644 index bccd4efa8f98c..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/constants.ts +++ /dev/null @@ -1,16 +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 { ALERT_RISK_SCORE } from '@kbn/rule-data-utils'; - -/** actions are disabled for these fields in tables and popovers */ -export const FIELDS_WITHOUT_CELL_ACTIONS = [ - 'signal.rule.risk_score', - 'signal.reason', - ALERT_RISK_SCORE, - 'kibana.alert.reason', -]; From 1dbcd45643ca22556d3fe794d919c12b27f23048 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Sat, 11 Feb 2023 15:27:21 +0000 Subject: [PATCH 16/18] [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' --- .../cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts index b3ff2c52964d2..ed26a18580ad1 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts @@ -19,7 +19,6 @@ import { addAlertPropertyToTimeline, filterForAlertProperty, showTopNAlertProperty, - copyToClipboardAlertProperty, } from '../../tasks/alerts'; import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; import { cleanKibana } from '../../tasks/common'; From 92dc22a80df99e8e7f354c3e2323700cd8a7022f Mon Sep 17 00:00:00 2001 From: semd Date: Mon, 13 Feb 2023 12:11:48 +0100 Subject: [PATCH 17/18] fix cypress --- .../alerts_cell_actions.cy.ts | 30 +++++++++++++++++-- .../security_solution/cypress/tasks/alerts.ts | 7 +---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts index b3ff2c52964d2..31a180ddd2d8c 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts @@ -6,7 +6,12 @@ */ import { getNewRule } from '../../objects/rule'; -import { FILTER_BADGE, SHOW_TOP_N_HEADER } from '../../screens/alerts'; +import { + ACTIONS_EXPAND_BUTTON, + CELL_COPY_BUTTON, + FILTER_BADGE, + SHOW_TOP_N_HEADER, +} from '../../screens/alerts'; import { ALERT_TABLE_FILE_NAME_HEADER, ALERT_TABLE_FILE_NAME_VALUES, @@ -19,7 +24,7 @@ import { addAlertPropertyToTimeline, filterForAlertProperty, showTopNAlertProperty, - copyToClipboardAlertProperty, + clickAction, } from '../../tasks/alerts'; import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; import { cleanKibana } from '../../tasks/common'; @@ -100,12 +105,31 @@ describe('Alerts cell actions', () => { cy.get(ALERT_TABLE_SEVERITY_VALUES) .first() .invoke('text') - .then((severityVal) => { + .then(() => { scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); showTopNAlertProperty(ALERT_TABLE_SEVERITY_VALUES, 0); cy.get(SHOW_TOP_N_HEADER).first().should('have.text', `Top kibana.alert.severity`); }); }); }); + + describe('Copy to clipboard', () => { + it('should copy to clipboard', () => { + cy.get(ALERT_TABLE_SEVERITY_VALUES) + .first() + .invoke('text') + .then(() => { + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + cy.window().then((win) => { + cy.stub(win, 'prompt').returns('DISABLED WINDOW PROMPT'); + }); + clickAction(ALERT_TABLE_SEVERITY_VALUES, 0, ACTIONS_EXPAND_BUTTON); + cy.get(CELL_COPY_BUTTON).should('exist'); + // We are not able to test the "copy to clipboard" action execution + // due to browsers security limitation accessing the clipboard services. + // We assume external `copy` library works + }); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index ff84768f8a8c6..c0b293f98bc49 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -35,7 +35,6 @@ import { CELL_ADD_TO_TIMELINE_BUTTON, CELL_FILTER_IN_BUTTON, CELL_SHOW_TOP_FIELD_BUTTON, - CELL_COPY_BUTTON, ACTIONS_EXPAND_BUTTON, } from '../screens/alerts'; import { LOADING_INDICATOR, REFRESH_BUTTON } from '../screens/security_header'; @@ -301,7 +300,7 @@ export const openAnalyzerForFirstAlertInTimeline = () => { cy.get(OPEN_ANALYZER_BTN).first().click({ force: true }); }; -const clickAction = (propertySelector: string, rowIndex: number, actionSelector: string) => { +export const clickAction = (propertySelector: string, rowIndex: number, actionSelector: string) => { cy.get(propertySelector).eq(rowIndex).trigger('mouseover'); cy.get(actionSelector).first().click({ force: true }); }; @@ -315,10 +314,6 @@ export const showTopNAlertProperty = (propertySelector: string, rowIndex: number clickAction(propertySelector, rowIndex, ACTIONS_EXPAND_BUTTON); cy.get(CELL_SHOW_TOP_FIELD_BUTTON).first().click({ force: true }); }; -export const copyToClipboardAlertProperty = (propertySelector: string, rowIndex: number) => { - clickAction(propertySelector, rowIndex, ACTIONS_EXPAND_BUTTON); - cy.get(CELL_COPY_BUTTON).first().click({ force: true }); -}; export const waitForAlerts = () => { /* From e52bac6b2542dd3758214fbec7ab1d7c33993948 Mon Sep 17 00:00:00 2001 From: semd Date: Mon, 13 Feb 2023 16:07:28 +0100 Subject: [PATCH 18/18] fix aggregatable call --- .../components/data_table/index.test.tsx | 19 ++++++++++--------- .../common/components/data_table/index.tsx | 1 + .../__snapshots__/index.test.tsx.snap | 9 +++------ 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx index a0d282a24736d..527eb6e3d0ab9 100644 --- a/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx @@ -108,13 +108,10 @@ describe('DataTable', () => { beforeEach(() => { mockDispatch.mockClear(); + mockUseDataGridColumnsCellActions.mockClear(); }); describe('rendering', () => { - beforeEach(() => { - mockDispatch.mockClear(); - }); - test('it renders the body data grid', () => { const wrapper = mount( @@ -177,10 +174,6 @@ describe('DataTable', () => { }); describe('cellActions', () => { - beforeEach(() => { - mockDispatch.mockClear(); - }); - test('calls useDataGridColumnsCellActions properly', () => { const data = mockTimelineData.slice(0, 1); const wrapper = mount( @@ -192,10 +185,18 @@ describe('DataTable', () => { expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith({ triggerId: CELL_ACTIONS_DEFAULT_TRIGGER, - fields: [{ name: '@timestamp', values: [data[0]?.data[0]?.value], type: 'date' }], + fields: [ + { + name: '@timestamp', + values: [data[0]?.data[0]?.value], + type: 'date', + aggregatable: true, + }, + ], metadata: { scopeId: 'table-test', }, + dataGridRef: expect.any(Object), }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx index b0e9ea161a295..bfddac06f64b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx @@ -313,6 +313,7 @@ export const DataTableComponent = React.memo( ({ data: columnData }) => columnData.find((rowData) => rowData.field === column.id)?.value ), + aggregatable: column.aggregatable, })); 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 23d6db8adf75e..89d876e5efa88 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 @@ -116,19 +116,18 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = ` >
-
@@ -163,19 +162,18 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = ` >
-
@@ -222,19 +220,18 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = ` >
-