mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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).

### 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:
parent
8b7ceea6c7
commit
256d12e0a5
12 changed files with 351 additions and 172 deletions
|
@ -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}
|
||||
|
|
|
@ -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()}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue