Skip to content

Commit

Permalink
[Actionable Observability] Add rule details locator and make AlertSum…
Browse files Browse the repository at this point in the history
…maryWidget clickable (#147103)

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
  • Loading branch information
maryam-saeidi authored Dec 7, 2022
1 parent 313537c commit a30d225
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 42 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/observability/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/observability/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"inspector",
"unifiedSearch",
"security",
"guidedOnboarding"
"guidedOnboarding",
"share"
],
"ui": true,
"server": true,
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/observability/public/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -39,6 +40,7 @@ export interface ObservabilityAppServices {
notifications: NotificationsStart;
overlays: OverlayStart;
savedObjectsClient: SavedObjectsStart['client'];
share: SharePluginStart;
stateTransfer: EmbeddableStateTransfer;
storage: IStorageWrapper;
theme: ThemeServiceStart;
Expand Down
49 changes: 49 additions & 0 deletions x-pack/plugins/observability/public/locators/rule_details.test.ts
Original file line number Diff line number Diff line change
@@ -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)"`
);
});
});
72 changes: 72 additions & 0 deletions x-pack/plugins/observability/public/locators/rule_details.ts
Original file line number Diff line number Diff line change
@@ -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<RuleDetailsLocatorParams> {
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: {},
};
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
39 changes: 31 additions & 8 deletions x-pack/plugins/observability/public/pages/rule_details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand All @@ -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<ObservabilityAppServices>().services;

const { ruleId } = useParams<RuleDetailsPathParams>();
Expand Down Expand Up @@ -106,6 +111,22 @@ export function RuleDetailsPage() {
const ruleQuery = useRef([
{ query: `kibana.alert.rule.uuid: ${ruleId}`, language: 'kuery' },
] as Query[]);
const tabsRef = useRef<HTMLDivElement>(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;
Expand Down Expand Up @@ -224,7 +245,7 @@ export function RuleDetailsPage() {
<ObservabilityAlertSearchbarWithUrlSync
appName={RULE_DETAILS_ALERTS_SEARCH_BAR_ID}
onEsQueryChange={setEsQuery}
urlStorageKey={URL_STORAGE_KEY}
urlStorageKey={SEARCH_BAR_URL_STORAGE_KEY}
defaultSearchQueries={ruleQuery.current}
/>
<EuiSpacer size="s" />
Expand Down Expand Up @@ -354,16 +375,18 @@ export function RuleDetailsPage() {
</EuiFlexItem>
<EuiSpacer size="m" />
<EuiFlexItem style={{ minWidth: 350 }}>
{getRuleAlertsSummary({
rule,
filteredRuleTypes,
})}
<AlertSummaryWidget
rule={rule}
filteredRuleTypes={filteredRuleTypes}
onClick={(status) => onAlertSummaryWidgetClick(status)}
/>
</EuiFlexItem>
<EuiSpacer size="m" />
{getRuleDefinition({ rule, onEditRule: () => reloadRule() } as RuleDefinitionProps)}
</EuiFlexGroup>

<EuiSpacer size="l" />
<div ref={tabsRef} />
<EuiTabbedContent
data-test-subj="ruleDetailsTabbedContent"
tabs={tabs}
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/observability/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { i18n } from '@kbn/i18n';
import { BehaviorSubject, from } from 'rxjs';
import { map } from 'rxjs/operators';
import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import {
AppDeepLink,
AppMountParameters,
Expand Down Expand Up @@ -38,6 +39,7 @@ import {
} from '@kbn/triggers-actions-ui-plugin/public';
import { SecurityPluginStart } from '@kbn/security-plugin/public';
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
import { RuleDetailsLocatorDefinition } from './locators/rule_details';
import { observabilityAppId, observabilityFeatureId, casesPath } from '../common';
import { createLazyObservabilityPageTemplate } from './components/shared';
import { registerDataHandler } from './data_handler';
Expand Down Expand Up @@ -78,6 +80,7 @@ export type ObservabilityPublicSetup = ReturnType<Plugin['setup']>;

export interface ObservabilityPublicPluginsSetup {
data: DataPublicPluginSetup;
share: SharePluginSetup;
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
home?: HomePublicPluginSetup;
usageCollection: UsageCollectionSetup;
Expand All @@ -88,6 +91,7 @@ export interface ObservabilityPublicPluginsStart {
cases: CasesUiStart;
embeddable: EmbeddableStart;
home?: HomePublicPluginStart;
share: SharePluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
Expand Down Expand Up @@ -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<unknown>) => {
// Load application bundle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { action } from '@storybook/addon-actions';
import { AlertsSummaryWidgetUI as Component } from './alert_summary_widget_ui';

export default {
Expand All @@ -17,5 +18,6 @@ export const Overview = {
active: 15,
recovered: 53,
timeRange: 'Last 30 days',
onClick: action('clicked'),
},
};
Loading

0 comments on commit a30d225

Please sign in to comment.