[RAM][Observability] Add alert fields table to Observability flyout (#174685)

## Summary

Adds the alert fields table from
https://github.com/elastic/kibana/pull/172830 to Observability alert
flyouts (triggered by the `View alert details` action).


![image](7e10517e-a5b6-497a-91c8-5bc2bc88ad69)

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
Umberto Pepato 2024-01-24 18:18:27 +01:00 committed by GitHub
parent 8b7ceea6c7
commit 256d12e0a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 351 additions and 172 deletions

View file

@ -17,6 +17,7 @@ import { css } from '@emotion/react';
import React, { memo, useCallback, useMemo, useState } from 'react';
import { Alert } from '@kbn/alerting-types';
import { euiThemeVars } from '@kbn/ui-theme';
import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table';
export const search = {
box: {
@ -28,7 +29,7 @@ export const search = {
},
};
const columns = [
const columns: Array<EuiBasicTableColumn<AlertField>> = [
{
field: 'key',
name: i18n.translate('alertsUIShared.alertFieldsTable.field', {
@ -86,18 +87,46 @@ const useFieldBrowserPagination = () => {
};
};
type AlertField = Exclude<
{
[K in keyof Alert]: { key: K; value: Alert[K] };
}[keyof Alert],
undefined
>;
export interface AlertFieldsTableProps {
/**
* The raw alert object
*/
alert: Alert;
/**
* A list of alert field keys to be shown in the table.
* When not defined, all the fields are shown.
*/
fields?: Array<keyof Alert>;
}
export const AlertFieldsTable = memo(({ alert }: AlertFieldsTableProps) => {
/**
* A paginated, filterable table to show alert object fields
*/
export const AlertFieldsTable = memo(({ alert, fields }: AlertFieldsTableProps) => {
const { onTableChange, paginationTableProp } = useFieldBrowserPagination();
const items = useMemo(() => {
let _items = Object.entries(alert).map(
([key, value]) =>
({
key,
value,
} as AlertField)
);
if (fields?.length) {
_items = _items.filter((f) => fields.includes(f.key));
}
return _items;
}, [alert, fields]);
return (
<EuiInMemoryTable
items={Object.entries(alert).map(([key, value]) => ({
key,
value: Array.isArray(value) ? value?.[0] : value,
}))}
items={items}
itemId="key"
columns={columns}
onTableChange={onTableChange}

View file

@ -5,13 +5,15 @@
* 2.0.
*/
import React from 'react';
import React, { ComponentProps } from 'react';
import * as useUiSettingHook from '@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting';
import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock';
import { render } from '../../utils/test_helper';
import { AlertsFlyout } from './alerts_flyout';
import type { TopAlert } from '../../typings/alerts';
const rawAlert = {} as ComponentProps<typeof AlertsFlyout>['rawAlert'];
describe('AlertsFlyout', () => {
jest
.spyOn(useUiSettingHook, 'useUiSetting')
@ -22,6 +24,7 @@ describe('AlertsFlyout', () => {
const flyout = render(
<AlertsFlyout
alert={activeAlert}
rawAlert={rawAlert}
observabilityRuleTypeRegistry={observabilityRuleTypeRegistryMock}
onClose={jest.fn()}
/>
@ -34,6 +37,7 @@ describe('AlertsFlyout', () => {
const flyout = render(
<AlertsFlyout
alert={recoveredAlert}
rawAlert={rawAlert}
observabilityRuleTypeRegistry={observabilityRuleTypeRegistryMock}
onClose={jest.fn()}
/>

View file

@ -9,6 +9,7 @@ import React, { useMemo } from 'react';
import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutProps } from '@elastic/eui';
import { ALERT_UUID } from '@kbn/rule-data-utils';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import { AlertsFlyoutHeader } from './alerts_flyout_header';
import { AlertsFlyoutBody } from './alerts_flyout_body';
import { AlertsFlyoutFooter } from './alerts_flyout_footer';
@ -18,6 +19,7 @@ import type { TopAlert } from '../../typings/alerts';
type AlertsFlyoutProps = {
alert?: TopAlert;
rawAlert?: EcsFieldsResponse;
alerts?: Array<Record<string, unknown>>;
isInApp?: boolean;
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry;
@ -26,6 +28,7 @@ type AlertsFlyoutProps = {
export function AlertsFlyout({
alert,
rawAlert,
alerts,
isInApp = false,
observabilityRuleTypeRegistry,
@ -41,16 +44,16 @@ export function AlertsFlyout({
if (!alertData) {
alertData = decoratedAlerts?.find((a) => a.fields[ALERT_UUID] === selectedAlertId) as TopAlert;
}
if (!alertData) {
if (!alertData || !rawAlert) {
return null;
}
return (
<EuiFlyout className="oblt__flyout" onClose={onClose} size="s" data-test-subj="alertsFlyout">
<EuiFlyout className="oblt__flyout" onClose={onClose} size="m" data-test-subj="alertsFlyout">
<EuiFlyoutHeader hasBorder>
<AlertsFlyoutHeader alert={alertData} />
</EuiFlyoutHeader>
<AlertsFlyoutBody alert={alertData} />
<AlertsFlyoutBody alert={alertData} rawAlert={rawAlert} />
<AlertsFlyoutFooter alert={alertData} isInApp={isInApp} />
</EuiFlyout>
);

View file

@ -12,25 +12,69 @@ import { AlertsFlyoutBody } from './alerts_flyout_body';
import { inventoryThresholdAlertEs } from '../../rules/fixtures/example_alerts';
import { parseAlert } from '../../pages/alerts/helpers/parse_alert';
import { RULE_DETAILS_PAGE_ID } from '../../pages/rule_details/constants';
import { fireEvent } from '@testing-library/react';
const tabsData = [
{ name: 'Overview', subj: 'overviewTab' },
{ name: 'Table', subj: 'tableTab' },
];
describe('AlertsFlyoutBody', () => {
jest
.spyOn(useUiSettingHook, 'useUiSetting')
.mockImplementation(() => 'MMM D, YYYY @ HH:mm:ss.SSS');
const observabilityRuleTypeRegistryMock = createObservabilityRuleTypeRegistryMock();
let flyout: ReturnType<typeof render>;
const setup = (id: string) => {
const alert = parseAlert(observabilityRuleTypeRegistryMock)(inventoryThresholdAlertEs);
return render(<AlertsFlyoutBody alert={alert} id={id} />);
flyout = render(
<AlertsFlyoutBody rawAlert={inventoryThresholdAlertEs} alert={alert} id={id} />
);
};
it('should render View rule detail link', async () => {
const flyout = setup('test');
setup('test');
expect(flyout.getByTestId('viewRuleDetailsFlyout')).toBeInTheDocument();
});
it('should NOT render View rule detail link for RULE_DETAILS_PAGE_ID', async () => {
const flyout = setup(RULE_DETAILS_PAGE_ID);
setup(RULE_DETAILS_PAGE_ID);
expect(flyout.queryByTestId('viewRuleDetailsFlyout')).not.toBeInTheDocument();
});
describe('tabs', () => {
beforeEach(() => {
setup('test');
});
tabsData.forEach(({ name: tab }) => {
test(`should render the ${tab} tab`, () => {
flyout.debug();
expect(flyout.getByText(tab)).toBeTruthy();
});
});
test('the Overview tab should be selected by default', () => {
expect(
flyout.container.querySelector(
'[data-test-subj="defaultAlertFlyoutTabs"] .euiTab-isSelected .euiTab__content'
)!.innerHTML
).toContain('Overview');
});
tabsData.forEach(({ subj, name }) => {
test(`should render the ${name} tab panel`, () => {
const tab = flyout.container.querySelector(
`[data-test-subj="defaultAlertFlyoutTabs"] [role="tablist"] [data-test-subj="${subj}"]`
);
fireEvent.click(tab!);
expect(
flyout.container.querySelector(
`[data-test-subj="defaultAlertFlyoutTabs"] [role="tabpanel"] [data-test-subj="${subj}Panel"]`
)
).toBeTruthy();
});
});
});
});

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { get } from 'lodash';
import {
EuiSpacer,
@ -13,7 +13,8 @@ import {
EuiLink,
EuiHorizontalRule,
EuiDescriptionList,
EuiFlyoutBody,
EuiPanel,
EuiTabbedContentTab,
} from '@elastic/eui';
import {
AlertStatus,
@ -27,9 +28,14 @@ import {
ALERT_STATUS,
} from '@kbn/rule-data-utils';
import { i18n } from '@kbn/i18n';
import { AlertLifecycleStatusBadge } from '@kbn/alerts-ui-shared';
import {
AlertFieldsTable,
AlertLifecycleStatusBadge,
ScrollableFlyoutTabbedContent,
} from '@kbn/alerts-ui-shared';
import moment from 'moment-timezone';
import { useUiSetting } from '@kbn/kibana-react-plugin/public';
import { AlertsTableFlyoutBaseProps } from '@kbn/triggers-actions-ui-plugin/public';
import { useKibana } from '../../utils/kibana_react';
import { asDuration } from '../../../common/utils/formatters';
import { paths } from '../../../common/locators/paths';
@ -38,11 +44,14 @@ import { RULE_DETAILS_PAGE_ID } from '../../pages/rule_details/constants';
import type { TopAlert } from '../../typings/alerts';
interface FlyoutProps {
rawAlert: AlertsTableFlyoutBaseProps['alert'];
alert: TopAlert;
id?: string;
}
export function AlertsFlyoutBody({ alert, id: pageId }: FlyoutProps) {
type TabId = 'overview' | 'table';
export function AlertsFlyoutBody({ alert, rawAlert, id: pageId }: FlyoutProps) {
const {
http: {
basePath: { prepend },
@ -57,107 +66,152 @@ export function AlertsFlyoutBody({ alert, id: pageId }: FlyoutProps) {
? prepend(paths.observability.ruleDetails(ruleId))
: null;
const overviewListItems = [
{
title: i18n.translate('xpack.observability.alertsFlyout.statusLabel', {
defaultMessage: 'Status',
const overviewTab = useMemo(() => {
const overviewListItems = [
{
title: i18n.translate('xpack.observability.alertsFlyout.statusLabel', {
defaultMessage: 'Status',
}),
description: (
<AlertLifecycleStatusBadge
alertStatus={alert.fields[ALERT_STATUS] as AlertStatus}
flapping={alert.fields[ALERT_FLAPPING]}
/>
),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.startedAtLabel', {
defaultMessage: 'Started at',
}),
description: (
<span title={alert.start.toString()}>{moment(alert.start).format(dateFormat)}</span>
),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.lastUpdatedLabel', {
defaultMessage: 'Last updated',
}),
description: (
<span title={alert.lastUpdated.toString()}>
{moment(alert.lastUpdated).format(dateFormat)}
</span>
),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.durationLabel', {
defaultMessage: 'Duration',
}),
description: asDuration(alert.fields[ALERT_DURATION], { extended: true }),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.expectedValueLabel', {
defaultMessage: 'Expected value',
}),
description: formatAlertEvaluationValue(
alert.fields[ALERT_RULE_TYPE_ID],
alert.fields[ALERT_EVALUATION_THRESHOLD]
),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.actualValueLabel', {
defaultMessage: 'Actual value',
}),
description: formatAlertEvaluationValue(
alert.fields[ALERT_RULE_TYPE_ID],
alert.fields[ALERT_EVALUATION_VALUE]
),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.ruleTypeLabel', {
defaultMessage: 'Rule type',
}),
description: alert.fields[ALERT_RULE_CATEGORY] ?? '-',
},
];
return {
id: 'overview',
'data-test-subj': 'overviewTab',
name: i18n.translate('xpack.observability.alertFlyout.overview', {
defaultMessage: 'Overview',
}),
description: (
<AlertLifecycleStatusBadge
alertStatus={alert.fields[ALERT_STATUS] as AlertStatus}
flapping={alert.fields[ALERT_FLAPPING]}
/>
content: (
<EuiPanel hasShadow={false} data-test-subj="overviewTabPanel">
<EuiTitle size="xs">
<h4>
{i18n.translate('xpack.observability.alertsFlyout.reasonTitle', {
defaultMessage: 'Reason',
})}
</h4>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s">{alert.reason}</EuiText>
<EuiSpacer size="s" />
{!!linkToRule && (
<EuiLink href={linkToRule} data-test-subj="viewRuleDetailsFlyout">
{i18n.translate('xpack.observability.alertsFlyout.viewRulesDetailsLinkText', {
defaultMessage: 'View rule details',
})}
</EuiLink>
)}
<EuiHorizontalRule size="full" />
<EuiTitle size="xs">
<h4>
{i18n.translate('xpack.observability.alertsFlyout.documentSummaryTitle', {
defaultMessage: 'Document Summary',
})}
</h4>
</EuiTitle>
<EuiSpacer size="m" />
<EuiDescriptionList
compressed={true}
type="responsiveColumn"
listItems={overviewListItems}
titleProps={{
'data-test-subj': 'alertsFlyoutDescriptionListTitle',
}}
descriptionProps={{
'data-test-subj': 'alertsFlyoutDescriptionListDescription',
}}
/>
</EuiPanel>
),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.startedAtLabel', {
defaultMessage: 'Started at',
}),
description: (
<span title={alert.start.toString()}>{moment(alert.start).format(dateFormat)}</span>
};
}, [alert.fields, alert.lastUpdated, alert.reason, alert.start, dateFormat, linkToRule]);
const tableTab = useMemo(
() => ({
id: 'table',
'data-test-subj': 'tableTab',
name: i18n.translate('xpack.observability.alertsFlyout.table', { defaultMessage: 'Table' }),
content: (
<EuiPanel hasShadow={false} data-test-subj="tableTabPanel">
<AlertFieldsTable alert={rawAlert} />
</EuiPanel>
),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.lastUpdatedLabel', {
defaultMessage: 'Last updated',
}),
description: (
<span title={alert.lastUpdated.toString()}>
{moment(alert.lastUpdated).format(dateFormat)}
</span>
),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.durationLabel', {
defaultMessage: 'Duration',
}),
description: asDuration(alert.fields[ALERT_DURATION], { extended: true }),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.expectedValueLabel', {
defaultMessage: 'Expected value',
}),
description: formatAlertEvaluationValue(
alert.fields[ALERT_RULE_TYPE_ID],
alert.fields[ALERT_EVALUATION_THRESHOLD]
),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.actualValueLabel', {
defaultMessage: 'Actual value',
}),
description: formatAlertEvaluationValue(
alert.fields[ALERT_RULE_TYPE_ID],
alert.fields[ALERT_EVALUATION_VALUE]
),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.ruleTypeLabel', {
defaultMessage: 'Rule type',
}),
description: alert.fields[ALERT_RULE_CATEGORY] ?? '-',
},
];
}),
[rawAlert]
);
const tabs = useMemo(() => [overviewTab, tableTab], [overviewTab, tableTab]);
const [selectedTabId, setSelectedTabId] = useState<TabId>('overview');
const handleTabClick = useCallback(
(tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as TabId),
[]
);
const selectedTab = useMemo(
() => tabs.find((tab) => tab.id === selectedTabId) ?? tabs[0],
[tabs, selectedTabId]
);
return (
<EuiFlyoutBody>
<EuiTitle size="xs">
<h4>
{i18n.translate('xpack.observability.alertsFlyout.reasonTitle', {
defaultMessage: 'Reason',
})}
</h4>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s">{alert.reason}</EuiText>
<EuiSpacer size="s" />
{!!linkToRule && (
<EuiLink href={linkToRule} data-test-subj="viewRuleDetailsFlyout">
{i18n.translate('xpack.observability.alertsFlyout.viewRulesDetailsLinkText', {
defaultMessage: 'View rule details',
})}
</EuiLink>
)}
<EuiHorizontalRule size="full" />
<EuiTitle size="xs">
<h4>
{i18n.translate('xpack.observability.alertsFlyout.documentSummaryTitle', {
defaultMessage: 'Document Summary',
})}
</h4>
</EuiTitle>
<EuiSpacer size="m" />
<EuiDescriptionList
compressed={true}
type="responsiveColumn"
listItems={overviewListItems}
titleProps={{
'data-test-subj': 'alertsFlyoutDescriptionListTitle',
}}
descriptionProps={{
'data-test-subj': 'alertsFlyoutDescriptionListDescription',
}}
/>
</EuiFlyoutBody>
<ScrollableFlyoutTabbedContent
tabs={tabs}
selectedTab={selectedTab}
onTabClick={handleTabClick}
expand
data-test-subj="defaultAlertFlyoutTabs"
/>
);
}

View file

@ -22,7 +22,7 @@ export const useGetAlertFlyoutComponents = (
const body = useCallback(
(props: AlertsTableFlyoutBaseProps) => {
const alert = parseAlert(observabilityRuleTypeRegistry)(props.alert);
return <AlertsFlyoutBody alert={alert} id={props.id} />;
return <AlertsFlyoutBody alert={alert} rawAlert={props.alert} id={props.id} />;
},
[observabilityRuleTypeRegistry]
);

View file

@ -10,8 +10,7 @@ import { kibanaStartMock } from '../utils/kibana_react.mock';
import * as pluginContext from './use_plugin_context';
import { createObservabilityRuleTypeRegistryMock } from '..';
import { PluginContextValue } from '../context/plugin_context/plugin_context';
import { useFetchAlertDetail } from './use_fetch_alert_detail';
import type { TopAlert } from '../typings/alerts';
import { AlertData, useFetchAlertDetail } from './use_fetch_alert_detail';
const mockUseKibanaReturnValue = kibanaStartMock.startContract();
@ -65,7 +64,7 @@ describe('useFetchAlertDetail', () => {
it('initially is not loading and does not have data', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, [boolean, TopAlert | null]>(() =>
const { result, waitForNextUpdate } = renderHook<string, [boolean, AlertData | null]>(() =>
useFetchAlertDetail(id)
);
@ -81,7 +80,7 @@ describe('useFetchAlertDetail', () => {
});
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, [boolean, TopAlert | null]>(() =>
const { result, waitForNextUpdate } = renderHook<string, [boolean, AlertData | null]>(() =>
useFetchAlertDetail('123')
);
@ -93,7 +92,7 @@ describe('useFetchAlertDetail', () => {
it('retrieves the alert data', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, [boolean, TopAlert | null]>(() =>
const { result, waitForNextUpdate } = renderHook<string, [boolean, AlertData | null]>(() =>
useFetchAlertDetail(id)
);
@ -104,16 +103,48 @@ describe('useFetchAlertDetail', () => {
Array [
false,
Object {
"0": "a",
"1": " ",
"2": "r",
"3": "e",
"4": "a",
"5": "s",
"6": "o",
"7": "n",
"active": true,
"fields": Object {
"formatted": Object {
"0": "a",
"1": " ",
"2": "r",
"3": "e",
"4": "a",
"5": "s",
"6": "o",
"7": "n",
"active": true,
"fields": Object {
"@timestamp": "2022-01-31T18:20:57.204Z",
"event.action": "active",
"event.kind": "signal",
"kibana.alert.duration.us": 13793555000,
"kibana.alert.instance.id": "*",
"kibana.alert.reason": "Document count reported no data in the last 1 hour for all hosts",
"kibana.alert.rule.category": "Metric threshold",
"kibana.alert.rule.consumer": "infrastructure",
"kibana.alert.rule.execution.uuid": "e62c418d-734d-47e7-bbeb-e6f182f5fb45",
"kibana.alert.rule.name": "A super rule",
"kibana.alert.rule.producer": "infrastructure",
"kibana.alert.rule.revision": 0,
"kibana.alert.rule.rule_type_id": "metrics.alert.threshold",
"kibana.alert.rule.tags": Array [],
"kibana.alert.rule.uuid": "69411af0-82a2-11ec-8139-c1568734434e",
"kibana.alert.start": "2022-01-31T14:31:03.649Z",
"kibana.alert.status": "active",
"kibana.alert.uuid": "73c0d0cd-2df4-4550-862c-1d447e9c1db2",
"kibana.alert.workflow_status": "open",
"kibana.space_ids": Array [
"default",
],
"kibana.version": "8.1.0",
"tags": Array [],
},
"lastUpdated": 1643653257204,
"link": undefined,
"reason": "Document count reported no data in the last 1 hour for all hosts",
"start": 1643639463649,
},
"raw": Object {
"@timestamp": "2022-01-31T18:20:57.204Z",
"event.action": "active",
"event.kind": "signal",
@ -139,10 +170,6 @@ describe('useFetchAlertDetail', () => {
"kibana.version": "8.1.0",
"tags": Array [],
},
"lastUpdated": 1643653257204,
"link": undefined,
"reason": "Document count reported no data in the last 1 hour for all hosts",
"start": 1643639463649,
},
]
`);
@ -151,9 +178,10 @@ describe('useFetchAlertDetail', () => {
it('does not populate the results when the request is canceled', async () => {
await act(async () => {
const { result, waitForNextUpdate, unmount } = renderHook<string, [boolean, TopAlert | null]>(
() => useFetchAlertDetail('123')
);
const { result, waitForNextUpdate, unmount } = renderHook<
string,
[boolean, AlertData | null]
>(() => useFetchAlertDetail('123'));
await waitForNextUpdate();
unmount();

View file

@ -9,19 +9,23 @@ import { isEmpty } from 'lodash';
import { HttpSetup } from '@kbn/core/public';
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common/constants';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import { usePluginContext } from './use_plugin_context';
import { ObservabilityRuleTypeRegistry } from '..';
import { useDataFetcher } from './use_data_fetcher';
import { parseAlert } from '../pages/alerts/helpers/parse_alert';
import type { TopAlert } from '../typings/alerts';
interface AlertDetailParams {
id: string;
ruleType: ObservabilityRuleTypeRegistry;
}
export const useFetchAlertDetail = (id: string): [boolean, TopAlert | null] => {
export interface AlertData {
formatted: TopAlert;
raw: EcsFieldsResponse;
}
export const useFetchAlertDetail = (id: string): [boolean, AlertData | null] => {
const { observabilityRuleTypeRegistry } = usePluginContext();
const params = useMemo(
() => ({ id, ruleType: observabilityRuleTypeRegistry }),
@ -33,35 +37,37 @@ export const useFetchAlertDetail = (id: string): [boolean, TopAlert | null] => {
[]
);
const { loading, data: alert } = useDataFetcher<AlertDetailParams, TopAlert | null>({
const { loading, data: rawAlert } = useDataFetcher<AlertDetailParams, EcsFieldsResponse | null>({
paramsForApiCall: params,
initialDataState: null,
executeApiCall: fetchAlert,
shouldExecuteApiCall,
});
return [loading, alert];
const data = rawAlert
? {
formatted: parseAlert(observabilityRuleTypeRegistry)(rawAlert),
raw: rawAlert,
}
: null;
return [loading, data];
};
const fetchAlert = async (
params: AlertDetailParams,
{ id }: AlertDetailParams,
abortController: AbortController,
http: HttpSetup
): Promise<TopAlert | null> => {
const { id, ruleType } = params;
try {
const response = await http.get<Record<string, unknown>>(BASE_RAC_ALERTS_API_PATH, {
) => {
return http
.get<EcsFieldsResponse>(BASE_RAC_ALERTS_API_PATH, {
query: {
id,
},
signal: abortController.signal,
})
.catch(() => {
// ignore error for retrieving alert
return null;
});
if (response !== undefined) {
return parseAlert(ruleType)(response);
}
} catch (error) {
// ignore error for retrieving alert
}
return null;
};

View file

@ -24,7 +24,7 @@ import { kibanaStartMock } from '../../utils/kibana_react.mock';
import { useFetchAlertDetail } from '../../hooks/use_fetch_alert_detail';
import { AlertDetails } from './alert_details';
import { ConfigSchema } from '../../plugin';
import { alert, alertWithNoData } from './mock/alert';
import { alertDetail, alertWithNoData } from './mock/alert';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@ -124,7 +124,7 @@ describe('Alert details', () => {
);
it('should show the alert detail page with all necessary components', async () => {
useFetchAlertDetailMock.mockReturnValue([false, alert]);
useFetchAlertDetailMock.mockReturnValue([false, alertDetail]);
const alertDetails = renderComponent();

View file

@ -61,22 +61,22 @@ export function AlertDetails() {
const { ObservabilityPageTemplate, config } = usePluginContext();
const { alertId } = useParams<AlertDetailsPathParams>();
const [isLoading, alert] = useFetchAlertDetail(alertId);
const [isLoading, alertDetail] = useFetchAlertDetail(alertId);
const [ruleTypeModel, setRuleTypeModel] = useState<RuleTypeModel | null>(null);
const CasesContext = getCasesContext();
const userCasesPermissions = canUseCases([observabilityFeatureId]);
const { rule } = useFetchRule({
ruleId: alert?.fields[ALERT_RULE_UUID],
ruleId: alertDetail?.formatted.fields[ALERT_RULE_UUID],
});
const [summaryFields, setSummaryFields] = useState<AlertSummaryField[]>();
const [alertStatus, setAlertStatus] = useState<AlertStatus>();
useEffect(() => {
if (alert) {
setRuleTypeModel(ruleTypeRegistry.get(alert?.fields[ALERT_RULE_TYPE_ID]!));
setAlertStatus(alert?.fields[ALERT_STATUS] as AlertStatus);
if (alertDetail) {
setRuleTypeModel(ruleTypeRegistry.get(alertDetail?.formatted.fields[ALERT_RULE_TYPE_ID]!));
setAlertStatus(alertDetail?.formatted?.fields[ALERT_STATUS] as AlertStatus);
}
}, [alert, ruleTypeRegistry]);
}, [alertDetail, ruleTypeRegistry]);
useBreadcrumbs([
{
href: http.basePath.prepend(paths.observability.alerts),
@ -86,7 +86,9 @@ export function AlertDetails() {
deepLinkId: 'observability-overview:alerts',
},
{
text: alert ? pageTitleContent(alert.fields[ALERT_RULE_CATEGORY]) : defaultBreadcrumb,
text: alertDetail
? pageTitleContent(alertDetail.formatted.fields[ALERT_RULE_CATEGORY])
: defaultBreadcrumb,
},
]);
@ -99,11 +101,11 @@ export function AlertDetails() {
}
// Redirect to the 404 page when the user hit the page url directly in the browser while the feature flag is off.
if (alert && !isAlertDetailsEnabledPerApp(alert, config)) {
if (alertDetail && !isAlertDetailsEnabledPerApp(alertDetail.formatted, config)) {
return <PageNotFound />;
}
if (!isLoading && !alert)
if (!isLoading && !alertDetail)
return (
<EuiPanel data-test-subj="alertDetailsError">
<EuiEmptyPrompt
@ -134,7 +136,7 @@ export function AlertDetails() {
pageHeader={{
pageTitle: (
<PageTitle
alert={alert}
alert={alertDetail?.formatted ?? null}
alertStatus={alertStatus}
dataTestSubj={rule?.ruleTypeId || 'alertDetailsPageTitle'}
/>
@ -146,7 +148,7 @@ export function AlertDetails() {
features={{ alerts: { sync: false } }}
>
<HeaderActions
alert={alert}
alert={alertDetail?.formatted ?? null}
alertStatus={alertStatus}
onUntrackAlert={onUntrackAlert}
/>
@ -161,7 +163,7 @@ export function AlertDetails() {
<EuiSpacer size="l" />
{AlertDetailsAppSection && rule && (
<AlertDetailsAppSection
alert={alert}
alert={alertDetail}
rule={rule}
timeZone={timeZone}
setAlertSummaryFields={setSummaryFields}

View file

@ -30,6 +30,7 @@ import {
TIMESTAMP,
VERSION,
} from '@kbn/rule-data-utils';
import { AlertData } from '../../../hooks/use_fetch_alert_detail';
import type { TopAlert } from '../../../typings/alerts';
export const tags: string[] = ['tag1', 'tag2', 'tag3'];
@ -67,6 +68,13 @@ export const alert: TopAlert = {
lastUpdated: 1630588131750,
};
export const alertDetail: AlertData = {
formatted: alert,
raw: Object.fromEntries(
Object.entries(alert.fields).map(([k, v]) => [k, !Array.isArray(v) ? [v] : v])
) as unknown as AlertData['raw'],
};
export const alertWithTags: TopAlert = {
...alert,
fields: {

View file

@ -34,7 +34,7 @@ export function Cases({ permissions }: CasesProps) {
const [selectedAlertId, setSelectedAlertId] = useState<string>('');
const [alertLoading, alert] = useFetchAlertDetail(selectedAlertId);
const [alertLoading, alertDetail] = useFetchAlertDetail(selectedAlertId);
const handleFlyoutClose = () => setSelectedAlertId('');
@ -65,10 +65,11 @@ export function Cases({ permissions }: CasesProps) {
useFetchAlertData={useFetchAlertData}
/>
{alert && selectedAlertId !== '' && alertLoading === false ? (
{alertDetail && selectedAlertId !== '' && !alertLoading ? (
<Suspense fallback={null}>
<LazyAlertsFlyout
alert={alert}
alert={alertDetail.formatted}
rawAlert={alertDetail.raw}
observabilityRuleTypeRegistry={observabilityRuleTypeRegistry}
onClose={handleFlyoutClose}
/>