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**
**Prebuilt rules table bulk install**
**Prebuilt rule details flyout**
### 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 7f7fab65b0d95..3b448219d6e01 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 b943022f5d53d..b4ff6ab29a3ff 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 0000000000000..ea83efae768fa
--- /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 5450fc1f64a1c..14e539ec40ae1 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 a3ea514571151..c335f7624afd8 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 70c40349fc80c..eaf3af79ee360 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 ca42502d93c4e..b573edd84343f 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',
{