Skip to content

Commit 2251285

Browse files
authored
[Observability] Add View in discover button in alert details header (#233259)
This PR is part of issue #230058. The View in discover button has been added in the header of the alert details page for custom threshold rules. Before: https://github.com/user-attachments/assets/c6b62dd2-4f00-425c-b9a9-b1964fbb58d1 After: https://github.com/user-attachments/assets/bf78ab66-dc97-4e3f-9fcf-85d8b7fcab2a
1 parent dd65328 commit 2251285

File tree

11 files changed

+263
-62
lines changed

11 files changed

+263
-62
lines changed

x-pack/platform/plugins/private/translations/translations/de-DE.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30957,7 +30957,6 @@
3095730957
"xpack.observability.customThreshold.rule.aggregators.p99": "99. Perzentil von {metric}",
3095830958
"xpack.observability.customThreshold.rule.aggregators.rate": "Rate von {metric}",
3095930959
"xpack.observability.customThreshold.rule.aggregators.sum": "Summe der {metric}",
30960-
"xpack.observability.customThreshold.rule.alertDetailsAppSection.openInDiscoverLabel": "In Discover öffnen",
3096130960
"xpack.observability.customThreshold.rule.alertDetailsAppSection.thresholdTitle": "Schwellenwert überschritten",
3096230961
"xpack.observability.customThreshold.rule.alertDetailUrlActionVariableDescription": "Link zur Ansicht zur Fehlerbehebung bei Alerts für weiteren Kontext und Details. Dies wird eine leere Zeichenfolge sein, wenn server.publicBaseUrl nicht konfiguriert ist.",
3096330962
"xpack.observability.customThreshold.rule.alertFlyout.addCondition": "Bedingung hinzufügen",

x-pack/platform/plugins/private/translations/translations/fr-FR.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31034,7 +31034,6 @@
3103431034
"xpack.observability.customThreshold.rule.aggregators.p99": "99e centile de {metric}",
3103531035
"xpack.observability.customThreshold.rule.aggregators.rate": "Taux de {metric}",
3103631036
"xpack.observability.customThreshold.rule.aggregators.sum": "Somme de {metric}",
31037-
"xpack.observability.customThreshold.rule.alertDetailsAppSection.openInDiscoverLabel": "Ouvrir dans Discover",
3103831037
"xpack.observability.customThreshold.rule.alertDetailsAppSection.thresholdTitle": "Seuil dépassé",
3103931038
"xpack.observability.customThreshold.rule.alertDetailUrlActionVariableDescription": "Lien vers l’affichage de résolution des problèmes d’alerte pour voir plus de contextes et de détails. La chaîne sera vide si server.publicBaseUrl n'est pas configuré.",
3104031039
"xpack.observability.customThreshold.rule.alertFlyout.addCondition": "Ajouter une condition",

x-pack/platform/plugins/private/translations/translations/ja-JP.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31071,7 +31071,6 @@
3107131071
"xpack.observability.customThreshold.rule.aggregators.p99": "{metric}の99パーセンタイル",
3107231072
"xpack.observability.customThreshold.rule.aggregators.rate": "{metric}の比率",
3107331073
"xpack.observability.customThreshold.rule.aggregators.sum": "{metric}の合計",
31074-
"xpack.observability.customThreshold.rule.alertDetailsAppSection.openInDiscoverLabel": "Discoverで開く",
3107531074
"xpack.observability.customThreshold.rule.alertDetailsAppSection.thresholdTitle": "しきい値を超えました",
3107631075
"xpack.observability.customThreshold.rule.alertDetailUrlActionVariableDescription": "アラートトラブルシューティングビューにリンクして、さらに詳しい状況や詳細を確認できます。server.publicBaseUrlが構成されていない場合は、空の文字列になります。",
3107731076
"xpack.observability.customThreshold.rule.alertFlyout.addCondition": "条件を追加",

x-pack/platform/plugins/private/translations/translations/zh-CN.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31054,7 +31054,6 @@
3105431054
"xpack.observability.customThreshold.rule.aggregators.p99": "{metric} 的第 99 个百分位",
3105531055
"xpack.observability.customThreshold.rule.aggregators.rate": "{metric} 的比率",
3105631056
"xpack.observability.customThreshold.rule.aggregators.sum": "{metric} 的总和",
31057-
"xpack.observability.customThreshold.rule.alertDetailsAppSection.openInDiscoverLabel": "在 Discover 中打开",
3105831057
"xpack.observability.customThreshold.rule.alertDetailsAppSection.thresholdTitle": "超出阈值",
3105931058
"xpack.observability.customThreshold.rule.alertDetailUrlActionVariableDescription": "链接到告警故障排除视图获取进一步的上下文和详情。 如果未配置 server.publicBaseUrl,这将为空字符串。",
3106031059
"xpack.observability.customThreshold.rule.alertFlyout.addCondition": "添加条件",

x-pack/solutions/observability/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,14 @@ export interface GetViewInAppUrlArgs {
2626
spaceId?: string;
2727
}
2828

29-
export const getViewInAppUrl = ({
29+
export const getViewInAppLocatorParams = ({
3030
dataViewId,
3131
endedAt,
3232
groups,
33-
logsLocator,
3433
metrics = [],
3534
searchConfiguration,
3635
startedAt = new Date().toISOString(),
37-
spaceId,
3836
}: GetViewInAppUrlArgs) => {
39-
if (!logsLocator) return '';
40-
4137
const searchConfigurationQuery = searchConfiguration?.query.query;
4238
const searchConfigurationFilters = searchConfiguration?.filter || [];
4339
const groupFilters = getGroupFilters(groups);
@@ -67,14 +63,35 @@ export const getViewInAppUrl = ({
6763
dataViewSpec = searchConfiguration.index as DataViewSpec;
6864
}
6965

70-
return logsLocator.getRedirectUrl(
71-
{
72-
dataViewId,
73-
dataViewSpec,
74-
timeRange,
75-
query,
76-
filters: [...searchConfigurationFilters, ...groupFilters],
77-
},
78-
{ spaceId }
79-
);
66+
return {
67+
dataViewId,
68+
dataViewSpec,
69+
timeRange,
70+
query,
71+
filters: [...searchConfigurationFilters, ...groupFilters],
72+
};
73+
};
74+
75+
export const getViewInAppUrl = ({
76+
dataViewId,
77+
endedAt,
78+
groups,
79+
logsLocator,
80+
metrics = [],
81+
searchConfiguration,
82+
startedAt = new Date().toISOString(),
83+
spaceId,
84+
}: GetViewInAppUrlArgs) => {
85+
if (!logsLocator) return '';
86+
87+
const params = getViewInAppLocatorParams({
88+
dataViewId,
89+
endedAt,
90+
groups,
91+
metrics,
92+
searchConfiguration,
93+
startedAt,
94+
});
95+
96+
return logsLocator.getRedirectUrl(params, { spaceId });
8097
};

x-pack/solutions/observability/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.test.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,31 +125,25 @@ describe('AlertDetailsAppSection', () => {
125125
expect(result.getByTestId('chartTitle-0').textContent).toBe(
126126
'Equation result for count (host.name: host-1)'
127127
);
128-
expect((result.getByTestId('viewLogs-0') as any).href).toBe('http://localhost/view-in-app-url');
129128

130129
expect(result.getByTestId('chartTitle-1').textContent).toBe(
131130
'Equation result for max (system.cpu.user.pct)'
132131
);
133-
expect((result.getByTestId('viewLogs-1') as any).href).toBe('http://localhost/view-in-app-url');
134132

135133
expect(result.getByTestId('chartTitle-2').textContent).toBe(
136134
'Equation result for min (system.memory.used.pct)'
137135
);
138-
expect((result.getByTestId('viewLogs-2') as any).href).toBe('http://localhost/view-in-app-url');
139136

140137
expect(result.getByTestId('chartTitle-3').textContent).toBe(
141138
'Equation result for min (system.memory.used.pct) + min (system.memory.used.pct) + min (system.memory.used.pct) + min (system.memory.used.pct...'
142139
);
143-
expect((result.getByTestId('viewLogs-3') as any).href).toBe('http://localhost/view-in-app-url');
144140

145141
expect(result.getByTestId('chartTitle-4').textContent).toBe(
146142
'Equation result for min (system.memory.used.pct) + min (system.memory.used.pct)'
147143
);
148-
expect((result.getByTestId('viewLogs-4') as any).href).toBe('http://localhost/view-in-app-url');
149144

150145
expect(result.getByTestId('chartTitle-5').textContent).toBe(
151146
'Equation result for min (system.memory.used.pct) + min (system.memory.used.pct) + min (system.memory.used.pct)'
152147
);
153-
expect((result.getByTestId('viewLogs-5') as any).href).toBe('http://localhost/view-in-app-url');
154148
});
155149
});

x-pack/solutions/observability/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import React, { useEffect, useState } from 'react';
1111
import {
1212
EuiFlexGroup,
1313
EuiFlexItem,
14-
EuiIcon,
15-
EuiLink,
1614
EuiPanel,
1715
EuiSpacer,
1816
EuiTitle,
@@ -35,8 +33,6 @@ import type {
3533
RangeEventAnnotationConfig,
3634
} from '@kbn/event-annotation-common';
3735
import moment from 'moment';
38-
import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
39-
import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common';
4036
import type { TimeRange } from '@kbn/es-query';
4137
import { getGroupFilters } from '../../../../../common/custom_threshold_rule/helpers/get_group';
4238
import { useKibana } from '../../../../utils/kibana_react';
@@ -45,8 +41,6 @@ import { Threshold } from '../threshold';
4541
import type { CustomThresholdAlert } from '../types';
4642
import { LogRateAnalysis } from './log_rate_analysis';
4743
import { RuleConditionChart } from '../../../rule_condition_chart/rule_condition_chart';
48-
import { getViewInAppUrl } from '../../../../../common/custom_threshold_rule/get_view_in_app_url';
49-
import type { SearchConfigurationWithExtractedReferenceType } from '../../../../../common/custom_threshold_rule/types';
5044
import { generateChartTitleAndTooltip } from './helpers/generate_chart_title_and_tooltip';
5145

5246
interface AppSectionProps {
@@ -56,14 +50,7 @@ interface AppSectionProps {
5650
// eslint-disable-next-line import/no-default-export
5751
export default function AlertDetailsAppSection({ alert }: AppSectionProps) {
5852
const services = useKibana().services;
59-
const {
60-
charts,
61-
data,
62-
application,
63-
share: {
64-
url: { locators },
65-
},
66-
} = services;
53+
const { charts, data, application } = services;
6754
const { euiTheme } = useEuiTheme();
6855
const aiopsEnabled = application.capabilities.aiops?.enabled ?? false;
6956
const [dataView, setDataView] = useState<DataView>();
@@ -134,17 +121,6 @@ export default function AlertDetailsAppSection({ alert }: AppSectionProps) {
134121
return (
135122
<EuiFlexGroup direction="column" data-test-subj="thresholdAlertOverviewSection">
136123
{ruleParams.criteria.map((criterion, index) => {
137-
const appUrl = getViewInAppUrl({
138-
dataViewId: dataView?.id,
139-
groups,
140-
logsLocator: locators.get<DiscoverAppLocatorParams>(DISCOVER_APP_LOCATOR),
141-
metrics: criterion?.metrics,
142-
searchConfiguration:
143-
ruleParams.searchConfiguration as SearchConfigurationWithExtractedReferenceType,
144-
startedAt: alertStart,
145-
endedAt: alertEnd,
146-
});
147-
148124
return (
149125
<EuiFlexItem key={`criterion-${index}`}>
150126
<EuiPanel hasBorder hasShadow={false}>
@@ -158,18 +134,6 @@ export default function AlertDetailsAppSection({ alert }: AppSectionProps) {
158134
</EuiTitle>
159135
</EuiToolTip>
160136
</EuiFlexItem>
161-
<EuiFlexItem grow={false}>
162-
<EuiLink data-test-subj={`viewLogs-${index}`} href={appUrl} color="text">
163-
<EuiIcon type="sortRight" />
164-
&nbsp;
165-
{i18n.translate(
166-
'xpack.observability.customThreshold.rule.alertDetailsAppSection.openInDiscoverLabel',
167-
{
168-
defaultMessage: 'Open in Discover',
169-
}
170-
)}
171-
</EuiLink>
172-
</EuiFlexItem>
173137
</EuiFlexGroup>
174138
<EuiSpacer size="m" />
175139
<EuiFlexGroup>

x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ jest.mock('./hooks/use_add_suggested_dashboard', () => ({
6060
}),
6161
}));
6262

63+
jest.mock('./hooks/use_discover_url', () => ({
64+
useDiscoverUrl: () => ({
65+
discoverUrl: null,
66+
}),
67+
}));
68+
6369
jest.mock('./hooks/use_related_dashboards', () => ({
6470
useRelatedDashboards: () => ({
6571
isLoadingSuggestedDashboards: false,

x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
} from './alert_details_rule_form_flyout';
3131
import { ObsCasesContext } from './obs_cases_context';
3232
import { AddToCaseButton } from './add_to_case_button';
33+
import { useDiscoverUrl } from '../hooks/use_discover_url';
3334

3435
export interface HeaderActionsProps extends AlertDetailsRuleFormFlyoutBaseProps {
3536
alert: TopAlert | null;
@@ -59,6 +60,8 @@ export function HeaderActions({
5960

6061
const { mutateAsync: untrackAlerts } = useBulkUntrackAlerts();
6162

63+
const { discoverUrl } = useDiscoverUrl({ alert, rule });
64+
6265
const handleUntrackAlert = useCallback(async () => {
6366
if (alert) {
6467
await untrackAlerts({
@@ -82,6 +85,23 @@ export function HeaderActions({
8285
return (
8386
<>
8487
<EuiFlexGroup direction="row" gutterSize="s" justifyContent="flexEnd">
88+
{discoverUrl && (
89+
<EuiFlexItem grow={false}>
90+
<EuiButtonEmpty
91+
href={discoverUrl}
92+
iconType="discoverApp"
93+
target="_blank"
94+
data-test-subj="view-in-discover-button"
95+
>
96+
<EuiText size="s">
97+
{i18n.translate('xpack.observability.alertDetails.viewInDiscover', {
98+
defaultMessage: 'View in Discover',
99+
})}
100+
</EuiText>
101+
</EuiButtonEmpty>
102+
</EuiFlexItem>
103+
)}
104+
85105
{cases && (
86106
<EuiFlexItem grow={false}>
87107
<ObsCasesContext>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { renderHook } from '@testing-library/react';
9+
import moment from 'moment';
10+
import { useDiscoverUrl } from './use_discover_url';
11+
import { useKibana } from '../../../utils/kibana_react';
12+
import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils';
13+
import type { Rule } from '@kbn/alerts-ui-shared';
14+
import type { TopAlert } from '../../../typings/alerts';
15+
16+
jest.mock('../../../utils/kibana_react', () => ({
17+
useKibana: jest.fn(),
18+
}));
19+
20+
const mockGetRedirectUrl = jest.fn();
21+
22+
const getServices = () => ({
23+
services: {
24+
discover: {
25+
locator: {
26+
getRedirectUrl: mockGetRedirectUrl,
27+
},
28+
},
29+
},
30+
});
31+
32+
const MOCK_ALERT = {
33+
start: Date.now(),
34+
} as unknown as TopAlert;
35+
36+
describe('useDiscoverUrl', () => {
37+
beforeEach(() => {
38+
jest.clearAllMocks();
39+
(useKibana as jest.Mock).mockReturnValue(getServices());
40+
});
41+
42+
it('returns null when rule or alert missing', () => {
43+
const { result } = renderHook(() => useDiscoverUrl({ alert: null, rule: undefined }));
44+
expect(result.current.discoverUrl).toBeNull();
45+
expect(mockGetRedirectUrl).not.toHaveBeenCalled();
46+
});
47+
48+
it('builds Discover url for custom threshold rule including filters', () => {
49+
const query = { language: 'kuery', query: 'message: error' };
50+
const rule = {
51+
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
52+
params: {
53+
searchConfiguration: {
54+
index: 'logs-data-view',
55+
query,
56+
},
57+
criteria: [{ metrics: [{ filter: 'service.name:test' }] }],
58+
},
59+
} as unknown as Rule;
60+
61+
const expectedTimeRange = {
62+
from: moment(MOCK_ALERT.start).subtract(30, 'minutes').toISOString(),
63+
to: moment(MOCK_ALERT.start).add(30, 'minutes').toISOString(),
64+
};
65+
66+
mockGetRedirectUrl.mockReturnValue('discover-url');
67+
68+
const { result } = renderHook(() => useDiscoverUrl({ alert: MOCK_ALERT, rule }));
69+
70+
expect(mockGetRedirectUrl).toHaveBeenCalledWith({
71+
dataViewId: 'logs-data-view',
72+
timeRange: expectedTimeRange,
73+
query,
74+
filters: [
75+
{
76+
$state: { store: 'appState' },
77+
bool: { minimum_should_match: 1, should: [{ match: { 'service.name': 'test' } }] },
78+
meta: {
79+
alias: null,
80+
disabled: true,
81+
index: 'logs-data-view',
82+
negate: false,
83+
type: 'custom',
84+
},
85+
},
86+
],
87+
});
88+
expect(result.current.discoverUrl).toBe('discover-url');
89+
});
90+
91+
it('ignores unsupported rule types', () => {
92+
const rule = {
93+
ruleTypeId: 'some_unsupported_rule',
94+
params: {},
95+
} as unknown as Rule;
96+
97+
const { result } = renderHook(() => useDiscoverUrl({ alert: MOCK_ALERT, rule }));
98+
99+
expect(result.current.discoverUrl).toBeNull();
100+
expect(mockGetRedirectUrl).not.toHaveBeenCalled();
101+
});
102+
});

0 commit comments

Comments
 (0)