From a30d225421176495bbb8137a6a05e143a0f70741 Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Wed, 7 Dec 2022 17:18:02 +0100 Subject: [PATCH] [Actionable Observability] Add rule details locator and make AlertSummaryWidget clickable (#147103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #141467 ## ๐Ÿ“ Summary - Added a [locator](https://docs.elastic.dev/kibana-dev-docs/routing-and-navigation#specifying-state) for the rule details page - Made AlertSummaryWidget clickable and implemented the related navigation for rule details page ## ๐Ÿงช How to test - Create a rule and go to the rule details page - You should be able to click on all/active/recovered sections in Alert Summary Widget and upon click going to alert tables with the correct filter https://user-images.githubusercontent.com/12370520/205959565-6c383910-763f-4214-9baa-cf191f012de9.mp4 --- x-pack/plugins/observability/common/index.ts | 1 + x-pack/plugins/observability/kibana.json | 3 +- .../observability/public/application/types.ts | 2 + .../public/locators/rule_details.test.ts | 49 +++++++++ .../public/locators/rule_details.ts | 72 +++++++++++++ .../public/pages/rule_details/constants.ts | 2 +- .../public/pages/rule_details/index.tsx | 39 +++++-- x-pack/plugins/observability/public/plugin.ts | 5 + .../alert_summary_widget_ui.stories.tsx | 2 + .../components/alert_summary_widget_ui.tsx | 102 ++++++++++++------ .../alert_summary/components/types.ts | 3 + .../rule_alerts_summary.test.tsx | 1 + .../alert_summary/rule_alerts_summary.tsx | 3 +- .../components/alert_summary/types.ts | 2 + 14 files changed, 244 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/observability/public/locators/rule_details.test.ts create mode 100644 x-pack/plugins/observability/public/locators/rule_details.ts diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index 5ed8c1934a4fa..14da44c470399 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -54,6 +54,7 @@ export const casesPath = '/cases'; export const uptimeOverviewLocatorID = 'UPTIME_OVERVIEW_LOCATOR'; export const syntheticsMonitorDetailLocatorID = 'SYNTHETICS_MONITOR_DETAIL_LOCATOR'; export const syntheticsEditMonitorLocatorID = 'SYNTHETICS_EDIT_MONITOR_LOCATOR'; +export const ruleDetailsLocatorID = 'RULE_DETAILS_LOCATOR'; export { NETWORK_TIMINGS_FIELDS, diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 43e69c1b4d952..fd131fb10add7 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -31,7 +31,8 @@ "inspector", "unifiedSearch", "security", - "guidedOnboarding" + "guidedOnboarding", + "share" ], "ui": true, "server": true, diff --git a/x-pack/plugins/observability/public/application/types.ts b/x-pack/plugins/observability/public/application/types.ts index e0d12548a2822..1bf29be34430d 100644 --- a/x-pack/plugins/observability/public/application/types.ts +++ b/x-pack/plugins/observability/public/application/types.ts @@ -22,6 +22,7 @@ import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { LensPublicStart } from '@kbn/lens-plugin/public'; +import { SharePluginStart } from '@kbn/share-plugin/public'; import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; import { CasesUiStart } from '@kbn/cases-plugin/public'; import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; @@ -39,6 +40,7 @@ export interface ObservabilityAppServices { notifications: NotificationsStart; overlays: OverlayStart; savedObjectsClient: SavedObjectsStart['client']; + share: SharePluginStart; stateTransfer: EmbeddableStateTransfer; storage: IStorageWrapper; theme: ThemeServiceStart; diff --git a/x-pack/plugins/observability/public/locators/rule_details.test.ts b/x-pack/plugins/observability/public/locators/rule_details.test.ts new file mode 100644 index 0000000000000..f63354d29d90a --- /dev/null +++ b/x-pack/plugins/observability/public/locators/rule_details.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { ACTIVE_ALERTS } from '../components/shared/alert_search_bar/constants'; +import { EXECUTION_TAB, ALERTS_TAB } from '../pages/rule_details/constants'; +import { getRuleDetailsPath, RuleDetailsLocatorDefinition } from './rule_details'; + +describe('RuleDetailsLocator', () => { + const locator = new RuleDetailsLocatorDefinition(); + const mockedRuleId = '389d3318-7e10-4996-bb45-128e1607fb7e'; + + it('should return correct url when only ruleId is provided', async () => { + const location = await locator.getLocation({ ruleId: mockedRuleId }); + expect(location.app).toEqual('observability'); + expect(location.path).toEqual(getRuleDetailsPath(mockedRuleId)); + }); + + it('should return correct url when tabId is execution', async () => { + const location = await locator.getLocation({ ruleId: mockedRuleId, tabId: EXECUTION_TAB }); + expect(location.path).toMatchInlineSnapshot( + `"/alerts/rules/389d3318-7e10-4996-bb45-128e1607fb7e?tabId=execution"` + ); + }); + + it('should return correct url when tabId is alerts without extra search params', async () => { + const location = await locator.getLocation({ ruleId: mockedRuleId, tabId: ALERTS_TAB }); + expect(location.path).toMatchInlineSnapshot( + `"/alerts/rules/389d3318-7e10-4996-bb45-128e1607fb7e?tabId=alerts&searchBarParams=(kuery:'',rangeFrom:now-15m,rangeTo:now,status:all)"` + ); + }); + + it('should return correct url when tabId is alerts with search params', async () => { + const location = await locator.getLocation({ + ruleId: mockedRuleId, + tabId: ALERTS_TAB, + rangeFrom: 'mockedRangeTo', + rangeTo: 'mockedRangeFrom', + kuery: 'mockedKuery', + status: ACTIVE_ALERTS.status, + }); + expect(location.path).toMatchInlineSnapshot( + `"/alerts/rules/389d3318-7e10-4996-bb45-128e1607fb7e?tabId=alerts&searchBarParams=(kuery:mockedKuery,rangeFrom:mockedRangeTo,rangeTo:mockedRangeFrom,status:active)"` + ); + }); +}); diff --git a/x-pack/plugins/observability/public/locators/rule_details.ts b/x-pack/plugins/observability/public/locators/rule_details.ts new file mode 100644 index 0000000000000..13eaddb5b4707 --- /dev/null +++ b/x-pack/plugins/observability/public/locators/rule_details.ts @@ -0,0 +1,72 @@ +/* + * 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 { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; +import type { SerializableRecord } from '@kbn/utility-types'; +import type { LocatorDefinition } from '@kbn/share-plugin/public'; +import { ruleDetailsLocatorID } from '../../common'; +import { ALL_ALERTS } from '../components/shared/alert_search_bar/constants'; +import { + ALERTS_TAB, + EXECUTION_TAB, + SEARCH_BAR_URL_STORAGE_KEY, +} from '../pages/rule_details/constants'; +import type { TabId } from '../pages/rule_details/types'; +import type { AlertStatus } from '../../common/typings'; + +export interface RuleDetailsLocatorParams extends SerializableRecord { + ruleId: string; + tabId?: TabId; + rangeFrom?: string; + rangeTo?: string; + kuery?: string; + status?: AlertStatus; +} + +export const getRuleDetailsPath = (ruleId: string) => { + return `/alerts/rules/${encodeURI(ruleId)}`; +}; + +export class RuleDetailsLocatorDefinition implements LocatorDefinition { + public readonly id = ruleDetailsLocatorID; + + public readonly getLocation = async (params: RuleDetailsLocatorParams) => { + const { ruleId, kuery, rangeTo, tabId, rangeFrom, status } = params; + const appState: { + tabId?: TabId; + rangeFrom?: string; + rangeTo?: string; + kuery?: string; + status?: AlertStatus; + } = {}; + + appState.rangeFrom = rangeFrom || 'now-15m'; + appState.rangeTo = rangeTo || 'now'; + appState.kuery = kuery || ''; + appState.status = status || ALL_ALERTS.status; + + let path = getRuleDetailsPath(ruleId); + + if (tabId === ALERTS_TAB) { + path = `${path}?tabId=${tabId}`; + path = setStateToKbnUrl( + SEARCH_BAR_URL_STORAGE_KEY, + appState, + { useHash: false, storeInHashQuery: false }, + path + ); + } else if (tabId === EXECUTION_TAB) { + path = `${path}?tabId=${tabId}`; + } + + return { + app: 'observability', + path, + state: {}, + }; + }; +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/constants.ts b/x-pack/plugins/observability/public/pages/rule_details/constants.ts index 8d23754f60ef1..ffca250a9f15f 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/constants.ts +++ b/x-pack/plugins/observability/public/pages/rule_details/constants.ts @@ -7,7 +7,7 @@ export const EXECUTION_TAB = 'execution'; export const ALERTS_TAB = 'alerts'; -export const URL_STORAGE_KEY = 'searchBarParams'; +export const SEARCH_BAR_URL_STORAGE_KEY = 'searchBarParams'; export const EVENT_ERROR_LOG_TAB = 'rule_error_log_list'; export const RULE_DETAILS_PAGE_ID = 'rule-details-alerts-o11y'; export const RULE_DETAILS_ALERTS_SEARCH_BAR_ID = 'rule-details-alerts-search-bar-o11y'; diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.tsx index 0aea50d568a16..1e0e95868f888 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/index.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/index.tsx @@ -46,7 +46,7 @@ import { ALERTS_TAB, RULE_DETAILS_PAGE_ID, RULE_DETAILS_ALERTS_SEARCH_BAR_ID, - URL_STORAGE_KEY, + SEARCH_BAR_URL_STORAGE_KEY, } from './constants'; import { RuleDetailsPathParams, TabId } from './types'; import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; @@ -57,7 +57,9 @@ import { PageTitle } from './components'; import { getHealthColor } from './config'; import { hasExecuteActionsCapability, hasAllPrivilege } from './config'; import { paths } from '../../config/paths'; -import { observabilityFeatureId } from '../../../common'; +import { ALERT_STATUS_ALL } from '../../../common/constants'; +import { AlertStatus } from '../../../common/typings'; +import { observabilityFeatureId, ruleDetailsLocatorID } from '../../../common'; import { ALERT_STATUS_LICENSE_ERROR, rulesStatusesTranslationsMapping } from './translations'; import { ObservabilityAppServices } from '../../application/types'; @@ -70,12 +72,15 @@ export function RuleDetailsPage() { getEditAlertFlyout, getRuleEventLogList, getAlertsStateTable: AlertsStateTable, - getRuleAlertsSummary, + getRuleAlertsSummary: AlertSummaryWidget, getRuleStatusPanel, getRuleDefinition, }, application: { capabilities, navigateToUrl }, notifications: { toasts }, + share: { + url: { locators }, + }, } = useKibana().services; const { ruleId } = useParams(); @@ -106,6 +111,22 @@ export function RuleDetailsPage() { const ruleQuery = useRef([ { query: `kibana.alert.rule.uuid: ${ruleId}`, language: 'kuery' }, ] as Query[]); + const tabsRef = useRef(null); + + const onAlertSummaryWidgetClick = async (status: AlertStatus = ALERT_STATUS_ALL) => { + await locators.get(ruleDetailsLocatorID)?.navigate( + { + ruleId, + tabId: ALERTS_TAB, + status, + }, + { + replace: true, + } + ); + setTabId(ALERTS_TAB); + tabsRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; const updateUrl = (nextQuery: { tabId: TabId }) => { const newTabId = nextQuery.tabId; @@ -224,7 +245,7 @@ export function RuleDetailsPage() { @@ -354,16 +375,18 @@ export function RuleDetailsPage() { - {getRuleAlertsSummary({ - rule, - filteredRuleTypes, - })} + onAlertSummaryWidgetClick(status)} + /> {getRuleDefinition({ rule, onEditRule: () => reloadRule() } as RuleDefinitionProps)} +
; export interface ObservabilityPublicPluginsSetup { data: DataPublicPluginSetup; + share: SharePluginSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; home?: HomePublicPluginSetup; usageCollection: UsageCollectionSetup; @@ -88,6 +91,7 @@ export interface ObservabilityPublicPluginsStart { cases: CasesUiStart; embeddable: EmbeddableStart; home?: HomePublicPluginStart; + share: SharePluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; @@ -171,6 +175,7 @@ export class Plugin this.observabilityRuleTypeRegistry = createObservabilityRuleTypeRegistry( pluginsSetup.triggersActionsUi.ruleTypeRegistry ); + pluginsSetup.share.url.locators.create(new RuleDetailsLocatorDefinition()); const mount = async (params: AppMountParameters) => { // Load application bundle diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.stories.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.stories.tsx index ab064893ae6fe..b40c0854e131f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.stories.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.stories.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { action } from '@storybook/addon-actions'; import { AlertsSummaryWidgetUI as Component } from './alert_summary_widget_ui'; export default { @@ -17,5 +18,6 @@ export const Overview = { active: 15, recovered: 53, timeRange: 'Last 30 days', + onClick: action('clicked'), }, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.tsx index e4003e803032d..ebeee35f5732d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.tsx @@ -5,9 +5,18 @@ * 2.0. */ +import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED, AlertStatus } from '@kbn/rule-data-utils'; import { euiLightVars } from '@kbn/ui-theme'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; -import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import React, { MouseEvent } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { AlertsSummaryWidgetUIProps } from './types'; @@ -15,9 +24,26 @@ export const AlertsSummaryWidgetUI = ({ active, recovered, timeRange, + onClick, }: AlertsSummaryWidgetUIProps) => { + const handleClick = ( + event: MouseEvent, + status?: AlertStatus + ) => { + event.preventDefault(); + event.stopPropagation(); + + onClick(status); + }; + return ( - + @@ -43,39 +69,53 @@ export const AlertsSummaryWidgetUI = ({ - -

{active + recovered}

-
- - - + + +

{active + recovered}

+
+ + + +
- - -

{recovered}

+ ) => + handleClick(event, ALERT_STATUS_ACTIVE) + } + > + +

{active}

-
- - - + + + +
- -

{active}

-
- - - + ) => + handleClick(event, ALERT_STATUS_RECOVERED) + } + > + + +

{recovered}

+
+
+ + + +
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/types.ts index 81437071fcea2..545d33be36aeb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/types.ts @@ -5,8 +5,11 @@ * 2.0. */ +import { AlertStatus } from '@kbn/rule-data-utils'; + export interface AlertsSummaryWidgetUIProps { active: number; recovered: number; timeRange: JSX.Element | string; + onClick: (status?: AlertStatus) => void; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.test.tsx index 019d4f2ba549b..5fccea7961729 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.test.tsx @@ -59,6 +59,7 @@ describe('Rule Alert Summary', () => { ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx index da43783568658..6cb64864e48ad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx @@ -14,7 +14,7 @@ import { useLoadRuleTypes } from '../../../../hooks/use_load_rule_types'; import { RuleAlertsSummaryProps } from '.'; import { AlertSummaryWidgetError, AlertsSummaryWidgetUI } from './components'; -export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummaryProps) => { +export const RuleAlertsSummary = ({ rule, filteredRuleTypes, onClick }: RuleAlertsSummaryProps) => { const [features, setFeatures] = useState(''); const { ruleTypes } = useLoadRuleTypes({ filteredRuleTypes, @@ -40,6 +40,7 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary return ( void; }