Skip to content

Commit

Permalink
[Security Solution] Add rule snoozing on the rule editing page (#155612)
Browse files Browse the repository at this point in the history
**Addresses:** #147737

## Summary

This PR adds rule snooze feature on the Rule editing page.

https://user-images.githubusercontent.com/3775283/234186169-72db1d91-ad34-4cea-922d-b0c96752c3d3.mov


### Checklist

- [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
  • Loading branch information
maximpn authored Apr 25, 2023
1 parent bd6ae3e commit 65a4ae6
Show file tree
Hide file tree
Showing 18 changed files with 212 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ const EditRulePageComponent: FC = () => {
<StepPanel loading={loading}>
{actionsStep.data != null && (
<StepRuleActions
ruleId={rule?.id}
isReadOnlyView={false}
isLoading={isLoading}
isUpdateView
Expand All @@ -319,6 +320,7 @@ const EditRulePageComponent: FC = () => {
},
],
[
rule?.id,
rule?.immutable,
rule?.type,
loading,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ jest.mock('react-router-dom', () => {
});

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

const mockRedirectLegacyUrl = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +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';
import { RuleSnoozeBadge } from '../../../rule_management/components/rule_snooze_badge';

/**
* 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 @@ -559,7 +559,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
</RuleStatus>
)}
<EuiFlexItem grow={false}>
<RuleDetailsSnoozeSettings id={ruleId} />
<RuleSnoozeBadge ruleId={ruleId} showTooltipInline />
</EuiFlexItem>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,9 @@ describe('Detections Rules API', () => {
describe('fetchRulesSnoozeSettings', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue({
data: [],
});
});

test('requests snooze settings of multiple rules by their IDs', () => {
Expand Down Expand Up @@ -836,5 +839,38 @@ describe('Detections Rules API', () => {
})
);
});

test('returns mapped data', async () => {
fetchMock.mockResolvedValue({
data: [
{
id: '1',
mute_all: false,
},
{
id: '1',
mute_all: false,
active_snoozes: [],
is_snoozed_until: '2023-04-24T19:31:46.765Z',
},
],
});

const result = await fetchRulesSnoozeSettings({ ids: ['id1'] });

expect(result).toEqual([
{
id: '1',
muteAll: false,
activeSnoozes: [],
},
{
id: '1',
muteAll: false,
activeSnoozes: [],
isSnoozedUntil: new Date('2023-04-24T19:31:46.765Z'),
},
]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ import type {
PrePackagedRulesStatusResponse,
PreviewRulesProps,
Rule,
RulesSnoozeSettingsResponse,
RuleSnoozeSettings,
RulesSnoozeSettingsBatchResponse,
UpdateRulesProps,
} from '../logic/types';
import { convertRulesFilterToKQL } from '../logic/utils';
Expand Down Expand Up @@ -197,8 +198,8 @@ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise<Rul
export const fetchRulesSnoozeSettings = async ({
ids,
signal,
}: FetchRuleSnoozingProps): Promise<RulesSnoozeSettingsResponse> =>
KibanaServices.get().http.fetch<RulesSnoozeSettingsResponse>(
}: FetchRuleSnoozingProps): Promise<RuleSnoozeSettings[]> => {
const response = await KibanaServices.get().http.fetch<RulesSnoozeSettingsBatchResponse>(
INTERNAL_ALERTING_API_FIND_RULES_PATH,
{
method: 'GET',
Expand All @@ -211,6 +212,17 @@ export const fetchRulesSnoozeSettings = async ({
}
);

return response.data?.map((snoozeSettings) => ({
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,
}));
};

export interface BulkActionSummary {
failed: number;
skipped: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ export const useFetchRulesSnoozeSettings = (
) => {
return useQuery(
[...FETCH_RULE_SNOOZE_SETTINGS_QUERY_KEY, ...ids],
async ({ signal }) => {
const response = await fetchRulesSnoozeSettings({ ids, signal });

return response.data;
},
({ signal }) => fetchRulesSnoozeSettings({ ids, signal }),
{
...DEFAULT_QUERY_OPTIONS,
...queryOptions,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export * from './rule_snooze_badge';
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,44 @@

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';
import type { RuleObjectId } from '../../../../../common/detection_engine/rule_schema';
import { useUserData } from '../../../../detections/components/user_info';
import { hasUserCRUDPermission } from '../../../../common/utils/privileges';
import { useKibana } from '../../../../common/lib/kibana';
import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../../api/hooks/use_fetch_rules_snooze_settings';
import { useRuleSnoozeSettings } from './use_rule_snooze_settings';

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

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

if (error) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import { i18n } from '@kbn/i18n';

export const UNABLE_TO_FETCH_RULE_SNOOZE_SETTINGS = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.rulesSnoozeSettings.error.unableToFetch',
export const UNABLE_TO_FETCH_RULES_SNOOZE_SETTINGS = i18n.translate(
'xpack.securitySolution.detectionEngine.rulesSnoozeBadge.error.unableToFetch',
{
defaultMessage: 'Unable to fetch snooze settings',
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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 type { RuleSnoozeSettings } from '../../logic';
import { useFetchRulesSnoozeSettings } from '../../api/hooks/use_fetch_rules_snooze_settings';
import { useRulesTableContextOptional } from '../../../rule_management_ui/components/rules_table/rules_table/rules_table_context';
import * as i18n from './translations';

interface UseRuleSnoozeSettingsResult {
snoozeSettings?: RuleSnoozeSettings;
error?: string;
}

export function useRuleSnoozeSettings(id: string): UseRuleSnoozeSettingsResult {
const {
state: { rulesSnoozeSettings: rulesTableSnoozeSettings },
} = useRulesTableContextOptional() ?? { state: {} };
const {
data: rulesSnoozeSettings,
isFetching: isSingleSnoozeSettingsFetching,
isError: isSingleSnoozeSettingsError,
} = useFetchRulesSnoozeSettings([id], {
enabled: !rulesTableSnoozeSettings?.data[id] && !rulesTableSnoozeSettings?.isFetching,
});
const snoozeSettings = rulesTableSnoozeSettings?.data[id] ?? rulesSnoozeSettings?.[0];
const isFetching = rulesTableSnoozeSettings?.isFetching || isSingleSnoozeSettingsFetching;
const isError = rulesTableSnoozeSettings?.isError || isSingleSnoozeSettingsError;

return {
snoozeSettings,
error:
isError || (!snoozeSettings && !isFetching)
? i18n.UNABLE_TO_FETCH_RULES_SNOOZE_SETTINGS
: undefined,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,23 @@ export interface FetchRulesProps {
}

export interface RuleSnoozeSettings {
id: string;
muteAll: boolean;
snoozeSchedule?: RuleSnooze;
activeSnoozes?: string[];
isSnoozedUntil?: Date;
}

interface RuleSnoozeSettingsResponse {
id: string;
mute_all: boolean;
snooze_schedule?: RuleSnooze;
active_snoozes?: string[];
is_snoozed_until?: string;
}

export interface RulesSnoozeSettingsResponse {
data: RuleSnoozeSettings[];
export interface RulesSnoozeSettingsBatchResponse {
data: RuleSnoozeSettingsResponse[];
}

export type SortingOptions = t.TypeOf<typeof SortingOptions>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ describe('RulesTableContextProvider', () => {
{ id: '2', name: 'rule 2' },
] as Rule[],
rulesSnoozeSettings: [
{ id: '1', mute_all: true, snooze_schedule: [] },
{ id: '2', mute_all: false, snooze_schedule: [] },
{ id: '1', muteAll: true, snoozeSchedule: [] },
{ id: '2', muteAll: false, snoozeSchedule: [] },
],
});

Expand All @@ -216,21 +216,21 @@ describe('RulesTableContextProvider', () => {
{ id: '2', name: 'rule 2' },
] as Rule[],
rulesSnoozeSettings: [
{ id: '1', mute_all: true, snooze_schedule: [] },
{ id: '2', mute_all: false, snooze_schedule: [] },
{ id: '1', muteAll: true, snoozeSchedule: [] },
{ id: '2', muteAll: false, snoozeSchedule: [] },
],
});

expect(state.rulesSnoozeSettings.data).toEqual({
'1': {
id: '1',
mute_all: true,
snooze_schedule: [],
muteAll: true,
snoozeSchedule: [],
},
'2': {
id: '2',
mute_all: false,
snooze_schedule: [],
muteAll: false,
snoozeSchedule: [],
},
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,3 @@ 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',
}
);
Loading

0 comments on commit 65a4ae6

Please sign in to comment.