diff --git a/x-pack/plugins/observability/public/components/shared/alert_status_indicator.tsx b/x-pack/plugins/observability/public/components/shared/alert_status_indicator.tsx
new file mode 100644
index 0000000000000..e8897ed92dc8c
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/alert_status_indicator.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiHealth, EuiText } from '@elastic/eui';
+import { ALERT_STATUS_ACTIVE, AlertStatus } from '@kbn/rule-data-utils';
+import { useTheme } from '../../hooks/use_theme';
+
+interface AlertStatusIndicatorProps {
+ alertStatus: AlertStatus;
+}
+
+export function AlertStatusIndicator({ alertStatus }: AlertStatusIndicatorProps) {
+ const theme = useTheme();
+
+ if (alertStatus === ALERT_STATUS_ACTIVE) {
+ return (
+
+ {i18n.translate('xpack.observability.alertsTGrid.statusActiveDescription', {
+ defaultMessage: 'Active',
+ })}
+
+ );
+ }
+
+ return (
+
+
+ {i18n.translate('xpack.observability.alertsTGrid.statusRecoveredDescription', {
+ defaultMessage: 'Recovered',
+ })}
+
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.test.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.test.tsx
new file mode 100644
index 0000000000000..4fdc8d245799a
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.test.tsx
@@ -0,0 +1,101 @@
+/*
+ * 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 React from 'react';
+import * as useUiSettingHook from '../../../../../../../src/plugins/kibana_react/public/ui_settings/use_ui_setting';
+import { createObservabilityRuleTypeRegistryMock } from '../../../rules/observability_rule_type_registry_mock';
+import { render } from '../../../utils/test_helper';
+import type { TopAlert } from '../';
+import { AlertsFlyout } from './';
+
+describe('AlertsFlyout', () => {
+ jest
+ .spyOn(useUiSettingHook, 'useUiSetting')
+ .mockImplementation(() => 'MMM D, YYYY @ HH:mm:ss.SSS');
+ const observabilityRuleTypeRegistryMock = createObservabilityRuleTypeRegistryMock();
+
+ it('should include a indicator for an active alert', async () => {
+ const flyout = render(
+
+ );
+
+ expect(flyout.getByText('Active')).toBeInTheDocument();
+ });
+
+ it('should include a indicator for a recovered alert', async () => {
+ const flyout = render(
+
+ );
+
+ expect(flyout.getByText('Recovered')).toBeInTheDocument();
+ });
+});
+
+const activeAlert: TopAlert = {
+ link: '/app/logs/link-to/default/logs?time=1630587249674',
+ reason: '1957 log entries (more than 100.25) match the conditions.',
+ fields: {
+ 'kibana.alert.status': 'active',
+ '@timestamp': '2021-09-02T13:08:51.750Z',
+ 'kibana.alert.duration.us': 882076000,
+ 'kibana.alert.reason': '1957 log entries (more than 100.25) match the conditions.',
+ 'kibana.alert.workflow_status': 'open',
+ 'kibana.alert.rule.uuid': 'db2ab7c0-0bec-11ec-9ae2-5b10ca924404',
+ 'kibana.alert.rule.producer': 'logs',
+ 'kibana.alert.rule.consumer': 'logs',
+ 'kibana.alert.rule.category': 'Log threshold',
+ 'kibana.alert.start': '2021-09-02T12:54:09.674Z',
+ 'kibana.alert.rule.rule_type_id': 'logs.alert.document.count',
+ 'event.action': 'active',
+ 'kibana.alert.evaluation.value': 1957,
+ 'kibana.alert.instance.id': '*',
+ 'kibana.alert.rule.name': 'Log threshold (from logs)',
+ 'kibana.alert.uuid': '756240e5-92fb-452f-b08e-cd3e0dc51738',
+ 'kibana.space_ids': ['default'],
+ 'kibana.version': '8.0.0',
+ 'event.kind': 'signal',
+ 'kibana.alert.evaluation.threshold': 100.25,
+ },
+ active: true,
+ start: 1630587249674,
+};
+
+const recoveredAlert: TopAlert = {
+ link: '/app/metrics/inventory',
+ reason: 'CPU usage is greater than a threshold of 38 (current value is 38%)',
+ fields: {
+ 'kibana.alert.status': 'recovered',
+ '@timestamp': '2021-09-02T13:08:45.729Z',
+ 'kibana.alert.duration.us': 189030000,
+ 'kibana.alert.reason': 'CPU usage is greater than a threshold of 38 (current value is 38%)',
+ 'kibana.alert.workflow_status': 'open',
+ 'kibana.alert.rule.uuid': '92f112f0-0bed-11ec-9ae2-5b10ca924404',
+ 'kibana.alert.rule.producer': 'infrastructure',
+ 'kibana.alert.rule.consumer': 'infrastructure',
+ 'kibana.alert.rule.category': 'Inventory',
+ 'kibana.alert.start': '2021-09-02T13:05:36.699Z',
+ 'kibana.alert.rule.rule_type_id': 'metrics.alert.inventory.threshold',
+ 'event.action': 'close',
+ 'kibana.alert.instance.id': 'gke-edge-oblt-gcp-edge-oblt-gcp-pool-b6b9e929-vde2',
+ 'kibana.alert.rule.name': 'Metrics inventory (from Metrics)',
+ 'kibana.alert.uuid': '4f3a9ee4-aa45-47fd-a39a-a78758782425',
+ 'kibana.space_ids': ['default'],
+ 'kibana.version': '8.0.0',
+ 'event.kind': 'signal',
+ 'kibana.alert.end': '2021-09-02T13:08:45.729Z',
+ },
+ active: false,
+ start: 1630587936699,
+};
diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx
index 9bad3908df4a5..7171daa4a56e3 100644
--- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx
+++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx
@@ -37,6 +37,7 @@ import {
ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED,
// @ts-expect-error
} from '@kbn/rule-data-utils/target_node/technical_field_names';
+import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
import moment from 'moment-timezone';
import React, { useMemo } from 'react';
import type { TopAlert } from '../';
@@ -44,6 +45,7 @@ import { useKibana, useUiSetting } from '../../../../../../../src/plugins/kibana
import { asDuration } from '../../../../common/utils/formatters';
import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry';
import { parseAlert } from '../parse_alert';
+import { AlertStatusIndicator } from '../../../components/shared/alert_status_indicator';
type AlertsFlyoutProps = {
alert?: TopAlert;
@@ -92,7 +94,11 @@ export function AlertsFlyout({
title: i18n.translate('xpack.observability.alertsFlyout.statusLabel', {
defaultMessage: 'Status',
}),
- description: alertData.active ? 'Active' : 'Recovered',
+ description: (
+
+ ),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.lastUpdatedLabel', {
diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx
index 2df3053c380cb..3b588c59260d1 100644
--- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx
+++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx
@@ -386,7 +386,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
query: `${ALERT_WORKFLOW_STATUS}: ${workflowStatus}${kuery !== '' ? ` and ${kuery}` : ''}`,
language: 'kuery',
},
- renderCellValue: getRenderCellValue({ rangeFrom, rangeTo, setFlyoutAlert }),
+ renderCellValue: getRenderCellValue({ setFlyoutAlert }),
rowRenderers: NO_ROW_RENDER,
start: rangeFrom,
setRefetch,
diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.test.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.test.tsx
new file mode 100644
index 0000000000000..55333e8b7ea76
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.test.tsx
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+
+// @ts-expect-error importing from a place other than root because we want to limit what we import from this package
+import { ALERT_STATUS } from '@kbn/rule-data-utils/target_node/technical_field_names';
+import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
+import type { CellValueElementProps } from '../../../../timelines/common';
+import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock';
+import * as PluginHook from '../../hooks/use_plugin_context';
+import { render } from '../../utils/test_helper';
+import { getRenderCellValue } from './render_cell_value';
+
+interface AlertsTableRow {
+ alertStatus: typeof ALERT_STATUS_ACTIVE | typeof ALERT_STATUS_RECOVERED;
+}
+
+describe('getRenderCellValue', () => {
+ const observabilityRuleTypeRegistryMock = createObservabilityRuleTypeRegistryMock();
+ jest.spyOn(PluginHook, 'usePluginContext').mockImplementation(
+ () =>
+ ({
+ observabilityRuleTypeRegistry: observabilityRuleTypeRegistryMock,
+ } as any)
+ );
+
+ const renderCellValue = getRenderCellValue({
+ setFlyoutAlert: jest.fn(),
+ });
+
+ describe('when column is alert status', () => {
+ it('should return an active indicator when alert status is active', async () => {
+ const cell = render(
+ renderCellValue({
+ ...requiredProperties,
+ columnId: ALERT_STATUS,
+ data: makeAlertsTableRow({ alertStatus: ALERT_STATUS_ACTIVE }),
+ })
+ );
+
+ expect(cell.getByText('Active')).toBeInTheDocument();
+ });
+
+ it('should return a recovered indicator when alert status is recovered', async () => {
+ const cell = render(
+ renderCellValue({
+ ...requiredProperties,
+ columnId: ALERT_STATUS,
+ data: makeAlertsTableRow({ alertStatus: ALERT_STATUS_RECOVERED }),
+ })
+ );
+
+ expect(cell.getByText('Recovered')).toBeInTheDocument();
+ });
+ });
+});
+
+function makeAlertsTableRow({ alertStatus }: AlertsTableRow) {
+ return [
+ {
+ field: ALERT_STATUS,
+ value: [alertStatus],
+ },
+ ];
+}
+
+const requiredProperties: CellValueElementProps = {
+ rowIndex: 0,
+ columnId: '',
+ setCellProps: jest.fn(),
+ isExpandable: false,
+ isExpanded: false,
+ isDetails: false,
+ data: [],
+ eventId: '',
+ header: {
+ id: '',
+ columnHeaderType: 'not-filtered',
+ },
+ isDraggable: false,
+ linkValues: [],
+ timelineId: '',
+};
diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx
index 7e33b61c9b35d..f7e14545048a7 100644
--- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx
+++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx
@@ -4,8 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { EuiLink, EuiHealth, EuiText } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
+import { EuiLink } from '@elastic/eui';
import React from 'react';
/**
* We need to produce types and code transpilation at different folders during the build of the package.
@@ -28,13 +27,13 @@ import {
} from '@kbn/rule-data-utils/target_node/technical_field_names';
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
import type { CellValueElementProps, TimelineNonEcsData } from '../../../../timelines/common';
+import { AlertStatusIndicator } from '../../components/shared/alert_status_indicator';
import { TimestampTooltip } from '../../components/shared/timestamp_tooltip';
import { asDuration } from '../../../common/utils/formatters';
import { SeverityBadge } from './severity_badge';
import { TopAlert } from '.';
import { parseAlert } from './parse_alert';
import { usePluginContext } from '../../hooks/use_plugin_context';
-import { useTheme } from '../../hooks/use_theme';
const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED;
const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED;
@@ -62,48 +61,25 @@ export const getMappedNonEcsValue = ({
*/
export const getRenderCellValue = ({
- rangeTo,
- rangeFrom,
setFlyoutAlert,
}: {
- rangeTo: string;
- rangeFrom: string;
setFlyoutAlert: (data: TopAlert) => void;
}) => {
- return ({ columnId, data, setCellProps }: CellValueElementProps) => {
+ return ({ columnId, data }: CellValueElementProps) => {
const { observabilityRuleTypeRegistry } = usePluginContext();
const value = getMappedNonEcsValue({
data,
fieldName: columnId,
})?.reduce((x) => x[0]);
- const theme = useTheme();
-
switch (columnId) {
case ALERT_STATUS:
- switch (value) {
- case ALERT_STATUS_ACTIVE:
- return (
-
- {i18n.translate('xpack.observability.alertsTGrid.statusActiveDescription', {
- defaultMessage: 'Active',
- })}
-
- );
- case ALERT_STATUS_RECOVERED:
- return (
-
-
- {i18n.translate('xpack.observability.alertsTGrid.statusRecoveredDescription', {
- defaultMessage: 'Recovered',
- })}
-
-
- );
- default:
- // NOTE: This fallback shouldn't be needed. Status should be either "active" or "recovered".
- return null;
+ if (value !== ALERT_STATUS_ACTIVE && value !== ALERT_STATUS_RECOVERED) {
+ // NOTE: This should only be needed to narrow down the type.
+ // Status should be either "active" or "recovered".
+ return null;
}
+ return ;
case TIMESTAMP:
return ;
case ALERT_DURATION: