mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Add rule snooze settings on the rule details page (#155407)
**Addresses:** https://github.com/elastic/kibana/issues/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
This commit is contained in:
parent
5f24c14d58
commit
84e6e36d0b
10 changed files with 176 additions and 80 deletions
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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',
|
||||
}
|
|
@ -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', () => {
|
||||
|
|
|
@ -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.
|
||||
|
@ -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 ? (
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* 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 { useInvalidateFetchRulesSnoozeSettingsQuery } from '../api/hooks/use_fetch_rules_snooze_settings';
|
||||
import { useRulesTableContext } from '../../rule_management_ui/components/rules_table/rules_table/rules_table_context';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface RuleSnoozeBadgeProps {
|
||||
id: string; // Rule SO's id (not ruleId)
|
||||
}
|
||||
|
||||
export function RuleSnoozeBadge({ id }: RuleSnoozeBadgeProps): JSX.Element {
|
||||
const RulesListNotifyBadge = useKibana().services.triggersActionsUi.getRulesListNotifyBadge;
|
||||
const [{ canUserCRUD }] = useUserData();
|
||||
const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD);
|
||||
const {
|
||||
state: { rulesSnoozeSettings },
|
||||
} = useRulesTableContext();
|
||||
const invalidateFetchRuleSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery();
|
||||
const rule = useMemo(() => {
|
||||
const ruleSnoozeSettings = rulesSnoozeSettings.data[id];
|
||||
|
||||
return {
|
||||
id: ruleSnoozeSettings?.id ?? '',
|
||||
muteAll: ruleSnoozeSettings?.mute_all ?? false,
|
||||
activeSnoozes: ruleSnoozeSettings?.active_snoozes ?? [],
|
||||
isSnoozedUntil: ruleSnoozeSettings?.is_snoozed_until
|
||||
? new Date(ruleSnoozeSettings.is_snoozed_until)
|
||||
: undefined,
|
||||
snoozeSchedule: ruleSnoozeSettings?.snooze_schedule,
|
||||
isEditable: hasCRUDPermissions,
|
||||
};
|
||||
}, [id, rulesSnoozeSettings, hasCRUDPermissions]);
|
||||
|
||||
if (rulesSnoozeSettings.isError) {
|
||||
return (
|
||||
<EuiToolTip content={i18n.UNABLE_TO_FETCH_RULE_SNOOZE_SETTINGS}>
|
||||
<EuiButtonIcon size="s" iconType="bellSlash" disabled />
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RulesListNotifyBadge
|
||||
rule={rule}
|
||||
isLoading={rulesSnoozeSettings.isLoading}
|
||||
onRuleChanged={invalidateFetchRuleSnoozeSettings}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -15,6 +15,7 @@ export const useRulesTableContextMock = {
|
|||
rulesSnoozeSettings: {
|
||||
data: {},
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
},
|
||||
pagination: {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -290,6 +300,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide
|
|||
const {
|
||||
data: rulesSnoozeSettings,
|
||||
isLoading: isSnoozeSettingsLoading,
|
||||
isFetching: isSnoozeSettingsFetching,
|
||||
isError: isSnoozeSettingsFetchError,
|
||||
refetch: refetchSnoozeSettings,
|
||||
} = useFetchRulesSnoozeSettings(
|
||||
|
@ -349,6 +360,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide
|
|||
rulesSnoozeSettings: {
|
||||
data: rulesSnoozeSettingsMap,
|
||||
isLoading: isSnoozeSettingsLoading,
|
||||
isFetching: isSnoozeSettingsFetching,
|
||||
isError: isSnoozeSettingsFetchError,
|
||||
},
|
||||
pagination: {
|
||||
|
@ -382,6 +394,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide
|
|||
rules,
|
||||
rulesSnoozeSettings,
|
||||
isSnoozeSettingsLoading,
|
||||
isSnoozeSettingsFetching,
|
||||
isSnoozeSettingsFetchError,
|
||||
page,
|
||||
perPage,
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
@ -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>;
|
||||
|
||||
|
@ -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]
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue