From 1928beade8b18003580ef6264cad9d178d0c8ba7 Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Fri, 3 Dec 2021 10:14:47 +0000 Subject: [PATCH] [Security Solution][Detections] Adds Rules monitoring table actions (#119644) [Security Solution][Detections] Adds actions for Rules monitoring table: single/bulk enable, disable, duplicate, export, remove (#119644) --- .../detection_rules/prebuilt_rules.spec.ts | 15 + .../cypress/screens/alerts_detection_rules.ts | 2 + .../rules/all_rules_tables/index.test.tsx | 122 ------ .../rules/all_rules_tables/index.tsx | 102 ----- .../rules/use_rule_status.test.tsx | 2 - .../rules/use_rule_status.tsx | 8 +- .../rules/all/columns.test.tsx | 4 - .../detection_engine/rules/all/columns.tsx | 392 ++++++++---------- .../detection_engine/rules/all/index.test.tsx | 2 - .../rules/all/popover_tooltip.tsx | 3 +- .../rules/all/rules_tables.tsx | 75 ++-- .../all/table_header_tooltip_cell.test.tsx | 36 ++ .../rules/all/table_header_tooltip_cell.tsx | 46 ++ 13 files changed, 334 insertions(+), 475 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/table_header_tooltip_cell.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/table_header_tooltip_cell.tsx diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts index b259c0f1d9e33..38f5eec836bd6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts @@ -13,6 +13,8 @@ import { RULES_EMPTY_PROMPT, RULE_SWITCH, SHOWING_RULES_TEXT, + RULES_MONIROTING_TABLE, + SELECT_ALL_RULES_ON_PAGE_CHECKBOX, } from '../../screens/alerts_detection_rules'; import { goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated } from '../../tasks/alerts'; @@ -94,6 +96,19 @@ describe('Actions with prebuilt rules', () => { cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'false'); }); + it('Allows to activate all rules on a page and deactivate single one at monitoring table', () => { + cy.get(RULES_MONIROTING_TABLE).click(); + cy.get(SELECT_ALL_RULES_ON_PAGE_CHECKBOX).click(); + activateSelectedRules(); + waitForRuleToChangeStatus(); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); + + selectNumberOfRules(1); + cy.get(RULE_SWITCH).first().click(); + waitForRuleToChangeStatus(); + cy.get(RULE_SWITCH).first().should('have.attr', 'aria-checked', 'false'); + }); + it('Allows to delete all rules at once', () => { selectAllRules(); deleteSelectedRules(); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index 39e08e29bdc2a..2fff4c2e92676 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -97,3 +97,5 @@ export const RULE_DETAILS_DELETE_BTN = '[data-test-subj="rules-details-delete-ru export const ALERT_DETAILS_CELLS = '[data-test-subj="dataGridRowCell"]'; export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; + +export const SELECT_ALL_RULES_ON_PAGE_CHECKBOX = '[data-test-subj="checkboxSelectAll"]'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx deleted file mode 100644 index d1dfd6ccfd565..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx +++ /dev/null @@ -1,122 +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, { useRef } from 'react'; -import { shallow } from 'enzyme'; - -import '../../../../common/mock/match_media'; -import { AllRulesTables } from './index'; -import { AllRulesTabs } from '../../../pages/detection_engine/rules/all'; - -describe('AllRulesTables', () => { - it('renders correctly', () => { - const Component = () => { - const ref = useRef(null); - - return ( - - ); - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(1); - }); - - it('renders rules tab when "selectedTab" is "rules"', () => { - const Component = () => { - const ref = useRef(null); - - return ( - - ); - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(1); - expect(wrapper.dive().find('[data-test-subj="monitoring-table"]')).toHaveLength(0); - }); - - it('renders monitoring tab when "selectedTab" is "monitoring"', () => { - const Component = () => { - const ref = useRef(null); - - return ( - - ); - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(0); - expect(wrapper.dive().find('[data-test-subj="monitoring-table"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.tsx deleted file mode 100644 index 82e54b5b86072..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.tsx +++ /dev/null @@ -1,102 +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 { - Direction, - EuiBasicTable, - EuiBasicTableColumn, - EuiEmptyPrompt, - EuiTableSelectionType, -} from '@elastic/eui'; - -import React, { memo } from 'react'; -import { Rule, Rules, RulesSortingFields } from '../../../containers/detection_engine/rules/types'; -import { AllRulesTabs } from '../../../pages/detection_engine/rules/all'; -import { - RulesColumns, - RuleStatusRowItemType, -} from '../../../pages/detection_engine/rules/all/columns'; -import * as i18n from '../../../pages/detection_engine/rules/translations'; -import { EuiBasicTableOnChange } from '../../../pages/detection_engine/rules/types'; - -export interface SortingType { - sort: { - field: RulesSortingFields; - direction: Direction; - }; -} - -interface AllRulesTablesProps { - euiBasicTableSelectionProps: EuiTableSelectionType; - hasPermissions: boolean; - monitoringColumns: Array>; - pagination: { - pageIndex: number; - pageSize: number; - totalItemCount: number; - pageSizeOptions: number[]; - }; - rules: Rules; - rulesColumns: RulesColumns[]; - rulesStatuses: RuleStatusRowItemType[]; - sorting: SortingType; - tableOnChangeCallback: ({ page, sort }: EuiBasicTableOnChange) => void; - tableRef?: React.MutableRefObject; - selectedTab: AllRulesTabs; -} - -const emptyPrompt = ( - {i18n.NO_RULES}} titleSize="xs" body={i18n.NO_RULES_BODY} /> -); - -export const AllRulesTablesComponent: React.FC = ({ - euiBasicTableSelectionProps, - hasPermissions, - monitoringColumns, - pagination, - rules, - rulesColumns, - rulesStatuses, - sorting, - tableOnChangeCallback, - tableRef, - selectedTab, -}) => { - return ( - <> - {selectedTab === AllRulesTabs.rules && ( - - )} - {selectedTab === AllRulesTabs.monitoring && ( - - )} - - ); -}; - -export const AllRulesTables = memo(AllRulesTablesComponent); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx index 4d01e2ff00ec1..a5809ea776322 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx @@ -178,8 +178,6 @@ describe('useRuleStatus', () => { }, failures: [], id: '12345678987654321', - activate: true, - name: 'Test rule', }, ], }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx index 4f524886935cd..d0c75e08ae01b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx @@ -9,7 +9,7 @@ import { useEffect, useRef, useState } from 'react'; import { isNotFoundError } from '@kbn/securitysolution-t-grid'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { RuleStatusRowItemType } from '../../../pages/detection_engine/rules/all/columns'; +import { EnhancedRuleStatus } from '../../../pages/detection_engine/rules/all/columns'; import { getRuleStatusById, getRulesStatusByIds } from './api'; import * as i18n from './translations'; import { RuleStatus, Rules } from './types'; @@ -18,7 +18,7 @@ type Func = (ruleId: string) => void; export type ReturnRuleStatus = [boolean, RuleStatus | null, Func | null]; export interface ReturnRulesStatuses { loading: boolean; - rulesStatuses: RuleStatusRowItemType[]; + rulesStatuses: EnhancedRuleStatus[]; } /** @@ -78,7 +78,7 @@ export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus = * */ export const useRulesStatuses = (rules: Rules): ReturnRulesStatuses => { - const [rulesStatuses, setRuleStatuses] = useState([]); + const [rulesStatuses, setRuleStatuses] = useState([]); const [loading, setLoading] = useState(false); const { addError } = useAppToasts(); @@ -98,8 +98,6 @@ export const useRulesStatuses = (rules: Rules): ReturnRulesStatuses => { setRuleStatuses( rules.map((rule) => ({ id: rule.id, - activate: rule.enabled, - name: rule.name, ...ruleStatusesResponse[rule.id], })) ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx index 3920aa40e1f15..59c09c415f50e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { scopedHistoryMock } from 'src/core/public/mocks'; import uuid from 'uuid'; import '../../../../../common/mock/match_media'; import { deleteRulesAction, duplicateRulesAction, editRuleAction } from './actions'; @@ -18,7 +17,6 @@ jest.mock('./actions', () => ({ editRuleAction: jest.fn(), })); -const history = scopedHistoryMock.create(); const duplicateRulesActionMock = duplicateRulesAction as jest.Mock; const deleteRulesActionMock = deleteRulesAction as jest.Mock; const editRuleActionMock = editRuleAction as jest.Mock; @@ -45,7 +43,6 @@ describe('AllRulesTable Columns', () => { const duplicateRulesActionObject = getActions( dispatch, dispatchToaster, - history, navigateToApp, reFetchRules, refetchPrePackagedRulesStatus, @@ -62,7 +59,6 @@ describe('AllRulesTable Columns', () => { const deleteRulesActionObject = getActions( dispatch, dispatchToaster, - history, navigateToApp, reFetchRules, refetchPrePackagedRulesStatus, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx index 235cdd9c740ee..d586f0e4856d4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx @@ -10,12 +10,10 @@ import { EuiTableActionsColumnType, EuiText, EuiToolTip, - EuiIcon, EuiLink, EuiBadge, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import * as H from 'history'; import { sum } from 'lodash'; import React, { Dispatch } from 'react'; @@ -40,6 +38,7 @@ import { RulesTableAction } from '../../../../containers/detection_engine/rules/ import { LinkAnchor } from '../../../../../common/components/links'; import { getToolTipContent, canEditRuleWithActions } from '../../../../../common/utils/privileges'; import { PopoverTooltip } from './popover_tooltip'; +import { TableHeaderTooltipCell } from './table_header_tooltip_cell'; import { APP_UI_ID, @@ -48,18 +47,25 @@ import { } from '../../../../../../common/constants'; import { DocLinksStart, NavigateToAppOptions } from '../../../../../../../../../src/core/public'; +type FormatUrl = (path: string) => string; +type HasReadActionsPrivileges = + | boolean + | Readonly<{ + [x: string]: boolean; + }>; + +export type TableItem = Rule & Partial; +export type TableColumn = EuiBasicTableColumn | EuiTableActionsColumnType; + +const extractRuleFromRow = ({ current_status: _, failures, ...rule }: TableItem): Rule => rule; + export const getActions = ( dispatch: React.Dispatch, dispatchToaster: Dispatch, - history: H.History, navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise, reFetchRules: () => Promise, refetchPrePackagedRulesStatus: () => Promise, - actionsPrivileges: - | boolean - | Readonly<{ - [x: string]: boolean; - }> + actionsPrivileges: HasReadActionsPrivileges ) => [ { 'data-test-subj': 'editRuleAction', @@ -72,8 +78,9 @@ export const getActions = ( i18n.EDIT_RULE_SETTINGS ), icon: 'controlsHorizontal', - onClick: (rowItem: Rule) => editRuleAction(rowItem.id, navigateToApp), - enabled: (rowItem: Rule) => canEditRuleWithActions(rowItem, actionsPrivileges), + onClick: (rowItem: TableItem) => editRuleAction(rowItem.id, navigateToApp), + enabled: (rowItem: TableItem) => + canEditRuleWithActions(extractRuleFromRow(rowItem), actionsPrivileges), }, { 'data-test-subj': 'duplicateRuleAction', @@ -86,10 +93,10 @@ export const getActions = ( ) : ( i18n.DUPLICATE_RULE ), - enabled: (rowItem: Rule) => canEditRuleWithActions(rowItem, actionsPrivileges), - onClick: async (rowItem: Rule) => { + enabled: (rowItem: TableItem) => canEditRuleWithActions(rowItem, actionsPrivileges), + onClick: async (rowItem: TableItem) => { const createdRules = await duplicateRulesAction( - [rowItem], + [extractRuleFromRow(rowItem)], [rowItem.id], dispatch, dispatchToaster @@ -120,97 +127,139 @@ export const getActions = ( }, ]; -export type RuleStatusRowItemType = RuleStatus & { - name: string; +export type EnhancedRuleStatus = RuleStatus & { id: string; }; -export type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType; -export type RulesStatusesColumns = EuiBasicTableColumn; -type FormatUrl = (path: string) => string; -interface GetColumns { + +interface GetColumnsProps { dispatch: React.Dispatch; - dispatchToaster: Dispatch; formatUrl: FormatUrl; - history: H.History; hasMlPermissions: boolean; hasPermissions: boolean; loadingRuleIds: string[]; navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise; + hasReadActionsPrivileges: HasReadActionsPrivileges; + dispatchToaster: Dispatch; reFetchRules: () => Promise; refetchPrePackagedRulesStatus: () => Promise; - hasReadActionsPrivileges: - | boolean - | Readonly<{ - [x: string]: boolean; - }>; + docLinks: DocLinksStart; } -export const getColumns = ({ - dispatch, - dispatchToaster, - formatUrl, - history, +const getColumnEnabled = ({ hasMlPermissions, + hasReadActionsPrivileges, + dispatch, hasPermissions, loadingRuleIds, +}: GetColumnsProps): TableColumn => ({ + field: 'enabled', + name: i18n.COLUMN_ACTIVATE, + render: (_, rule: TableItem) => ( + + + + ), + width: '95px', + sortable: true, +}); + +const getColumnRuleName = ({ navigateToApp, formatUrl }: GetColumnsProps): TableColumn => ({ + field: 'name', + name: i18n.COLUMN_RULE, + render: (value: Rule['name'], item: TableItem) => ( + + void }) => { + ev.preventDefault(); + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsUrl(item.id), + }); + }} + href={formatUrl(getRuleDetailsUrl(item.id))} + > + {value} + + + ), + width: '38%', + sortable: true, + truncateText: true, +}); + +const getColumnTags = (): TableColumn => ({ + field: 'tags', + name: null, + align: 'center', + render: (tags: Rule['tags']) => { + if (tags.length === 0) { + return null; + } + + const renderItem = (tag: string, i: number) => ( + + {tag} + + ); + return ( + + ); + }, + width: '65px', + truncateText: true, +}); + +const getActionsColumns = ({ + hasPermissions, + hasReadActionsPrivileges, + dispatch, + dispatchToaster, navigateToApp, reFetchRules, refetchPrePackagedRulesStatus, - hasReadActionsPrivileges, -}: GetColumns): RulesColumns[] => { - const cols: RulesColumns[] = [ - { - field: 'name', - name: i18n.COLUMN_RULE, - render: (value: Rule['name'], item: Rule) => ( - - void }) => { - ev.preventDefault(); - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(item.id), - }); - }} - href={formatUrl(getRuleDetailsUrl(item.id))} - > - {value} - - - ), - width: '38%', - sortable: true, - truncateText: true, - }, - { - field: 'tags', - name: null, - align: 'center', - render: (tags: Rule['tags']) => { - if (tags.length === 0) { - return null; - } +}: GetColumnsProps): TableColumn[] => + hasPermissions + ? [ + { + actions: getActions( + dispatch, + dispatchToaster, + navigateToApp, + reFetchRules, + refetchPrePackagedRulesStatus, + hasReadActionsPrivileges + ), + width: '40px', + } as EuiTableActionsColumnType, + ] + : []; - const renderItem = (tag: string, i: number) => ( - - {tag} - - ); - return ( - - ); - }, - width: '65px', - truncateText: true, - }, +export const getRulesColumns = (columnsProps: GetColumnsProps): TableColumn[] => { + return [ + getColumnRuleName(columnsProps), + getColumnTags(), { field: 'risk_score', name: i18n.COLUMN_RISK_SCORE, @@ -288,112 +337,41 @@ export const getColumns = ({ width: '65px', truncateText: true, }, - { - align: 'center', - field: 'enabled', - name: i18n.COLUMN_ACTIVATE, - render: (value: Rule['enabled'], item: Rule) => ( - - - - ), - sortable: true, - width: '95px', - truncateText: true, - }, - ]; - const actions: RulesColumns[] = [ - { - actions: getActions( - dispatch, - dispatchToaster, - history, - navigateToApp, - reFetchRules, - refetchPrePackagedRulesStatus, - hasReadActionsPrivileges - ), - width: '40px', - } as EuiTableActionsColumnType, + getColumnEnabled(columnsProps), + ...getActionsColumns(columnsProps), ]; - - return hasPermissions ? [...cols, ...actions] : cols; }; -export const getMonitoringColumns = ( - navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise, - formatUrl: FormatUrl, - docLinks: DocLinksStart -): RulesStatusesColumns[] => { - const cols: RulesStatusesColumns[] = [ - { - field: 'name', - name: i18n.COLUMN_RULE, - render: (value: RuleStatus['current_status']['status'], item: RuleStatusRowItemType) => { - return ( - - void }) => { - ev.preventDefault(); - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(item.id), - }); - }} - href={formatUrl(getRuleDetailsUrl(item.id))} - > - {value} - - - ); - }, - width: '28%', - truncateText: true, - }, +export const getMonitoringColumns = (columnsProps: GetColumnsProps): TableColumn[] => { + const { docLinks } = columnsProps; + return [ + { ...getColumnRuleName(columnsProps), width: '28%' }, + getColumnTags(), { field: 'current_status.bulk_create_time_durations', name: ( - <> - {i18n.COLUMN_INDEXING_TIMES} - - - - + ), - render: (value: RuleStatus['current_status']['bulk_create_time_durations']) => ( + render: (value: RuleStatus['current_status']['bulk_create_time_durations'] | undefined) => ( {value?.length ? sum(value.map(Number)).toFixed() : getEmptyTagValue()} ), - width: '14%', + width: '16%', truncateText: true, }, { field: 'current_status.search_after_time_durations', name: ( - <> - {i18n.COLUMN_QUERY_TIMES} - - - - + ), - render: (value: RuleStatus['current_status']['search_after_time_durations']) => ( + render: (value: RuleStatus['current_status']['search_after_time_durations'] | undefined) => ( {value?.length ? sum(value.map(Number)).toFixed() : getEmptyTagValue()} @@ -404,28 +382,32 @@ export const getMonitoringColumns = ( { field: 'current_status.gap', name: ( - <> - {i18n.COLUMN_GAP} - - -

- - {'see documentation'} - - ), - }} - /> -

-
-
- + + + +

+ + {'see documentation'} + + ), + }} + /> +

+
+
+ + } + /> ), - render: (value: RuleStatus['current_status']['gap']) => ( + render: (value: RuleStatus['current_status']['gap'] | undefined) => ( {value ?? getEmptyTagValue()} @@ -436,7 +418,7 @@ export const getMonitoringColumns = ( { field: 'current_status.status', name: i18n.COLUMN_LAST_RESPONSE, - render: (value: RuleStatus['current_status']['status']) => ( + render: (value: RuleStatus['current_status']['status'] | undefined) => ( ), width: '12%', @@ -445,7 +427,7 @@ export const getMonitoringColumns = ( { field: 'current_status.status_date', name: i18n.COLUMN_LAST_COMPLETE_RUN, - render: (value: RuleStatus['current_status']['status_date']) => { + render: (value: RuleStatus['current_status']['status_date'] | undefined) => { return value == null ? ( getEmptyTagValue() ) : ( @@ -457,20 +439,10 @@ export const getMonitoringColumns = ( /> ); }, - width: '18%', + width: '16%', truncateText: true, }, - { - field: 'activate', - name: i18n.COLUMN_ACTIVATE, - render: (value: Rule['enabled']) => ( - - {value ? i18n.ACTIVE : i18n.INACTIVE} - - ), - width: '95px', - }, + getColumnEnabled(columnsProps), + ...getActionsColumns(columnsProps), ]; - - return cols; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index 200bf0c719320..487d0862cf467 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -166,8 +166,6 @@ describe('AllRules', () => { }, failures: [], id: '12345678987654321', - activate: true, - name: 'Test rule', }, ], }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/popover_tooltip.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/popover_tooltip.tsx index 5cf0a0c0b28fc..22bad4fffade9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/popover_tooltip.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/popover_tooltip.tsx @@ -31,9 +31,10 @@ const PopoverTooltipComponent = ({ columnName, children }: PopoverTooltipProps) setIsPopoverOpen(!isPopoverOpen)} - size="s" + size="xs" color="primary" iconType="questionInCircle" + style={{ height: 'auto' }} /> } > diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx index bc2172a257635..54c2500d03b03 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx @@ -13,6 +13,7 @@ import { EuiProgress, EuiConfirmModal, EuiWindowEvent, + EuiEmptyPrompt, } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { debounce } from 'lodash/fp'; @@ -23,7 +24,6 @@ import { useRulesStatuses, CreatePreBuiltRules, FilterOptions, - Rule, RulesSortingFields, } from '../../../../containers/detection_engine/rules'; @@ -33,12 +33,11 @@ import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana'; import { useStateToaster } from '../../../../../common/components/toasters'; import { Loader } from '../../../../../common/components/loader'; import { PrePackagedRulesPrompt } from '../../../../components/rules/pre_packaged_rules/load_empty_prompt'; -import { AllRulesTables, SortingType } from '../../../../components/rules/all_rules_tables'; import { getPrePackagedRuleStatus } from '../helpers'; import * as i18n from '../translations'; import { EuiBasicTableOnChange } from '../types'; import { getBatchItems } from './batch_actions'; -import { getColumns, getMonitoringColumns } from './columns'; +import { getRulesColumns, getMonitoringColumns, TableItem } from './columns'; import { showRulesTable } from './helpers'; import { RulesTableFilters } from './rules_table_filters/rules_table_filters'; import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use_ml_capabilities'; @@ -167,7 +166,7 @@ export const RulesTables = React.memo( }, [loadingRuleIds, loadingRulesAction]); const sorting = useMemo( - (): SortingType => ({ + () => ({ sort: { field: filterOptions.sortField, direction: filterOptions.sortOrder, @@ -265,12 +264,10 @@ export const RulesTables = React.memo( [updateOptions, setLastRefreshDate] ); - const rulesColumns = useMemo(() => { - return getColumns({ + const [rulesColumns, monitoringColumns] = useMemo(() => { + const props = { dispatch, - dispatchToaster, formatUrl, - history, hasMlPermissions, hasPermissions, loadingRuleIds: @@ -279,10 +276,14 @@ export const RulesTables = React.memo( ? loadingRuleIds : [], navigateToApp, + hasReadActionsPrivileges: hasActionsPrivileges, + dispatchToaster, + history, reFetchRules, refetchPrePackagedRulesStatus, - hasReadActionsPrivileges: hasActionsPrivileges, - }); + docLinks, + }; + return [getRulesColumns(props), getMonitoringColumns(props)]; }, [ dispatch, dispatchToaster, @@ -296,13 +297,9 @@ export const RulesTables = React.memo( loadingRulesAction, navigateToApp, reFetchRules, + docLinks, ]); - const monitoringColumns = useMemo( - () => getMonitoringColumns(navigateToApp, formatUrl, docLinks), - [navigateToApp, formatUrl, docLinks] - ); - useEffect(() => { setRefreshRulesData(reFetchRules); }, [reFetchRules, setRefreshRulesData]); @@ -332,8 +329,8 @@ export const RulesTables = React.memo( const euiBasicTableSelectionProps = useMemo( () => ({ - selectable: (item: Rule) => !loadingRuleIds.includes(item.id), - onSelectionChange: (selected: Rule[]) => { + selectable: (item: TableItem) => !loadingRuleIds.includes(item.id), + onSelectionChange: (selected: TableItem[]) => { /** * EuiBasicTable doesn't provide declarative API to control selected rows. * This limitation requires us to synchronize selection state manually using setSelection(). @@ -447,6 +444,25 @@ export const RulesTables = React.memo( [initLoading, prePackagedRuleStatus, rulesCustomInstalled] ); + const items = useMemo(() => { + const rulesStatusesMap = new Map(rulesStatuses.map((item) => [item.id, item])); + + return rules.map((rule) => { + return { + ...rule, + ...rulesStatusesMap.get(rule.id), + }; + }); + }, [rulesStatuses, rules]); + + const tableProps = + selectedTab === AllRulesTabs.rules + ? { + 'data-test-subj': 'rules-table', + columns: rulesColumns, + } + : { 'data-test-subj': 'monitoring-table', columns: monitoringColumns }; + return ( <> @@ -535,18 +551,23 @@ export const RulesTables = React.memo( onToggleSelectAll={toggleSelectAll} showBulkActions /> - {i18n.NO_RULES}} + titleSize="xs" + body={i18n.NO_RULES_BODY} + /> + } + onChange={tableOnChangeCallback} pagination={paginationMemo} - rules={rules} - rulesColumns={rulesColumns} - rulesStatuses={rulesStatuses} + ref={tableRef} + selection={euiBasicTableSelectionProps} sorting={sorting} - tableOnChangeCallback={tableOnChangeCallback} - tableRef={tableRef} + {...tableProps} /> )} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/table_header_tooltip_cell.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/table_header_tooltip_cell.test.tsx new file mode 100644 index 0000000000000..216dcc98c0e41 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/table_header_tooltip_cell.test.tsx @@ -0,0 +1,36 @@ +/* + * 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 { TableHeaderTooltipCell } from './table_header_tooltip_cell'; + +import { render, screen, fireEvent } from '@testing-library/react'; + +describe('Component TableHeaderTooltipCell', () => { + it('shoud render text with icon and tooltip', async () => { + render(); + + expect(screen.getByText('test title')).toBeInTheDocument(); + expect(screen.getByTestId('tableHeaderIcon')).toBeInTheDocument(); + + fireEvent.mouseOver(screen.getByTestId('tableHeaderIcon')); + expect(await screen.findByText('test tooltip content')).toBeInTheDocument(); + }); + + it('shoud render test element as custom tooltip', () => { + render( + } + /> + ); + + expect(screen.getByText('test title')).toBeInTheDocument(); + expect(screen.getByTestId('customTestTooltip')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/table_header_tooltip_cell.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/table_header_tooltip_cell.tsx new file mode 100644 index 0000000000000..3ec474974917b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/table_header_tooltip_cell.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiToolTip, EuiIcon, EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; + +interface Props { + title: string; + tooltipContent?: string; + customTooltip?: React.ReactNode; +} + +/** + * Table header cell component that includes icon(question mark) tooltip with additional details about column + * Icon tooltip will never be truncated and always be visible for user interaction + * @param title string - column header title + * @param tooltipContent string - text content of tooltip + * @param customTooltip React.ReactNode - any custom tooltip + */ +const TableHeaderTooltipCellComponent = ({ title, tooltipContent, customTooltip }: Props) => ( + + + {title} + + {customTooltip ?? ( + + + + )} + +); + +export const TableHeaderTooltipCell = React.memo(TableHeaderTooltipCellComponent); + +TableHeaderTooltipCell.displayName = 'TableHeaderTooltipCell';