Skip to content

Commit

Permalink
[Observability] Align alerts flyout status with table status visually (
Browse files Browse the repository at this point in the history
…#111959) (#113368)

* [Observability] Add common alert status indicator component (#109381)
* [Observability] Use common alert status indicator in alerts table (#109381)
* [Observability] Use common alert status indicator in alerts flyout (#109381)

Co-authored-by: Kibana Machine <[email protected]>

Co-authored-by: Milton Hultgren <[email protected]>
  • Loading branch information
kibanamachine and miltonhultgren authored Sep 29, 2021
1 parent a7e76e5 commit 51f6b13
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -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 (
<EuiHealth color="primary" textSize="xs">
{i18n.translate('xpack.observability.alertsTGrid.statusActiveDescription', {
defaultMessage: 'Active',
})}
</EuiHealth>
);
}

return (
<EuiHealth color={theme.eui.euiColorLightShade} textSize="xs">
<EuiText color="subdued" size="relative">
{i18n.translate('xpack.observability.alertsTGrid.statusRecoveredDescription', {
defaultMessage: 'Recovered',
})}
</EuiText>
</EuiHealth>
);
}
Original file line number Diff line number Diff line change
@@ -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(
<AlertsFlyout
alert={activeAlert}
observabilityRuleTypeRegistry={observabilityRuleTypeRegistryMock}
onClose={jest.fn()}
/>
);

expect(flyout.getByText('Active')).toBeInTheDocument();
});

it('should include a indicator for a recovered alert', async () => {
const flyout = render(
<AlertsFlyout
alert={recoveredAlert}
observabilityRuleTypeRegistry={observabilityRuleTypeRegistryMock}
onClose={jest.fn()}
/>
);

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,
};
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ 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 '../';
import { useKibana, useUiSetting } from '../../../../../../../src/plugins/kibana_react/public';
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;
Expand Down Expand Up @@ -92,7 +94,11 @@ export function AlertsFlyout({
title: i18n.translate('xpack.observability.alertsFlyout.statusLabel', {
defaultMessage: 'Status',
}),
description: alertData.active ? 'Active' : 'Recovered',
description: (
<AlertStatusIndicator
alertStatus={alertData.active ? ALERT_STATUS_ACTIVE : ALERT_STATUS_RECOVERED}
/>
),
},
{
title: i18n.translate('xpack.observability.alertsFlyout.lastUpdatedLabel', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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: '',
};
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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 (
<EuiHealth color="primary" textSize="xs">
{i18n.translate('xpack.observability.alertsTGrid.statusActiveDescription', {
defaultMessage: 'Active',
})}
</EuiHealth>
);
case ALERT_STATUS_RECOVERED:
return (
<EuiHealth color={theme.eui.euiColorLightShade} textSize="xs">
<EuiText color="subdued" size="relative">
{i18n.translate('xpack.observability.alertsTGrid.statusRecoveredDescription', {
defaultMessage: 'Recovered',
})}
</EuiText>
</EuiHealth>
);
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 <AlertStatusIndicator alertStatus={value} />;
case TIMESTAMP:
return <TimestampTooltip time={new Date(value ?? '').getTime()} timeUnit="milliseconds" />;
case ALERT_DURATION:
Expand Down

0 comments on commit 51f6b13

Please sign in to comment.