Skip to content

Commit

Permalink
[Security Solution] Add rule snooze settings on the rule details page (
Browse files Browse the repository at this point in the history
…#155407)

**Addresses:** #155406

## Summary

This PR adds rule snoozing support on the Rule Details page.

https://user-images.githubusercontent.com/3775283/233387056-47a29066-f2af-4bbe-ad4f-f1002b216d7e.mov


### Checklist

- [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials
- [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
  • Loading branch information
maximpn authored Apr 24, 2023
1 parent 5f24c14 commit 84e6e36
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 80 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import React, { useMemo } from 'react';
import { useUserData } from '../../detections/components/user_info';
import { hasUserCRUDPermission } from '../../common/utils/privileges';
import { useKibana } from '../../common/lib/kibana';
import type { RuleSnoozeSettings } from '../rule_management/logic';
import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../rule_management/api/hooks/use_fetch_rules_snooze_settings';

interface RuleSnoozeBadgeProps {
/**
* Rule's snooze settings, when set to `undefined` considered as a loading state
*/
snoozeSettings: RuleSnoozeSettings | undefined;
/**
* It should represent a user readable error message happened during data snooze settings fetching
*/
error?: string;
showTooltipInline?: boolean;
}

export function RuleSnoozeBadge({
snoozeSettings,
error,
showTooltipInline = false,
}: RuleSnoozeBadgeProps): JSX.Element {
const RulesListNotifyBadge = useKibana().services.triggersActionsUi.getRulesListNotifyBadge;
const [{ canUserCRUD }] = useUserData();
const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD);
const invalidateFetchRuleSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery();
const isLoading = !snoozeSettings;
const rule = useMemo(() => {
return {
id: snoozeSettings?.id ?? '',
muteAll: snoozeSettings?.mute_all ?? false,
activeSnoozes: snoozeSettings?.active_snoozes ?? [],
isSnoozedUntil: snoozeSettings?.is_snoozed_until
? new Date(snoozeSettings.is_snoozed_until)
: undefined,
snoozeSchedule: snoozeSettings?.snooze_schedule,
isEditable: hasCRUDPermissions,
};
}, [snoozeSettings, hasCRUDPermissions]);

if (error) {
return (
<EuiToolTip content={error}>
<EuiButtonIcon size="s" iconType="bellSlash" disabled />
</EuiToolTip>
);
}

return (
<RulesListNotifyBadge
rule={rule}
isLoading={isLoading}
showTooltipInline={showTooltipInline}
onRuleChanged={invalidateFetchRuleSnoozeSettings}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 { useFetchRulesSnoozeSettings } from '../../../../../rule_management/api/hooks/use_fetch_rules_snooze_settings';
import { RuleSnoozeBadge } from '../../../../../components/rule_snooze_badge';
import * as i18n from './translations';

interface RuleDetailsSnoozeBadge {
/**
* Rule's SO id (not ruleId)
*/
id: string;
}

export function RuleDetailsSnoozeSettings({ id }: RuleDetailsSnoozeBadge): JSX.Element {
const { data: rulesSnoozeSettings, isFetching, isError } = useFetchRulesSnoozeSettings([id]);
const snoozeSettings = rulesSnoozeSettings?.[0];

return (
<RuleSnoozeBadge
snoozeSettings={snoozeSettings}
error={
isError || (!snoozeSettings && !isFetching)
? i18n.UNABLE_TO_FETCH_RULE_SNOOZE_SETTINGS
: undefined
}
showTooltipInline={true}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';

export const UNABLE_TO_FETCH_RULE_SNOOZE_SETTINGS = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagement.ruleSnoozeBadge.error.unableToFetch',
'xpack.securitySolution.detectionEngine.ruleDetails.rulesSnoozeSettings.error.unableToFetch',
{
defaultMessage: 'Unable to fetch snooze settings',
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ jest.mock('react-router-dom', () => {
};
});

// RuleDetailsSnoozeSettings is an isolated component and not essential for existing tests
jest.mock('./components/rule_details_snooze_settings', () => ({
RuleDetailsSnoozeSettings: () => <></>,
}));

const mockRedirectLegacyUrl = jest.fn();
const mockGetLegacyUrlConflict = jest.fn();
jest.mock('../../../../common/lib/kibana', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ import { EditRuleSettingButtonLink } from '../../../../detections/pages/detectio
import { useStartMlJobs } from '../../../rule_management/logic/use_start_ml_jobs';
import { useBulkDuplicateExceptionsConfirmation } from '../../../rule_management_ui/components/rules_table/bulk_actions/use_bulk_duplicate_confirmation';
import { BulkActionDuplicateExceptionsConfirmation } from '../../../rule_management_ui/components/rules_table/bulk_actions/bulk_duplicate_exceptions_confirmation';
import { RuleDetailsSnoozeSettings } from './components/rule_details_snooze_settings';

/**
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
Expand Down Expand Up @@ -539,23 +540,30 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
const lastExecutionMessage = lastExecution?.message ?? '';

const ruleStatusInfo = useMemo(() => {
return ruleLoading ? (
<EuiFlexItem>
<EuiLoadingSpinner size="m" data-test-subj="rule-status-loader" />
</EuiFlexItem>
) : (
<RuleStatus status={lastExecutionStatus} date={lastExecutionDate}>
<EuiButtonIcon
data-test-subj="refreshButton"
color="primary"
onClick={refreshRule}
iconType="refresh"
aria-label={ruleI18n.REFRESH}
isDisabled={!isExistingRule}
/>
</RuleStatus>
return (
<>
{ruleLoading ? (
<EuiFlexItem>
<EuiLoadingSpinner size="m" data-test-subj="rule-status-loader" />
</EuiFlexItem>
) : (
<RuleStatus status={lastExecutionStatus} date={lastExecutionDate}>
<EuiButtonIcon
data-test-subj="refreshButton"
color="primary"
onClick={refreshRule}
iconType="refresh"
aria-label={ruleI18n.REFRESH}
isDisabled={!isExistingRule}
/>
</RuleStatus>
)}
<EuiFlexItem grow={false}>
<RuleDetailsSnoozeSettings id={ruleId} />
</EuiFlexItem>
</>
);
}, [lastExecutionStatus, lastExecutionDate, ruleLoading, isExistingRule, refreshRule]);
}, [ruleId, lastExecutionStatus, lastExecutionDate, ruleLoading, isExistingRule, refreshRule]);

const ruleError = useMemo(() => {
return ruleLoading ? (
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const useRulesTableContextMock = {
rulesSnoozeSettings: {
data: {},
isLoading: false,
isFetching: false,
isError: false,
},
pagination: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,18 @@ import { RuleSource } from './rules_table_saved_state';
import { useRulesTableSavedState } from './use_rules_table_saved_state';

interface RulesSnoozeSettings {
data: Record<string, RuleSnoozeSettings>; // The key is a rule SO's id (not ruleId)
/**
* A map object using rule SO's id (not ruleId) as keys and snooze settings as values
*/
data: Record<string, RuleSnoozeSettings>;
/**
* Sets to true during the first data loading
*/
isLoading: boolean;
/**
* Sets to true during data loading
*/
isFetching: boolean;
isError: boolean;
}

Expand Down Expand Up @@ -290,6 +300,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide
const {
data: rulesSnoozeSettings,
isLoading: isSnoozeSettingsLoading,
isFetching: isSnoozeSettingsFetching,
isError: isSnoozeSettingsFetchError,
refetch: refetchSnoozeSettings,
} = useFetchRulesSnoozeSettings(
Expand Down Expand Up @@ -349,6 +360,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide
rulesSnoozeSettings: {
data: rulesSnoozeSettingsMap,
isLoading: isSnoozeSettingsLoading,
isFetching: isSnoozeSettingsFetching,
isError: isSnoozeSettingsFetchError,
},
pagination: {
Expand Down Expand Up @@ -382,6 +394,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide
rules,
rulesSnoozeSettings,
isSnoozeSettingsLoading,
isSnoozeSettingsFetching,
isSnoozeSettingsFetchError,
page,
perPage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ export const ML_RULE_JOBS_WARNING_BUTTON_LABEL = i18n.translate(
defaultMessage: 'Visit rule details page to investigate',
}
);

export const UNABLE_TO_FETCH_RULES_SNOOZE_SETTINGS = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagement.rulesSnoozeSettings.error.unableToFetch',
{
defaultMessage: 'Unable to fetch snooze settings',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type {
} from '../../../../../common/detection_engine/rule_monitoring';
import { isMlRule } from '../../../../../common/machine_learning/helpers';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { RuleSnoozeBadge } from '../../../rule_management/components/rule_snooze_badge';
import { RuleSnoozeBadge } from '../../../components/rule_snooze_badge';
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
import { SecuritySolutionLinkAnchor } from '../../../../common/components/links';
import { getRuleDetailsTabUrl } from '../../../../common/components/link_to/redirect_to_detection_engine';
Expand All @@ -46,6 +46,7 @@ import { useHasActionsPrivileges } from './use_has_actions_privileges';
import { useHasMlPermissions } from './use_has_ml_permissions';
import { useRulesTableActions } from './use_rules_table_actions';
import { MlRuleWarningPopover } from './ml_rule_warning_popover';
import * as rulesTableI18n from './translations';

export type TableColumn = EuiBasicTableColumn<Rule> | EuiTableActionsColumnType<Rule>;

Expand Down Expand Up @@ -108,15 +109,33 @@ const useEnabledColumn = ({ hasCRUDPermissions, startMlJobs }: ColumnsProps): Ta
};

const useRuleSnoozeColumn = (): TableColumn => {
const {
state: { rulesSnoozeSettings },
} = useRulesTableContext();

return useMemo(
() => ({
field: 'snooze',
name: i18n.COLUMN_SNOOZE,
render: (_, rule: Rule) => <RuleSnoozeBadge id={rule.id} />,
render: (_, rule: Rule) => {
const snoozeSettings = rulesSnoozeSettings.data[rule.id];
const { isFetching, isError } = rulesSnoozeSettings;

return (
<RuleSnoozeBadge
snoozeSettings={snoozeSettings}
error={
isError || (!snoozeSettings && !isFetching)
? rulesTableI18n.UNABLE_TO_FETCH_RULES_SNOOZE_SETTINGS
: undefined
}
/>
);
},
width: '100px',
sortable: false,
}),
[]
[rulesSnoozeSettings]
);
};

Expand Down

0 comments on commit 84e6e36

Please sign in to comment.