From 3bea483f34e03ea1bbeb836350c650fd06673f10 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:56:10 -0400 Subject: [PATCH] [Security Solution] Adds enable on install UI workflow to prebuilt rules page (#191529) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds overflow button UI to all prebuilt rules install buttons in order to enable the rule when it is successfully installed. Previously, a user would have to navigate back to the rules page and find the rule(s) they just installed to enable, this combines those two workflows into a single button action - speeding up the out of the box rule implementation. ### Screenshots **Prebuilt rules table columns** Screenshot 2024-09-04 at 10 38 05 AM **Prebuilt rules table bulk install** Screenshot 2024-09-04 at 10 38 16 AM **Prebuilt rule details flyout** Screenshot 2024-09-04 at 10 38 44 AM ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [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 - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- ...perform_specific_rules_install_mutation.ts | 25 +++- .../add_prebuilt_rules_header_buttons.tsx | 86 +++++++++++-- .../add_prebuilt_rules_install_button.tsx | 118 ++++++++++++++++++ .../add_prebuilt_rules_table_context.tsx | 82 +++++++----- .../add_prebuilt_rules_table/translations.ts | 29 +++++ .../use_add_prebuilt_rules_table_columns.tsx | 34 ++--- .../detection_engine/rules/translations.ts | 8 -- 7 files changed, 304 insertions(+), 78 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts index 7f7fab65b0d95d..3b448219d6e01e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts @@ -16,8 +16,10 @@ import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; import { useInvalidateFetchRuleManagementFiltersQuery } from '../use_fetch_rule_management_filters_query'; import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../use_fetch_rules_snooze_settings_query'; import { useInvalidateFetchPrebuiltRulesInstallReviewQuery } from './use_fetch_prebuilt_rules_install_review_query'; +import type { BulkAction } from '../../api'; import { performInstallSpecificRules } from '../../api'; import { useInvalidateFetchCoverageOverviewQuery } from '../use_fetch_coverage_overview_query'; +import { useBulkActionMutation } from '../use_bulk_action_mutation'; export const PERFORM_SPECIFIC_RULES_INSTALLATION_KEY = [ 'POST', @@ -25,11 +27,16 @@ export const PERFORM_SPECIFIC_RULES_INSTALLATION_KEY = [ PERFORM_RULE_INSTALLATION_URL, ]; +export interface UsePerformSpecificRulesInstallParams { + rules: InstallSpecificRulesRequest['rules']; + enable?: boolean; +} + export const usePerformSpecificRulesInstallMutation = ( options?: UseMutationOptions< PerformRuleInstallationResponseBody, Error, - InstallSpecificRulesRequest['rules'] + UsePerformSpecificRulesInstallParams > ) => { const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); @@ -40,15 +47,15 @@ export const usePerformSpecificRulesInstallMutation = ( useInvalidateFetchPrebuiltRulesInstallReviewQuery(); const invalidateRuleStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); const invalidateFetchCoverageOverviewQuery = useInvalidateFetchCoverageOverviewQuery(); + const { mutateAsync } = useBulkActionMutation(); return useMutation< PerformRuleInstallationResponseBody, Error, - InstallSpecificRulesRequest['rules'] + UsePerformSpecificRulesInstallParams >( - (rulesToInstall: InstallSpecificRulesRequest['rules']) => { - return performInstallSpecificRules(rulesToInstall); - }, + (rulesToInstall: UsePerformSpecificRulesInstallParams) => + performInstallSpecificRules(rulesToInstall.rules), { ...options, mutationKey: PERFORM_SPECIFIC_RULES_INSTALLATION_KEY, @@ -62,6 +69,14 @@ export const usePerformSpecificRulesInstallMutation = ( invalidateRuleStatus(); invalidateFetchCoverageOverviewQuery(); + const [response, , { enable }] = args; + + if (response && enable) { + const ruleIdsToEnable = response.results.created.map((rule) => rule.id); + const bulkAction: BulkAction = { type: 'enable', ids: ruleIdsToEnable }; + mutateAsync({ bulkAction }); + } + if (options?.onSettled) { options.onSettled(...args); } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx index b943022f5d53d2..b4ff6ab29a3ffa 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx @@ -5,8 +5,18 @@ * 2.0. */ -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import React from 'react'; +import { + EuiButton, + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPopover, +} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useBoolean } from 'react-use'; import { useUserData } from '../../../../../detections/components/user_info'; import { useAddPrebuiltRulesTableContext } from './add_prebuilt_rules_table_context'; import * as i18n from './translations'; @@ -31,19 +41,69 @@ export const AddPrebuiltRulesHeaderButtons = () => { const isRuleInstalling = loadingRules.length > 0; const isRequestInProgress = isRuleInstalling || isRefetching || isUpgradingSecurityPackages; + const [isOverflowPopoverOpen, setOverflowPopover] = useBoolean(false); + + const onOverflowButtonClick = () => { + setOverflowPopover(!isOverflowPopoverOpen); + }; + + const closeOverflowPopover = useCallback(() => { + setOverflowPopover(false); + }, [setOverflowPopover]); + + const enableOnClick = useCallback(() => { + installSelectedRules(true); + closeOverflowPopover(); + }, [closeOverflowPopover, installSelectedRules]); + + const installOnClick = useCallback(() => { + installSelectedRules(); + }, [installSelectedRules]); + + const overflowItems = useMemo( + () => [ + + {i18n.INSTALL_AND_ENABLE_BUTTON_LABEL} + , + ], + [enableOnClick] + ); + return ( {shouldDisplayInstallSelectedRulesButton ? ( - - - {i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)} - {isRuleInstalling ? : undefined} - - + <> + + + {i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)} + {isRuleInstalling && } + + + + + } + isOpen={isOverflowPopoverOpen} + closePopover={closeOverflowPopover} + panelPaddingSize="s" + anchorPosition="downRight" + > + + + + ) : null} { aria-label={i18n.INSTALL_ALL_ARIA_LABEL} > {i18n.INSTALL_ALL} - {isRuleInstalling ? : undefined} + {isRuleInstalling && } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx new file mode 100644 index 00000000000000..ea83efae768fa1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPopover, +} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useBoolean } from 'react-use'; +import type { Rule } from '../../../../rule_management/logic'; +import type { RuleSignatureId } from '../../../../../../common/api/detection_engine'; +import type { AddPrebuiltRulesTableActions } from './add_prebuilt_rules_table_context'; +import * as i18n from './translations'; + +export interface PrebuiltRulesInstallButtonProps { + ruleId: RuleSignatureId; + record: Rule; + installOneRule: AddPrebuiltRulesTableActions['installOneRule']; + loadingRules: RuleSignatureId[]; + isDisabled: boolean; +} + +export const PrebuiltRulesInstallButton = ({ + ruleId, + record, + installOneRule, + loadingRules, + isDisabled, +}: PrebuiltRulesInstallButtonProps) => { + const isRuleInstalling = loadingRules.includes(ruleId); + const isInstallButtonDisabled = isRuleInstalling || isDisabled; + const [isPopoverOpen, setPopover] = useBoolean(false); + + const onOverflowButtonClick = useCallback(() => { + setPopover(!isPopoverOpen); + }, [isPopoverOpen, setPopover]); + + const closeOverflowPopover = useCallback(() => { + setPopover(false); + }, [setPopover]); + + const enableOnClick = useCallback(() => { + installOneRule(ruleId, true); + closeOverflowPopover(); + }, [closeOverflowPopover, installOneRule, ruleId]); + + const installOnClick = useCallback(() => { + installOneRule(ruleId); + }, [installOneRule, ruleId]); + + const overflowItems = useMemo( + () => [ + + {i18n.INSTALL_AND_ENABLE_BUTTON_LABEL} + , + ], + [enableOnClick] + ); + + const popoverButton = useMemo( + () => ( + + ), + [isInstallButtonDisabled, onOverflowButtonClick] + ); + + if (isRuleInstalling) { + return ( + + ); + } + return ( + + + + {i18n.INSTALL_BUTTON_LABEL} + + + + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx index 5450fc1f64a1cb..14e539ec40ae1c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx @@ -7,7 +7,7 @@ import type { Dispatch, SetStateAction } from 'react'; import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; -import { EuiButton } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useUserData } from '../../../../../detections/components/user_info'; import { useFetchPrebuiltRulesStatusQuery } from '../../../../rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query'; import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; @@ -75,9 +75,9 @@ export interface AddPrebuiltRulesTableState { export interface AddPrebuiltRulesTableActions { reFetchRules: () => void; - installOneRule: (ruleId: RuleSignatureId) => void; + installOneRule: (ruleId: RuleSignatureId, enable?: boolean) => void; installAllRules: () => void; - installSelectedRules: () => void; + installSelectedRules: (enable?: boolean) => void; setFilterOptions: Dispatch>; selectRules: (rules: RuleResponse[]) => void; openRulePreview: (ruleId: RuleSignatureId) => void; @@ -140,13 +140,16 @@ export const AddPrebuiltRulesTableContextProvider = ({ const filteredRules = useFilterPrebuiltRulesToInstall({ filterOptions, rules }); const installOneRule = useCallback( - async (ruleId: RuleSignatureId) => { + async (ruleId: RuleSignatureId, enable?: boolean) => { const rule = rules.find((r) => r.rule_id === ruleId); invariant(rule, `Rule with id ${ruleId} not found`); setLoadingRules((prev) => [...prev, ruleId]); try { - await installSpecificRulesRequest([{ rule_id: ruleId, version: rule.version }]); + await installSpecificRulesRequest({ + rules: [{ rule_id: ruleId, version: rule.version }], + enable, + }); } finally { setLoadingRules((prev) => prev.filter((id) => id !== ruleId)); } @@ -154,19 +157,24 @@ export const AddPrebuiltRulesTableContextProvider = ({ [installSpecificRulesRequest, rules] ); - const installSelectedRules = useCallback(async () => { - const rulesToUpgrade = selectedRules.map((rule) => ({ - rule_id: rule.rule_id, - version: rule.version, - })); - setLoadingRules((prev) => [...prev, ...rulesToUpgrade.map((r) => r.rule_id)]); - try { - await installSpecificRulesRequest(rulesToUpgrade); - } finally { - setLoadingRules((prev) => prev.filter((id) => !rulesToUpgrade.some((r) => r.rule_id === id))); - setSelectedRules([]); - } - }, [installSpecificRulesRequest, selectedRules]); + const installSelectedRules = useCallback( + async (enable?: boolean) => { + const rulesToUpgrade = selectedRules.map((rule) => ({ + rule_id: rule.rule_id, + version: rule.version, + })); + setLoadingRules((prev) => [...prev, ...rulesToUpgrade.map((r) => r.rule_id)]); + try { + await installSpecificRulesRequest({ rules: rulesToUpgrade, enable }); + } finally { + setLoadingRules((prev) => + prev.filter((id) => !rulesToUpgrade.some((r) => r.rule_id === id)) + ); + setSelectedRules([]); + } + }, + [installSpecificRulesRequest, selectedRules] + ); const installAllRules = useCallback(async () => { // Unselect all rules so that the table doesn't show the "bulk actions" bar @@ -188,17 +196,33 @@ export const AddPrebuiltRulesTableContextProvider = ({ !(isPreviewRuleLoading || isRefetching || isUpgradingSecurityPackages); return ( - { - installOneRule(rule.rule_id); - closeRulePreview(); - }} - fill - data-test-subj="installPrebuiltRuleFromFlyoutButton" - > - {i18n.INSTALL_BUTTON_LABEL} - + + + { + installOneRule(rule.rule_id); + closeRulePreview(); + }} + data-test-subj="installPrebuiltRuleFromFlyoutButton" + > + {i18n.INSTALL_WITHOUT_ENABLING_BUTTON_LABEL} + + + + { + installOneRule(rule.rule_id, true); + closeRulePreview(); + }} + fill + data-test-subj="installAndEnablePrebuiltRuleFromFlyoutButton" + > + {i18n.INSTALL_AND_ENABLE_BUTTON_LABEL} + + + ); }, [ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/translations.ts index a3ea514571151a..c335f7624afd8c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/translations.ts @@ -44,3 +44,32 @@ export const INSTALL_BUTTON_LABEL = i18n.translate( defaultMessage: 'Install', } ); + +export const INSTALL_WITHOUT_ENABLING_BUTTON_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.installWithoutEnablingButtonLabel', + { + defaultMessage: 'Install without enabling', + } +); + +export const INSTALL_AND_ENABLE_BUTTON_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.installAndEnableButtonLabel', + { + defaultMessage: 'Install and enable', + } +); + +export const INSTALL_RULE_BUTTON_ARIA_LABEL = (ruleName: string) => + i18n.translate('xpack.securitySolution.addRules.installRuleButton.ariaLabel', { + defaultMessage: 'Install "{ruleName}"', + values: { + ruleName, + }, + }); + +export const INSTALL_RULES_OVERFLOW_BUTTON_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.installOverflowButton.ariaLabel', + { + defaultMessage: 'More install options', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx index 70c40349fc80c0..eaf3af79ee360e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx @@ -6,7 +6,7 @@ */ import type { EuiBasicTableColumn } from '@elastic/eui'; -import { EuiButtonEmpty, EuiBadge, EuiText, EuiLoadingSpinner, EuiLink } from '@elastic/eui'; +import { EuiBadge, EuiText, EuiLink } from '@elastic/eui'; import React, { useMemo } from 'react'; import { RulesTableEmptyColumnName } from '../rules_table_empty_column_name'; import { SHOW_RELATED_INTEGRATIONS_SETTING } from '../../../../../../common/constants'; @@ -25,6 +25,7 @@ import type { RuleResponse, } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { getNormalizedSeverity } from '../helpers'; +import { PrebuiltRulesInstallButton } from './add_prebuilt_rules_install_button'; export type TableColumn = EuiBasicTableColumn; @@ -113,28 +114,15 @@ const createInstallButtonColumn = ( ): TableColumn => ({ field: 'rule_id', name: , - render: (ruleId: RuleSignatureId, record: Rule) => { - const isRuleInstalling = loadingRules.includes(ruleId); - const isInstallButtonDisabled = isRuleInstalling || isDisabled; - return ( - installOneRule(ruleId)} - data-test-subj={`installSinglePrebuiltRuleButton-${ruleId}`} - aria-label={i18n.INSTALL_RULE_BUTTON_ARIA_LABEL(record.name)} - > - {isRuleInstalling ? ( - - ) : ( - i18n.INSTALL_RULE_BUTTON - )} - - ); - }, + render: (ruleId: RuleSignatureId, record: Rule) => ( + + ), width: '10%', align: 'center', }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index ca42502d93c4e3..b573edd84343f2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -1393,14 +1393,6 @@ export const INSTALL_RULE_BUTTON = i18n.translate( } ); -export const INSTALL_RULE_BUTTON_ARIA_LABEL = (ruleName: string) => - i18n.translate('xpack.securitySolution.addRules.installRuleButton.ariaLabel', { - defaultMessage: 'Install "{ruleName}"', - values: { - ruleName, - }, - }); - export const UPDATE_RULE_BUTTON = i18n.translate( 'xpack.securitySolution.addRules.upgradeRuleButton', {