Skip to content

Commit

Permalink
[Security Solution] Adds enable on install UI workflow to prebuilt ru…
Browse files Browse the repository at this point in the history
…les page (#191529)

## 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**
<img width="530" alt="Screenshot 2024-09-04 at 10 38 05 AM"
src="https://github.com/user-attachments/assets/4a009afa-a8f0-4eaa-a76b-8f4e509f35a3">


**Prebuilt rules table bulk install**
<img width="1478" alt="Screenshot 2024-09-04 at 10 38 16 AM"
src="https://github.com/user-attachments/assets/eb6deb9b-9b4e-4be3-a4ac-0da06d6f1e8e">


**Prebuilt rule details flyout**
<img width="1489" alt="Screenshot 2024-09-04 at 10 38 44 AM"
src="https://github.com/user-attachments/assets/a4bce22d-7e90-42e4-8522-cf411a297659">


### 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)
  • Loading branch information
dplumlee authored Sep 18, 2024
1 parent 4c51c00 commit 3bea483
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,27 @@ 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',
'SPECIFIC_RULES',
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();
Expand All @@ -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,
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(
() => [
<EuiContextMenuItem key="copy" icon={'play'} onClick={enableOnClick}>
{i18n.INSTALL_AND_ENABLE_BUTTON_LABEL}
</EuiContextMenuItem>,
],
[enableOnClick]
);

return (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
{shouldDisplayInstallSelectedRulesButton ? (
<EuiFlexItem grow={false}>
<EuiButton
onClick={installSelectedRules}
disabled={!canUserEditRules || isRequestInProgress}
data-test-subj="installSelectedRulesButton"
>
{i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)}
{isRuleInstalling ? <EuiLoadingSpinner size="s" /> : undefined}
</EuiButton>
</EuiFlexItem>
<>
<EuiFlexItem grow={false}>
<EuiButton
onClick={installOnClick}
disabled={!canUserEditRules || isRequestInProgress}
data-test-subj="installSelectedRulesButton"
>
{i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)}
{isRuleInstalling && <EuiLoadingSpinner size="s" />}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiButtonIcon
display="base"
size="m"
iconType="boxesVertical"
aria-label={i18n.INSTALL_RULES_OVERFLOW_BUTTON_ARIA_LABEL}
onClick={onOverflowButtonClick}
disabled={!canUserEditRules || isRequestInProgress}
/>
}
isOpen={isOverflowPopoverOpen}
closePopover={closeOverflowPopover}
panelPaddingSize="s"
anchorPosition="downRight"
>
<EuiContextMenuPanel size="s" items={overflowItems} />
</EuiPopover>
</EuiFlexItem>
</>
) : null}
<EuiFlexItem grow={false}>
<EuiButton
Expand All @@ -55,7 +115,7 @@ export const AddPrebuiltRulesHeaderButtons = () => {
aria-label={i18n.INSTALL_ALL_ARIA_LABEL}
>
{i18n.INSTALL_ALL}
{isRuleInstalling ? <EuiLoadingSpinner size="s" /> : undefined}
{isRuleInstalling && <EuiLoadingSpinner size="s" />}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
() => [
<EuiContextMenuItem key="copy" icon={'play'} onClick={enableOnClick}>
{i18n.INSTALL_AND_ENABLE_BUTTON_LABEL}
</EuiContextMenuItem>,
],
[enableOnClick]
);

const popoverButton = useMemo(
() => (
<EuiButtonIcon
display="empty"
size="s"
iconType="boxesVertical"
aria-label={i18n.INSTALL_RULES_OVERFLOW_BUTTON_ARIA_LABEL}
onClick={onOverflowButtonClick}
disabled={isInstallButtonDisabled}
/>
),
[isInstallButtonDisabled, onOverflowButtonClick]
);

if (isRuleInstalling) {
return (
<EuiLoadingSpinner
size="s"
data-test-subj={`installSinglePrebuiltRuleButton-loadingSpinner-${ruleId}`}
/>
);
}
return (
<EuiFlexGroup responsive={false} gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
disabled={isInstallButtonDisabled}
onClick={installOnClick}
data-test-subj={`installSinglePrebuiltRuleButton-${ruleId}`}
aria-label={i18n.INSTALL_RULE_BUTTON_ARIA_LABEL(record.name)}
>
{i18n.INSTALL_BUTTON_LABEL}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
button={popoverButton}
isOpen={isPopoverOpen}
closePopover={closeOverflowPopover}
panelPaddingSize="s"
anchorPosition="downRight"
>
<EuiContextMenuPanel size="s" items={overflowItems} />
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
);
};
Loading

0 comments on commit 3bea483

Please sign in to comment.