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: