diff --git a/x-pack/plugins/security_solution/public/detection_engine/components/rule_snooze_badge.tsx b/x-pack/plugins/security_solution/public/detection_engine/components/rule_snooze_badge.tsx new file mode 100644 index 0000000000000..7fa16826eec60 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/components/rule_snooze_badge.tsx @@ -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 ( + + + + ); + } + + return ( + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/index.tsx new file mode 100644 index 0000000000000..e610715d676ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/index.tsx @@ -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 ( + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/translations.ts similarity index 81% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management/components/translations.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/translations.ts index 1b98a9c6212eb..37b3b6c75ba6e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/translations.ts @@ -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', } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx index 28ed5a658558d..07cbd4294cb22 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx @@ -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', () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index e53b23d16a46d..6e1b4fddbd167 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -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 = ({ const lastExecutionMessage = lastExecution?.message ?? ''; const ruleStatusInfo = useMemo(() => { - return ruleLoading ? ( - - - - ) : ( - - - + return ( + <> + {ruleLoading ? ( + + + + ) : ( + + + + )} + + + + ); - }, [lastExecutionStatus, lastExecutionDate, ruleLoading, isExistingRule, refreshRule]); + }, [ruleId, lastExecutionStatus, lastExecutionDate, ruleLoading, isExistingRule, refreshRule]); const ruleError = useMemo(() => { return ruleLoading ? ( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge.tsx deleted file mode 100644 index f2a06cd5475e0..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge.tsx +++ /dev/null @@ -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 ( - - - - ); - } - - return ( - - ); -} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/__mocks__/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/__mocks__/rules_table_context.tsx index 8ab84b2e60a60..9ee763899eab7 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/__mocks__/rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/__mocks__/rules_table_context.tsx @@ -15,6 +15,7 @@ export const useRulesTableContextMock = { rulesSnoozeSettings: { data: {}, isLoading: false, + isFetching: false, isError: false, }, pagination: { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx index fa2c64f01d2d4..938174d0c567d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx @@ -40,8 +40,18 @@ import { RuleSource } from './rules_table_saved_state'; import { useRulesTableSavedState } from './use_rules_table_saved_state'; interface RulesSnoozeSettings { - data: Record; // 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; + /** + * 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, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/translations.ts index 52b4a5d4ba622..ad3cd89604030 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/translations.ts @@ -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', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx index e20a2f2c70e4f..0ffb0ac7574a6 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx @@ -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 | EuiTableActionsColumnType; @@ -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) => , + render: (_, rule: Rule) => { + const snoozeSettings = rulesSnoozeSettings.data[rule.id]; + const { isFetching, isError } = rulesSnoozeSettings; + + return ( + + ); + }, width: '100px', sortable: false, }), - [] + [rulesSnoozeSettings] ); };