From f3f36bb0fbbc21248526611aac81854291821c6e Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Thu, 8 Sep 2022 17:10:22 +0200 Subject: [PATCH 1/7] Add Alert Details page header --- .../observability/public/config/paths.ts | 5 +- .../components/header_actions.tsx | 82 ++++++++++ .../pages/alert_details/components/index.ts | 1 + .../components/page_title.stories.tsx | 44 ++++++ .../alert_details/components/page_title.tsx | 38 +++++ .../public/pages/alert_details/config.ts | 35 +++++ .../public/pages/alert_details/index.test.tsx | 98 ++++++++++++ .../public/pages/alert_details/index.tsx | 68 +++++++++ .../pages/alert_details/translations.ts | 143 ++++++++++++++++++ .../public/pages/alert_details/types.ts | 12 ++ 10 files changed, 525 insertions(+), 1 deletion(-) 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.tsx create mode 100644 x-pack/plugins/observability/public/pages/alert_details/config.ts create mode 100644 x-pack/plugins/observability/public/pages/alert_details/index.test.tsx create mode 100644 x-pack/plugins/observability/public/pages/alert_details/index.tsx create mode 100644 x-pack/plugins/observability/public/pages/alert_details/translations.ts diff --git a/x-pack/plugins/observability/public/config/paths.ts b/x-pack/plugins/observability/public/config/paths.ts index 41a019ca62602..26d39d1b87c05 100644 --- a/x-pack/plugins/observability/public/config/paths.ts +++ b/x-pack/plugins/observability/public/config/paths.ts @@ -5,8 +5,11 @@ * 2.0. */ -export const ALERT_PAGE_LINK = '/app/observability/alerts'; +const APP_BASE = '/app/observability'; +export const ALERT_PAGE_LINK = `${APP_BASE}/alerts`; export const RULES_PAGE_LINK = `${ALERT_PAGE_LINK}/rules`; +export const CASES_PAGE_LINK = `${APP_BASE}/cases`; +export const CASES_CREATE_NEW_PAGE_LINK = `${CASES_PAGE_LINK}/create`; export const paths = { observability: { 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..6a3bffecc3f64 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.tsx @@ -0,0 +1,82 @@ +/* + * 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 { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiPopover, EuiText } from '@elastic/eui'; +import { HeaderActionsProps } from '../types'; + +export function HeaderActions({ + onViewEditRuleConditions, + onAddToExistingCase, + onCreateNewCase, +}: HeaderActionsProps) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const togglePopover = () => setIsPopoverOpen(!isPopoverOpen); + + const handleClosePopover = () => setIsPopoverOpen(false); + + return ( + + {i18n.translate('xpack.observability.alertDetails.actionsButtonLabel', { + defaultMessage: 'Actions', + })} + + } + > + + + + {i18n.translate('xpack.observability.alertDetails.editRuleConditions', { + defaultMessage: 'View/Edit rule conditions', + })} + + + + + {i18n.translate('xpack.observability.alertDetails.addToExistingCase', { + defaultMessage: 'Add to existing case', + })} + + + + + {i18n.translate('xpack.observability.alertDetails.createNewCaseButton', { + defaultMessage: 'Create new case', + })} + + + + + ); +} 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..c2091fd6251a4 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,6 @@ * 2.0. */ +export { PageTitle } from './page_title'; export { AlertSummary } from './alert_summary'; export { AlertDetails } from './alert_details'; 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..f97de3f8e0cc5 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.stories.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 { EuiPageTemplate } from '@elastic/eui'; +import { Alert } from '../types'; + +import { PageTitle as Component } from './page_title'; + +export default { + component: Component, + title: 'app/AlertDetails/PageTitle', +}; + +const alert: Alert = { + alertId: 'alertId', + ruleId: 'ruleId', + name: 'Avg latency is 84% above the threshold', + updatedAt: '2022-09-06', + updatedBy: 'Elastic', + createdAt: '2022-09-06', + createdBy: 'Elastic', +}; + +export const PageTitle = () => { + return ; +}; + +export const PageTitleUsedInObservabilityPageTemplate = () => { + return ( + , + bottomBorder: false, + }} + > + <> + + ); +}; 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..672cddaa9aae4 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx @@ -0,0 +1,38 @@ +/* + * 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 moment from 'moment'; +import { EuiText, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { PageTitleProps } from '../types'; +import { LAST_UPDATED_MESSAGE, CREATED_WORD, BY_WORD, ON_WORD } from '../translations'; + +export function PageTitle({ alert }: PageTitleProps) { + return ( + <> + + + {alert.name} + + + + + + + + + {LAST_UPDATED_MESSAGE} {BY_WORD} {alert.updatedBy} {ON_WORD}  + {moment(alert.updatedAt).format('ll')}   + {CREATED_WORD} {BY_WORD} {alert.createdBy} {ON_WORD}  + {moment(alert.createdAt).format('ll')} + + + + + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/alert_details/config.ts b/x-pack/plugins/observability/public/pages/alert_details/config.ts new file mode 100644 index 0000000000000..410c893aba7a3 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/config.ts @@ -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 { RuleExecutionStatuses } from '@kbn/alerting-plugin/common'; +import { RuleType, Rule } from '@kbn/triggers-actions-ui-plugin/public'; + +export function getHealthColor(status: RuleExecutionStatuses) { + switch (status) { + case 'active': + return 'success'; + case 'error': + return 'danger'; + case 'ok': + return 'primary'; + case 'pending': + return 'accent'; + default: + return 'subdued'; + } +} + +type Capabilities = Record; + +export type InitialRule = Partial & + Pick; + +export function hasAllPrivilege(rule: InitialRule, ruleType?: RuleType): boolean { + return ruleType?.authorizedConsumers[rule.consumer]?.all ?? false; +} + +export const hasExecuteActionsCapability = (capabilities: Capabilities) => + capabilities?.actions?.execute; diff --git a/x-pack/plugins/observability/public/pages/alert_details/index.test.tsx b/x-pack/plugins/observability/public/pages/alert_details/index.test.tsx new file mode 100644 index 0000000000000..8022f0884c253 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/index.test.tsx @@ -0,0 +1,98 @@ +/* + * 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, fireEvent } from '@testing-library/react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { Router } from 'react-router-dom'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; + +import * as pluginContext from '../../hooks/use_plugin_context'; +import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock'; +import { AlertDetailsPage } from '.'; +import { kibanaStartMock } from '../../utils/kibana_react.mock'; +import { AppMountParameters, CoreStart } from '@kbn/core/public'; +import { createMemoryHistory } from 'history'; + +const mockUseKibanaReturnValue = kibanaStartMock.startContract(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ alertId: 'foo' }), +})); + +jest.mock('../../utils/kibana_react', () => ({ + __esModule: true, + useKibana: jest.fn(() => mockUseKibanaReturnValue), +})); + +jest.mock('../../hooks/use_breadcrumbs', () => ({ + useBreadcrumbs: jest.fn(), +})); + +jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ + appMountParameters: {} as AppMountParameters, + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), + ObservabilityPageTemplate: KibanaPageTemplate, + kibanaFeatures: [], + core: {} as CoreStart, +})); + +const history = createMemoryHistory({ initialEntries: ['/alerts'] }); + +describe('Alert Details Page', () => { + const renderComp = () => + render( + + + + + + ); + + it('Supports viewing / editing of rule conditions', async () => { + const { getByTestId } = renderComp(); + + const actionsButton = getByTestId('alert-details-actions-menu-button'); + + fireEvent.click(actionsButton); + + const editRuleConditionsButton = getByTestId('edit-rule-conditions-button'); + + fireEvent.click(editRuleConditionsButton); + + expect(history.location.pathname).toBe('/alerts/rules/foo'); + }); + + it('Supports adding alert to existing case', () => { + const { getByTestId } = renderComp(); + + const actionsButton = getByTestId('alert-details-actions-menu-button'); + + fireEvent.click(actionsButton); + + const addToExistingCaseButton = getByTestId('add-to-existing-case-button'); + + fireEvent.click(addToExistingCaseButton); + + expect(history.location.pathname).toBe('/cases'); + }); + + it('Supports adding alert to a new case', () => { + const { getByTestId } = renderComp(); + + const actionsButton = getByTestId('alert-details-actions-menu-button'); + + fireEvent.click(actionsButton); + + const editRuleConditionsButton = getByTestId('create-new-case-button'); + + fireEvent.click(editRuleConditionsButton); + + expect(history.location.pathname).toBe('/cases/create'); + }); +}); diff --git a/x-pack/plugins/observability/public/pages/alert_details/index.tsx b/x-pack/plugins/observability/public/pages/alert_details/index.tsx new file mode 100644 index 0000000000000..9dfdd99bdea4d --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/index.tsx @@ -0,0 +1,68 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { useParams } from 'react-router-dom'; +import { EuiFlexGroup, EuiFlexItem } 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 PageNotFound from '../404'; + +// import { useParams } from 'react-router'; +// import { AlertDetailsPathParams } from './types'; + +export function AlertDetailsPage() { + const { + http, + application: { navigateToUrl }, + } = useKibana().services; + + const { ObservabilityPageTemplate, config } = usePluginContext(); + // const { alertId } = useParams(); + const alert = {}; + + useBreadcrumbs([ + { + href: http.basePath.prepend(paths.observability.alerts), + text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { + defaultMessage: 'Alerts', + }), + }, + ]); + + // Redirect to the the 404 page when the user hit the page url directly in the browser while the feature flag is off. + if (!config.unsafe.alertDetails.enabled) { + return ; + } + + return ( + , + rightSideItems: [ + + + + + , + ], + bottomBorder: false, + }} + > + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/alert_details/translations.ts b/x-pack/plugins/observability/public/pages/alert_details/translations.ts new file mode 100644 index 0000000000000..e30178e15cf47 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/translations.ts @@ -0,0 +1,143 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const RULE_LOAD_ERROR = (errorMessage: string) => + i18n.translate('xpack.observability.ruleDetails.ruleLoadError', { + defaultMessage: 'Unable to load rule. Reason: {message}', + values: { message: errorMessage }, + }); + +export const EXECUTION_LOG_ERROR = (errorMessage: string) => + i18n.translate('xpack.observability.ruleDetails.executionLogError', { + defaultMessage: 'Unable to load rule execution log. Reason: {message}', + values: { message: errorMessage }, + }); + +export const TAGS_TITLE = i18n.translate('xpack.observability.ruleDetails.tagsTitle', { + defaultMessage: 'Tags', +}); + +export const LAST_UPDATED_MESSAGE = i18n.translate( + 'xpack.observability.ruleDetails.lastUpdatedMessage', + { + defaultMessage: 'Last updated', + } +); + +export const BY_WORD = i18n.translate('xpack.observability.ruleDetails.byWord', { + defaultMessage: 'by', +}); + +export const ON_WORD = i18n.translate('xpack.observability.ruleDetails.onWord', { + defaultMessage: 'on', +}); + +export const CREATED_WORD = i18n.translate('xpack.observability.ruleDetails.createdWord', { + defaultMessage: 'Created', +}); + +export const confirmModalText = ( + numIdsToDelete: number, + singleTitle: string, + multipleTitle: string +) => + i18n.translate('xpack.observability.rules.deleteSelectedIdsConfirmModal.descriptionText', { + defaultMessage: + "You can't recover {numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}.", + values: { numIdsToDelete, singleTitle, multipleTitle }, + }); + +export const confirmButtonText = ( + numIdsToDelete: number, + singleTitle: string, + multipleTitle: string +) => + i18n.translate('xpack.observability.rules.deleteSelectedIdsConfirmModal.deleteButtonLabel', { + defaultMessage: + 'Delete {numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}} ', + values: { numIdsToDelete, singleTitle, multipleTitle }, + }); + +export const cancelButtonText = i18n.translate( + 'xpack.observability.rules.deleteSelectedIdsConfirmModal.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } +); + +export const deleteSuccessText = ( + numSuccesses: number, + singleTitle: string, + multipleTitle: string +) => + i18n.translate('xpack.observability.rules.deleteSelectedIdsSuccessNotification.descriptionText', { + defaultMessage: + 'Deleted {numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}', + values: { numSuccesses, singleTitle, multipleTitle }, + }); + +export const deleteErrorText = (numErrors: number, singleTitle: string, multipleTitle: string) => + i18n.translate('xpack.observability.rules.deleteSelectedIdsErrorNotification.descriptionText', { + defaultMessage: + 'Failed to delete {numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}', + values: { numErrors, singleTitle, multipleTitle }, + }); +export const ALERT_STATUS_LICENSE_ERROR = i18n.translate( + 'xpack.observability.ruleDetails.ruleStatusLicenseError', + { + defaultMessage: 'License Error', + } +); + +export const ALERT_STATUS_OK = i18n.translate('xpack.observability.ruleDetails.ruleStatusOk', { + defaultMessage: 'Ok', +}); + +export const ALERT_STATUS_ACTIVE = i18n.translate( + 'xpack.observability.ruleDetails.ruleStatusActive', + { + defaultMessage: 'Active', + } +); + +export const ALERT_STATUS_ERROR = i18n.translate( + 'xpack.observability.ruleDetails.ruleStatusError', + { + defaultMessage: 'Error', + } +); + +export const ALERT_STATUS_PENDING = i18n.translate( + 'xpack.observability.ruleDetails.ruleStatusPending', + { + defaultMessage: 'Pending', + } +); + +export const ALERT_STATUS_UNKNOWN = i18n.translate( + 'xpack.observability.ruleDetails.ruleStatusUnknown', + { + defaultMessage: 'Unknown', + } +); + +export const ALERT_STATUS_WARNING = i18n.translate( + 'xpack.observability.ruleDetails.ruleStatusWarning', + { + defaultMessage: 'Warning', + } +); + +export const rulesStatusesTranslationsMapping = { + ok: ALERT_STATUS_OK, + active: ALERT_STATUS_ACTIVE, + error: ALERT_STATUS_ERROR, + pending: ALERT_STATUS_PENDING, + unknown: ALERT_STATUS_UNKNOWN, + warning: ALERT_STATUS_WARNING, +}; diff --git a/x-pack/plugins/observability/public/pages/alert_details/types.ts b/x-pack/plugins/observability/public/pages/alert_details/types.ts index d05e0f4c73ff6..3670d651c10be 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/types.ts +++ b/x-pack/plugins/observability/public/pages/alert_details/types.ts @@ -15,4 +15,16 @@ export interface AlertDetailsPathParams { alertId: string; } +export interface AlertSummaryItemProps { + formattedMessageId: string; + defaultMessage: string; + children: JSX.Element; +} + +export interface HeaderActionsProps { + onViewEditRuleConditions: () => void; + onAddToExistingCase: () => void; + onCreateNewCase: () => void; +} + export const ALERT_DETAILS_PAGE_ID = 'alert-details-o11y'; From 0e5df4793dadeaf07905e82708412650943a0d9c Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Mon, 12 Sep 2022 11:08:28 +0200 Subject: [PATCH 2/7] Added mocked feature flag for test --- .../observability/public/pages/alert_details/index.test.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/observability/public/pages/alert_details/index.test.tsx b/x-pack/plugins/observability/public/pages/alert_details/index.test.tsx index 8022f0884c253..f5a1f60a21560 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/index.test.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/index.test.tsx @@ -17,6 +17,7 @@ import { AlertDetailsPage } from '.'; import { kibanaStartMock } from '../../utils/kibana_react.mock'; import { AppMountParameters, CoreStart } from '@kbn/core/public'; import { createMemoryHistory } from 'history'; +import { ConfigSchema } from '../../plugin'; const mockUseKibanaReturnValue = kibanaStartMock.startContract(); @@ -40,6 +41,11 @@ jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ ObservabilityPageTemplate: KibanaPageTemplate, kibanaFeatures: [], core: {} as CoreStart, + config: { + unsafe: { + alertDetails: { enabled: true }, + }, + } as ConfigSchema, })); const history = createMemoryHistory({ initialEntries: ['/alerts'] }); From 049a2a02f635d63a623d59e48351c20cf8e31238 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Mon, 12 Sep 2022 15:10:40 +0200 Subject: [PATCH 3/7] Add tags --- .../components/page_title.stories.tsx | 10 +++------- .../alert_details/components/page_title.tsx | 20 ++++++++++++++++--- 2 files changed, 20 insertions(+), 10 deletions(-) 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 index f97de3f8e0cc5..195e50ca2a3d7 100644 --- 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 @@ -24,6 +24,7 @@ const alert: Alert = { updatedBy: 'Elastic', createdAt: '2022-09-06', createdBy: 'Elastic', + tags: ['kibana', 'region:na', 'kibana'], }; export const PageTitle = () => { @@ -32,13 +33,8 @@ export const PageTitle = () => { export const PageTitleUsedInObservabilityPageTemplate = () => { return ( - , - bottomBorder: false, - }} - > - <> + + } bottomBorder={false} /> ); }; 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 index 672cddaa9aae4..87ceed6b0fe77 100644 --- 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 @@ -6,7 +6,7 @@ */ import React from 'react'; import moment from 'moment'; -import { EuiText, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { EuiText, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiBetaBadge } from '@elastic/eui'; import { PageTitleProps } from '../types'; import { LAST_UPDATED_MESSAGE, CREATED_WORD, BY_WORD, ON_WORD } from '../translations'; @@ -18,9 +18,23 @@ export function PageTitle({ alert }: PageTitleProps) { {alert.name} - + + {alert.tags ? ( + <> + + + {alert.tags.map((tag) => ( + + + + ))} + + + + ) : ( - + )} + From 9b79aa1964bb804374ef99a989b3081c55ac264e Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Mon, 12 Sep 2022 15:30:34 +0200 Subject: [PATCH 4/7] Update Action popover actions --- .../components/header_actions.tsx | 26 ++--- .../public/pages/alert_details/index.test.tsx | 104 ------------------ .../public/pages/alert_details/index.tsx | 6 +- .../public/pages/alert_details/types.ts | 2 +- 4 files changed, 19 insertions(+), 119 deletions(-) delete mode 100644 x-pack/plugins/observability/public/pages/alert_details/index.test.tsx 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 index 6a3bffecc3f64..f38f3a313f7f7 100644 --- 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 @@ -11,9 +11,9 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiPopover, EuiText } from '@e import { HeaderActionsProps } from '../types'; export function HeaderActions({ - onViewEditRuleConditions, onAddToExistingCase, onCreateNewCase, + onSnoozeRule, }: HeaderActionsProps) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -43,36 +43,36 @@ export function HeaderActions({ - {i18n.translate('xpack.observability.alertDetails.editRuleConditions', { - defaultMessage: 'View/Edit rule conditions', + {i18n.translate('xpack.observability.alertDetails.addToExistingCase', { + defaultMessage: 'Add to existing case', })} - {i18n.translate('xpack.observability.alertDetails.addToExistingCase', { - defaultMessage: 'Add to existing case', + {i18n.translate('xpack.observability.alertDetails.createNewCaseButton', { + defaultMessage: 'Add to new case', })} - {i18n.translate('xpack.observability.alertDetails.createNewCaseButton', { - defaultMessage: 'Create new case', + {i18n.translate('xpack.observability.alertDetails.editSnoozeRule', { + defaultMessage: 'Edit or snooze the rule', })} diff --git a/x-pack/plugins/observability/public/pages/alert_details/index.test.tsx b/x-pack/plugins/observability/public/pages/alert_details/index.test.tsx deleted file mode 100644 index f5a1f60a21560..0000000000000 --- a/x-pack/plugins/observability/public/pages/alert_details/index.test.tsx +++ /dev/null @@ -1,104 +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 React from 'react'; -import { render, fireEvent } from '@testing-library/react'; -import { I18nProvider } from '@kbn/i18n-react'; -import { Router } from 'react-router-dom'; -import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; - -import * as pluginContext from '../../hooks/use_plugin_context'; -import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock'; -import { AlertDetailsPage } from '.'; -import { kibanaStartMock } from '../../utils/kibana_react.mock'; -import { AppMountParameters, CoreStart } from '@kbn/core/public'; -import { createMemoryHistory } from 'history'; -import { ConfigSchema } from '../../plugin'; - -const mockUseKibanaReturnValue = kibanaStartMock.startContract(); - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ alertId: 'foo' }), -})); - -jest.mock('../../utils/kibana_react', () => ({ - __esModule: true, - useKibana: jest.fn(() => mockUseKibanaReturnValue), -})); - -jest.mock('../../hooks/use_breadcrumbs', () => ({ - useBreadcrumbs: jest.fn(), -})); - -jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ - appMountParameters: {} as AppMountParameters, - observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), - ObservabilityPageTemplate: KibanaPageTemplate, - kibanaFeatures: [], - core: {} as CoreStart, - config: { - unsafe: { - alertDetails: { enabled: true }, - }, - } as ConfigSchema, -})); - -const history = createMemoryHistory({ initialEntries: ['/alerts'] }); - -describe('Alert Details Page', () => { - const renderComp = () => - render( - - - - - - ); - - it('Supports viewing / editing of rule conditions', async () => { - const { getByTestId } = renderComp(); - - const actionsButton = getByTestId('alert-details-actions-menu-button'); - - fireEvent.click(actionsButton); - - const editRuleConditionsButton = getByTestId('edit-rule-conditions-button'); - - fireEvent.click(editRuleConditionsButton); - - expect(history.location.pathname).toBe('/alerts/rules/foo'); - }); - - it('Supports adding alert to existing case', () => { - const { getByTestId } = renderComp(); - - const actionsButton = getByTestId('alert-details-actions-menu-button'); - - fireEvent.click(actionsButton); - - const addToExistingCaseButton = getByTestId('add-to-existing-case-button'); - - fireEvent.click(addToExistingCaseButton); - - expect(history.location.pathname).toBe('/cases'); - }); - - it('Supports adding alert to a new case', () => { - const { getByTestId } = renderComp(); - - const actionsButton = getByTestId('alert-details-actions-menu-button'); - - fireEvent.click(actionsButton); - - const editRuleConditionsButton = getByTestId('create-new-case-button'); - - fireEvent.click(editRuleConditionsButton); - - expect(history.location.pathname).toBe('/cases/create'); - }); -}); diff --git a/x-pack/plugins/observability/public/pages/alert_details/index.tsx b/x-pack/plugins/observability/public/pages/alert_details/index.tsx index 9dfdd99bdea4d..d95a0d829572a 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/index.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/index.tsx @@ -29,6 +29,10 @@ export function AlertDetailsPage() { // const { alertId } = useParams(); const alert = {}; + const handleSnoozeRule = () => { + navigateToUrl(http.basePath.prepend(paths.observability.ruleDetails(alert.ruleId))); + }; + useBreadcrumbs([ { href: http.basePath.prepend(paths.observability.alerts), @@ -52,9 +56,9 @@ export function AlertDetailsPage() { , diff --git a/x-pack/plugins/observability/public/pages/alert_details/types.ts b/x-pack/plugins/observability/public/pages/alert_details/types.ts index 3670d651c10be..21deae5afa7ad 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/types.ts +++ b/x-pack/plugins/observability/public/pages/alert_details/types.ts @@ -22,9 +22,9 @@ export interface AlertSummaryItemProps { } export interface HeaderActionsProps { - onViewEditRuleConditions: () => void; onAddToExistingCase: () => void; onCreateNewCase: () => void; + onSnoozeRule: () => void; } export const ALERT_DETAILS_PAGE_ID = 'alert-details-o11y'; From 5fdd1001ce53de08cee3680e71df97176d5fef70 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Mon, 12 Sep 2022 17:46:23 +0200 Subject: [PATCH 5/7] Add tests --- .../components/page_title.test.tsx | 55 +++++++++++++++++++ .../alert_details/components/page_title.tsx | 2 +- .../public/pages/alert_details/index.tsx | 9 ++- 3 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx 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..10c9908ac6395 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx @@ -0,0 +1,55 @@ +/* + * 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 } from './page_title'; +import { Alert } from '../types'; + +describe('page title', () => { + const defaultName = 'Avg latency is 84% above the threshold'; + const defaultTags = ['tag-1', 'tag-2', 'tag-3']; + const defaultAlert: Alert = { + alertId: 'alertId', + ruleId: 'ruleId', + name: defaultName, + updatedAt: '2022-09-06', + updatedBy: 'Elastic', + createdAt: '2022-09-06', + createdBy: 'Elastic', + tags: defaultTags, + }; + + const renderComp = (alert: Alert) => { + return render(); + }; + + it('should display the page title', () => { + const { queryByText, rerender } = renderComp(defaultAlert); + expect(queryByText(defaultName)).toBeTruthy(); + + rerender(); + + expect(queryByText(defaultName)).not.toBeTruthy(); + }); + + it('should display tags as badges when passed', () => { + const { queryAllByText, rerender } = renderComp(defaultAlert); + + expect(queryAllByText(/tag-./)).toHaveLength(defaultTags.length); + + rerender(); + + expect(queryAllByText(/tag-./)).toHaveLength(0); + }); + + it('should display created by and last updated by when passed', () => { + const { queryByTestId } = renderComp(defaultAlert); + expect(queryByTestId('lastUpdatedCreatedBy')).toBeTruthy(); + }); +}); 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 index 87ceed6b0fe77..627a335c9d02c 100644 --- 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 @@ -37,7 +37,7 @@ export function PageTitle({ alert }: PageTitleProps) { - + {LAST_UPDATED_MESSAGE} {BY_WORD} {alert.updatedBy} {ON_WORD}  {moment(alert.updatedAt).format('ll')}   {CREATED_WORD} {BY_WORD} {alert.createdBy} {ON_WORD}  diff --git a/x-pack/plugins/observability/public/pages/alert_details/index.tsx b/x-pack/plugins/observability/public/pages/alert_details/index.tsx index d95a0d829572a..169fda66edd73 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/index.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/index.tsx @@ -8,7 +8,9 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { useParams } from 'react-router-dom'; +import { noop } from 'lodash'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + import { useKibana } from '../../utils/kibana_react'; import { ObservabilityAppServices } from '../../application/types'; import { usePluginContext } from '../../hooks/use_plugin_context'; @@ -20,17 +22,14 @@ import PageNotFound from '../404'; // import { AlertDetailsPathParams } from './types'; export function AlertDetailsPage() { - const { - http, - application: { navigateToUrl }, - } = useKibana().services; + const { http } = useKibana().services; const { ObservabilityPageTemplate, config } = usePluginContext(); // const { alertId } = useParams(); const alert = {}; const handleSnoozeRule = () => { - navigateToUrl(http.basePath.prepend(paths.observability.ruleDetails(alert.ruleId))); + noop(); }; useBreadcrumbs([ From bfb6b58604940f24a5cccf363747a0ac80f3d6eb Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Mon, 26 Sep 2022 14:23:44 +0200 Subject: [PATCH 6/7] Create PageHeader comp again --- .../case_view/components/case_view_alerts.tsx | 1 + x-pack/plugins/cases/public/mocks.ts | 6 +- .../observability/public/config/paths.ts | 5 +- .../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 | 102 ++++++++ .../components/header_actions.tsx | 228 +++++++++++++----- .../pages/alert_details/components/index.ts | 3 +- .../components/page_title.stories.tsx | 44 ++-- .../components/page_title.test.tsx | 55 ++--- .../alert_details/components/page_title.tsx | 67 +++-- .../public/pages/alert_details/config.ts | 35 --- .../public/pages/alert_details/index.tsx | 71 ------ .../public/pages/alert_details/mock/alert.ts | 4 +- .../pages/alert_details/translations.ts | 143 ----------- .../public/pages/alert_details/types.ts | 12 - .../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 + 23 files changed, 570 insertions(+), 440 deletions(-) create mode 100644 x-pack/plugins/observability/public/pages/alert_details/components/header_actions.test.tsx delete mode 100644 x-pack/plugins/observability/public/pages/alert_details/config.ts delete mode 100644 x-pack/plugins/observability/public/pages/alert_details/index.tsx delete mode 100644 x-pack/plugins/observability/public/pages/alert_details/translations.ts 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/config/paths.ts b/x-pack/plugins/observability/public/config/paths.ts index 26d39d1b87c05..41a019ca62602 100644 --- a/x-pack/plugins/observability/public/config/paths.ts +++ b/x-pack/plugins/observability/public/config/paths.ts @@ -5,11 +5,8 @@ * 2.0. */ -const APP_BASE = '/app/observability'; -export const ALERT_PAGE_LINK = `${APP_BASE}/alerts`; +export const ALERT_PAGE_LINK = '/app/observability/alerts'; export const RULES_PAGE_LINK = `${ALERT_PAGE_LINK}/rules`; -export const CASES_PAGE_LINK = `${APP_BASE}/cases`; -export const CASES_CREATE_NEW_PAGE_LINK = `${CASES_PAGE_LINK}/create`; export const paths = { observability: { 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..3884447cd8933 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.test.tsx @@ -0,0 +1,102 @@ +/* + * 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(); + }); + + const mockViewInExternalApp = jest.fn(); + + 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', + }, + ], + }); + }); + }); + + it('should show a connector action if passed an externalConnector prop', async () => { + const { findByRole, getByTestId } = render( + + ); + + fireEvent.click(await findByRole('button', { name: 'Actions' })); + const button = getByTestId('view-in-external-app-button'); + expect(button).toBeTruthy(); + + fireEvent.click(button); + + expect(mockViewInExternalApp).toBeCalledTimes(1); + }); +}); 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 index f38f3a313f7f7..7b851dc09bbba 100644 --- 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 @@ -7,76 +7,180 @@ 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 { HeaderActionsProps } from '../types'; +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; + externalConnector?: { + name: string; + onViewInExternalApp: (alertId: string) => void; + }; +} + +export function HeaderActions({ alert, externalConnector }: HeaderActionsProps) { + const { + http, + cases: { + hooks: { getUseCasesAddToExistingCaseModal }, + }, + triggersActionsUi: { getEditAlertFlyout, getRuleSnoozeModal }, + } = useKibana().services; + + const { rule, reloadRule } = useFetchRule({ + http, + ruleId: alert?.fields[ALERT_RULE_UUID] || '', + }); -export function HeaderActions({ - onAddToExistingCase, - onCreateNewCase, - onSnoozeRule, -}: HeaderActionsProps) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [ruleConditionsFlyoutOpen, setRuleConditionsFlyoutOpen] = useState(false); + const [snoozeModalOpen, setSnoozeModalOpen] = useState(false); - const togglePopover = () => setIsPopoverOpen(!isPopoverOpen); + 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 handleViewRuleConditions = () => { + setIsPopoverOpen(false); + setRuleConditionsFlyoutOpen(true); + }; + + const handleOpenSnoozeModal = () => { + setIsPopoverOpen(false); + setSnoozeModalOpen(true); + }; + + const handleOpenInExternalApp = () => { + if (alert) { + externalConnector?.onViewInExternalApp(alert.fields[ALERT_UUID]); + } + }; + return ( - - {i18n.translate('xpack.observability.alertDetails.actionsButtonLabel', { - defaultMessage: 'Actions', - })} - - } - > - - - - {i18n.translate('xpack.observability.alertDetails.addToExistingCase', { - defaultMessage: 'Add to existing case', - })} - - - - - {i18n.translate('xpack.observability.alertDetails.createNewCaseButton', { - defaultMessage: 'Add to new case', + <> + + {i18n.translate('xpack.observability.alertDetails.actionsButtonLabel', { + defaultMessage: 'Actions', })} - - - - - {i18n.translate('xpack.observability.alertDetails.editSnoozeRule', { - defaultMessage: 'Edit or snooze the rule', - })} - - - - + + } + > + + + + {i18n.translate('xpack.observability.alertDetails.addToCase', { + defaultMessage: 'Add to case', + })} + + + + + + {i18n.translate('xpack.observability.alertDetails.viewRuleConditons', { + defaultMessage: 'View rule conditions', + })} + + + + + + {i18n.translate('xpack.observability.alertDetails.editSnoozeRule', { + defaultMessage: 'Snooze the rule', + })} + + + + {externalConnector ? ( + + + {i18n.translate('xpack.observability.alertDetails.viewInExternalApp', { + defaultMessage: 'View in {name}', + values: { name: externalConnector.name }, + })} + + + ) : null} + + + + {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 c2091fd6251a4..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,6 +5,7 @@ * 2.0. */ -export { PageTitle } from './page_title'; +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 index 195e50ca2a3d7..ab0109ef1c214 100644 --- 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 @@ -6,35 +6,37 @@ */ import React from 'react'; +import { ComponentStory } from '@storybook/react'; import { EuiPageTemplate } from '@elastic/eui'; -import { Alert } from '../types'; -import { PageTitle as Component } from './page_title'; +import { PageTitle as Component, PageTitleProps } from './page_title'; export default { component: Component, title: 'app/AlertDetails/PageTitle', + argTypes: { + title: { control: 'text' }, + active: { control: 'boolean' }, + }, }; -const alert: Alert = { - alertId: 'alertId', - ruleId: 'ruleId', - name: 'Avg latency is 84% above the threshold', - updatedAt: '2022-09-06', - updatedBy: 'Elastic', - createdAt: '2022-09-06', - createdBy: 'Elastic', - tags: ['kibana', 'region:na', 'kibana'], -}; +const Template: ComponentStory = (props: PageTitleProps) => ( + +); -export const PageTitle = () => { - return ; -}; +const TemplateWithPageTemplate: ComponentStory = (props: PageTitleProps) => ( + + } bottomBorder={false} /> + +); -export const PageTitleUsedInObservabilityPageTemplate = () => { - return ( - - } 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 index 10c9908ac6395..e669e57cd6021 100644 --- 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 @@ -8,48 +8,37 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { PageTitle } from './page_title'; -import { Alert } from '../types'; - -describe('page title', () => { - const defaultName = 'Avg latency is 84% above the threshold'; - const defaultTags = ['tag-1', 'tag-2', 'tag-3']; - const defaultAlert: Alert = { - alertId: 'alertId', - ruleId: 'ruleId', - name: defaultName, - updatedAt: '2022-09-06', - updatedBy: 'Elastic', - createdAt: '2022-09-06', - createdBy: 'Elastic', - tags: defaultTags, - }; +import { PageTitle, PageTitleProps } from './page_title'; - const renderComp = (alert: Alert) => { - return render(); +describe('Page Title', () => { + const defaultProps = { + title: 'Great success', + active: true, }; - it('should display the page title', () => { - const { queryByText, rerender } = renderComp(defaultAlert); - expect(queryByText(defaultName)).toBeTruthy(); - - rerender(); + const renderComp = (props: PageTitleProps) => { + return render(); + }; - expect(queryByText(defaultName)).not.toBeTruthy(); + it('should display a title when it is passed', () => { + const { getByText } = renderComp(defaultProps); + expect(getByText('Great success')).toBeTruthy(); }); - it('should display tags as badges when passed', () => { - const { queryAllByText, rerender } = renderComp(defaultAlert); - - expect(queryAllByText(/tag-./)).toHaveLength(defaultTags.length); + it('should display an active badge when active is true', async () => { + const { getByText } = renderComp(defaultProps); + expect(getByText('Active')).toBeTruthy(); + }); - rerender(); + it('should display an inactive badge when active is false', async () => { + const { getByText } = renderComp({ ...defaultProps, active: false }); - expect(queryAllByText(/tag-./)).toHaveLength(0); + expect(getByText('Recovered')).toBeTruthy(); }); - it('should display created by and last updated by when passed', () => { - const { queryByTestId } = renderComp(defaultAlert); - expect(queryByTestId('lastUpdatedCreatedBy')).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 index 627a335c9d02c..61301f16e37a9 100644 --- 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 @@ -4,49 +4,40 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import React from 'react'; -import moment from 'moment'; -import { EuiText, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiBetaBadge } from '@elastic/eui'; -import { PageTitleProps } from '../types'; -import { LAST_UPDATED_MESSAGE, CREATED_WORD, BY_WORD, ON_WORD } from '../translations'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -export function PageTitle({ alert }: PageTitleProps) { - return ( - <> - - - {alert.name} - - +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', + }); - {alert.tags ? ( - <> - - - {alert.tags.map((tag) => ( - - - - ))} - - - - ) : ( - - )} + return ( +
+ {title} - - - - {LAST_UPDATED_MESSAGE} {BY_WORD} {alert.updatedBy} {ON_WORD}  - {moment(alert.updatedAt).format('ll')}   - {CREATED_WORD} {BY_WORD} {alert.createdBy} {ON_WORD}  - {moment(alert.createdAt).format('ll')} - + + +
+ {typeof active === 'boolean' ? ( + + {label} + + ) : null} +
-
- - +
); } diff --git a/x-pack/plugins/observability/public/pages/alert_details/config.ts b/x-pack/plugins/observability/public/pages/alert_details/config.ts deleted file mode 100644 index 410c893aba7a3..0000000000000 --- a/x-pack/plugins/observability/public/pages/alert_details/config.ts +++ /dev/null @@ -1,35 +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 { RuleExecutionStatuses } from '@kbn/alerting-plugin/common'; -import { RuleType, Rule } from '@kbn/triggers-actions-ui-plugin/public'; - -export function getHealthColor(status: RuleExecutionStatuses) { - switch (status) { - case 'active': - return 'success'; - case 'error': - return 'danger'; - case 'ok': - return 'primary'; - case 'pending': - return 'accent'; - default: - return 'subdued'; - } -} - -type Capabilities = Record; - -export type InitialRule = Partial & - Pick; - -export function hasAllPrivilege(rule: InitialRule, ruleType?: RuleType): boolean { - return ruleType?.authorizedConsumers[rule.consumer]?.all ?? false; -} - -export const hasExecuteActionsCapability = (capabilities: Capabilities) => - capabilities?.actions?.execute; diff --git a/x-pack/plugins/observability/public/pages/alert_details/index.tsx b/x-pack/plugins/observability/public/pages/alert_details/index.tsx deleted file mode 100644 index 169fda66edd73..0000000000000 --- a/x-pack/plugins/observability/public/pages/alert_details/index.tsx +++ /dev/null @@ -1,71 +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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { useParams } from 'react-router-dom'; -import { noop } from 'lodash'; -import { EuiFlexGroup, EuiFlexItem } 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 PageNotFound from '../404'; - -// import { useParams } from 'react-router'; -// import { AlertDetailsPathParams } from './types'; - -export function AlertDetailsPage() { - const { http } = useKibana().services; - - const { ObservabilityPageTemplate, config } = usePluginContext(); - // const { alertId } = useParams(); - const alert = {}; - - const handleSnoozeRule = () => { - noop(); - }; - - useBreadcrumbs([ - { - href: http.basePath.prepend(paths.observability.alerts), - text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { - defaultMessage: 'Alerts', - }), - }, - ]); - - // Redirect to the the 404 page when the user hit the page url directly in the browser while the feature flag is off. - if (!config.unsafe.alertDetails.enabled) { - return ; - } - - return ( - , - rightSideItems: [ - - - - - , - ], - bottomBorder: false, - }} - > - - - ); -} 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/observability/public/pages/alert_details/translations.ts b/x-pack/plugins/observability/public/pages/alert_details/translations.ts deleted file mode 100644 index e30178e15cf47..0000000000000 --- a/x-pack/plugins/observability/public/pages/alert_details/translations.ts +++ /dev/null @@ -1,143 +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 { i18n } from '@kbn/i18n'; - -export const RULE_LOAD_ERROR = (errorMessage: string) => - i18n.translate('xpack.observability.ruleDetails.ruleLoadError', { - defaultMessage: 'Unable to load rule. Reason: {message}', - values: { message: errorMessage }, - }); - -export const EXECUTION_LOG_ERROR = (errorMessage: string) => - i18n.translate('xpack.observability.ruleDetails.executionLogError', { - defaultMessage: 'Unable to load rule execution log. Reason: {message}', - values: { message: errorMessage }, - }); - -export const TAGS_TITLE = i18n.translate('xpack.observability.ruleDetails.tagsTitle', { - defaultMessage: 'Tags', -}); - -export const LAST_UPDATED_MESSAGE = i18n.translate( - 'xpack.observability.ruleDetails.lastUpdatedMessage', - { - defaultMessage: 'Last updated', - } -); - -export const BY_WORD = i18n.translate('xpack.observability.ruleDetails.byWord', { - defaultMessage: 'by', -}); - -export const ON_WORD = i18n.translate('xpack.observability.ruleDetails.onWord', { - defaultMessage: 'on', -}); - -export const CREATED_WORD = i18n.translate('xpack.observability.ruleDetails.createdWord', { - defaultMessage: 'Created', -}); - -export const confirmModalText = ( - numIdsToDelete: number, - singleTitle: string, - multipleTitle: string -) => - i18n.translate('xpack.observability.rules.deleteSelectedIdsConfirmModal.descriptionText', { - defaultMessage: - "You can't recover {numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}.", - values: { numIdsToDelete, singleTitle, multipleTitle }, - }); - -export const confirmButtonText = ( - numIdsToDelete: number, - singleTitle: string, - multipleTitle: string -) => - i18n.translate('xpack.observability.rules.deleteSelectedIdsConfirmModal.deleteButtonLabel', { - defaultMessage: - 'Delete {numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}} ', - values: { numIdsToDelete, singleTitle, multipleTitle }, - }); - -export const cancelButtonText = i18n.translate( - 'xpack.observability.rules.deleteSelectedIdsConfirmModal.cancelButtonLabel', - { - defaultMessage: 'Cancel', - } -); - -export const deleteSuccessText = ( - numSuccesses: number, - singleTitle: string, - multipleTitle: string -) => - i18n.translate('xpack.observability.rules.deleteSelectedIdsSuccessNotification.descriptionText', { - defaultMessage: - 'Deleted {numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}', - values: { numSuccesses, singleTitle, multipleTitle }, - }); - -export const deleteErrorText = (numErrors: number, singleTitle: string, multipleTitle: string) => - i18n.translate('xpack.observability.rules.deleteSelectedIdsErrorNotification.descriptionText', { - defaultMessage: - 'Failed to delete {numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}', - values: { numErrors, singleTitle, multipleTitle }, - }); -export const ALERT_STATUS_LICENSE_ERROR = i18n.translate( - 'xpack.observability.ruleDetails.ruleStatusLicenseError', - { - defaultMessage: 'License Error', - } -); - -export const ALERT_STATUS_OK = i18n.translate('xpack.observability.ruleDetails.ruleStatusOk', { - defaultMessage: 'Ok', -}); - -export const ALERT_STATUS_ACTIVE = i18n.translate( - 'xpack.observability.ruleDetails.ruleStatusActive', - { - defaultMessage: 'Active', - } -); - -export const ALERT_STATUS_ERROR = i18n.translate( - 'xpack.observability.ruleDetails.ruleStatusError', - { - defaultMessage: 'Error', - } -); - -export const ALERT_STATUS_PENDING = i18n.translate( - 'xpack.observability.ruleDetails.ruleStatusPending', - { - defaultMessage: 'Pending', - } -); - -export const ALERT_STATUS_UNKNOWN = i18n.translate( - 'xpack.observability.ruleDetails.ruleStatusUnknown', - { - defaultMessage: 'Unknown', - } -); - -export const ALERT_STATUS_WARNING = i18n.translate( - 'xpack.observability.ruleDetails.ruleStatusWarning', - { - defaultMessage: 'Warning', - } -); - -export const rulesStatusesTranslationsMapping = { - ok: ALERT_STATUS_OK, - active: ALERT_STATUS_ACTIVE, - error: ALERT_STATUS_ERROR, - pending: ALERT_STATUS_PENDING, - unknown: ALERT_STATUS_UNKNOWN, - warning: ALERT_STATUS_WARNING, -}; diff --git a/x-pack/plugins/observability/public/pages/alert_details/types.ts b/x-pack/plugins/observability/public/pages/alert_details/types.ts index 21deae5afa7ad..d05e0f4c73ff6 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/types.ts +++ b/x-pack/plugins/observability/public/pages/alert_details/types.ts @@ -15,16 +15,4 @@ export interface AlertDetailsPathParams { alertId: string; } -export interface AlertSummaryItemProps { - formattedMessageId: string; - defaultMessage: string; - children: JSX.Element; -} - -export interface HeaderActionsProps { - onAddToExistingCase: () => void; - onCreateNewCase: () => void; - onSnoozeRule: () => void; -} - export const ALERT_DETAILS_PAGE_ID = 'alert-details-o11y'; 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); + }, }; } From 7d2bd7d6ad395fd364024e49b8755fb52b539d7f Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Fri, 30 Sep 2022 15:30:35 +0200 Subject: [PATCH 7/7] Adjust order and naming of header action buttons --- .../components/header_actions.test.tsx | 19 ------- .../components/header_actions.tsx | 56 +++++-------------- .../components/page_title.test.tsx | 2 +- 3 files changed, 16 insertions(+), 61 deletions(-) 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 index 3884447cd8933..8bbec59c52b95 100644 --- 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 @@ -52,8 +52,6 @@ describe('Header Actions', () => { mockKibana(); }); - const mockViewInExternalApp = jest.fn(); - it('should display an actions button', () => { const { queryByTestId } = render(); expect(queryByTestId('alert-details-header-actions-menu-button')).toBeTruthy(); @@ -82,21 +80,4 @@ describe('Header Actions', () => { }); }); }); - - it('should show a connector action if passed an externalConnector prop', async () => { - const { findByRole, getByTestId } = render( - - ); - - fireEvent.click(await findByRole('button', { name: 'Actions' })); - const button = getByTestId('view-in-external-app-button'); - expect(button).toBeTruthy(); - - fireEvent.click(button); - - expect(mockViewInExternalApp).toBeCalledTimes(1); - }); }); 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 index 7b851dc09bbba..e7a2c773dc68f 100644 --- 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 @@ -20,13 +20,9 @@ import { TopAlert } from '../../alerts'; export interface HeaderActionsProps { alert: TopAlert | null; - externalConnector?: { - name: string; - onViewInExternalApp: (alertId: string) => void; - }; } -export function HeaderActions({ alert, externalConnector }: HeaderActionsProps) { +export function HeaderActions({ alert }: HeaderActionsProps) { const { http, cases: { @@ -69,7 +65,7 @@ export function HeaderActions({ alert, externalConnector }: HeaderActionsProps) selectCaseModal.open({ attachments }); }; - const handleViewRuleConditions = () => { + const handleViewRuleDetails = () => { setIsPopoverOpen(false); setRuleConditionsFlyoutOpen(true); }; @@ -79,12 +75,6 @@ export function HeaderActions({ alert, externalConnector }: HeaderActionsProps) setSnoozeModalOpen(true); }; - const handleOpenInExternalApp = () => { - if (alert) { - externalConnector?.onViewInExternalApp(alert.fields[ALERT_UUID]); - } - }; - return ( <> - {i18n.translate('xpack.observability.alertDetails.addToCase', { - defaultMessage: 'Add to case', + {i18n.translate('xpack.observability.alertDetails.viewRuleDetails', { + defaultMessage: 'View rule details', })} @@ -121,13 +112,12 @@ export function HeaderActions({ alert, externalConnector }: HeaderActionsProps) - {i18n.translate('xpack.observability.alertDetails.viewRuleConditons', { - defaultMessage: 'View rule conditions', + {i18n.translate('xpack.observability.alertDetails.editSnoozeRule', { + defaultMessage: 'Snooze the rule', })} @@ -135,31 +125,15 @@ export function HeaderActions({ alert, externalConnector }: HeaderActionsProps) - {i18n.translate('xpack.observability.alertDetails.editSnoozeRule', { - defaultMessage: 'Snooze the rule', + {i18n.translate('xpack.observability.alertDetails.addToCase', { + defaultMessage: 'Add to case', })} - - {externalConnector ? ( - - - {i18n.translate('xpack.observability.alertDetails.viewInExternalApp', { - defaultMessage: 'View in {name}', - values: { name: externalConnector.name }, - })} - - - ) : null}
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 index e669e57cd6021..bd0b15e8ffc2a 100644 --- 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 @@ -22,7 +22,7 @@ describe('Page Title', () => { it('should display a title when it is passed', () => { const { getByText } = renderComp(defaultProps); - expect(getByText('Great success')).toBeTruthy(); + expect(getByText(defaultProps.title)).toBeTruthy(); }); it('should display an active badge when active is true', async () => {