From c7a56604e77c944bf2069096ce2fa71fc1ec1aa0 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Tue, 4 Oct 2022 09:10:38 +0200 Subject: [PATCH] [Actionable Observability] Add Alert Details page header (#140299) Co-authored-by: Faisal Kanout --- .../case_view/components/case_view_alerts.tsx | 1 + x-pack/plugins/cases/public/mocks.ts | 6 +- .../public/hooks/use_fetch_alert_detail.ts | 1 - .../components/alert_details.test.tsx | 55 +++++- .../components/alert_details.tsx | 42 ++++- .../components/alert_summary.tsx | 4 +- .../components/header_actions.test.tsx | 83 +++++++++ .../components/header_actions.tsx | 160 ++++++++++++++++++ .../pages/alert_details/components/index.ts | 2 + .../components/page_title.stories.tsx | 42 +++++ .../components/page_title.test.tsx | 44 +++++ .../alert_details/components/page_title.tsx | 43 +++++ .../public/pages/alert_details/mock/alert.ts | 4 +- .../public/application/sections/index.tsx | 3 + .../components/rule_snooze_modal.tsx | 104 ++++++++++++ .../public/common/get_rule_snooze_modal.tsx | 15 ++ .../triggers_actions_ui/public/mocks.ts | 4 + .../triggers_actions_ui/public/plugin.ts | 6 + 18 files changed, 599 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/observability/public/pages/alert_details/components/header_actions.test.tsx create mode 100644 x-pack/plugins/observability/public/pages/alert_details/components/header_actions.tsx create mode 100644 x-pack/plugins/observability/public/pages/alert_details/components/page_title.stories.tsx create mode 100644 x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx create mode 100644 x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze_modal.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/get_rule_snooze_modal.tsx diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_alerts.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_alerts.tsx index f492240a8f9b8..0bd582e1cef62 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_alerts.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_alerts.tsx @@ -28,6 +28,7 @@ export const CaseViewAlerts = ({ caseData }: CaseViewAlertsProps) => { }), [caseData.comments] ); + const alertRegistrationContexts = useMemo( () => getRegistrationContextFromAlerts(caseData.comments), [caseData.comments] diff --git a/x-pack/plugins/cases/public/mocks.ts b/x-pack/plugins/cases/public/mocks.ts index 57eead3b80b10..10a4c1f6fd059 100644 --- a/x-pack/plugins/cases/public/mocks.ts +++ b/x-pack/plugins/cases/public/mocks.ts @@ -21,9 +21,13 @@ const uiMock: jest.Mocked = { getRecentCases: jest.fn(), }; +export const openAddToExistingCaseModalMock = jest.fn(); + const hooksMock: jest.Mocked = { getUseCasesAddToNewCaseFlyout: jest.fn(), - getUseCasesAddToExistingCaseModal: jest.fn(), + getUseCasesAddToExistingCaseModal: jest.fn().mockImplementation(() => ({ + open: openAddToExistingCaseModalMock, + })), }; const helpersMock: jest.Mocked = { diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_alert_detail.ts b/x-pack/plugins/observability/public/hooks/use_fetch_alert_detail.ts index aa604659f8c08..a86b97741f24c 100644 --- a/x-pack/plugins/observability/public/hooks/use_fetch_alert_detail.ts +++ b/x-pack/plugins/observability/public/hooks/use_fetch_alert_detail.ts @@ -21,7 +21,6 @@ interface AlertDetailParams { export const useFetchAlertDetail = (id: string): [boolean, TopAlert | null] => { const { observabilityRuleTypeRegistry } = usePluginContext(); - const params = useMemo( () => ({ id, ruleType: observabilityRuleTypeRegistry }), [id, observabilityRuleTypeRegistry] diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.test.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.test.tsx index d2c67c85c021e..eaa253efbc51f 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.test.tsx @@ -7,23 +7,58 @@ import React from 'react'; import * as useUiSettingHook from '@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting'; +import { useParams } from 'react-router-dom'; +import { Chance } from 'chance'; +import { waitFor } from '@testing-library/react'; +import { casesPluginMock } from '@kbn/cases-plugin/public/mocks'; +import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks'; + import { render } from '../../../utils/test_helper'; +import { useKibana } from '../../../utils/kibana_react'; +import { kibanaStartMock } from '../../../utils/kibana_react.mock'; import { useFetchAlertDetail } from '../../../hooks/use_fetch_alert_detail'; -import { AlertDetails } from './alert_details'; -import { Chance } from 'chance'; -import { useParams } from 'react-router-dom'; import { useBreadcrumbs } from '../../../hooks/use_breadcrumbs'; +import { AlertDetails } from './alert_details'; import { ConfigSchema } from '../../../plugin'; import { alert, alertWithNoData } from '../mock/alert'; -import { waitFor } from '@testing-library/react'; -jest.mock('../../../hooks/use_fetch_alert_detail'); -jest.mock('../../../hooks/use_breadcrumbs'); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useParams: jest.fn(), })); +jest.mock('../../../utils/kibana_react'); + +const useKibanaMock = useKibana as jest.Mock; + +const mockKibana = () => { + useKibanaMock.mockReturnValue({ + services: { + ...kibanaStartMock.startContract(), + cases: casesPluginMock.createStartContract(), + http: { + basePath: { + prepend: jest.fn(), + }, + }, + triggersActionsUi: triggersActionsUiMock.createStart(), + }, + }); +}; + +jest.mock('../../../hooks/use_fetch_alert_detail'); +jest.mock('../../../hooks/use_breadcrumbs'); +jest.mock('../../../hooks/use_get_user_cases_permissions', () => ({ + useGetUserCasesPermissions: () => ({ + all: true, + create: true, + delete: true, + push: true, + read: true, + update: true, + }), +})); + const useFetchAlertDetailMock = useFetchAlertDetail as jest.Mock; const useParamsMock = useParams as jest.Mock; const useBreadcrumbsMock = useBreadcrumbs as jest.Mock; @@ -49,16 +84,20 @@ describe('Alert details', () => { jest.clearAllMocks(); useParamsMock.mockReturnValue(params); useBreadcrumbsMock.mockReturnValue([]); + mockKibana(); }); - it('should show alert summary', async () => { + it('should show the alert detail page with all necessary components', async () => { useFetchAlertDetailMock.mockReturnValue([false, alert]); const alertDetails = render(, config); - expect(alertDetails.queryByTestId('alertDetails')).toBeTruthy(); await waitFor(() => expect(alertDetails.queryByTestId('centerJustifiedSpinner')).toBeFalsy()); + + expect(alertDetails.queryByTestId('alertDetails')).toBeTruthy(); expect(alertDetails.queryByTestId('alertDetailsError')).toBeFalsy(); + expect(alertDetails.queryByTestId('page-title-container')).toBeTruthy(); + expect(alertDetails.queryByTestId('alert-summary-container')).toBeTruthy(); }); it('should show error loading the alert details', async () => { diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.tsx index a2cd7fd68a2ce..1b501e62a7dde 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.tsx @@ -9,23 +9,36 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { useParams } from 'react-router-dom'; import { EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; + import { useKibana } from '../../../utils/kibana_react'; -import { ObservabilityAppServices } from '../../../application/types'; import { usePluginContext } from '../../../hooks/use_plugin_context'; import { useBreadcrumbs } from '../../../hooks/use_breadcrumbs'; -import { paths } from '../../../config/paths'; -import { AlertDetailsPathParams } from '../types'; +import { useFetchAlertDetail } from '../../../hooks/use_fetch_alert_detail'; + +import { AlertSummary, HeaderActions, PageTitle } from '.'; import { CenterJustifiedSpinner } from '../../rule_details/components/center_justified_spinner'; -import { AlertSummary } from '.'; import PageNotFound from '../../404'; -import { useFetchAlertDetail } from '../../../hooks/use_fetch_alert_detail'; + +import { ObservabilityAppServices } from '../../../application/types'; +import { AlertDetailsPathParams } from '../types'; +import { observabilityFeatureId } from '../../../../common'; +import { paths } from '../../../config/paths'; export function AlertDetails() { - const { http } = useKibana().services; + const { + http, + cases: { + helpers: { canUseCases }, + ui: { getCasesContext }, + }, + } = useKibana().services; const { ObservabilityPageTemplate, config } = usePluginContext(); const { alertId } = useParams(); const [isLoading, alert] = useFetchAlertDetail(alertId); + const CasesContext = getCasesContext(); + const userCasesPermissions = canUseCases(); + useBreadcrumbs([ { href: http.basePath.prepend(paths.observability.alerts), @@ -69,7 +82,22 @@ export function AlertDetails() { ); return ( - + , + rightSideItems: [ + + + , + ], + bottomBorder: false, + }} + data-test-subj="alertDetails" + > ); diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/alert_summary.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/alert_summary.tsx index 4a1d88e928fb2..eada6a3925521 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/alert_summary.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/components/alert_summary.tsx @@ -30,7 +30,7 @@ export function AlertSummary({ alert }: AlertSummaryProps) { const tags = alert?.fields[ALERT_RULE_TAGS]; return ( - <> +
@@ -161,6 +161,6 @@ export function AlertSummary({ alert }: AlertSummaryProps) {
- + ); } diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.test.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.test.tsx new file mode 100644 index 0000000000000..8bbec59c52b95 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.test.tsx @@ -0,0 +1,83 @@ +/* + * 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 { fireEvent } from '@testing-library/react'; +import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks'; +import { casesPluginMock, openAddToExistingCaseModalMock } from '@kbn/cases-plugin/public/mocks'; + +import { render } from '../../../utils/test_helper'; +import { useKibana } from '../../../utils/kibana_react'; +import { kibanaStartMock } from '../../../utils/kibana_react.mock'; +import { alertWithTags, mockAlertUuid } from '../mock/alert'; + +import { HeaderActions } from './header_actions'; + +jest.mock('../../../utils/kibana_react'); + +const useKibanaMock = useKibana as jest.Mock; + +const mockKibana = () => { + useKibanaMock.mockReturnValue({ + services: { + ...kibanaStartMock.startContract(), + triggersActionsUi: triggersActionsUiMock.createStart(), + cases: casesPluginMock.createStartContract(), + }, + }); +}; + +const ruleId = '123'; +const ruleName = '456'; + +jest.mock('../../../hooks/use_fetch_rule', () => { + return { + useFetchRule: () => ({ + reloadRule: jest.fn(), + rule: { + id: ruleId, + name: ruleName, + }, + }), + }; +}); + +describe('Header Actions', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockKibana(); + }); + + it('should display an actions button', () => { + const { queryByTestId } = render(); + expect(queryByTestId('alert-details-header-actions-menu-button')).toBeTruthy(); + }); + + describe('when clicking the actions button', () => { + it('should offer an "add to case" button which opens the add to case modal', async () => { + const { getByTestId, findByRole } = render(); + + fireEvent.click(await findByRole('button', { name: 'Actions' })); + + fireEvent.click(getByTestId('add-to-case-button')); + + expect(openAddToExistingCaseModalMock).toBeCalledWith({ + attachments: [ + { + alertId: mockAlertUuid, + index: '.internal.alerts-observability.metrics.alerts-*', + rule: { + id: ruleId, + name: ruleName, + }, + type: 'alert', + }, + ], + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.tsx new file mode 100644 index 0000000000000..e7a2c773dc68f --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.tsx @@ -0,0 +1,160 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { noop } from 'lodash'; +import { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public/types'; +import { CommentType } from '@kbn/cases-plugin/common'; +import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiPopover, EuiText } from '@elastic/eui'; +import { ALERT_RULE_UUID, ALERT_UUID } from '@kbn/rule-data-utils'; + +import { useKibana } from '../../../utils/kibana_react'; +import { useFetchRule } from '../../../hooks/use_fetch_rule'; +import { ObservabilityAppServices } from '../../../application/types'; +import { TopAlert } from '../../alerts'; + +export interface HeaderActionsProps { + alert: TopAlert | null; +} + +export function HeaderActions({ alert }: HeaderActionsProps) { + const { + http, + cases: { + hooks: { getUseCasesAddToExistingCaseModal }, + }, + triggersActionsUi: { getEditAlertFlyout, getRuleSnoozeModal }, + } = useKibana().services; + + const { rule, reloadRule } = useFetchRule({ + http, + ruleId: alert?.fields[ALERT_RULE_UUID] || '', + }); + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [ruleConditionsFlyoutOpen, setRuleConditionsFlyoutOpen] = useState(false); + const [snoozeModalOpen, setSnoozeModalOpen] = useState(false); + + const selectCaseModal = getUseCasesAddToExistingCaseModal(); + + const handleTogglePopover = () => setIsPopoverOpen(!isPopoverOpen); + const handleClosePopover = () => setIsPopoverOpen(false); + + const attachments: CaseAttachmentsWithoutOwner = + alert && rule + ? [ + { + alertId: alert?.fields[ALERT_UUID] || '', + index: '.internal.alerts-observability.metrics.alerts-*', + rule: { + id: rule.id, + name: rule.name, + }, + type: CommentType.alert, + }, + ] + : []; + + const handleAddToCase = () => { + setIsPopoverOpen(false); + selectCaseModal.open({ attachments }); + }; + + const handleViewRuleDetails = () => { + setIsPopoverOpen(false); + setRuleConditionsFlyoutOpen(true); + }; + + const handleOpenSnoozeModal = () => { + setIsPopoverOpen(false); + setSnoozeModalOpen(true); + }; + + return ( + <> + + {i18n.translate('xpack.observability.alertDetails.actionsButtonLabel', { + defaultMessage: 'Actions', + })} + + } + > + + + + {i18n.translate('xpack.observability.alertDetails.viewRuleDetails', { + defaultMessage: 'View rule details', + })} + + + + + + {i18n.translate('xpack.observability.alertDetails.editSnoozeRule', { + defaultMessage: 'Snooze the rule', + })} + + + + + + {i18n.translate('xpack.observability.alertDetails.addToCase', { + defaultMessage: 'Add to case', + })} + + + + + + {rule && ruleConditionsFlyoutOpen + ? getEditAlertFlyout({ + initialRule: rule, + onClose: () => { + setRuleConditionsFlyoutOpen(false); + }, + onSave: reloadRule, + }) + : null} + + {rule && snoozeModalOpen + ? getRuleSnoozeModal({ + rule, + onClose: () => setSnoozeModalOpen(false), + onRuleChanged: reloadRule, + onLoading: noop, + }) + : null} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/index.ts b/x-pack/plugins/observability/public/pages/alert_details/components/index.ts index f49d7bd3c721a..9e2ae5d34dc18 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/index.ts +++ b/x-pack/plugins/observability/public/pages/alert_details/components/index.ts @@ -5,5 +5,7 @@ * 2.0. */ +export { HeaderActions } from './header_actions'; export { AlertSummary } from './alert_summary'; export { AlertDetails } from './alert_details'; +export { PageTitle } from './page_title'; diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/page_title.stories.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.stories.tsx new file mode 100644 index 0000000000000..ab0109ef1c214 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.stories.tsx @@ -0,0 +1,42 @@ +/* + * 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 { ComponentStory } from '@storybook/react'; +import { EuiPageTemplate } from '@elastic/eui'; + +import { PageTitle as Component, PageTitleProps } from './page_title'; + +export default { + component: Component, + title: 'app/AlertDetails/PageTitle', + argTypes: { + title: { control: 'text' }, + active: { control: 'boolean' }, + }, +}; + +const Template: ComponentStory = (props: PageTitleProps) => ( + +); + +const TemplateWithPageTemplate: ComponentStory = (props: PageTitleProps) => ( + + } bottomBorder={false} /> + +); + +const defaultProps = { + title: 'host.cpu.usage is 0.2024 in the last 1 min for all hosts. Alert when > 0.02.', + active: true, +}; + +export const PageTitle = Template.bind({}); +PageTitle.args = defaultProps; + +export const PageTitleUsedWithinPageTemplate = TemplateWithPageTemplate.bind({}); +PageTitleUsedWithinPageTemplate.args = defaultProps; diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx new file mode 100644 index 0000000000000..bd0b15e8ffc2a --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx @@ -0,0 +1,44 @@ +/* + * 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 { render } from '@testing-library/react'; + +import { PageTitle, PageTitleProps } from './page_title'; + +describe('Page Title', () => { + const defaultProps = { + title: 'Great success', + active: true, + }; + + const renderComp = (props: PageTitleProps) => { + return render(); + }; + + it('should display a title when it is passed', () => { + const { getByText } = renderComp(defaultProps); + expect(getByText(defaultProps.title)).toBeTruthy(); + }); + + it('should display an active badge when active is true', async () => { + const { getByText } = renderComp(defaultProps); + expect(getByText('Active')).toBeTruthy(); + }); + + it('should display an inactive badge when active is false', async () => { + const { getByText } = renderComp({ ...defaultProps, active: false }); + + expect(getByText('Recovered')).toBeTruthy(); + }); + + it('should display no badge when active is not passed', async () => { + const { queryByTestId } = renderComp({ title: '123' }); + + expect(queryByTestId('page-title-active-badge')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx new file mode 100644 index 0000000000000..61301f16e37a9 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx @@ -0,0 +1,43 @@ +/* + * 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 { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface PageTitleProps { + title: string | undefined; + active?: boolean; +} + +export function PageTitle({ title, active }: PageTitleProps) { + const label = active + ? i18n.translate('xpack.observability.alertDetails.alertActiveState', { + defaultMessage: 'Active', + }) + : i18n.translate('xpack.observability.alertDetails.alertRecoveredState', { + defaultMessage: 'Recovered', + }); + + return ( +
+ {title} + + + +
+ {typeof active === 'boolean' ? ( + + {label} + + ) : null} +
+
+
+
+ ); +} diff --git a/x-pack/plugins/observability/public/pages/alert_details/mock/alert.ts b/x-pack/plugins/observability/public/pages/alert_details/mock/alert.ts index ed129fc3d24ec..a3031fb0aa18d 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/mock/alert.ts +++ b/x-pack/plugins/observability/public/pages/alert_details/mock/alert.ts @@ -32,6 +32,8 @@ import { TopAlert } from '../../alerts'; export const tags: string[] = ['tag1', 'tag2', 'tag3']; +export const mockAlertUuid = '756240e5-92fb-452f-b08e-cd3e0dc51738'; + export const alert: TopAlert = { reason: '1957 log entries (more than 100.25) match the conditions.', fields: { @@ -50,7 +52,7 @@ export const alert: TopAlert = { [ALERT_EVALUATION_VALUE]: 1957, [ALERT_INSTANCE_ID]: '*', [ALERT_RULE_NAME]: 'Log threshold (from logs)', - [ALERT_UUID]: '756240e5-92fb-452f-b08e-cd3e0dc51738', + [ALERT_UUID]: mockAlertUuid, [SPACE_IDS]: ['default'], [VERSION]: '8.0.0', [EVENT_KIND]: 'signal', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx index f4ae5d399e6f9..2e476855926d4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -49,6 +49,9 @@ export const RulesList = suspendedComponentWithProps( export const RulesListNotifyBadge = suspendedComponentWithProps( lazy(() => import('./rules_list/components/rules_list_notify_badge')) ); +export const RuleSnoozeModal = suspendedComponentWithProps( + lazy(() => import('./rules_list/components/rule_snooze_modal')) +); export const RuleDefinition = suspendedComponentWithProps( lazy(() => import('./rule_details/components/rule_definition')) ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze_modal.tsx new file mode 100644 index 0000000000000..c4624e7ea8e61 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze_modal.tsx @@ -0,0 +1,104 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { EuiModal, EuiModalBody, EuiSpacer } from '@elastic/eui'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { snoozeRule, unsnoozeRule } from '../../../lib/rule_api'; +import { + SNOOZE_FAILED_MESSAGE, + SNOOZE_SUCCESS_MESSAGE, + UNSNOOZE_SUCCESS_MESSAGE, +} from './rules_list_notify_badge'; +import { SnoozePanel, futureTimeToInterval } from './rule_snooze'; +import { Rule, RuleTypeParams, SnoozeSchedule } from '../../../../types'; + +export interface RuleSnoozeModalProps { + rule: Rule; + onClose: () => void; + onLoading: (isLoading: boolean) => void; + onRuleChanged: () => void; +} + +const isRuleSnoozed = (rule: { isSnoozedUntil?: Date | null; muteAll: boolean }) => + Boolean( + (rule.isSnoozedUntil && new Date(rule.isSnoozedUntil).getTime() > Date.now()) || rule.muteAll + ); + +export const RuleSnoozeModal: React.FunctionComponent = ({ + rule, + onClose, + onLoading, + onRuleChanged, +}) => { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const isSnoozed = useMemo(() => { + return isRuleSnoozed(rule); + }, [rule]); + + const onApplySnooze = useCallback( + async (snoozeSchedule: SnoozeSchedule) => { + try { + onLoading(true); + onClose(); + + await snoozeRule({ http, id: rule.id, snoozeSchedule }); + + onRuleChanged(); + + toasts.addSuccess(SNOOZE_SUCCESS_MESSAGE); + } catch (e) { + toasts.addDanger(SNOOZE_FAILED_MESSAGE); + } finally { + onLoading(false); + } + }, + [onLoading, onClose, http, rule.id, onRuleChanged, toasts] + ); + + const onApplyUnsnooze = useCallback( + async (scheduleIds?: string[]) => { + try { + onLoading(true); + onClose(); + await unsnoozeRule({ http, id: rule.id, scheduleIds }); + onRuleChanged(); + toasts.addSuccess(UNSNOOZE_SUCCESS_MESSAGE); + } catch (e) { + toasts.addDanger(SNOOZE_FAILED_MESSAGE); + } finally { + onLoading(false); + } + }, + [onLoading, onClose, http, rule.id, onRuleChanged, toasts] + ); + + return ( + + + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { RuleSnoozeModal as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_snooze_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_snooze_modal.tsx new file mode 100644 index 0000000000000..b22fe16f77312 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_snooze_modal.tsx @@ -0,0 +1,15 @@ +/* + * 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 { RuleSnoozeModal } from '../application/sections'; +import { RuleSnoozeModalProps } from '../application/sections/rules_list/components/rule_snooze_modal'; + +export const getRuleSnoozeModalLazy = (props: RuleSnoozeModalProps) => { + return ; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index 02722bc0ee73b..7c18ea1b6fa2c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -43,6 +43,7 @@ import { getFieldBrowserLazy } from './common/get_field_browser'; import { getRuleAlertsSummaryLazy } from './common/get_rule_alerts_summary'; import { getRuleDefinitionLazy } from './common/get_rule_definition'; import { getRuleStatusPanelLazy } from './common/get_rule_status_panel'; +import { getRuleSnoozeModalLazy } from './common/get_rule_snooze_modal'; function createStartMock(): TriggersAndActionsUIPublicPluginStart { const actionTypeRegistry = new TypeRegistry(); @@ -124,6 +125,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { getRuleStatusPanel: (props) => { return getRuleStatusPanelLazy(props); }, + getRuleSnoozeModal: (props) => { + return getRuleSnoozeModalLazy(props); + }, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 10c5e5637f159..1374f10355e16 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -78,6 +78,8 @@ import { getRuleDefinitionLazy } from './common/get_rule_definition'; import { RuleStatusPanelProps } from './application/sections/rule_details/components/rule_status_panel'; import { RuleAlertsSummaryProps } from './application/sections/rule_details/components/alert_summary'; import { getRuleAlertsSummaryLazy } from './common/get_rule_alerts_summary'; +import { RuleSnoozeModalProps } from './application/sections/rules_list/components/rule_snooze_modal'; +import { getRuleSnoozeModalLazy } from './common/get_rule_snooze_modal'; export interface TriggersAndActionsUIPublicPluginSetup { actionTypeRegistry: TypeRegistry; @@ -123,6 +125,7 @@ export interface TriggersAndActionsUIPublicPluginStart { getRuleDefinition: (props: RuleDefinitionProps) => ReactElement; getRuleStatusPanel: (props: RuleStatusPanelProps) => ReactElement; getRuleAlertsSummary: (props: RuleAlertsSummaryProps) => ReactElement; + getRuleSnoozeModal: (props: RuleSnoozeModalProps) => ReactElement; } interface PluginsSetup { @@ -352,6 +355,9 @@ export class Plugin getRuleAlertsSummary: (props: RuleAlertsSummaryProps) => { return getRuleAlertsSummaryLazy(props); }, + getRuleSnoozeModal: (props: RuleSnoozeModalProps) => { + return getRuleSnoozeModalLazy(props); + }, }; }