From d313c950d1de154b17827eafa71bf5b7ef85d22d Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Tue, 24 Jan 2023 19:44:43 +0100 Subject: [PATCH] [145663] Refactor explore pages to migrate HoverActions to CellActions (#148056) Epic: https://github.com/elastic/kibana/issues/144943 ## Summary Update explore pages to use the new `CellActions` component instead of `HoverActions `. ### What is included? * Update the user, host, and network page tables. Screenshot 2023-01-17 at 13 12 16 Screenshot 2023-01-17 at 13 19 34 * Fields rendered when clicking on "+{N} more" Screenshot 2023-01-17 at 12 51 38 ### What is NOT included? * Visualizations * Fields on details pages. They are also used by the Timeline and need to be draggable. * Timeline * Datagrid tables - Events and Alerts * Plugins ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../src/components/cell_actions.test.tsx | 2 +- .../src/components/cell_actions.tsx | 6 +- .../src/components/hover_actions_popover.tsx | 254 +++++++++--------- .../src/components/inline_actions.tsx | 12 +- .../cypress/e2e/hosts/host_risk_tab.cy.ts | 4 +- .../cypress/screens/hosts/all_hosts.ts | 2 +- .../cypress/screens/hosts/host_risk.ts | 3 +- .../screens/hosts/uncommon_processes.ts | 2 +- .../cypress/screens/network/flows.ts | 14 +- .../cypress/tasks/host_risk.ts | 2 +- .../default/add_to_timeline.test.ts | 2 +- .../default/add_to_timeline.tsx | 9 +- .../default/copy_to_clipboard.test.ts | 2 +- .../default/copy_to_clipboard.tsx | 14 +- .../actions/filter/default/filter_in.test.ts | 2 +- .../actions/filter/default/filter_in.tsx | 4 +- .../actions/filter/default/filter_out.test.ts | 2 +- .../actions/filter/default/filter_out.tsx | 4 +- .../actions/filter/timeline/filter_in.test.ts | 2 +- .../actions/filter/timeline/filter_in.tsx | 4 +- .../filter/timeline/filter_out.test.ts | 2 +- .../actions/filter/timeline/filter_out.tsx | 6 +- .../show_top_n/default/show_top_n.test.tsx | 3 +- .../actions/show_top_n/default/show_top_n.tsx | 12 +- .../show_top_n/show_top_n_component.test.tsx | 3 +- .../show_top_n/show_top_n_component.tsx | 2 +- .../security_solution/public/app/app.tsx | 18 +- .../events_tab/events_query_tab_body.test.tsx | 1 + .../common/components/header_page/index.tsx | 12 +- .../components/header_page/title.test.tsx | 20 -- .../common/components/header_page/title.tsx | 21 +- .../common/components/header_page/types.ts | 5 - .../hover_actions/actions/show_top_n.test.tsx | 8 + .../matrix_histogram/index.test.tsx | 68 ++--- .../components/matrix_histogram/index.tsx | 21 +- .../ml/__snapshots__/entity.test.tsx.snap | 18 ++ .../entity_draggable.test.tsx.snap | 25 -- ...ity_draggable.test.tsx => entity.test.tsx} | 14 +- .../public/common/components/ml/entity.tsx | 38 +++ .../common/components/ml/entity_draggable.tsx | 72 ----- .../__snapshots__/anomaly_score.test.tsx.snap | 3 +- .../draggable_score.test.tsx.snap | 49 ---- .../score/__snapshots__/score.test.tsx.snap | 35 +++ .../components/ml/score/anomaly_score.tsx | 9 +- .../components/ml/score/draggable_score.tsx | 80 ------ ...raggable_score.test.tsx => score.test.tsx} | 10 +- .../common/components/ml/score/score.tsx | 52 ++++ .../get_anomalies_host_table_columns.tsx | 11 +- .../get_anomalies_network_table_columns.tsx | 11 +- .../ml/tables/get_anomalies_table_columns.tsx | 30 +-- .../get_anomalies_user_table_columns.tsx | 11 +- .../__snapshots__/helpers.test.tsx.snap | 117 +------- .../common/components/tables/helpers.test.tsx | 184 ++++--------- .../common/components/tables/helpers.tsx | 180 ++++--------- .../common/components/top_n/index.test.tsx | 1 + .../common/components/top_n/top_n.test.tsx | 1 + .../public/common/components/top_n/top_n.tsx | 2 + .../public/common/mock/test_providers.tsx | 15 +- .../common/utils/route/use_route_spy.tsx | 3 + .../alerts_histogram_panel/index.tsx | 4 +- .../components/authentication/helpers.tsx | 50 ++-- .../host_risk_score_table/columns.tsx | 45 +--- .../hosts/components/hosts_table/columns.tsx | 88 +++--- .../uncommon_process_table/index.tsx | 48 ++-- .../components/network_dns_table/columns.tsx | 48 ++-- .../components/network_http_table/columns.tsx | 54 ++-- .../network_top_countries_table/columns.tsx | 46 ++-- .../network_top_n_flow_table/columns.tsx | 108 +++----- .../network/components/tls_table/columns.tsx | 48 ++-- .../components/users_table/columns.tsx | 41 ++- .../explore/network/pages/details/index.tsx | 19 +- .../users/components/all_users/index.tsx | 16 +- .../user_risk_score_table/columns.tsx | 43 +-- .../authentications_query_tab_body.test.tsx | 5 + .../security_solution/public/helpers.tsx | 10 + .../components/events_by_dataset/index.tsx | 3 + .../signals_by_category.tsx | 3 + .../security_solution/public/plugin.tsx | 2 +- .../field_renderers/field_renderers.test.tsx | 117 ++++---- .../field_renderers/field_renderers.tsx | 128 +++------ .../plugins/security_solution/tsconfig.json | 1 + 81 files changed, 943 insertions(+), 1503 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity_draggable.test.tsx.snap rename x-pack/plugins/security_solution/public/common/components/ml/{entity_draggable.test.tsx => entity.test.tsx} (76%) create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/entity.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/draggable_score.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/score.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/common/components/ml/score/draggable_score.tsx rename x-pack/plugins/security_solution/public/common/components/ml/score/{draggable_score.test.tsx => score.test.tsx} (73%) create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx diff --git a/packages/kbn-cell-actions/src/components/cell_actions.test.tsx b/packages/kbn-cell-actions/src/components/cell_actions.test.tsx index d23d7f731b156..c7b685da3e097 100644 --- a/packages/kbn-cell-actions/src/components/cell_actions.test.tsx +++ b/packages/kbn-cell-actions/src/components/cell_actions.test.tsx @@ -32,7 +32,7 @@ describe('CellActions', () => { await getActionsPromise; }); - expect(queryByTestId('cellActions')).toBeInTheDocument(); + expect(queryByTestId(`cellActions-renderContent-${FIELD.name}`)).toBeInTheDocument(); }); it('renders InlineActions when mode is INLINE', async () => { diff --git a/packages/kbn-cell-actions/src/components/cell_actions.tsx b/packages/kbn-cell-actions/src/components/cell_actions.tsx index 682233eaa76b7..8dd6f875f19e3 100644 --- a/packages/kbn-cell-actions/src/components/cell_actions.tsx +++ b/packages/kbn-cell-actions/src/components/cell_actions.tsx @@ -34,9 +34,11 @@ export const CellActions: React.FC = ({ [field, triggerId, metadata] ); + const dataTestSubj = `cellActions-renderContent-${field.name}`; + if (mode === CellActionsMode.HOVER) { return ( -
+
= ({ } return ( -
+
{children} ( - ({ children, visibleCellActions, actionContext, showActionTooltips }) => { - const contentRef = useRef(null); - const [isExtraActionsPopoverOpen, setIsExtraActionsPopoverOpen] = useState(false); - const [showHoverContent, setShowHoverContent] = useState(false); - const popoverRef = useRef(null); - - const [{ value: actions }, loadActions] = useLoadActionsFn(); - - const { visibleActions, extraActions } = useMemo( - () => partitionActions(actions ?? [], visibleCellActions), - [actions, visibleCellActions] - ); - - const closePopover = useCallback(() => { - setShowHoverContent(false); - }, []); - - const closeExtraActions = useCallback( - () => setIsExtraActionsPopoverOpen(false), - [setIsExtraActionsPopoverOpen] - ); - - const onShowExtraActionsClick = useCallback(() => { - setIsExtraActionsPopoverOpen(true); - closePopover(); - }, [closePopover, setIsExtraActionsPopoverOpen]); - - const openPopOverDebounced = useMemo( - () => - debounce(() => { - if (!document.body.classList.contains(IS_DRAGGING_CLASS_NAME)) { - setShowHoverContent(true); - } - }, HOVER_INTENT_DELAY), - [] - ); - - // 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; - - // memoize actions after the first call - if (actions === undefined) { - loadActions(actionContext); - } - - 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. - // This hack is required because we anchor the popover to the hover content instead - // of anchoring it to the button that triggers the popover. - // eslint-disable-next-line jsx-a11y/click-events-have-key-events -
- {children} -
- ); - }, [onMouseEnter, closeExtraActions, children]); - +export const HoverActionsPopover: React.FC = ({ + children, + visibleCellActions, + actionContext, + showActionTooltips, +}) => { + 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 { visibleActions, extraActions } = useMemo( + () => partitionActions(actions ?? [], visibleCellActions), + [actions, visibleCellActions] + ); + + const openPopOverDebounced = useMemo( + () => + debounce(() => { + if (!document.body.classList.contains(IS_DRAGGING_CLASS_NAME)) { + setShowHoverContent(true); + } + }, HOVER_INTENT_DELAY), + [] + ); + + const closePopover = useCallback(() => { + openPopOverDebounced.cancel(); + setShowHoverContent(false); + }, [openPopOverDebounced]); + + const closeExtraActions = useCallback( + () => setIsExtraActionsPopoverOpen(false), + [setIsExtraActionsPopoverOpen] + ); + + const onShowExtraActionsClick = useCallback(() => { + setIsExtraActionsPopoverOpen(true); + 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; + + // memoize actions after the first call + if (actions === undefined) { + loadActions(actionContext); + } + + openPopOverDebounced(); + }, [isExtraActionsPopoverOpen, actions, openPopOverDebounced, loadActions, actionContext]); + + const onMouseLeave = useCallback(() => { + closePopover(); + }, [closePopover]); + + const content = useMemo(() => { return ( - <> -
- - {showHoverContent ? ( -
- -

{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(actionContext.field.name)}

-
- {visibleActions.map((action) => ( - - ))} - {extraActions.length > 0 ? ( - - ) : null} -
- ) : null} -
-
- - + // Hack - Forces extra actions popover to close when hover content is clicked. + // This hack is required because we anchor the popover to the hover content instead + // of anchoring it to the button that triggers the popover. + // eslint-disable-next-line jsx-a11y/click-events-have-key-events +
+ {children} +
); - } -); + }, [onMouseEnter, closeExtraActions, children]); + + useEffect(() => { + if (error) { + throw error; + } + }, [error]); + + return ( + <> +
+ + {showHoverContent ? ( +
+ +

{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(actionContext.field.name)}

+
+ {visibleActions.map((action) => ( + + ))} + {extraActions.length > 0 ? ( + + ) : null} +
+ ) : null} +
+
+ + + ); +}; diff --git a/packages/kbn-cell-actions/src/components/inline_actions.tsx b/packages/kbn-cell-actions/src/components/inline_actions.tsx index bcf8222ce83ca..7d568ebb1bf0a 100644 --- a/packages/kbn-cell-actions/src/components/inline_actions.tsx +++ b/packages/kbn-cell-actions/src/components/inline_actions.tsx @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ActionItem } from './cell_action_item'; import { usePartitionActions } from '../hooks/actions'; import { ExtraActionsPopOver } from './extra_actions_popover'; import { ExtraActionsButton } from './extra_actions_button'; -import type { CellActionExecutionContext } from '../types'; +import { CellActionExecutionContext } from '../types'; import { useLoadActions } from '../hooks/use_load_actions'; interface InlineActionsProps { @@ -25,7 +25,7 @@ export const InlineActions: React.FC = ({ showActionTooltips, visibleCellActions, }) => { - const { value: allActions } = useLoadActions(actionContext); + const { value: allActions, error } = useLoadActions(actionContext); const { extraActions, visibleActions } = usePartitionActions( allActions ?? [], visibleCellActions @@ -38,6 +38,12 @@ export const InlineActions: React.FC = ({ [togglePopOver, showActionTooltips] ); + useEffect(() => { + if (error) { + throw error; + } + }, [error]); + return ( {visibleActions.map((action, index) => ( diff --git a/x-pack/plugins/security_solution/cypress/e2e/hosts/host_risk_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/hosts/host_risk_tab.cy.ts index bc8ccf8bcbe61..a5ff55899e669 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/hosts/host_risk_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/hosts/host_risk_tab.cy.ts @@ -10,7 +10,7 @@ import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { navigateToHostRiskDetailTab, openRiskTableFilterAndSelectTheCriticalOption, - removeCritialFilter, + removeCriticalFilter, selectFiveItemsPerPageOption, } from '../../tasks/host_risk'; import { @@ -51,7 +51,7 @@ describe('risk tab', () => { cy.get(HOST_BY_RISK_TABLE_CELL).eq(3).should('not.have.text', 'siem-kibana'); - removeCritialFilter(); + removeCriticalFilter(); }); it('should be able to change items count per page', () => { diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts index b76add918beaf..38d26708cad0f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts @@ -7,4 +7,4 @@ export const ALL_HOSTS_TABLE = '[data-test-subj="table-allHosts-loading-false"]'; -export const HOSTS_NAMES = '[data-test-subj="render-content-host.name"] a.euiLink'; +export const HOSTS_NAMES = '[data-test-subj="cellActions-renderContent-host.name"] a.euiLink'; diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts index c9a1e0691f4fe..7de7c6ccd9a37 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts @@ -27,4 +27,5 @@ export const HOST_BY_RISK_TABLE_PERPAGE_OPTIONS = export const HOST_BY_RISK_TABLE_NEXT_PAGE_BUTTON = '[data-test-subj="numberedPagination"] [data-test-subj="pagination-button-next"]'; -export const HOST_BY_RISK_TABLE_HOSTNAME_CELL = '[data-test-subj="render-content-host.name"]'; +export const HOST_BY_RISK_TABLE_HOSTNAME_CELL = + '[data-test-subj="cellActions-renderContent-host.name"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts index f2a712f868850..d06b435fd772c 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts @@ -5,6 +5,6 @@ * 2.0. */ -export const PROCESS_NAME_FIELD = '[data-test-subj="render-content-process.name"]'; +export const PROCESS_NAME_FIELD = '[data-test-subj="cellActions-renderContent-process.name"]'; export const UNCOMMON_PROCESSES_TABLE = '[data-test-subj="table-uncommonProcesses-loading-false"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/network/flows.ts b/x-pack/plugins/security_solution/cypress/screens/network/flows.ts index a906a985b183d..d20e1d69199af 100644 --- a/x-pack/plugins/security_solution/cypress/screens/network/flows.ts +++ b/x-pack/plugins/security_solution/cypress/screens/network/flows.ts @@ -9,19 +9,19 @@ export const IPS_TABLE_LOADED = '[data-test-subj="table-topNFlowSource-loading-f export const EXPAND_OVERFLOW_ITEMS = '[data-test-subj="overflow-button"]'; -export const FILTER_IN = '[data-test-subj="hover-actions-filter-for"]'; +export const FILTER_IN = '[data-test-subj="actionItem-security_filterIn"]'; -export const FILTER_OUT = '[data-test-subj="hover-actions-filter-out"]'; +export const FILTER_OUT = '[data-test-subj="actionItem-security_filterOut"]'; -export const ADD_TO_TIMELINE = '[data-test-subj="add-to-timeline"]'; +export const ADD_TO_TIMELINE = '[data-test-subj="actionItem-security_addToTimeline"]'; -export const SHOW_TOP_FIELD = '[data-test-subj="show-top-field"]'; +export const SHOW_TOP_FIELD = '[data-test-subj="actionItem-security_showTopN"]'; -export const COPY = '[data-test-subj="clipboard"]'; +export const COPY = '[data-test-subj="actionItem-security_copyToClipboard"]'; export const TOP_N_CONTAINER = '[data-test-subj="topN-container"]'; -export const DESTINATION_DOMAIN = `[data-test-subj="more-container"] [data-test-subj="render-content-destination.domain"]`; +export const DESTINATION_DOMAIN = `[data-test-subj="more-container"] [data-test-subj="cellActions-renderContent-destination.domain"]`; export const OVERFLOW_ITEM = - '[data-test-subj="more-container"] [data-test-subj="render-content-destination.domain"]'; + '[data-test-subj="more-container"] [data-test-subj="cellActions-renderContent-destination.domain"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts b/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts index c1817cbea8548..200578a73363a 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts @@ -30,7 +30,7 @@ export const openRiskTableFilterAndSelectTheLowOption = () => { cy.get(HOST_BY_RISK_TABLE_FILTER_LOW).click(); }; -export const removeCritialFilter = () => { +export const removeCriticalFilter = () => { cy.get(HOST_BY_RISK_TABLE_FILTER_CRITICAL).click(); }; 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 938e80f0c64e1..32aeb870521d4 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 @@ -12,7 +12,7 @@ import { Subject } from 'rxjs'; import { TimelineId } from '../../../../common/types'; import { addProvider } from '../../../timelines/store/timeline/actions'; import { createAddToTimelineAction } from './add_to_timeline'; -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { GEO_FIELD_TYPE } from '../../../timelines/components/timeline/body/renderers/constants'; jest.mock('../../../common/lib/kibana'); 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 2ba8db918ffe5..f0710983da2da 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,7 +5,7 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { createAction } from '@kbn/ui-actions-plugin/public'; import { addProvider } from '../../../timelines/store/timeline/actions'; import { TimelineId } from '../../../../common/types'; @@ -58,8 +58,13 @@ export const createAddToTimelineAction = ({ if (dataProviders.length > 0) { store.dispatch(addProvider({ id: TimelineId.active, providers: dataProviders })); + let messageValue = ''; + if (field.value != null) { + messageValue = Array.isArray(field.value) ? field.value.join(', ') : field.value; + } + notificationsService.toasts.addSuccess({ - title: ADD_TO_TIMELINE_SUCCESS_TITLE(field.value), + title: ADD_TO_TIMELINE_SUCCESS_TITLE(messageValue), }); } else { notificationsService.toasts.addWarning({ diff --git a/x-pack/plugins/security_solution/public/actions/copy_to_clipboard/default/copy_to_clipboard.test.ts b/x-pack/plugins/security_solution/public/actions/copy_to_clipboard/default/copy_to_clipboard.test.ts index 54366a371d105..6c17b4019f98b 100644 --- a/x-pack/plugins/security_solution/public/actions/copy_to_clipboard/default/copy_to_clipboard.test.ts +++ b/x-pack/plugins/security_solution/public/actions/copy_to_clipboard/default/copy_to_clipboard.test.ts @@ -7,7 +7,7 @@ import { KibanaServices } from '../../../common/lib/kibana'; import { createCopyToClipboardAction } from './copy_to_clipboard'; -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; jest.mock('../../../common/lib/kibana'); const mockSuccessToast = jest.fn(); 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 ae65be87219a6..0cc7a34efb39c 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,9 +5,9 @@ * 2.0. */ -import { createAction } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import copy from 'copy-to-clipboard'; -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public/cell_actions/components/cell_actions'; +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'; @@ -24,7 +24,15 @@ export const createCopyToClipboardAction = ({ order }: { order?: number }) => isCompatible: async (context) => context.field.name != null && context.field.value != null, execute: async ({ field }) => { const { notifications } = KibanaServices.get(); - const text = `${field.name}: "${field.value}"`; + + 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 }); if (isSuccess) { diff --git a/x-pack/plugins/security_solution/public/actions/filter/default/filter_in.test.ts b/x-pack/plugins/security_solution/public/actions/filter/default/filter_in.test.ts index 72307650a8faf..c4d1906279faf 100644 --- a/x-pack/plugins/security_solution/public/actions/filter/default/filter_in.test.ts +++ b/x-pack/plugins/security_solution/public/actions/filter/default/filter_in.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { KibanaServices } from '../../../common/lib/kibana'; import { createFilterInAction } from './filter_in'; 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 2917fa5cd747e..7beea5154bd9d 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,7 +5,7 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { createAction } from '@kbn/ui-actions-plugin/public'; import { i18n } from '@kbn/i18n'; import { createFilter } from '../helpers'; @@ -30,7 +30,7 @@ export const createFilterInAction = ({ order }: { order?: number }) => const services = KibanaServices.get(); const filterManager = services.data.query.filterManager; - const makeFilter = (currentVal: string | null | undefined) => + const makeFilter = (currentVal: string | string[] | null | undefined) => currentVal?.length === 0 ? createFilter(field.name, undefined) : createFilter(field.name, currentVal); diff --git a/x-pack/plugins/security_solution/public/actions/filter/default/filter_out.test.ts b/x-pack/plugins/security_solution/public/actions/filter/default/filter_out.test.ts index 690f5f9c67709..521ebea58254f 100644 --- a/x-pack/plugins/security_solution/public/actions/filter/default/filter_out.test.ts +++ b/x-pack/plugins/security_solution/public/actions/filter/default/filter_out.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { KibanaServices } from '../../../common/lib/kibana'; import { createFilterOutAction } from './filter_out'; 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 af8742db8871d..0b2560e53563f 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,7 +5,7 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { createAction } from '@kbn/ui-actions-plugin/public'; import { i18n } from '@kbn/i18n'; import { createFilter } from '../helpers'; @@ -31,7 +31,7 @@ export const createFilterOutAction = ({ order }: { order?: number }) => const services = KibanaServices.get(); const filterManager = services.data.query.filterManager; - const makeFilter = (currentVal: string | null | undefined) => + const makeFilter = (currentVal: string | string[] | null | undefined) => currentVal == null || currentVal?.length === 0 ? createFilter(field.name, null, false) : createFilter(field.name, currentVal, true); diff --git a/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_in.test.ts b/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_in.test.ts index 6997e7df0e16f..20964ab5328b6 100644 --- a/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_in.test.ts +++ b/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_in.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; import { TimelineId } from '../../../../common/types'; import { 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 1335c1012d074..d22fc7d312605 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,7 +5,7 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { createAction } from '@kbn/ui-actions-plugin/public'; import { i18n } from '@kbn/i18n'; import { createFilter } from '../helpers'; @@ -44,7 +44,7 @@ export const createFilterInAction = ({ isCompatible: async ({ field }) => isInSecurityApp(currentAppId) && field.name != null && field.value != null, execute: async ({ field }) => { - const makeFilter = (currentVal: string | null | undefined) => + const makeFilter = (currentVal?: string[] | string | null) => currentVal?.length === 0 ? createFilter(field.name, undefined) : createFilter(field.name, currentVal); diff --git a/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_out.test.ts b/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_out.test.ts index 1d331080814e9..e13d68c68cf68 100644 --- a/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_out.test.ts +++ b/x-pack/plugins/security_solution/public/actions/filter/timeline/filter_out.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; import { TimelineId } from '../../../../common/types'; import { 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 c0c79d371fb83..2efc3d8614203 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,7 +5,7 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { createAction } from '@kbn/ui-actions-plugin/public'; import { i18n } from '@kbn/i18n'; import { createFilter } from '../helpers'; @@ -43,8 +43,8 @@ export const createFilterOutAction = ({ getDisplayNameTooltip: () => FILTER_OUT, isCompatible: async ({ field }) => isInSecurityApp(currentAppId) && field.name != null && field.value != null, - execute: async ({ field, metadata }) => { - const makeFilter = (currentVal: string | null | undefined) => + execute: async ({ field }) => { + const makeFilter = (currentVal?: string[] | string | null) => currentVal == null || currentVal?.length === 0 ? createFilter(field.name, null, false) : createFilter(field.name, currentVal, true); 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 956849ff73929..29430cfa2371a 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 @@ -5,7 +5,7 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { Subject } from 'rxjs'; import { APP_UI_ID } from '../../../../common/constants'; import { @@ -52,6 +52,7 @@ describe('createShowTopNAction', () => { }); const context = { field: { name: 'user.name', value: 'the-value', type: 'keyword' }, + trigger: { id: 'trigger' }, extraContentNodeRef: { 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 6bcba150ff22d..ae612dd6d6ae2 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,7 +5,7 @@ * 2.0. */ -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { createAction } from '@kbn/ui-actions-plugin/public'; import { i18n } from '@kbn/i18n'; import ReactDOM, { unmountComponentAtNode } from 'react-dom'; @@ -69,10 +69,12 @@ export const createShowTopNAction = ({ field.value != null && !UNSUPPORTED_FIELD_TYPES.includes(field.type), execute: async (context) => { + const node = context.extraContentNodeRef?.current; + + if (!node) return; + const onClose = () => { - if (context.extraContentNodeRef.current !== null) { - unmountComponentAtNode(context.extraContentNodeRef.current); - } + unmountComponentAtNode(node); }; const element = ( @@ -92,7 +94,7 @@ export const createShowTopNAction = ({ ); - ReactDOM.render(element, context.extraContentNodeRef.current); + ReactDOM.render(element, node); }, }); }; 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 68af0eff06c0a..01514308fbc63 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 @@ -10,7 +10,7 @@ import React from 'react'; import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context'; import { TestProviders } from '../../common/mock'; import { TopNAction } from './show_top_n_component'; -import type { CellActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; import type { CasesUiStart } from '@kbn/cases-plugin/public'; jest.mock('react-router-dom', () => { @@ -30,6 +30,7 @@ document.body.appendChild(element); const context = { field: { name: 'user.name', value: 'the-value', type: 'keyword' }, + trigger: { id: 'trigger' }, nodeRef: { current: element, }, diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.tsx index 1a244afc6bb63..47c9a2615930e 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 @@ -30,7 +30,7 @@ export const TopNAction = ({ const userCasesPermissions = useGetUserCasesPermissions(); const CasesContext = casesService.ui.getCasesContext(); - if (!context.nodeRef.current) return null; + if (!context.nodeRef?.current) return null; return ( diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index b20b8ace7de17..a986d21d2f356 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -16,6 +16,7 @@ import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import type { AppLeaveHandler, AppMountParameters } from '@kbn/core/public'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; +import { CellActionsProvider } from '@kbn/cell-actions'; import { ManageUserInfo } from '../detections/components/user_info'; import { DEFAULT_DARK_MODE, APP_NAME } from '../../common/constants'; import { ErrorToastDispatcher } from '../common/components/error_toast_dispatcher'; @@ -49,6 +50,7 @@ const StartAppComponent: FC = ({ const { i18n, application: { capabilities }, + uiActions, } = useKibana().services; const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); return ( @@ -62,13 +64,17 @@ const StartAppComponent: FC = ({ - - {children} - + + {children} + + diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx index 6737fdf2c525c..c06390b4f9a16 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx @@ -50,6 +50,7 @@ jest.mock('../../lib/kibana', () => { jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useHistory: () => mockHistory, + useLocation: jest.fn().mockReturnValue({ pathname: '/test' }), })); const FakeStatefulEventsViewer = ({ additionalFilters }: { additionalFilters: JSX.Element }) => ( diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx index 4b79d36dfef80..7872476d19b20 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx @@ -14,7 +14,7 @@ import { LinkIcon } from '../link_icon'; import type { SubtitleProps } from '../subtitle'; import { Subtitle } from '../subtitle'; import { Title } from './title'; -import type { DraggableArguments, BadgeOptions, TitleProp } from './types'; +import type { BadgeOptions, TitleProp } from './types'; import { useFormatUrl } from '../link_to'; import type { SecurityPageName } from '../../../app/types'; import { useKibana } from '../../lib/kibana'; @@ -64,7 +64,6 @@ export interface HeaderPageProps extends HeaderProps { backComponent?: React.ReactNode; badgeOptions?: BadgeOptions; children?: React.ReactNode; - draggableArguments?: DraggableArguments; rightSideItems?: React.ReactNode[]; subtitle?: SubtitleProps['items']; subtitle2?: SubtitleProps['items']; @@ -103,7 +102,6 @@ const HeaderPageComponent: React.FC = ({ badgeOptions, border, children, - draggableArguments, isLoading, rightSideItems, subtitle, @@ -117,13 +115,7 @@ const HeaderPageComponent: React.FC = ({ {backOptions && } {!backOptions && backComponent && <>{backComponent}} - {titleNode || ( - - )} + {titleNode || <Title title={title} badgeOptions={badgeOptions} />} {subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />} {subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />} diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx index d21adbd00cc20..d2f030fb15ba3 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx @@ -38,24 +38,4 @@ describe('Title', () => { expect(wrapper.find('[data-test-subj="header-page-title"]').first().exists()).toBe(true); }); - - test('it renders as a draggable when arguments provided', () => { - const wrapper = mount( - <TestProviders> - <Title draggableArguments={{ field: 'neat', value: 'cool' }} title="Test title" /> - </TestProviders> - ); - - expect(wrapper.find('[data-test-subj="header-page-draggable"]').first().exists()).toBe(true); - }); - - test('it DOES NOT render as a draggable when arguments not provided', () => { - const wrapper = mount( - <TestProviders> - <Title title="Test title" /> - </TestProviders> - ); - - expect(wrapper.find('[data-test-subj="header-page-draggable"]').first().exists()).toBe(false); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/title.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/title.tsx index 0f54c6f579a9f..50602b95d996e 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/title.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/title.tsx @@ -9,8 +9,7 @@ import React from 'react'; import { EuiBetaBadge, EuiBadge, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; -import type { DraggableArguments, BadgeOptions, TitleProp } from './types'; -import { DefaultDraggable } from '../draggables'; +import type { BadgeOptions, TitleProp } from './types'; import { TruncatableText } from '../truncatable_text'; const Header = styled.h1` @@ -34,24 +33,14 @@ TitleWrapper.displayName = 'TitleWrapper'; interface Props { badgeOptions?: BadgeOptions; title: TitleProp; - draggableArguments?: DraggableArguments; } -const TitleComponent: React.FC<Props> = ({ draggableArguments, title, badgeOptions }) => ( +const TitleComponent: React.FC<Props> = ({ title, badgeOptions }) => ( <EuiTitle size="l"> <Header data-test-subj="header-page-title"> - {!draggableArguments ? ( - <TitleWrapper> - <TruncatableText tooltipContent={title}>{title}</TruncatableText> - </TitleWrapper> - ) : ( - <DefaultDraggable - data-test-subj="header-page-draggable" - id={`header-page-draggable-${draggableArguments.field}-${draggableArguments.value}`} - field={draggableArguments.field} - value={`${draggableArguments.value}`} - /> - )} + <TitleWrapper> + <TruncatableText tooltipContent={title}>{title}</TruncatableText> + </TitleWrapper> {badgeOptions && ( <> {badgeOptions.beta ? ( diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/types.ts b/x-pack/plugins/security_solution/public/common/components/header_page/types.ts index 698178480045d..558c454f6484c 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/header_page/types.ts @@ -10,11 +10,6 @@ import type { BetaBadgeSize } from '@elastic/eui/src/components/badge/beta_badge import type React from 'react'; export type TitleProp = string | React.ReactNode; -export interface DraggableArguments { - field: string; - value: string; -} - export interface BadgeOptions { beta?: boolean; text: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.test.tsx index 8428c16ae9bc0..e7f80e613fab3 100644 --- a/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.test.tsx @@ -34,6 +34,14 @@ jest.mock('../../../lib/kibana', () => { }; }); +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + return { + ...original, + useLocation: jest.fn().mockReturnValue({ pathname: '/test' }), + }; +}); + describe('show topN button', () => { const defaultProps = { field: 'signal.rule.name', diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx index 61efd3c906543..ac5a43d23a28b 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx @@ -15,7 +15,6 @@ import { MatrixHistogramType } from '../../../../common/search_strategy/security import { TestProviders } from '../../mock'; import { mockRuntimeMappings } from '../../containers/source/mock'; import { dnsTopDomainsLensAttributes } from '../visualization_actions/lens_attributes/network/dns_top_domains'; -import { useRouteSpy } from '../../utils/route/use_route_spy'; import { useQueryToggle } from '../../containers/query_toggle'; jest.mock('../../containers/query_toggle'); @@ -41,15 +40,16 @@ jest.mock('./utils', () => ({ getCustomChartData: jest.fn().mockReturnValue(true), })); -jest.mock('../../utils/route/use_route_spy', () => ({ - useRouteSpy: jest.fn().mockReturnValue([ - { - detailName: 'mockHost', - pageName: 'hosts', - tabName: 'events', - }, - ]), -})); +const mockLocation = jest.fn().mockReturnValue({ pathname: '/test' }); + +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useLocation: () => mockLocation(), + }; +}); describe('Matrix Histogram Component', () => { let wrapper: ReactWrapper; @@ -177,13 +177,7 @@ describe('Matrix Histogram Component', () => { describe('Inspect button', () => { test("it doesn't render Inspect button by default on Host page", () => { - (useRouteSpy as jest.Mock).mockReturnValue([ - { - detailName: 'mockHost', - pageName: 'hosts', - tabName: 'events', - }, - ]); + mockLocation.mockReturnValue({ pathname: '/hosts' }); const testProps = { ...mockMatrixOverTimeHistogramProps, @@ -196,13 +190,7 @@ describe('Matrix Histogram Component', () => { }); test("it doesn't render Inspect button by default on Network page", () => { - (useRouteSpy as jest.Mock).mockReturnValue([ - { - detailName: undefined, - pageName: 'network', - tabName: 'external-alerts', - }, - ]); + mockLocation.mockReturnValue({ pathname: '/network' }); const testProps = { ...mockMatrixOverTimeHistogramProps, @@ -215,13 +203,7 @@ describe('Matrix Histogram Component', () => { }); test('it render Inspect button by default on other pages', () => { - (useRouteSpy as jest.Mock).mockReturnValue([ - { - detailName: undefined, - pageName: 'overview', - tabName: undefined, - }, - ]); + mockLocation.mockReturnValue({ pathname: '/overview' }); const testProps = { ...mockMatrixOverTimeHistogramProps, @@ -236,13 +218,7 @@ describe('Matrix Histogram Component', () => { describe('VisualizationActions', () => { test('it renders VisualizationActions on Host page if lensAttributes is provided', () => { - (useRouteSpy as jest.Mock).mockReturnValue([ - { - detailName: 'mockHost', - pageName: 'hosts', - tabName: 'events', - }, - ]); + mockLocation.mockReturnValue({ pathname: '/hosts' }); const testProps = { ...mockMatrixOverTimeHistogramProps, @@ -258,13 +234,7 @@ describe('Matrix Histogram Component', () => { }); test('it renders VisualizationActions on Network page if lensAttributes is provided', () => { - (useRouteSpy as jest.Mock).mockReturnValue([ - { - detailName: undefined, - pageName: 'network', - tabName: 'events', - }, - ]); + mockLocation.mockReturnValue({ pathname: '/network' }); const testProps = { ...mockMatrixOverTimeHistogramProps, @@ -285,13 +255,7 @@ describe('Matrix Histogram Component', () => { lensAttributes: dnsTopDomainsLensAttributes, }; - (useRouteSpy as jest.Mock).mockReturnValue([ - { - detailName: undefined, - pageName: 'overview', - tabName: undefined, - }, - ]); + mockLocation.mockReturnValue({ pathname: '/overview' }); wrapper = mount(<MatrixHistogram {...testProps} />, { wrappingComponent: TestProviders, diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index afc6c133e4c27..7b44b9295218c 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -11,6 +11,7 @@ import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect, EuiSpacer } from '@elastic/eui'; import { useDispatch } from 'react-redux'; +import { useLocation } from 'react-router-dom'; import * as i18n from './translations'; import { BarChart } from '../charts/barchart'; import { HeaderSection } from '../header_section'; @@ -33,10 +34,9 @@ import { InputsModelId } from '../../store/inputs/constants'; import { HoverVisibilityContainer } from '../hover_visibility_container'; import { VisualizationActions } from '../visualization_actions'; import type { GetLensAttributes, LensAttributes } from '../visualization_actions/types'; -import { SecurityPageName } from '../../../../common/constants'; -import { useRouteSpy } from '../../utils/route/use_route_spy'; import { useQueryToggle } from '../../containers/query_toggle'; import { VISUALIZATION_ACTIONS_BUTTON_CLASS } from '../visualization_actions/utils'; +import { isExplorePage } from '../../../helpers'; export type MatrixHistogramComponentProps = MatrixHistogramProps & Omit<MatrixHistogramQueryProps, 'stackByField'> & { @@ -60,6 +60,7 @@ export type MatrixHistogramComponentProps = MatrixHistogramProps & subtitle?: string | GetSubTitle; scopeId?: string; title: string | GetTitle; + hideQueryToggle?: boolean; }; const DEFAULT_PANEL_HEIGHT = 300; @@ -103,8 +104,10 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> = titleSize, yTickFormatter, skip, + hideQueryToggle = false, }) => { const dispatch = useDispatch(); + const { pathname } = useLocation(); const handleBrushEnd = useCallback( ({ x }) => { @@ -151,6 +154,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> = }, [defaultStackByOption, stackByOptions] ); + const { toggleStatus, setToggleStatus } = useQueryToggle(id); const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); useEffect(() => { @@ -180,12 +184,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> = }; const [loading, { data, inspect, totalCount, refetch }] = useMatrixHistogramCombined(matrixHistogramRequest); - const [{ pageName }] = useRouteSpy(); - - const onHostOrNetworkOrUserPage = - pageName === SecurityPageName.hosts || - pageName === SecurityPageName.network || - pageName === SecurityPageName.users; + const onExplorePage = isExplorePage(pathname); const titleWithStackByField = useMemo( () => (title != null && typeof title === 'function' ? title(selectedStackByOption) : title), @@ -259,14 +258,14 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> = title={titleWithStackByField} titleSize={titleSize} toggleStatus={toggleStatus} - toggleQuery={toggleQuery} + toggleQuery={hideQueryToggle ? undefined : toggleQuery} subtitle={subtitleWithCounts} inspectMultiple - showInspectButton={showInspectButton || !onHostOrNetworkOrUserPage} + showInspectButton={showInspectButton || !onExplorePage} isInspectDisabled={filterQuery === undefined} > <EuiFlexGroup alignItems="center" gutterSize="none"> - {onHostOrNetworkOrUserPage && (getLensAttributes || lensAttributes) && timerange && ( + {onExplorePage && (getLensAttributes || lensAttributes) && timerange && ( <EuiFlexItem grow={false}> <VisualizationActions className="histogram-viz-actions" diff --git a/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap new file mode 100644 index 0000000000000..98e5faaf31f47 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`entity_draggable renders correctly against snapshot 1`] = ` +<CellActions + field={ + Object { + "name": "entity-name", + "type": "keyword", + "value": "entity-value", + } + } + mode="hover" + triggerId="security-solution-default-cellActions" + visibleCellActions={5} +> + entity-name: "entity-value" +</CellActions> +`; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity_draggable.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity_draggable.test.tsx.snap deleted file mode 100644 index eaa7721271d98..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity_draggable.test.tsx.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`entity_draggable renders correctly against snapshot 1`] = ` -<DraggableWrapper - dataProvider={ - Object { - "and": Array [], - "enabled": true, - "excluded": false, - "id": "entity-draggable-id-prefix-entity-name-entity-value", - "kqlQuery": "", - "name": "entity-value", - "queryMatch": Object { - "field": "entity-name", - "operator": ":", - "value": "entity-value", - }, - } - } - fieldType="keyword" - isAggregatable={true} - key="entity-draggable-id-prefix-entity-name-entity-value" - render={[Function]} -/> -`; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/entity.test.tsx similarity index 76% rename from x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx rename to x-pack/plugins/security_solution/public/common/components/ml/entity.test.tsx index 5f093db3f69bb..4441e483f063b 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/entity.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import '../../mock/match_media'; -import { EntityDraggableComponent } from './entity_draggable'; +import { EntityComponent } from './entity'; import { TestProviders } from '../../mock/test_providers'; import { useMountAppended } from '../../utils/use_mount_appended'; @@ -28,11 +28,7 @@ describe('entity_draggable', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - <EntityDraggableComponent - idPrefix="id-prefix" - entityName="entity-name" - entityValue="entity-value" - /> + <EntityComponent entityName="entity-name" entityValue="entity-value" /> ); expect(wrapper).toMatchSnapshot(); }); @@ -40,11 +36,7 @@ describe('entity_draggable', () => { test('renders with entity name with entity value as text', () => { const wrapper = mount( <TestProviders> - <EntityDraggableComponent - idPrefix="id-prefix" - entityName="entity-name" - entityValue="entity-value" - /> + <EntityComponent entityName="entity-name" entityValue="entity-value" /> </TestProviders> ); expect(wrapper.text()).toEqual('entity-name: "entity-value"'); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx b/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx new file mode 100644 index 0000000000000..64d7f9be82d2f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx @@ -0,0 +1,38 @@ +/* + * 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 React from 'react'; +import { CellActions, CellActionsMode } from '@kbn/cell-actions'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../common/constants'; + +interface Props { + entityName: string; + entityValue: string; +} + +export const EntityComponent: React.FC<Props> = ({ entityName, entityValue }) => { + return ( + <CellActions + field={{ + name: entityName, + value: entityValue, + type: 'keyword', + }} + triggerId={CELL_ACTIONS_DEFAULT_TRIGGER} + mode={CellActionsMode.HOVER} + visibleCellActions={5} + > + {`${entityName}: "${entityValue}"`} + </CellActions> + ); +}; + +EntityComponent.displayName = 'EntityComponent'; + +export const Entity = React.memo(EntityComponent); + +Entity.displayName = 'Entity'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.tsx b/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.tsx deleted file mode 100644 index 1b1c03d66f6c3..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.tsx +++ /dev/null @@ -1,72 +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 React, { useCallback, useMemo } from 'react'; -import { DraggableWrapper, DragEffects } from '../drag_and_drop/draggable_wrapper'; -import type { QueryOperator } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; -import { escapeDataProviderId } from '../drag_and_drop/helpers'; - -interface Props { - idPrefix: string; - entityName: string; - entityValue: string; -} - -export const EntityDraggableComponent: React.FC<Props> = ({ - idPrefix, - entityName, - entityValue, -}) => { - const id = escapeDataProviderId(`entity-draggable-${idPrefix}-${entityName}-${entityValue}`); - - const dataProviderProp = useMemo( - () => ({ - and: [], - enabled: true, - id, - name: entityValue, - excluded: false, - kqlQuery: '', - queryMatch: { - field: entityName, - value: entityValue, - operator: IS_OPERATOR as QueryOperator, - }, - }), - [entityName, entityValue, id] - ); - - const render = useCallback( - (dataProvider, _, snapshot) => - snapshot.isDragging ? ( - <DragEffects> - <Provider dataProvider={dataProvider} /> - </DragEffects> - ) : ( - <>{`${entityName}: "${entityValue}"`}</> - ), - [entityName, entityValue] - ); - - return ( - <DraggableWrapper - key={id} - dataProvider={dataProviderProp} - render={render} - isAggregatable={true} - fieldType={'keyword'} - /> - ); -}; - -EntityDraggableComponent.displayName = 'EntityDraggableComponent'; - -export const EntityDraggable = React.memo(EntityDraggableComponent); - -EntityDraggable.displayName = 'EntityDraggable'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap index dbdb6eec840fa..eb1247dded8a0 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap @@ -5,8 +5,7 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = ` <EuiFlexItem grow={false} > - <DraggableScore - id="anomaly-scores-job-key-1" + <Score index={0} score={ Object { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/draggable_score.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/draggable_score.test.tsx.snap deleted file mode 100644 index 392c0bd4ba606..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/draggable_score.test.tsx.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`draggable_score renders correctly against snapshot 1`] = ` -<DraggableWrapper - dataProvider={ - Object { - "and": Array [], - "enabled": true, - "excluded": false, - "id": "some-id", - "kqlQuery": "", - "name": "process.name", - "queryMatch": Object { - "field": "process.name", - "operator": ":", - "value": "du", - }, - } - } - fieldType="keyword" - isAggregatable={true} - key="draggable-score-draggable-wrapper-some-id" - render={[Function]} -/> -`; - -exports[`draggable_score renders correctly against snapshot when the index is not included 1`] = ` -<DraggableWrapper - dataProvider={ - Object { - "and": Array [], - "enabled": true, - "excluded": false, - "id": "some-id", - "kqlQuery": "", - "name": "process.name", - "queryMatch": Object { - "field": "process.name", - "operator": ":", - "value": "du", - }, - } - } - fieldType="keyword" - isAggregatable={true} - key="draggable-score-draggable-wrapper-some-id" - render={[Function]} -/> -`; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/score.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/score.test.tsx.snap new file mode 100644 index 0000000000000..3aa3f9ef67ac6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/score.test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`draggable_score renders correctly against snapshot 1`] = ` +<CellActions + field={ + Object { + "name": "process.name", + "type": "keyword", + "value": "du", + } + } + mode="hover" + triggerId="security-solution-default-cellActions" + visibleCellActions={5} +> + 17 +</CellActions> +`; + +exports[`draggable_score renders correctly against snapshot when the index is not included 1`] = ` +<CellActions + field={ + Object { + "name": "process.name", + "type": "keyword", + "value": "du", + } + } + mode="hover" + triggerId="security-solution-default-cellActions" + visibleCellActions={5} +> + 17 +</CellActions> +`; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.tsx index 28fff36fb2fe5..ad87c23cf0067 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.tsx @@ -9,8 +9,7 @@ import React, { useState } from 'react'; import { EuiPopover, EuiDescriptionList, EuiFlexItem, EuiIcon } from '@elastic/eui'; import styled from 'styled-components'; import type { NarrowDateRange, Anomaly } from '../types'; -import { DraggableScore } from './draggable_score'; -import { escapeDataProviderId } from '../../drag_and_drop/helpers'; +import { Score } from './score'; import { createDescriptionList } from './create_description_list'; interface Args { @@ -45,11 +44,7 @@ export const AnomalyScoreComponent = ({ return ( <> <EuiFlexItem grow={false}> - <DraggableScore - id={escapeDataProviderId(`anomaly-scores-${jobKey}`)} - index={index} - score={score} - /> + <Score index={index} score={score} /> </EuiFlexItem> <EuiFlexItem grow={false}> <EuiPopover diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/draggable_score.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/draggable_score.tsx deleted file mode 100644 index 59baeae534682..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/draggable_score.tsx +++ /dev/null @@ -1,80 +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 React, { useCallback, useMemo } from 'react'; -import { DraggableWrapper, DragEffects } from '../../drag_and_drop/draggable_wrapper'; -import type { Anomaly } from '../types'; -import type { QueryOperator } from '../../../../timelines/components/timeline/data_providers/data_provider'; -import { IS_OPERATOR } from '../../../../timelines/components/timeline/data_providers/data_provider'; -import { Provider } from '../../../../timelines/components/timeline/data_providers/provider'; -import { Spacer } from '../../page'; -import { getScoreString } from './score_health'; - -export const DraggableScoreComponent = ({ - id, - index = 0, - score, -}: { - id: string; - index?: number; - score: Anomaly; -}): JSX.Element => { - const scoreString = getScoreString(score.severity); - - const dataProviderProp = useMemo( - () => ({ - and: [], - enabled: true, - id, - name: score.entityName, - excluded: false, - kqlQuery: '', - queryMatch: { - field: score.entityName, - value: score.entityValue, - operator: IS_OPERATOR as QueryOperator, - }, - }), - [id, score.entityName, score.entityValue] - ); - - const render = useCallback( - (dataProvider, _, snapshot) => - snapshot.isDragging ? ( - <DragEffects> - <Provider dataProvider={dataProvider} /> - </DragEffects> - ) : ( - <> - {index !== 0 && ( - <> - {','} - <Spacer /> - </> - )} - {scoreString} - </> - ), - [index, scoreString] - ); - - return ( - <DraggableWrapper - key={`draggable-score-draggable-wrapper-${id}`} - dataProvider={dataProviderProp} - render={render} - isAggregatable={true} - fieldType="keyword" - /> - ); -}; - -DraggableScoreComponent.displayName = 'DraggableScoreComponent'; - -export const DraggableScore = React.memo(DraggableScoreComponent); - -DraggableScore.displayName = 'DraggableScore'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/draggable_score.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/score.test.tsx similarity index 73% rename from x-pack/plugins/security_solution/public/common/components/ml/score/draggable_score.test.tsx rename to x-pack/plugins/security_solution/public/common/components/ml/score/score.test.tsx index a58ef50a3d34d..e47d4d890e5f5 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/draggable_score.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/score.test.tsx @@ -10,7 +10,7 @@ import { cloneDeep } from 'lodash/fp'; import { shallow } from 'enzyme'; import '../../../mock/match_media'; -import { DraggableScoreComponent } from './draggable_score'; +import { ScoreComponent } from './score'; import { mockAnomalies } from '../mock'; describe('draggable_score', () => { @@ -21,16 +21,12 @@ describe('draggable_score', () => { }); test('renders correctly against snapshot', () => { - const wrapper = shallow( - <DraggableScoreComponent id="some-id" index={0} score={anomalies.anomalies[0]} /> - ); + const wrapper = shallow(<ScoreComponent index={0} score={anomalies.anomalies[0]} />); expect(wrapper).toMatchSnapshot(); }); test('renders correctly against snapshot when the index is not included', () => { - const wrapper = shallow( - <DraggableScoreComponent id="some-id" score={anomalies.anomalies[0]} /> - ); + const wrapper = shallow(<ScoreComponent score={anomalies.anomalies[0]} />); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx new file mode 100644 index 0000000000000..dd72775ee2588 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx @@ -0,0 +1,52 @@ +/* + * 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 React from 'react'; +import { CellActions, CellActionsMode } from '@kbn/cell-actions'; +import type { Anomaly } from '../types'; +import { Spacer } from '../../page'; +import { getScoreString } from './score_health'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../../common/constants'; + +export const ScoreComponent = ({ + index = 0, + score, +}: { + index?: number; + score: Anomaly; +}): JSX.Element => { + const scoreString = getScoreString(score.severity); + + return ( + <CellActions + mode={CellActionsMode.HOVER} + field={{ + name: score.entityName, + value: score.entityValue, + type: 'keyword', + }} + triggerId={CELL_ACTIONS_DEFAULT_TRIGGER} + visibleCellActions={5} + > + <> + {index !== 0 && ( + <> + {','} + <Spacer /> + </> + )} + {scoreString} + </> + </CellActions> + ); +}; + +ScoreComponent.displayName = 'ScoreComponent'; + +export const Score = React.memo(ScoreComponent); + +Score.displayName = 'Score'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx index 165f215f7ae0b..198f27bb7c829 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx @@ -8,7 +8,7 @@ import React from 'react'; import type { Columns } from '../../../../explore/components/paginated_table'; import type { AnomaliesByHost, Anomaly } from '../types'; -import { getRowItemDraggable } from '../../tables/helpers'; +import { getRowItemsWithActions } from '../../tables/helpers'; import { createCompoundAnomalyKey } from './create_compound_key'; import { HostDetailsLink } from '../../links'; import * as i18n from './translations'; @@ -31,15 +31,14 @@ export const getAnomaliesHostTableColumns = ( field: 'hostName', sortable: true, render: (hostName, anomaliesByHost) => - getRowItemDraggable({ - rowItem: hostName, - attrName: 'host.name', + getRowItemsWithActions({ + values: [hostName], + fieldName: 'host.name', idPrefix: `anomalies-host-table-hostName-${createCompoundAnomalyKey( anomaliesByHost.anomaly )}-hostName`, - render: (item) => <HostDetailsLink hostName={item} />, - isAggregatable: true, fieldType: 'keyword', + render: (item) => <HostDetailsLink hostName={item} />, }), }, ...getAnomaliesDefaultTableColumns(startDate, endDate), diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx index 78468320ab24f..3edd314d93ec2 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx @@ -9,7 +9,7 @@ import React from 'react'; import type { Columns } from '../../../../explore/components/paginated_table'; import type { Anomaly, AnomaliesByNetwork } from '../types'; -import { getRowItemDraggable } from '../../tables/helpers'; +import { getRowItemsWithActions } from '../../tables/helpers'; import { createCompoundAnomalyKey } from './create_compound_key'; import { NetworkDetailsLink } from '../../links'; @@ -35,15 +35,14 @@ export const getAnomaliesNetworkTableColumns = ( field: 'ip', sortable: true, render: (ip, anomaliesByNetwork) => - getRowItemDraggable({ - rowItem: ip, - attrName: anomaliesByNetwork.type, + getRowItemsWithActions({ + values: [ip], + fieldName: anomaliesByNetwork.type, idPrefix: `anomalies-network-table-ip-${createCompoundAnomalyKey( anomaliesByNetwork.anomaly )}`, - render: (item) => <NetworkDetailsLink ip={item} flowTarget={flowTarget} />, - isAggregatable: true, fieldType: 'ip', + render: (item) => <NetworkDetailsLink ip={item} flowTarget={flowTarget} />, }), }, ...getAnomaliesDefaultTableColumns(startDate, endDate), diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_table_columns.tsx index e8d4b13bc1b73..ea45e95c8cf80 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_table_columns.tsx @@ -10,14 +10,13 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { Columns } from '../../../../explore/components/paginated_table'; import type { AnomaliesBy, Anomaly } from '../types'; -import { EntityDraggable } from '../entity_draggable'; +import { Entity } from '../entity'; import { createCompoundAnomalyKey } from './create_compound_key'; import * as i18n from './translations'; import { getEntries } from '../get_entries'; -import { DraggableScore } from '../score/draggable_score'; +import { Score } from '../score/score'; import { ExplorerLink } from '../links/create_explorer_link'; -import { escapeDataProviderId } from '../../drag_and_drop/helpers'; import { FormattedRelativePreferenceDate } from '../../formatted_date'; export const getAnomaliesDefaultTableColumns = ( @@ -47,27 +46,14 @@ export const getAnomaliesDefaultTableColumns = ( name: i18n.SCORE, field: 'anomaly.severity', sortable: true, - render: (_, anomalyBy) => ( - <DraggableScore - id={escapeDataProviderId( - `anomalies-table-severity-${createCompoundAnomalyKey(anomalyBy.anomaly)}` - )} - score={anomalyBy.anomaly} - /> - ), + render: (_, anomalyBy) => <Score score={anomalyBy.anomaly} />, }, { name: i18n.ENTITY, field: 'anomaly.entityValue', sortable: true, render: (entityValue, anomalyBy) => ( - <EntityDraggable - idPrefix={`anomalies-table-entityValue${createCompoundAnomalyKey( - anomalyBy.anomaly - )}-entity`} - entityName={anomalyBy.anomaly.entityName} - entityValue={entityValue} - /> + <Entity entityName={anomalyBy.anomaly.entityName} entityValue={entityValue} /> ), }, { @@ -87,13 +73,7 @@ export const getAnomaliesDefaultTableColumns = ( > <EuiFlexGroup gutterSize="none" responsive={false}> <EuiFlexItem grow={false}> - <EntityDraggable - idPrefix={`anomalies-table-influencers-${entityName}-${entityValue}-${createCompoundAnomalyKey( - anomalyBy.anomaly - )}`} - entityName={entityName} - entityValue={entityValue} - /> + <Entity entityName={entityName} entityValue={entityValue} /> </EuiFlexItem> </EuiFlexGroup> </EuiFlexItem> diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.tsx index 02f36d9d504e4..ebd731b9a180a 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.tsx @@ -8,7 +8,7 @@ import React from 'react'; import type { Columns } from '../../../../explore/components/paginated_table'; import type { AnomaliesByUser, Anomaly } from '../types'; -import { getRowItemDraggable } from '../../tables/helpers'; +import { getRowItemsWithActions } from '../../tables/helpers'; import { createCompoundAnomalyKey } from './create_compound_key'; import { UserDetailsLink } from '../../links'; @@ -32,15 +32,14 @@ export const getAnomaliesUserTableColumns = ( field: 'userName', sortable: true, render: (userName, anomaliesByUser) => - getRowItemDraggable({ - rowItem: userName, - attrName: 'user.name', + getRowItemsWithActions({ + values: [userName], + fieldName: 'user.name', idPrefix: `anomalies-user-table-userName-${createCompoundAnomalyKey( anomaliesByUser.anomaly )}-userName`, - render: (item) => <UserDetailsLink userName={item} />, - isAggregatable: true, fieldType: 'keyword', + render: (item) => <UserDetailsLink userName={item} />, }), }, ...getAnomaliesDefaultTableColumns(startDate, endDate), diff --git a/x-pack/plugins/security_solution/public/common/components/tables/__snapshots__/helpers.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/tables/__snapshots__/helpers.test.tsx.snap index a84e71149deeb..a88b00c573b2e 100644 --- a/x-pack/plugins/security_solution/public/common/components/tables/__snapshots__/helpers.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/tables/__snapshots__/helpers.test.tsx.snap @@ -10,11 +10,12 @@ exports[`Table Helpers #RowItemOverflow it returns correctly against snapshot 1` size="xs" > <MoreContainer - attrName="attrName" + fieldName="attrName" + fieldType="keyword" idPrefix="idPrefix" moreMaxHeight="none" overflowIndexStart={1} - rowItems={ + values={ Array [ "item1", "item2", @@ -42,118 +43,6 @@ exports[`Table Helpers #RowItemOverflow it returns correctly against snapshot 1` </Fragment> `; -exports[`Table Helpers #getRowItemDraggable it returns correctly against snapshot 1`] = ` -<DraggableWrapper - dataProvider={ - Object { - "and": Array [], - "enabled": true, - "excluded": false, - "id": "idPrefix-attrName-item1", - "kqlQuery": "", - "name": "item1", - "queryMatch": Object { - "displayValue": "item1", - "field": "attrName", - "operator": ":", - "value": "item1", - }, - } - } - key="idPrefix-attrName-item1" - render={[Function]} -/> -`; - -exports[`Table Helpers #getRowItemDraggables it returns correctly against snapshot 1`] = ` -<DragDropContext - onDragEnd={[MockFunction]} -> - <DraggableWrapper - dataProvider={ - Object { - "and": Array [], - "enabled": true, - "excluded": false, - "id": "idPrefix-attrName-item1-0", - "kqlQuery": "", - "name": "item1", - "queryMatch": Object { - "displayValue": "item1", - "field": "attrName", - "operator": ":", - "value": "item1", - }, - } - } - fieldType="keyword" - isAggregatable={false} - key="idPrefix-attrName-item1-0" - render={[Function]} - /> - <DraggableWrapper - dataProvider={ - Object { - "and": Array [], - "enabled": true, - "excluded": false, - "id": "idPrefix-attrName-item2-1", - "kqlQuery": "", - "name": "item2", - "queryMatch": Object { - "displayValue": "item2", - "field": "attrName", - "operator": ":", - "value": "item2", - }, - } - } - fieldType="keyword" - isAggregatable={false} - key="idPrefix-attrName-item2-1" - render={[Function]} - /> - <DraggableWrapper - dataProvider={ - Object { - "and": Array [], - "enabled": true, - "excluded": false, - "id": "idPrefix-attrName-item3-2", - "kqlQuery": "", - "name": "item3", - "queryMatch": Object { - "displayValue": "item3", - "field": "attrName", - "operator": ":", - "value": "item3", - }, - } - } - fieldType="keyword" - isAggregatable={false} - key="idPrefix-attrName-item3-2" - render={[Function]} - /> - - <Memo(RowItemOverflowComponent) - attrName="attrName" - fieldType="keyword" - idPrefix="idPrefix" - isAggregatable={false} - maxOverflowItems={5} - overflowIndexStart={5} - rowItems={ - Array [ - "item1", - "item2", - "item3", - ] - } - /> -</DragDropContext> -`; - exports[`Table Helpers OverflowField it returns correctly against snapshot 1`] = ` <span> This string is exactly fifty-one chars in length!! diff --git a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx index d5c924bbea172..668d51562305a 100644 --- a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx @@ -10,14 +10,14 @@ import { shallow } from 'enzyme'; import '../../mock/match_media'; import { - getRowItemDraggables, RowItemOverflowComponent, - getRowItemDraggable, OverflowFieldComponent, + getRowItemsWithActions, } from './helpers'; import { TestProviders } from '../../mock'; -import { getEmptyValue } from '../empty_value'; import { useMountAppended } from '../../utils/use_mount_appended'; +import { getEmptyValue } from '../empty_value'; +import { render } from '@testing-library/react'; jest.mock('../../lib/kibana'); @@ -25,159 +25,75 @@ describe('Table Helpers', () => { const items = ['item1', 'item2', 'item3']; const mount = useMountAppended(); - describe('#getRowItemDraggable', () => { - test('it returns correctly against snapshot', () => { - const rowItem = getRowItemDraggable({ - rowItem: 'item1', - attrName: 'attrName', + describe('#getRowItemsWithActions', () => { + test('it returns empty value when values is undefined', () => { + const rowItem = getRowItemsWithActions({ + values: undefined, + fieldName: 'attrName', idPrefix: 'idPrefix', }); - const wrapper = shallow(<TestProviders>{rowItem}</TestProviders>); - expect(wrapper.find('DraggableWrapper')).toMatchSnapshot(); - }); - test('it returns empty value when rowItem is undefined', () => { - const rowItem = getRowItemDraggable({ - rowItem: undefined, - attrName: 'attrName', - idPrefix: 'idPrefix', - displayCount: 0, - }); - const wrapper = mount(<TestProviders>{rowItem}</TestProviders>); - expect(wrapper.find('DragDropContext').text()).toBe(getEmptyValue()); + const { container } = render(<TestProviders>{rowItem}</TestProviders>); + + expect(container.textContent).toBe(getEmptyValue()); }); - test('it returns empty string value when rowItem is empty', () => { - const rowItem = getRowItemDraggable({ - rowItem: '', - attrName: 'attrName', + test('it returns empty string value when values is empty', () => { + const rowItem = getRowItemsWithActions({ + values: [''], + fieldName: 'attrName', idPrefix: 'idPrefix', - displayCount: 0, }); - const wrapper = mount(<TestProviders>{rowItem}</TestProviders>); - expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( - '(Empty string)' - ); + const { container } = render(<TestProviders>{rowItem}</TestProviders>); + + expect(container.textContent).toContain('(Empty string)'); }); test('it returns empty value when rowItem is null', () => { - const rowItem = getRowItemDraggable({ - rowItem: null, - attrName: 'attrName', + const rowItem = getRowItemsWithActions({ + values: null, + fieldName: 'attrName', idPrefix: 'idPrefix', displayCount: 0, }); - const wrapper = mount(<TestProviders>{rowItem}</TestProviders>); - - expect(wrapper.text()).toBe(getEmptyValue()); + const { container } = render(<TestProviders>{rowItem}</TestProviders>); + expect(container.textContent).toBe(getEmptyValue()); }); test('it uses custom renderer', () => { const renderer = (item: string) => <>{`Hi ${item} renderer`}</>; - const rowItem = getRowItemDraggable({ - rowItem: 'item1', - attrName: 'attrName', + const rowItem = getRowItemsWithActions({ + values: ['item1'], + fieldName: 'attrName', idPrefix: 'idPrefix', render: renderer, }); - const wrapper = mount(<TestProviders>{rowItem}</TestProviders>); - expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( - 'Hi item1 renderer' - ); - }); - }); - - describe('#getRowItemDraggables', () => { - test('it returns correctly against snapshot', () => { - const rowItems = getRowItemDraggables({ - rowItems: items, - attrName: 'attrName', - idPrefix: 'idPrefix', - }); - const wrapper = shallow(<TestProviders>{rowItems}</TestProviders>); - expect(wrapper.find('DragDropContext')).toMatchSnapshot(); - }); - - test('it returns empty value when rowItems is undefined', () => { - const rowItems = getRowItemDraggables({ - rowItems: undefined, - attrName: 'attrName', - idPrefix: 'idPrefix', - displayCount: 0, - }); - const wrapper = mount(<TestProviders>{rowItems}</TestProviders>); - expect(wrapper.text()).toBe(getEmptyValue()); - }); - - test('it returns empty string value when rowItem is empty', () => { - const rowItems = getRowItemDraggables({ - rowItems: [''], - attrName: 'attrName', - idPrefix: 'idPrefix', - }); - const wrapper = mount(<TestProviders>{rowItems}</TestProviders>); - expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( - '(Empty string)' - ); - }); + const { container } = render(<TestProviders>{rowItem}</TestProviders>); - test('it returns empty value when rowItems is null', () => { - const rowItems = getRowItemDraggables({ - rowItems: null, - attrName: 'attrName', - idPrefix: 'idPrefix', - displayCount: 0, - }); - const wrapper = mount(<TestProviders>{rowItems}</TestProviders>); - expect(wrapper.text()).toBe(getEmptyValue()); - }); - - test('it returns no items when provided a 0 displayCount', () => { - const rowItems = getRowItemDraggables({ - rowItems: items, - attrName: 'attrName', - idPrefix: 'idPrefix', - displayCount: 0, - }); - const wrapper = mount(<TestProviders>{rowItems}</TestProviders>); - expect(wrapper.text()).toBe(getEmptyValue()); + expect(container.textContent).toContain('Hi item1 renderer'); }); test('it returns no items when provided an empty array', () => { - const rowItems = getRowItemDraggables({ - rowItems: [], - attrName: 'attrName', + const rowItems = getRowItemsWithActions({ + values: [], + fieldName: 'attrName', idPrefix: 'idPrefix', }); - const wrapper = mount(<TestProviders>{rowItems}</TestProviders>); - expect(wrapper.text()).toBe(getEmptyValue()); + const { container } = render(<TestProviders>{rowItems}</TestProviders>); + expect(container.textContent).toBe(getEmptyValue()); }); - // Using hostNodes due to this issue: https://github.com/airbnb/enzyme/issues/836 - - test('it returns 2 items then overflows', () => { - const rowItems = getRowItemDraggables({ - rowItems: items, - attrName: 'attrName', + test('it returns 2 items then overflows when displayCount is 2', () => { + const rowItems = getRowItemsWithActions({ + values: items, + fieldName: 'attrName', idPrefix: 'idPrefix', displayCount: 2, }); - const wrapper = mount(<TestProviders>{rowItems}</TestProviders>); - expect(wrapper.find('[data-test-subj="withHoverActionsButton"]').hostNodes().length).toBe(2); - }); + const { queryAllByTestId, queryByTestId } = render(<TestProviders>{rowItems}</TestProviders>); - test('it uses custom renderer', () => { - const renderer = (item: string) => <>{`Hi ${item} renderer`}</>; - const rowItems = getRowItemDraggables({ - rowItems: items, - attrName: 'attrName', - idPrefix: 'idPrefix', - render: renderer, - }); - const wrapper = mount(<TestProviders>{rowItems}</TestProviders>); - expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( - 'Hi item1 renderer' - ); + expect(queryAllByTestId('cellActions-renderContent-attrName').length).toBe(2); + expect(queryByTestId('overflow-button')).toBeInTheDocument(); }); }); @@ -185,11 +101,12 @@ describe('Table Helpers', () => { test('it returns correctly against snapshot', () => { const wrapper = shallow( <RowItemOverflowComponent - rowItems={items} - attrName="attrName" + values={items} + fieldName="attrName" idPrefix="idPrefix" maxOverflowItems={1} overflowIndexStart={1} + fieldType="keyword" /> ); expect(wrapper).toMatchSnapshot(); @@ -198,11 +115,12 @@ describe('Table Helpers', () => { test('it does not show "more not shown" when maxOverflowItems are not exceeded', () => { const wrapper = shallow( <RowItemOverflowComponent - rowItems={items} - attrName="attrName" + values={items} + fieldName="attrName" idPrefix="idPrefix" maxOverflowItems={5} overflowIndexStart={1} + fieldType="keyword" /> ); expect(wrapper.find('[data-test-subj="popover-additional-overflow"]').length).toBe(0); @@ -212,11 +130,12 @@ describe('Table Helpers', () => { const wrapper = mount( <TestProviders> <RowItemOverflowComponent - rowItems={items} - attrName="attrName" + values={items} + fieldName="attrName" idPrefix="idPrefix" maxOverflowItems={5} overflowIndexStart={1} + fieldType="keyword" /> </TestProviders> ); @@ -231,11 +150,12 @@ describe('Table Helpers', () => { test('it shows "more not shown" when maxOverflowItems are exceeded', () => { const wrapper = shallow( <RowItemOverflowComponent - rowItems={items} - attrName="attrName" + values={items} + fieldName="attrName" idPrefix="idPrefix" maxOverflowItems={1} overflowIndexStart={1} + fieldType="keyword" /> ); expect(wrapper.find('[data-test-subj="popover-additional-overflow"]').length).toBe(1); diff --git a/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx index ccd51e24fcb1b..3e2ac32167334 100644 --- a/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx @@ -8,149 +8,67 @@ import React, { useCallback, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiLink, EuiPopover, EuiToolTip, EuiText, EuiTextColor } from '@elastic/eui'; import styled from 'styled-components'; - -import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; +import { CellActions, CellActionsMode } from '@kbn/cell-actions'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { defaultToEmptyTag, getEmptyTagValue } from '../empty_value'; import { MoreRowItems } from '../page'; -import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; - +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../common/constants'; import { MoreContainer } from '../../../timelines/components/field_renderers/field_renderers'; const Subtext = styled.div` font-size: ${(props) => props.theme.eui.euiFontSizeXS}; `; -interface GetRowItemDraggableParams { - rowItem: string | null | undefined; - attrName: string; - idPrefix: string; - render?: (item: string) => JSX.Element; +interface GetRowItemsWithActionsParams { + values: string[] | null | undefined; + fieldName: string; fieldType?: string; - isAggregatable?: boolean; - displayCount?: number; - dragDisplayValue?: string; - maxOverflow?: number; -} - -export const getRowItemDraggable = ({ - rowItem, - attrName, - idPrefix, - fieldType, - isAggregatable, - render, - dragDisplayValue, -}: GetRowItemDraggableParams): JSX.Element => { - if (rowItem != null) { - const id = escapeDataProviderId(`${idPrefix}-${attrName}-${rowItem}`); - return ( - <DraggableWrapper - key={id} - dataProvider={{ - and: [], - enabled: true, - id, - name: rowItem, - excluded: false, - kqlQuery: '', - queryMatch: { - field: attrName, - value: rowItem, - displayValue: dragDisplayValue || rowItem, - operator: IS_OPERATOR, - }, - }} - fieldType={fieldType} - isAggregatable={isAggregatable} - render={(dataProvider, _, snapshot) => - snapshot.isDragging ? ( - <DragEffects> - <Provider dataProvider={dataProvider} /> - </DragEffects> - ) : ( - <>{render ? render(rowItem) : defaultToEmptyTag(rowItem)}</> - ) - } - /> - ); - } else { - return getEmptyTagValue(); - } -}; - -interface GetRowItemDraggablesParams { - rowItems: string[] | null | undefined; - attrName: string; idPrefix: string; render?: (item: string) => JSX.Element; - fieldType?: string; - isAggregatable?: boolean; displayCount?: number; - dragDisplayValue?: string; maxOverflow?: number; } -export const getRowItemDraggables = ({ - rowItems, - attrName, + +export const getRowItemsWithActions = ({ + values, + fieldName, + fieldType = 'keyword', idPrefix, render, - dragDisplayValue, - fieldType = 'keyword', - isAggregatable = false, displayCount = 5, maxOverflow = 5, -}: GetRowItemDraggablesParams): JSX.Element => { - if (rowItems != null && rowItems.length > 0) { - const draggables = rowItems.slice(0, displayCount).map((rowItem, index) => { - const id = escapeDataProviderId(`${idPrefix}-${attrName}-${rowItem}-${index}`); +}: GetRowItemsWithActionsParams): JSX.Element => { + if (values != null && values.length > 0) { + const visibleItems = values.slice(0, displayCount).map((value, index) => { + const id = escapeDataProviderId(`${idPrefix}-${fieldName}-${value}-${index}`); return ( - <React.Fragment key={id}> - <DraggableWrapper - key={id} - dataProvider={{ - and: [], - enabled: true, - id, - name: rowItem, - excluded: false, - kqlQuery: '', - queryMatch: { - field: attrName, - value: rowItem, - displayValue: dragDisplayValue || rowItem, - operator: IS_OPERATOR, - }, - }} - fieldType={fieldType} - isAggregatable={isAggregatable} - render={(dataProvider, _, snapshot) => - snapshot.isDragging ? ( - <DragEffects> - <Provider dataProvider={dataProvider} /> - </DragEffects> - ) : ( - <>{render ? render(rowItem) : defaultToEmptyTag(rowItem)}</> - ) - } - /> - </React.Fragment> + <CellActions + key={id} + mode={CellActionsMode.HOVER} + visibleCellActions={5} + showActionTooltips + triggerId={CELL_ACTIONS_DEFAULT_TRIGGER} + field={{ + name: fieldName, + value, + type: fieldType, + }} + > + <>{render ? render(value) : defaultToEmptyTag(value)}</> + </CellActions> ); }); - return draggables.length > 0 ? ( + return visibleItems.length > 0 ? ( <> - {draggables}{' '} + {visibleItems}{' '} <RowItemOverflow - attrName={attrName} - dragDisplayValue={dragDisplayValue} + fieldName={fieldName} + values={values} + fieldType={fieldType} idPrefix={idPrefix} maxOverflowItems={maxOverflow} overflowIndexStart={displayCount} - rowItems={rowItems} - fieldType={fieldType} - isAggregatable={isAggregatable} /> </> ) : ( @@ -162,46 +80,40 @@ export const getRowItemDraggables = ({ }; interface RowItemOverflowProps { - attrName: string; - dragDisplayValue?: string; + fieldName: string; + fieldType: string; + values: string[]; idPrefix: string; maxOverflowItems: number; overflowIndexStart: number; - rowItems: string[]; - fieldType?: string; - isAggregatable?: boolean; } export const RowItemOverflowComponent: React.FC<RowItemOverflowProps> = ({ - attrName, - dragDisplayValue, + fieldName, + values, + fieldType, idPrefix, maxOverflowItems = 5, overflowIndexStart = 5, - rowItems, - fieldType, - isAggregatable, }) => { return ( <> - {rowItems.length > overflowIndexStart && ( - <Popover count={rowItems.length - overflowIndexStart} idPrefix={idPrefix}> + {values.length > overflowIndexStart && ( + <Popover count={values.length - overflowIndexStart} idPrefix={idPrefix}> <EuiText size="xs"> <MoreContainer - attrName={attrName} - dragDisplayValue={dragDisplayValue} + fieldName={fieldName} idPrefix={idPrefix} + fieldType={fieldType} + values={values} overflowIndexStart={overflowIndexStart} - rowItems={rowItems} moreMaxHeight="none" - fieldType={fieldType} - isAggregatable={isAggregatable} /> - {rowItems.length > overflowIndexStart + maxOverflowItems && ( + {values.length > overflowIndexStart + maxOverflowItems && ( <p data-test-subj="popover-additional-overflow"> <EuiTextColor color="subdued"> - {rowItems.length - overflowIndexStart - maxOverflowItems}{' '} + {values.length - overflowIndexStart - maxOverflowItems}{' '} <FormattedMessage id="xpack.securitySolution.tables.rowItemHelper.moreDescription" defaultMessage="more not shown" diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index faf7056dd41ff..180063d1786e5 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -36,6 +36,7 @@ jest.mock('react-router-dom', () => { useHistory: () => ({ useHistory: jest.fn(), }), + useLocation: jest.fn().mockReturnValue({ pathname: '/test' }), }; }); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx index 7349f053f8b13..b52746ad14c57 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx @@ -23,6 +23,7 @@ jest.mock('react-router-dom', () => { return { ...original, + useLocation: jest.fn().mockReturnValue({ pathname: '' }), useHistory: () => ({ useHistory: jest.fn(), }), diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx index 365435ff24d28..fa82951e4ba12 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx @@ -145,6 +145,7 @@ const TopNComponent: React.FC<Props> = ({ toggleTopN={toggleTopN} scopeId={scopeId} to={to} + hideQueryToggle /> ) : ( <SignalsByCategory @@ -157,6 +158,7 @@ const TopNComponent: React.FC<Props> = ({ showLegend={showLegend} setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} runtimeMappings={runtimeMappings} + hideQueryToggle /> )} </TopNContent> diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 3907232cc1120..5761e98dbe366 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -18,6 +18,8 @@ import { ThemeProvider } from 'styled-components'; import type { Capabilities } from '@kbn/core/public'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import type { Action } from '@kbn/ui-actions-plugin/public'; +import { CellActionsProvider } from '@kbn/cell-actions'; import { ConsoleManager } from '../../management/components/console'; import type { State } from '../store'; import { createStore } from '../store'; @@ -38,6 +40,7 @@ interface Props { children?: React.ReactNode; store?: Store; onDragEnd?: (result: DropResult, provided: ResponderProvided) => void; + cellActions?: Action[]; } export const kibanaObservable = new BehaviorSubject(createStartServicesMock()); @@ -54,6 +57,7 @@ export const TestProvidersComponent: React.FC<Props> = ({ children, store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage), onDragEnd = jest.fn(), + cellActions = [], }) => { const queryClient = new QueryClient(); return ( @@ -63,7 +67,11 @@ export const TestProvidersComponent: React.FC<Props> = ({ <ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}> <QueryClientProvider client={queryClient}> <ConsoleManager> - <DragDropContext onDragEnd={onDragEnd}>{children}</DragDropContext> + <CellActionsProvider + getTriggerCompatibleActions={() => Promise.resolve(cellActions)} + > + <DragDropContext onDragEnd={onDragEnd}>{children}</DragDropContext> + </CellActionsProvider> </ConsoleManager> </QueryClientProvider> </ThemeProvider> @@ -81,6 +89,7 @@ const TestProvidersWithPrivilegesComponent: React.FC<Props> = ({ children, store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage), onDragEnd = jest.fn(), + cellActions = [], }) => ( <I18nProvider> <MockKibanaContextProvider> @@ -94,7 +103,9 @@ const TestProvidersWithPrivilegesComponent: React.FC<Props> = ({ } as unknown as Capabilities } > - <DragDropContext onDragEnd={onDragEnd}>{children}</DragDropContext> + <CellActionsProvider getTriggerCompatibleActions={() => Promise.resolve(cellActions)}> + <DragDropContext onDragEnd={onDragEnd}>{children}</DragDropContext> + </CellActionsProvider> </UserPrivilegesProvider> </ThemeProvider> </ReduxStoreProvider> diff --git a/x-pack/plugins/security_solution/public/common/utils/route/use_route_spy.tsx b/x-pack/plugins/security_solution/public/common/utils/route/use_route_spy.tsx index 0dcb861f609c2..daa913a08b8d5 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/use_route_spy.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/route/use_route_spy.tsx @@ -8,4 +8,7 @@ import { useContext } from 'react'; import { RouterSpyStateContext } from './helpers'; +/** + * @deprecated + */ export const useRouteSpy = () => useContext(RouterSpyStateContext); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index 535c50057ba53..96f5b938a136a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -100,6 +100,7 @@ interface AlertsHistogramPanelProps { title?: React.ReactNode; updateDateRange: UpdateDateRange; runtimeMappings?: MappingRuntimeFields; + hideQueryToggle?: boolean; } const NO_LEGEND_DATA: LegendItem[] = []; @@ -136,6 +137,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>( updateDateRange, titleSize = 'm', runtimeMappings, + hideQueryToggle = false, }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(false); @@ -349,7 +351,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>( title={titleText} titleSize={titleSize} toggleStatus={toggleStatus} - toggleQuery={toggleQuery} + toggleQuery={hideQueryToggle ? undefined : toggleQuery} showInspectButton={chartOptionsContextMenu == null} subtitle={!isInitialLoading && showTotalAlertsCount && totalAlerts} isInspectDisabled={isInspectDisabled} diff --git a/x-pack/plugins/security_solution/public/explore/components/authentication/helpers.tsx b/x-pack/plugins/security_solution/public/explore/components/authentication/helpers.tsx index 62aa2537f11a5..797167a54207b 100644 --- a/x-pack/plugins/security_solution/public/explore/components/authentication/helpers.tsx +++ b/x-pack/plugins/security_solution/public/explore/components/authentication/helpers.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { getEmptyTagValue } from '../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; import type { Columns, ItemsPerRow } from '../paginated_table'; -import { getRowItemDraggables } from '../../../common/components/tables/helpers'; +import { getRowItemsWithActions } from '../../../common/components/tables/helpers'; import * as i18n from './translations'; import { @@ -99,11 +99,10 @@ const LAST_SUCCESSFUL_SOURCE_COLUMN: Columns<AuthenticationsEdges, Authenticatio truncateText: false, mobileOptions: { show: true }, render: ({ node }) => - getRowItemDraggables({ - rowItems: node.lastSuccess?.source?.ip || null, - isAggregatable: true, + getRowItemsWithActions({ + values: node.lastSuccess?.source?.ip || null, + fieldName: 'source.ip', fieldType: 'ip', - attrName: 'source.ip', idPrefix: `authentications-table-${node._id}-lastSuccessSource`, render: (item) => <NetworkDetailsLink ip={item} />, }), @@ -113,11 +112,10 @@ const LAST_SUCCESSFUL_DESTINATION_COLUMN: Columns<AuthenticationsEdges, Authenti truncateText: false, mobileOptions: { show: true }, render: ({ node }) => - getRowItemDraggables({ - rowItems: node.lastSuccess?.host?.name ?? null, - isAggregatable: true, + getRowItemsWithActions({ + values: node.lastSuccess?.host?.name ?? null, + fieldName: 'host.name', fieldType: 'keyword', - attrName: 'host.name', idPrefix: `authentications-table-${node._id}-lastSuccessfulDestination`, render: (item) => <HostDetailsLink hostName={item} />, }), @@ -138,11 +136,10 @@ const LAST_FAILED_SOURCE_COLUMN: Columns<AuthenticationsEdges, AuthenticationsEd truncateText: false, mobileOptions: { show: true }, render: ({ node }) => - getRowItemDraggables({ - rowItems: node.lastFailure?.source?.ip || null, - isAggregatable: true, + getRowItemsWithActions({ + values: node.lastFailure?.source?.ip || null, + fieldName: 'source.ip', fieldType: 'ip', - attrName: 'source.ip', idPrefix: `authentications-table-${node._id}-lastFailureSource`, render: (item) => <NetworkDetailsLink ip={item} />, }), @@ -152,13 +149,12 @@ const LAST_FAILED_DESTINATION_COLUMN: Columns<AuthenticationsEdges, Authenticati truncateText: false, mobileOptions: { show: true }, render: ({ node }) => - getRowItemDraggables({ - rowItems: node.lastFailure?.host?.name || null, - attrName: 'host.name', + getRowItemsWithActions({ + values: node.lastFailure?.host?.name || null, + fieldName: 'host.name', + fieldType: 'keyword', idPrefix: `authentications-table-${node._id}-lastFailureDestination`, render: (item) => <HostDetailsLink hostName={item} />, - isAggregatable: true, - fieldType: 'ip', }), }; @@ -167,12 +163,11 @@ const USER_COLUMN: Columns<AuthenticationsEdges, AuthenticationsEdges> = { truncateText: false, mobileOptions: { show: true }, render: ({ node }) => - getRowItemDraggables({ - rowItems: node.stackedValue, - attrName: 'user.name', - isAggregatable: true, - fieldType: 'keyword', + getRowItemsWithActions({ + values: node.stackedValue, + fieldName: 'user.name', idPrefix: `authentications-table-${node._id}-userName`, + fieldType: 'keyword', render: (item) => <UserDetailsLink userName={item} />, }), }; @@ -182,12 +177,11 @@ const HOST_COLUMN: Columns<AuthenticationsEdges, AuthenticationsEdges> = { truncateText: false, mobileOptions: { show: true }, render: ({ node }) => - getRowItemDraggables({ - rowItems: node.stackedValue, - attrName: 'host.name', - isAggregatable: true, - fieldType: 'keyword', + getRowItemsWithActions({ + values: node.stackedValue, + fieldName: 'host.name', idPrefix: `authentications-table-${node._id}-hostName`, + fieldType: 'keyword', render: (item) => <HostDetailsLink hostName={item} />, }), }; diff --git a/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx index b6c34088bc592..9142ac1b2a226 100644 --- a/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx @@ -7,22 +7,16 @@ import React from 'react'; import { EuiIcon, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; -import { - DragEffects, - DraggableWrapper, -} from '../../../../common/components/drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../../../../common/components/drag_and_drop/helpers'; +import { CellActions, CellActionsMode } from '@kbn/cell-actions'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { HostDetailsLink } from '../../../../common/components/links'; -import { IS_OPERATOR } from '../../../../timelines/components/timeline/data_providers/data_provider'; -import { Provider } from '../../../../timelines/components/timeline/data_providers/provider'; import type { HostRiskScoreColumns } from '.'; - import * as i18n from './translations'; import { HostsTableType } from '../../store/model'; import type { RiskSeverity } from '../../../../../common/search_strategy'; import { RiskScoreFields } from '../../../../../common/search_strategy'; import { RiskScore } from '../../../components/risk_score/severity/common'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../../common/constants'; export const getHostRiskScoreColumns = ({ dispatchSeverityUpdate, @@ -37,31 +31,20 @@ export const getHostRiskScoreColumns = ({ sortable: true, render: (hostName) => { if (hostName != null && hostName.length > 0) { - const id = escapeDataProviderId(`host-risk-score-table-hostName-${hostName}`); return ( - <DraggableWrapper - key={id} - dataProvider={{ - and: [], - enabled: true, - excluded: false, - id, - name: hostName, - kqlQuery: '', - queryMatch: { field: 'host.name', value: hostName, operator: IS_OPERATOR }, + <CellActions + mode={CellActionsMode.HOVER} + visibleCellActions={5} + showActionTooltips + triggerId={CELL_ACTIONS_DEFAULT_TRIGGER} + field={{ + name: 'host.name', + value: hostName, + type: 'keyword', }} - isAggregatable={true} - fieldType={'keyword'} - render={(dataProvider, _, snapshot) => - snapshot.isDragging ? ( - <DragEffects> - <Provider dataProvider={dataProvider} /> - </DragEffects> - ) : ( - <HostDetailsLink hostName={hostName} hostTab={HostsTableType.risk} /> - ) - } - /> + > + <HostDetailsLink hostName={hostName} hostTab={HostsTableType.risk} /> + </CellActions> ); } return getEmptyTagValue(); diff --git a/x-pack/plugins/security_solution/public/explore/hosts/components/hosts_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/hosts/components/hosts_table/columns.tsx index d1214b0d97026..188d5eb8b3809 100644 --- a/x-pack/plugins/security_solution/public/explore/hosts/components/hosts_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/hosts/components/hosts_table/columns.tsx @@ -7,23 +7,16 @@ import { EuiIcon, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; import React from 'react'; -import { - DragEffects, - DraggableWrapper, -} from '../../../../common/components/drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../../../../common/components/drag_and_drop/helpers'; +import { CellActions, CellActionsMode } from '@kbn/cell-actions'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { HostDetailsLink } from '../../../../common/components/links'; import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; -import { IS_OPERATOR } from '../../../../timelines/components/timeline/data_providers/data_provider'; -import { Provider } from '../../../../timelines/components/timeline/data_providers/provider'; -import { DefaultDraggable } from '../../../../common/components/draggables'; import type { HostsTableColumns } from '.'; - import * as i18n from './translations'; import type { Maybe, RiskSeverity } from '../../../../../common/search_strategy'; import { VIEW_HOSTS_BY_SEVERITY } from '../host_risk_score_table/translations'; import { RiskScore } from '../../../components/risk_score/severity/common'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../../common/constants'; export const getHostsColumns = ( showRiskColumn: boolean, @@ -38,31 +31,20 @@ export const getHostsColumns = ( sortable: true, render: (hostName) => { if (hostName != null && hostName.length > 0) { - const id = escapeDataProviderId(`hosts-table-hostName-${hostName[0]}`); return ( - <DraggableWrapper - key={id} - dataProvider={{ - and: [], - enabled: true, - excluded: false, - id, - name: hostName[0], - kqlQuery: '', - queryMatch: { field: 'host.name', value: hostName[0], operator: IS_OPERATOR }, + <CellActions + mode={CellActionsMode.HOVER} + visibleCellActions={5} + showActionTooltips + triggerId={CELL_ACTIONS_DEFAULT_TRIGGER} + field={{ + name: 'host.name', + value: hostName[0], + type: 'keyword', }} - isAggregatable={true} - fieldType={'keyword'} - render={(dataProvider, _, snapshot) => - snapshot.isDragging ? ( - <DragEffects> - <Provider dataProvider={dataProvider} /> - </DragEffects> - ) : ( - <HostDetailsLink hostName={hostName[0]} /> - ) - } - /> + > + <HostDetailsLink hostName={hostName[0]} /> + </CellActions> ); } return getEmptyTagValue(); @@ -107,14 +89,19 @@ export const getHostsColumns = ( render: (hostOsName) => { if (hostOsName != null) { return ( - <DefaultDraggable - id={`host-page-draggable-host.os.name-${hostOsName[0]}`} - field={'host.os.name'} - value={hostOsName[0]} - isDraggable={false} - hideTopN={true} - tooltipContent={null} - /> + <CellActions + mode={CellActionsMode.HOVER} + visibleCellActions={5} + showActionTooltips + triggerId={CELL_ACTIONS_DEFAULT_TRIGGER} + field={{ + name: 'host.os.name', + value: hostOsName[0], + type: 'keyword', + }} + > + {hostOsName} + </CellActions> ); } return getEmptyTagValue(); @@ -129,14 +116,19 @@ export const getHostsColumns = ( render: (hostOsVersion) => { if (hostOsVersion != null) { return ( - <DefaultDraggable - id={`host-page-draggable-host.os.version-${hostOsVersion[0]}`} - field={'host.os.version'} - value={hostOsVersion[0]} - isDraggable={false} - hideTopN={true} - tooltipContent={null} - /> + <CellActions + mode={CellActionsMode.HOVER} + visibleCellActions={5} + showActionTooltips + triggerId={CELL_ACTIONS_DEFAULT_TRIGGER} + field={{ + name: 'host.os.version', + value: hostOsVersion[0], + type: 'keyword', + }} + > + {hostOsVersion} + </CellActions> ); } return getEmptyTagValue(); diff --git a/x-pack/plugins/security_solution/public/explore/hosts/components/uncommon_process_table/index.tsx b/x-pack/plugins/security_solution/public/explore/hosts/components/uncommon_process_table/index.tsx index ceef9e8f3bff3..81c1edebf04c3 100644 --- a/x-pack/plugins/security_solution/public/explore/hosts/components/uncommon_process_table/index.tsx +++ b/x-pack/plugins/security_solution/public/explore/hosts/components/uncommon_process_table/index.tsx @@ -15,9 +15,8 @@ import { defaultToEmptyTag, getEmptyValue } from '../../../../common/components/ import { HostDetailsLink } from '../../../../common/components/links'; import type { Columns, ItemsPerRow } from '../../../components/paginated_table'; import { PaginatedTable } from '../../../components/paginated_table'; - import * as i18n from './translations'; -import { getRowItemDraggables } from '../../../../common/components/tables/helpers'; +import { getRowItemsWithActions } from '../../../../common/components/tables/helpers'; import { HostsType } from '../../store/model'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; @@ -147,15 +146,14 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [ name: i18n.NAME, truncateText: false, mobileOptions: { show: true }, + width: '20%', render: ({ node }) => - getRowItemDraggables({ - rowItems: node.process.name, - attrName: 'process.name', - idPrefix: `uncommon-process-table-${node._id}-processName`, - isAggregatable: true, + getRowItemsWithActions({ + values: node.process.name, + fieldName: 'process.name', fieldType: 'keyword', + idPrefix: `uncommon-process-table-${node._id}-processName`, }), - width: '20%', }, { align: 'right', @@ -177,43 +175,41 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [ name: i18n.HOSTS, truncateText: false, mobileOptions: { show: true }, + width: '25%', render: ({ node }) => - getRowItemDraggables({ - rowItems: getHostNames(node.hosts), - attrName: 'host.name', + getRowItemsWithActions({ + values: getHostNames(node.hosts), + fieldName: 'host.name', + fieldType: 'keyword', idPrefix: `uncommon-process-table-${node._id}-processHost`, render: (item) => <HostDetailsLink hostName={item} />, - isAggregatable: true, - fieldType: 'keyword', }), - width: '25%', }, { name: i18n.LAST_COMMAND, truncateText: false, mobileOptions: { show: true }, + width: '25%', render: ({ node }) => - getRowItemDraggables({ - rowItems: node.process != null ? node.process.args : null, - attrName: 'process.args', - idPrefix: `uncommon-process-table-${node._id}-processArgs`, - displayCount: 1, // TODO: Change this back once we have improved the UI - isAggregatable: true, + getRowItemsWithActions({ + values: node.process != null ? node.process.args : null, + fieldName: 'process.args', fieldType: 'keyword', + idPrefix: `uncommon-process-table-${node._id}-processArgs`, + render: (item) => <HostDetailsLink hostName={item} />, + displayCount: 1, }), - width: '25%', }, { name: i18n.LAST_USER, truncateText: false, mobileOptions: { show: true }, render: ({ node }) => - getRowItemDraggables({ - rowItems: node.user != null ? node.user.name : null, - attrName: 'user.name', - idPrefix: `uncommon-process-table-${node._id}-processUser`, - isAggregatable: true, + getRowItemsWithActions({ + values: node.user != null ? node.user.name : null, + fieldName: 'user.name', fieldType: 'keyword', + idPrefix: `uncommon-process-table-${node._id}-processUser`, }), }, ]; diff --git a/x-pack/plugins/security_solution/public/explore/network/components/network_dns_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/network_dns_table/columns.tsx index 7de86b8cde273..72e4268eb399e 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/network_dns_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/network_dns_table/columns.tsx @@ -8,20 +8,16 @@ import numeral from '@elastic/numeral'; import React from 'react'; +import { CellActions, CellActionsMode } from '@kbn/cell-actions'; import type { NetworkDnsItem } from '../../../../../common/search_strategy'; import { NetworkDnsFields } from '../../../../../common/search_strategy'; -import { - DragEffects, - DraggableWrapper, -} from '../../../../common/components/drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../../../../common/components/drag_and_drop/helpers'; import { defaultToEmptyTag, getEmptyTagValue } from '../../../../common/components/empty_value'; import type { Columns } from '../../../components/paginated_table'; -import { IS_OPERATOR } from '../../../../timelines/components/timeline/data_providers/data_provider'; import { PreferenceFormattedBytes } from '../../../../common/components/formatted_bytes'; -import { Provider } from '../../../../timelines/components/timeline/data_providers/provider'; import * as i18n from './translations'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../../common/constants'; export type NetworkDnsColumns = [ Columns<NetworkDnsItem['dnsName']>, Columns<NetworkDnsItem['queryCount']>, @@ -39,35 +35,21 @@ export const getNetworkDnsColumns = (): NetworkDnsColumns => [ sortable: true, render: (dnsName) => { if (dnsName != null) { - const id = escapeDataProviderId(`networkDns-table--name-${dnsName}`); return ( - <DraggableWrapper - key={id} - dataProvider={{ - and: [], - enabled: true, - id, - name: dnsName, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'dns.question.registered_domain', - value: dnsName, - operator: IS_OPERATOR, - }, + <CellActions + key={escapeDataProviderId(`networkDns-table--name-${dnsName}`)} + mode={CellActionsMode.HOVER} + visibleCellActions={5} + showActionTooltips + triggerId={CELL_ACTIONS_DEFAULT_TRIGGER} + field={{ + name: 'dns.question.registered_domain', + value: dnsName, + type: 'keyword', }} - isAggregatable={true} - fieldType={'keyword'} - render={(dataProvider, _, snapshot) => - snapshot.isDragging ? ( - <DragEffects> - <Provider dataProvider={dataProvider} /> - </DragEffects> - ) : ( - defaultToEmptyTag(dnsName) - ) - } - /> + > + {defaultToEmptyTag(dnsName)} + </CellActions> ); } else { return getEmptyTagValue(); diff --git a/x-pack/plugins/security_solution/public/explore/network/components/network_http_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/network_http_table/columns.tsx index ae3df4159e8da..76aafb69da9aa 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/network_http_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/network_http_table/columns.tsx @@ -18,10 +18,7 @@ import { NetworkDetailsLink } from '../../../../common/components/links'; import type { Columns } from '../../../components/paginated_table'; import * as i18n from './translations'; -import { - getRowItemDraggable, - getRowItemDraggables, -} from '../../../../common/components/tables/helpers'; +import { getRowItemsWithActions } from '../../../../common/components/tables/helpers'; export type NetworkHttpColumns = [ Columns<NetworkHttpEdges>, Columns<NetworkHttpEdges>, @@ -37,13 +34,12 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ name: i18n.METHOD, render: ({ node: { methods, path } }) => { return Array.isArray(methods) && methods.length > 0 - ? getRowItemDraggables({ - attrName: 'http.request.method', - displayCount: 3, + ? getRowItemsWithActions({ + fieldName: 'http.request.method', + values: methods, idPrefix: escapeDataProviderId(`${tableId}-table-methods-${path}`), - rowItems: methods, - isAggregatable: true, fieldType: 'keyword', + displayCount: 3, }) : getEmptyTagValue(); }, @@ -52,12 +48,10 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ name: i18n.DOMAIN, render: ({ node: { domains, path } }) => Array.isArray(domains) && domains.length > 0 - ? getRowItemDraggables({ - attrName: 'url.domain', - displayCount: 3, + ? getRowItemsWithActions({ + values: domains, + fieldName: 'url.domain', idPrefix: escapeDataProviderId(`${tableId}-table-domains-${path}`), - rowItems: domains, - isAggregatable: true, fieldType: 'keyword', }) : getEmptyTagValue(), @@ -67,11 +61,10 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ name: i18n.PATH, render: (path) => path != null - ? getRowItemDraggable({ - attrName: 'url.path', + ? getRowItemsWithActions({ + values: [path], + fieldName: 'url.path', idPrefix: escapeDataProviderId(`${tableId}-table-path-${path}`), - rowItem: path, - isAggregatable: true, fieldType: 'keyword', }) : getEmptyTagValue(), @@ -80,13 +73,12 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ name: i18n.STATUS, render: ({ node: { statuses, path } }) => Array.isArray(statuses) && statuses.length > 0 - ? getRowItemDraggables({ - attrName: 'http.response.status_code', - displayCount: 3, + ? getRowItemsWithActions({ + values: statuses, + fieldName: 'http.response.status_code', idPrefix: escapeDataProviderId(`${tableId}-table-statuses-${path}`), - rowItems: statuses, - isAggregatable: true, fieldType: 'keyword', + displayCount: 3, }) : getEmptyTagValue(), }, @@ -94,11 +86,10 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ name: i18n.LAST_HOST, render: ({ node: { lastHost, path } }) => lastHost != null - ? getRowItemDraggable({ - attrName: 'host.name', + ? getRowItemsWithActions({ + values: [lastHost], + fieldName: 'host.name', idPrefix: escapeDataProviderId(`${tableId}-table-lastHost-${path}`), - rowItem: lastHost, - isAggregatable: true, fieldType: 'keyword', }) : getEmptyTagValue(), @@ -107,13 +98,12 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ name: i18n.LAST_SOURCE_IP, render: ({ node: { lastSourceIp, path } }) => lastSourceIp != null - ? getRowItemDraggable({ - attrName: 'source.ip', + ? getRowItemsWithActions({ + values: [lastSourceIp], + fieldName: 'source.ip', idPrefix: escapeDataProviderId(`${tableId}-table-lastSourceIp-${path}`), - rowItem: lastSourceIp, - render: () => <NetworkDetailsLink ip={lastSourceIp} />, - isAggregatable: true, fieldType: 'keyword', + render: () => <NetworkDetailsLink ip={lastSourceIp} />, }) : getEmptyTagValue(), }, diff --git a/x-pack/plugins/security_solution/public/explore/network/components/network_top_countries_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/network_top_countries_table/columns.tsx index af7fe2f42e63b..b84446d844a94 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/network_top_countries_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/network_top_countries_table/columns.tsx @@ -9,24 +9,19 @@ import { get } from 'lodash/fp'; import numeral from '@elastic/numeral'; import React from 'react'; import type { DataViewBase } from '@kbn/es-query'; -import { CountryFlagAndName } from '../source_destination/country_flag'; +import { CellActions, CellActionsMode } from '@kbn/cell-actions'; import type { NetworkTopCountriesEdges, TopNetworkTablesEcsField, } from '../../../../../common/search_strategy/security_solution/network'; import { FlowTargetSourceDest } from '../../../../../common/search_strategy/security_solution/network'; import { networkModel } from '../../store'; -import { - DragEffects, - DraggableWrapper, -} from '../../../../common/components/drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../../../../common/components/drag_and_drop/helpers'; -import { getEmptyTagValue } from '../../../../common/components/empty_value'; +import { defaultToEmptyTag, getEmptyTagValue } from '../../../../common/components/empty_value'; import type { Columns } from '../../../components/paginated_table'; -import { IS_OPERATOR } from '../../../../timelines/components/timeline/data_providers/data_provider'; -import { Provider } from '../../../../timelines/components/timeline/data_providers/provider'; import * as i18n from './translations'; import { PreferenceFormattedBytes } from '../../../../common/components/formatted_bytes'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../../common/constants'; export type NetworkTopCountriesColumns = [ Columns<NetworkTopCountriesEdges>, @@ -59,31 +54,20 @@ export const getNetworkTopCountriesColumns = ( const id = escapeDataProviderId(`${tableId}-table-${flowTarget}-country-${geo}`); if (geo != null) { return ( - <DraggableWrapper + <CellActions key={id} - dataProvider={{ - and: [], - enabled: true, - id, - name: geo, - excluded: false, - kqlQuery: '', - queryMatch: { field: geoAttr, value: geo, operator: IS_OPERATOR }, + mode={CellActionsMode.HOVER} + visibleCellActions={5} + showActionTooltips + triggerId={CELL_ACTIONS_DEFAULT_TRIGGER} + field={{ + name: geoAttr, + value: geo, + type: 'keyword', }} - isAggregatable={true} - fieldType={'keyword'} - render={(dataProvider, _, snapshot) => - snapshot.isDragging ? ( - <DragEffects> - <Provider dataProvider={dataProvider} /> - </DragEffects> - ) : ( - <> - <CountryFlagAndName countryCode={geo} /> - </> - ) - } - /> + > + {defaultToEmptyTag(geo)} + </CellActions> ); } else { return getEmptyTagValue(); diff --git a/x-pack/plugins/security_solution/public/explore/network/components/network_top_n_flow_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/network_top_n_flow_table/columns.tsx index ef36956ff98a6..fdef6c214e4f5 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/network_top_n_flow_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/network_top_n_flow_table/columns.tsx @@ -9,6 +9,7 @@ import { get } from 'lodash/fp'; import numeral from '@elastic/numeral'; import React from 'react'; +import { CellActions, CellActionsMode } from '@kbn/cell-actions'; import { CountryFlag } from '../source_destination/country_flag'; import type { AutonomousSystemItem, @@ -17,22 +18,14 @@ import type { } from '../../../../../common/search_strategy'; import { FlowTargetSourceDest } from '../../../../../common/search_strategy'; import { networkModel } from '../../store'; -import { - DragEffects, - DraggableWrapper, -} from '../../../../common/components/drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../../../../common/components/drag_and_drop/helpers'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { NetworkDetailsLink } from '../../../../common/components/links'; import type { Columns } from '../../../components/paginated_table'; -import { IS_OPERATOR } from '../../../../timelines/components/timeline/data_providers/data_provider'; -import { Provider } from '../../../../timelines/components/timeline/data_providers/provider'; import * as i18n from './translations'; -import { - getRowItemDraggable, - getRowItemDraggables, -} from '../../../../common/components/tables/helpers'; +import { getRowItemsWithActions } from '../../../../common/components/tables/helpers'; import { PreferenceFormattedBytes } from '../../../../common/components/formatted_bytes'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../../common/constants'; export type NetworkTopNFlowColumns = [ Columns<NetworkTopNFlowEdges>, @@ -70,57 +63,37 @@ export const getNetworkTopNFlowColumns = ( if (ip != null) { return ( <> - <DraggableWrapper + <CellActions key={id} - dataProvider={{ - and: [], - enabled: true, - id, - name: ip, - excluded: false, - kqlQuery: '', - queryMatch: { field: ipAttr, value: ip, operator: IS_OPERATOR }, + mode={CellActionsMode.HOVER} + visibleCellActions={5} + showActionTooltips + triggerId={CELL_ACTIONS_DEFAULT_TRIGGER} + field={{ + name: ipAttr, + value: ip, + type: 'keyword', }} - isAggregatable={true} - fieldType={'ip'} - render={(dataProvider, _, snapshot) => - snapshot.isDragging ? ( - <DragEffects> - <Provider dataProvider={dataProvider} /> - </DragEffects> - ) : ( - <NetworkDetailsLink ip={ip} flowTarget={flowTarget} /> - ) - } - /> + > + <NetworkDetailsLink ip={ip} flowTarget={flowTarget} /> + </CellActions> {geo && ( - <DraggableWrapper + <CellActions key={`${id}-${geo}`} - dataProvider={{ - and: [], - enabled: true, - id: `${id}-${geo}`, - name: geo, - excluded: false, - kqlQuery: '', - queryMatch: { field: geoAttrName, value: geo, operator: IS_OPERATOR }, + mode={CellActionsMode.HOVER} + visibleCellActions={5} + showActionTooltips + triggerId={CELL_ACTIONS_DEFAULT_TRIGGER} + field={{ + name: geoAttrName, + value: geo, + type: 'geo_point', }} - isAggregatable={true} - fieldType={'geo_point'} - render={(dataProvider, _, snapshot) => - snapshot.isDragging ? ( - <DragEffects> - <Provider dataProvider={dataProvider} /> - </DragEffects> - ) : ( - <> - {' '} - <CountryFlag countryCode={geo} /> {geo} - </> - ) - } - /> + > + {' '} + <CountryFlag countryCode={geo} /> {geo} + </CellActions> )} </> ); @@ -140,13 +113,12 @@ export const getNetworkTopNFlowColumns = ( if (Array.isArray(domains) && domains.length > 0) { const id = escapeDataProviderId(`${tableId}-table-${ip}`); - return getRowItemDraggables({ - rowItems: domains, - attrName: domainAttr, + return getRowItemsWithActions({ + values: domains, + fieldName: domainAttr, + fieldType: 'keyword', idPrefix: id, displayCount: 1, - isAggregatable: true, - fieldType: 'keyword', }); } else { return getEmptyTagValue(); @@ -164,22 +136,20 @@ export const getNetworkTopNFlowColumns = ( return ( <> {as.name && - getRowItemDraggable({ - rowItem: as.name, - attrName: `${flowTarget}.as.organization.name`, - idPrefix: `${id}-name`, - isAggregatable: true, + getRowItemsWithActions({ + values: [as.name], + fieldName: `${flowTarget}.as.organization.name`, fieldType: 'keyword', + idPrefix: `${id}-name`, })} {as.number && ( <> {' '} - {getRowItemDraggable({ - rowItem: `${as.number}`, - attrName: `${flowTarget}.as.number`, + {getRowItemsWithActions({ + values: [`${as.number}`], + fieldName: `${flowTarget}.as.number`, idPrefix: `${id}-number`, - isAggregatable: true, fieldType: 'keyword', })} </> diff --git a/x-pack/plugins/security_solution/public/explore/network/components/tls_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/tls_table/columns.tsx index b76d438a4fd01..ca348ec7c73a7 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/tls_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/tls_table/columns.tsx @@ -10,10 +10,7 @@ import moment from 'moment'; import type { NetworkTlsNode } from '../../../../../common/search_strategy'; import type { Columns } from '../../../components/paginated_table'; -import { - getRowItemDraggables, - getRowItemDraggable, -} from '../../../../common/components/tables/helpers'; +import { getRowItemsWithActions } from '../../../../common/components/tables/helpers'; import { LocalizedDateTooltip } from '../../../../common/components/localized_date_tooltip'; import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; @@ -35,12 +32,11 @@ export const getTlsColumns = (tableId: string): TlsColumns => [ mobileOptions: { show: true }, sortable: false, render: ({ _id, issuers }) => - getRowItemDraggables({ - rowItems: issuers, - attrName: 'tls.server.issuer', - idPrefix: `${tableId}-${_id}-table-issuers`, - isAggregatable: true, + getRowItemsWithActions({ + values: issuers, + fieldName: 'tls.server.issuer', fieldType: 'keyword', + idPrefix: `${tableId}-${_id}-table-issuers`, }), }, { @@ -50,12 +46,11 @@ export const getTlsColumns = (tableId: string): TlsColumns => [ mobileOptions: { show: true }, sortable: false, render: ({ _id, subjects }) => - getRowItemDraggables({ - rowItems: subjects, - attrName: 'tls.server.subject', - idPrefix: `${tableId}-${_id}-table-subjects`, - isAggregatable: true, + getRowItemsWithActions({ + values: subjects, + fieldName: 'tls.server.subject', fieldType: 'keyword', + idPrefix: `${tableId}-${_id}-table-subjects`, }), }, { @@ -65,12 +60,11 @@ export const getTlsColumns = (tableId: string): TlsColumns => [ mobileOptions: { show: true }, sortable: true, render: (sha1) => - getRowItemDraggable({ - rowItem: sha1, - attrName: 'tls.server.hash.sha1', - idPrefix: `${tableId}-${sha1}-table-sha1`, - isAggregatable: true, + getRowItemsWithActions({ + values: sha1 ? [sha1] : undefined, + fieldName: 'tls.server.hash.sha1', fieldType: 'keyword', + idPrefix: `${tableId}-${sha1}-table-sha1`, }), }, { @@ -80,12 +74,11 @@ export const getTlsColumns = (tableId: string): TlsColumns => [ mobileOptions: { show: true }, sortable: false, render: ({ _id, ja3 }) => - getRowItemDraggables({ - rowItems: ja3, - attrName: 'tls.server.ja3s', - idPrefix: `${tableId}-${_id}-table-ja3`, - isAggregatable: true, + getRowItemsWithActions({ + values: ja3, + fieldName: 'tls.server.ja3s', fieldType: 'keyword', + idPrefix: `${tableId}-${_id}-table-ja3`, }), }, { @@ -95,9 +88,10 @@ export const getTlsColumns = (tableId: string): TlsColumns => [ mobileOptions: { show: true }, sortable: false, render: ({ _id, notAfter }) => - getRowItemDraggables({ - rowItems: notAfter, - attrName: 'tls.server.not_after', + getRowItemsWithActions({ + values: notAfter, + fieldName: 'tls.server.not_after', + fieldType: 'date', idPrefix: `${tableId}-${_id}-table-notAfter`, render: (validUntil) => ( <LocalizedDateTooltip date={moment(new Date(validUntil)).toDate()}> diff --git a/x-pack/plugins/security_solution/public/explore/network/components/users_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/users_table/columns.tsx index 12ffc2b3a834b..45c0632aa3863 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/users_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/users_table/columns.tsx @@ -10,10 +10,7 @@ import { defaultToEmptyTag } from '../../../../common/components/empty_value'; import type { Columns } from '../../../components/paginated_table'; import * as i18n from './translations'; -import { - getRowItemDraggables, - getRowItemDraggable, -} from '../../../../common/components/tables/helpers'; +import { getRowItemsWithActions } from '../../../../common/components/tables/helpers'; export type UsersColumns = [ Columns<NetworkUsersItem['name']>, @@ -34,12 +31,11 @@ export const getUsersColumns = ( mobileOptions: { show: true }, sortable: true, render: (userName) => - getRowItemDraggable({ - rowItem: userName, - attrName: 'user.name', - idPrefix: `${tableId}-table-${flowTarget}-user`, - isAggregatable: true, + getRowItemsWithActions({ + values: userName ? [userName] : undefined, + fieldName: 'user.name', fieldType: 'keyword', + idPrefix: `${tableId}-table-${flowTarget}-user`, }), }, { @@ -49,12 +45,11 @@ export const getUsersColumns = ( mobileOptions: { show: true }, sortable: false, render: (userIds) => - getRowItemDraggables({ - rowItems: userIds, - attrName: 'user.id', - idPrefix: `${tableId}-table-${flowTarget}`, - isAggregatable: true, + getRowItemsWithActions({ + values: userIds, + fieldName: 'user.id', fieldType: 'keyword', + idPrefix: `${tableId}-table-${flowTarget}`, }), }, { @@ -64,12 +59,11 @@ export const getUsersColumns = ( mobileOptions: { show: true }, sortable: false, render: (groupNames) => - getRowItemDraggables({ - rowItems: groupNames, - attrName: 'user.group.name', - idPrefix: `${tableId}-table-${flowTarget}`, - isAggregatable: true, + getRowItemsWithActions({ + values: groupNames, + fieldName: 'user.group.name', fieldType: 'keyword', + idPrefix: `${tableId}-table-${flowTarget}`, }), }, { @@ -79,12 +73,11 @@ export const getUsersColumns = ( mobileOptions: { show: true }, sortable: false, render: (groupId) => - getRowItemDraggables({ - rowItems: groupId, - attrName: 'user.group.id', - idPrefix: `${tableId}-table-${flowTarget}`, - isAggregatable: true, + getRowItemsWithActions({ + values: groupId, + fieldName: 'user.group.id', fieldType: 'keyword', + idPrefix: `${tableId}-table-${flowTarget}`, }), }, { diff --git a/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx b/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx index 7430a4ece4257..f7d605789b082 100644 --- a/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx @@ -13,6 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elasti import { getEsQueryConfig } from '@kbn/data-plugin/common'; import { buildEsQuery } from '@kbn/es-query'; +import { CellActions, CellActionsMode } from '@kbn/cell-actions'; import { AlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { InputsModelId } from '../../../../common/store/inputs/constants'; @@ -52,6 +53,7 @@ import { useAlertsPrivileges } from '../../../../detections/containers/detection import { navTabsNetworkDetails } from './nav_tabs'; import { NetworkDetailsTabs } from './details_tabs'; import { useInstalledSecurityJobNameById } from '../../../../common/components/ml/hooks/use_installed_security_jobs'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../../common/constants'; export { getTrailingBreadcrumbs } from './utils'; @@ -148,11 +150,6 @@ const NetworkDetailsComponent: React.FC = () => { aggregationInterval: 'auto', }); - const headerDraggableArguments = useMemo( - () => ({ field: `${flowTarget}.ip`, value: ip }), - [flowTarget, ip] - ); - const entityFilter = useMemo( () => ({ field: `${flowTarget}.ip`, @@ -173,7 +170,6 @@ const NetworkDetailsComponent: React.FC = () => { <HeaderPage border data-test-subj="network-details-headline" - draggableArguments={headerDraggableArguments} subtitle={ <LastEventTime indexKey={LastEventIndexKey.ipDetails} @@ -181,7 +177,16 @@ const NetworkDetailsComponent: React.FC = () => { ip={ip} /> } - title={ip} + title={ + <CellActions + field={{ type: 'ip', value: ip, name: `${flowTarget}.ip` }} + mode={CellActionsMode.HOVER} + visibleCellActions={5} + triggerId={CELL_ACTIONS_DEFAULT_TRIGGER} + > + {ip} + </CellActions> + } > <FlowTargetSelectConnected flowTarget={flowTarget} /> </HeaderPage> diff --git a/x-pack/plugins/security_solution/public/explore/users/components/all_users/index.tsx b/x-pack/plugins/security_solution/public/explore/users/components/all_users/index.tsx index c720b4393b372..3f545462a30aa 100644 --- a/x-pack/plugins/security_solution/public/explore/users/components/all_users/index.tsx +++ b/x-pack/plugins/security_solution/public/explore/users/components/all_users/index.tsx @@ -19,7 +19,7 @@ import { import type { Columns, Criteria, ItemsPerRow } from '../../../components/paginated_table'; import { PaginatedTable } from '../../../components/paginated_table'; -import { getRowItemDraggables } from '../../../../common/components/tables/helpers'; +import { getRowItemsWithActions } from '../../../../common/components/tables/helpers'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import * as i18n from './translations'; @@ -80,12 +80,11 @@ const getUsersColumns = ( mobileOptions: { show: true }, render: (name) => name != null && name.length > 0 - ? getRowItemDraggables({ - rowItems: [name], - attrName: 'user.name', + ? getRowItemsWithActions({ + fieldName: 'user.name', + values: [name], idPrefix: `users-table-${name}-name`, render: (item) => <UserDetailsLink userName={item} />, - isAggregatable: true, fieldType: 'keyword', }) : getOrEmptyTagFromValue(name), @@ -106,11 +105,10 @@ const getUsersColumns = ( mobileOptions: { show: true }, render: (domain) => domain != null && domain.length > 0 - ? getRowItemDraggables({ - rowItems: [domain], - attrName: 'user.domain', + ? getRowItemsWithActions({ + fieldName: 'user.domain', + values: [domain], idPrefix: `users-table-${domain}-domain`, - isAggregatable: true, fieldType: 'keyword', }) : getOrEmptyTagFromValue(domain), diff --git a/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx index 5359739dc8609..b2734cdf0513b 100644 --- a/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx @@ -7,23 +7,17 @@ import React from 'react'; import { EuiIcon, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; -import { - DragEffects, - DraggableWrapper, -} from '../../../../common/components/drag_and_drop/draggable_wrapper'; +import { CellActions, CellActionsMode } from '@kbn/cell-actions'; import { escapeDataProviderId } from '../../../../common/components/drag_and_drop/helpers'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; - -import { IS_OPERATOR } from '../../../../timelines/components/timeline/data_providers/data_provider'; -import { Provider } from '../../../../timelines/components/timeline/data_providers/provider'; import type { UserRiskScoreColumns } from '.'; - import * as i18n from './translations'; import { RiskScore } from '../../../components/risk_score/severity/common'; import type { RiskSeverity } from '../../../../../common/search_strategy'; import { RiskScoreFields } from '../../../../../common/search_strategy'; import { UserDetailsLink } from '../../../../common/components/links'; import { UsersTableType } from '../../store/model'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../../common/constants'; export const getUserRiskScoreColumns = ({ dispatchSeverityUpdate, @@ -40,29 +34,20 @@ export const getUserRiskScoreColumns = ({ if (userName != null && userName.length > 0) { const id = escapeDataProviderId(`user-risk-score-table-userName-${userName}`); return ( - <DraggableWrapper + <CellActions key={id} - dataProvider={{ - and: [], - enabled: true, - excluded: false, - id, - name: userName, - kqlQuery: '', - queryMatch: { field: 'user.name', value: userName, operator: IS_OPERATOR }, + mode={CellActionsMode.HOVER} + visibleCellActions={5} + showActionTooltips + triggerId={CELL_ACTIONS_DEFAULT_TRIGGER} + field={{ + name: 'user.name', + value: userName, + type: 'keyword', }} - render={(dataProvider, _, snapshot) => - snapshot.isDragging ? ( - <DragEffects> - <Provider dataProvider={dataProvider} /> - </DragEffects> - ) : ( - <UserDetailsLink userName={userName} userTab={UsersTableType.risk} /> - ) - } - isAggregatable={true} - fieldType={'keyword'} - /> + > + <UserDetailsLink userName={userName} userTab={UsersTableType.risk} /> + </CellActions> ); } return getEmptyTagValue(); diff --git a/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx index 5a62bce6b9c3a..ee2d86faca6c2 100644 --- a/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx @@ -17,6 +17,11 @@ jest.mock('../../../containers/authentications'); jest.mock('../../../../common/containers/query_toggle'); jest.mock('../../../../common/lib/kibana'); +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; +}); + describe('Authentications query tab body', () => { const mockUseAuthentications = useAuthentications as jest.Mock; const mockUseQueryToggle = useQueryToggle as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/helpers.tsx b/x-pack/plugins/security_solution/public/helpers.tsx index 7a0f918d4e169..713ea0876ade6 100644 --- a/x-pack/plugins/security_solution/public/helpers.tsx +++ b/x-pack/plugins/security_solution/public/helpers.tsx @@ -20,10 +20,13 @@ import { CASES_FEATURE_ID, CASES_PATH, EXCEPTIONS_PATH, + HOSTS_PATH, LANDING_PATH, + NETWORK_PATH, RULES_PATH, SERVER_APP_ID, THREAT_INTELLIGENCE_PATH, + USERS_PATH, } from '../common/constants'; import type { FactoryQueryTypes, @@ -192,6 +195,13 @@ export const isThreatIntelligencePath = (pathname: string): boolean => { }); }; +export const isExplorePage = (pathname: string): boolean => { + return !!matchPath(pathname, { + path: `(${HOSTS_PATH}|${USERS_PATH}|${NETWORK_PATH})`, + strict: false, + }); +}; + export const getSubPluginRoutesByCapabilities = ( subPlugins: StartedSubPlugins, capabilities: Capabilities diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index 381b36c20e0c6..a8dbcd624d9e2 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -58,6 +58,7 @@ interface Props extends Pick<GlobalTimeArgs, 'from' | 'to' | 'deleteQuery' | 'se showSpacer?: boolean; scopeId?: string; toggleTopN?: () => void; + hideQueryToggle?: boolean; } const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ @@ -92,6 +93,7 @@ const EventsByDatasetComponent: React.FC<Props> = ({ scopeId, to, toggleTopN, + hideQueryToggle = false, }) => { const uniqueQueryId = useMemo(() => `${ID}-${queryType}`, [queryType]); @@ -202,6 +204,7 @@ const EventsByDatasetComponent: React.FC<Props> = ({ scopeId={scopeId} {...eventsByDatasetHistogramConfigs} title={onlyField != null ? i18n.TOP(onlyField) : eventsByDatasetHistogramConfigs.title} + hideQueryToggle={hideQueryToggle} /> ); }; diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/signals_by_category.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/signals_by_category.tsx index 6186964386077..f03058683f2a7 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/signals_by_category.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/signals_by_category.tsx @@ -34,6 +34,7 @@ interface Props { setAbsoluteRangeDatePickerTarget?: InputsModelId; showLegend?: boolean; runtimeMappings?: MappingRuntimeFields; + hideQueryToggle?: boolean; } const SignalsByCategoryComponent: React.FC<Props> = ({ @@ -46,6 +47,7 @@ const SignalsByCategoryComponent: React.FC<Props> = ({ showLegend, setAbsoluteRangeDatePickerTarget = InputsModelId.global, runtimeMappings, + hideQueryToggle = false, }) => { const dispatch = useDispatch(); const { signalIndexName } = useSignalIndex(); @@ -86,6 +88,7 @@ const SignalsByCategoryComponent: React.FC<Props> = ({ title={i18n.ALERT_TREND} titleSize={onlyField == null ? 'm' : 's'} updateDateRange={updateDateRangeCallback} + hideQueryToggle={hideQueryToggle} /> ); }; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 538845eaeefa9..abc9c41101e25 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -191,7 +191,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S return renderApp({ ...params, - services: await startServices(params), + services, store, usageCollection: plugins.usageCollection, subPluginRoutes: getSubPluginRoutesByCapabilities( diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx index 47a3bc62b91ce..c62c1c967771d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx @@ -274,14 +274,16 @@ describe('Field Renderers', () => { test('it should only render the items after overflowIndexStart', () => { render( - <MoreContainer - fieldType="keyword" - idPrefix={idPrefix} - isAggregatable={true} - moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} - overflowIndexStart={5} - rowItems={rowItems} - /> + <TestProviders> + <MoreContainer + fieldType="keyword" + idPrefix={idPrefix} + moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} + overflowIndexStart={5} + values={rowItems} + fieldName="mock.attr" + /> + </TestProviders> ); expect(screen.getByTestId('more-container').textContent).toEqual('item6item7'); @@ -289,14 +291,16 @@ describe('Field Renderers', () => { test('it should render all the items when overflowIndexStart is zero', () => { render( - <MoreContainer - fieldType="keyword" - idPrefix={idPrefix} - isAggregatable={true} - moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} - overflowIndexStart={0} - rowItems={rowItems} - /> + <TestProviders> + <MoreContainer + fieldType="keyword" + idPrefix={idPrefix} + moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} + overflowIndexStart={0} + values={rowItems} + fieldName="mock.attr" + /> + </TestProviders> ); expect(screen.getByTestId('more-container').textContent).toEqual( @@ -306,14 +310,16 @@ describe('Field Renderers', () => { test('it should have the eui-yScroll to enable scrolling when necessary', () => { render( - <MoreContainer - fieldType="keyword" - idPrefix={idPrefix} - isAggregatable={true} - moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} - overflowIndexStart={5} - rowItems={rowItems} - /> + <TestProviders> + <MoreContainer + fieldType="keyword" + idPrefix={idPrefix} + moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} + overflowIndexStart={5} + values={rowItems} + fieldName="mock.attr" + /> + </TestProviders> ); expect(screen.getByTestId('more-container')).toHaveClass('eui-yScroll'); @@ -321,14 +327,16 @@ describe('Field Renderers', () => { test('it should use the moreMaxHeight prop as the value for the max-height style', () => { render( - <MoreContainer - fieldType="keyword" - idPrefix={idPrefix} - isAggregatable={true} - moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} - overflowIndexStart={5} - rowItems={rowItems} - /> + <TestProviders> + <MoreContainer + fieldType="keyword" + idPrefix={idPrefix} + moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} + overflowIndexStart={5} + values={rowItems} + fieldName="mock.attr" + /> + </TestProviders> ); expect(screen.getByTestId('more-container')).toHaveStyle( @@ -338,35 +346,38 @@ describe('Field Renderers', () => { test('it should render with correct attrName prop', () => { render( - <MoreContainer - fieldType="keyword" - idPrefix={idPrefix} - isAggregatable={true} - moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} - overflowIndexStart={5} - rowItems={rowItems} - attrName="mock.attr" - /> + <TestProviders> + <MoreContainer + fieldType="keyword" + idPrefix={idPrefix} + moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} + overflowIndexStart={5} + values={rowItems} + fieldName="mock.attr" + /> + </TestProviders> ); screen - .getAllByTestId('render-content-mock.attr') + .getAllByTestId('cellActions-renderContent-mock.attr') .forEach((element) => expect(element).toBeInTheDocument()); }); - test('it should only invoke the optional render function, when provided, for the items after overflowIndexStart', () => { + test('it should only invoke the optional render function when provided', () => { const renderFn = jest.fn(); render( - <MoreContainer - fieldType="keyword" - idPrefix={idPrefix} - isAggregatable={true} - moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} - overflowIndexStart={5} - render={renderFn} - rowItems={rowItems} - /> + <TestProviders> + <MoreContainer + fieldType="keyword" + idPrefix={idPrefix} + moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} + overflowIndexStart={5} + render={renderFn} + values={rowItems} + fieldName="mock.attr" + /> + </TestProviders> ); expect(renderFn).toHaveBeenCalledTimes(2); @@ -387,6 +398,7 @@ describe('Field Renderers', () => { moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} overflowIndexStart={5} rowItems={rowItems} + attrName={'mock.attr'} /> </TestProviders> ); @@ -407,6 +419,7 @@ describe('Field Renderers', () => { moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} overflowIndexStart={5} rowItems={rowItems} + attrName={'mock.attr'} /> </TestProviders> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx index 57b782e9895c9..655f4b753af2d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx @@ -8,9 +8,10 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { getOr } from 'lodash/fp'; -import React, { useCallback, Fragment, useMemo, useState } from 'react'; +import React, { useCallback, Fragment, useMemo, useState, useContext } from 'react'; import styled from 'styled-components'; +import { CellActions, CellActionsMode } from '@kbn/cell-actions'; import type { HostEcs } from '@kbn/securitysolution-ecs'; import type { AutonomousSystem, @@ -25,9 +26,11 @@ import { FormattedRelativePreferenceDate } from '../../../common/components/form import { HostDetailsLink, ReputationLink, WhoIsLink } from '../../../common/components/links'; import { Spacer } from '../../../common/components/page'; import * as i18n from '../../../explore/network/components/details/translations'; -import type { QueryOperator } from '../../../../common/types'; -import { IS_OPERATOR } from '../../../../common/types'; -import { DraggableWrapper } from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { + CELL_ACTIONS_DEFAULT_TRIGGER, + CELL_ACTIONS_TIMELINE_TRIGGER, +} from '../../../../common/constants'; +import { TimelineContext } from '../timeline'; const DraggableContainerFlexGroup = styled(EuiFlexGroup)` flex-grow: unset; @@ -270,8 +273,8 @@ export const DefaultFieldRenderer = React.memo(DefaultFieldRendererComponent); DefaultFieldRenderer.displayName = 'DefaultFieldRenderer'; interface DefaultFieldRendererOverflowProps { - attrName?: string; - fieldType?: string; + attrName: string; + fieldType: string; rowItems: string[]; idPrefix: string; isAggregatable?: boolean; @@ -281,91 +284,50 @@ interface DefaultFieldRendererOverflowProps { } interface MoreContainerProps { - attrName?: string; - dragDisplayValue?: string; - fieldType?: string; + fieldName: string; + fieldType: string; + values: string[]; idPrefix: string; - isAggregatable?: boolean; moreMaxHeight: string; overflowIndexStart: number; render?: (item: string) => React.ReactNode; - rowItems: string[]; } -/** A container (with overflow) for showing "More" items in a popover */ export const MoreContainer = React.memo<MoreContainerProps>( - ({ - attrName, - dragDisplayValue, - fieldType, - idPrefix, - isAggregatable, - moreMaxHeight, - overflowIndexStart, - render, - rowItems, - }) => { + ({ fieldName, fieldType, idPrefix, moreMaxHeight, overflowIndexStart, render, values }) => { + const { timelineId } = useContext(TimelineContext); + const moreItemsWithHoverActions = useMemo( () => - rowItems.slice(overflowIndexStart).reduce<React.ReactElement[]>((acc, rowItem, index) => { - const id = escapeDataProviderId(`${idPrefix}-${attrName}-${rowItem}-${index}`); - const dataProvider = - typeof rowItem === 'string' && attrName != null - ? { - and: [], - enabled: true, - id, - name: rowItem, - excluded: false, - kqlQuery: '', - queryMatch: { - field: attrName, - value: rowItem, - displayValue: dragDisplayValue ?? rowItem, - operator: IS_OPERATOR as QueryOperator, - }, - } - : undefined; + values.slice(overflowIndexStart).reduce<React.ReactElement[]>((acc, value, index) => { + const id = escapeDataProviderId(`${idPrefix}-${fieldName}-${value}-${index}`); - if (dataProvider != null) { + if (typeof value === 'string' && fieldName != null) { acc.push( - <EuiFlexItem key={`${idPrefix}-${id}`}> - <DraggableWrapper - dataProvider={dataProvider} - isDraggable={false} - render={() => (render && render(rowItem)) ?? defaultToEmptyTag(rowItem)} - scopeId={undefined} - fieldType={fieldType} - isAggregatable={isAggregatable} - /> + <EuiFlexItem key={id}> + <CellActions + key={id} + mode={CellActionsMode.HOVER} + visibleCellActions={5} + showActionTooltips + triggerId={ + timelineId ? CELL_ACTIONS_TIMELINE_TRIGGER : CELL_ACTIONS_DEFAULT_TRIGGER + } + field={{ + name: fieldName, + value, + type: fieldType, + }} + > + <>{render ? render(value) : defaultToEmptyTag(value)}</> + </CellActions> </EuiFlexItem> ); } return acc; }, []), - [ - attrName, - dragDisplayValue, - fieldType, - idPrefix, - isAggregatable, - overflowIndexStart, - render, - rowItems, - ] - ); - - const moreItems = useMemo( - () => - rowItems.slice(overflowIndexStart).map((rowItem, index) => { - return ( - <EuiFlexItem grow={1} key={`${rowItem}-${index}`}> - {(render && render(rowItem)) ?? defaultToEmptyTag(rowItem)} - </EuiFlexItem> - ); - }), - [overflowIndexStart, render, rowItems] + [fieldName, fieldType, idPrefix, overflowIndexStart, render, values, timelineId] ); return ( @@ -378,7 +340,7 @@ export const MoreContainer = React.memo<MoreContainerProps>( }} > <EuiFlexGroup gutterSize="s" direction="column" data-test-subj="overflow-items"> - {attrName != null ? moreItemsWithHoverActions : moreItems} + {moreItemsWithHoverActions} </EuiFlexGroup> </div> ); @@ -387,16 +349,7 @@ export const MoreContainer = React.memo<MoreContainerProps>( MoreContainer.displayName = 'MoreContainer'; export const DefaultFieldRendererOverflow = React.memo<DefaultFieldRendererOverflowProps>( - ({ - attrName, - idPrefix, - moreMaxHeight, - overflowIndexStart = 5, - render, - rowItems, - fieldType, - isAggregatable, - }) => { + ({ attrName, idPrefix, moreMaxHeight, overflowIndexStart = 5, render, rowItems, fieldType }) => { const [isOpen, setIsOpen] = useState(false); const togglePopover = useCallback(() => setIsOpen((currentIsOpen) => !currentIsOpen), []); const button = useMemo( @@ -431,14 +384,13 @@ export const DefaultFieldRendererOverflow = React.memo<DefaultFieldRendererOverf panelClassName="withHoverActions__popover" > <MoreContainer - attrName={attrName} + fieldName={attrName} idPrefix={idPrefix} render={render} - rowItems={rowItems} + values={rowItems} moreMaxHeight={moreMaxHeight} overflowIndexStart={overflowIndexStart} fieldType={fieldType} - isAggregatable={isAggregatable} /> </EuiPopover> )} diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index f9a3820b27971..4522a35f793ec 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -136,5 +136,6 @@ "@kbn/user-profile-components", "@kbn/guided-onboarding", "@kbn/securitysolution-ecs", + "@kbn/cell-actions", ] }