mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[OBS-UX-MNGMT] Enrich the alert flyout with a new overview tab. (#178863)
## Summary It closes https://github.com/elastic/kibana/issues/173139 by enriching the AlertFlyout for the Observability rules. ## How to test the PR - Create one rule for each rule type from the list under observability and ensure the rule fires alerts - Open the alert flyout and check the info is there and well-formatted ## Knowing issues 🐞: - ~~Missing formatting for Inventory threshold for selected Metric, e.g. CPU, Memo~~ ✅ - ~~Missing formatting for APM threshold for Latency and Error rate~~ ✅ - ~~Missing tests~~ ✅ - ~~Tested on SLO~~✅ - NOT tested on Anomaly detection, and APM anomaly Here are some screenshots:     
This commit is contained in:
parent
4ca52b7549
commit
0fe636f745
18 changed files with 1201 additions and 134 deletions
|
@ -26,7 +26,7 @@ const AWS_SQS_QUEUE = 'aws.sqs.instance.id';
|
|||
|
||||
const METRICS_DETAILS_PATH = '/app/metrics/detail';
|
||||
|
||||
const infraSources = [
|
||||
export const infraSources = [
|
||||
HOST_NAME,
|
||||
CONTAINER_ID,
|
||||
KUBERNETES_POD_ID,
|
||||
|
@ -36,7 +36,7 @@ const infraSources = [
|
|||
AWS_SQS_QUEUE,
|
||||
];
|
||||
|
||||
const apmSources = [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME];
|
||||
export const apmSources = [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME];
|
||||
|
||||
const infraSourceLinks: Record<string, string> = {
|
||||
[HOST_NAME]: `${METRICS_DETAILS_PATH}/host`,
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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, { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { EuiInMemoryTable } from '@elastic/eui';
|
||||
import {
|
||||
ALERT_CASE_IDS,
|
||||
ALERT_DURATION,
|
||||
ALERT_END,
|
||||
ALERT_EVALUATION_VALUE,
|
||||
ALERT_EVALUATION_VALUES,
|
||||
ALERT_FLAPPING,
|
||||
ALERT_RULE_CATEGORY,
|
||||
ALERT_RULE_NAME,
|
||||
ALERT_RULE_UUID,
|
||||
ALERT_START,
|
||||
ALERT_STATUS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { useUiSetting } from '@kbn/kibana-react-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
|
||||
|
||||
import { paths } from '../../../../common/locators/paths';
|
||||
import { TimeRange } from '../../../../common/custom_threshold_rule/types';
|
||||
import { TopAlert } from '../../../typings/alerts';
|
||||
import { useFetchBulkCases } from '../../../hooks/use_fetch_bulk_cases';
|
||||
import { useCaseViewNavigation } from '../../../hooks/use_case_view_navigation';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import {
|
||||
FlyoutThresholdData,
|
||||
mapRuleParamsWithFlyout,
|
||||
} from './helpers/map_rules_params_with_flyout';
|
||||
import { ColumnIDs, overviewColumns } from './overview_columns';
|
||||
import { getSources } from './helpers/get_sources';
|
||||
|
||||
export const Overview = memo(({ alert }: { alert: TopAlert }) => {
|
||||
const { http } = useKibana().services;
|
||||
const { cases, isLoading } = useFetchBulkCases({ ids: alert.fields[ALERT_CASE_IDS] || [] });
|
||||
const dateFormat = useUiSetting<string>('dateFormat');
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>({ from: 'now-15m', to: 'now' });
|
||||
const [ruleCriteria, setRuleCriteria] = useState<FlyoutThresholdData[] | undefined>([]);
|
||||
const alertStart = alert.fields[ALERT_START];
|
||||
const alertEnd = alert.fields[ALERT_END];
|
||||
|
||||
useEffect(() => {
|
||||
const mappedRuleParams = mapRuleParamsWithFlyout(alert);
|
||||
setRuleCriteria(mappedRuleParams);
|
||||
}, [alert]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeRange(getPaddedAlertTimeRange(alertStart!, alertEnd));
|
||||
}, [alertStart, alertEnd]);
|
||||
|
||||
const { navigateToCaseView } = useCaseViewNavigation();
|
||||
const items = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
id: ColumnIDs.STATUS,
|
||||
key: i18n.translate('xpack.observability.alertFlyout.overviewTab.status', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
value: alert.fields[ALERT_STATUS],
|
||||
meta: {
|
||||
flapping: alert.fields[ALERT_FLAPPING],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ColumnIDs.SOURCE,
|
||||
key: i18n.translate('xpack.observability.alertFlyout.overviewTab.sources', {
|
||||
defaultMessage: 'Affected entity / source',
|
||||
}),
|
||||
value: [],
|
||||
meta: {
|
||||
alertEnd,
|
||||
timeRange,
|
||||
groups: getSources(alert) || [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ColumnIDs.TRIGGERED,
|
||||
key: i18n.translate('xpack.observability.alertFlyout.overviewTab.triggered', {
|
||||
defaultMessage: 'Triggered',
|
||||
}),
|
||||
value: alert.fields[ALERT_START],
|
||||
meta: {
|
||||
dateFormat,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ColumnIDs.DURATION,
|
||||
key: i18n.translate('xpack.observability.alertFlyout.overviewTab.duration', {
|
||||
defaultMessage: 'Duration',
|
||||
}),
|
||||
value: alert.fields[ALERT_DURATION],
|
||||
},
|
||||
{
|
||||
id: ColumnIDs.OBSERVED_VALUE,
|
||||
key: i18n.translate('xpack.observability.alertFlyout.overviewTab.observedValue', {
|
||||
defaultMessage: 'Observed value',
|
||||
}),
|
||||
value: alert.fields[ALERT_EVALUATION_VALUES] || [alert.fields[ALERT_EVALUATION_VALUE]],
|
||||
meta: {
|
||||
ruleCriteria,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ColumnIDs.THRESHOLD,
|
||||
key: i18n.translate('xpack.observability.alertFlyout.overviewTab.threshold', {
|
||||
defaultMessage: 'Threshold',
|
||||
}),
|
||||
value: [],
|
||||
meta: {
|
||||
ruleCriteria,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ColumnIDs.RULE_NAME,
|
||||
key: i18n.translate('xpack.observability.alertFlyout.overviewTab.ruleName', {
|
||||
defaultMessage: 'Rule name',
|
||||
}),
|
||||
value: alert.fields[ALERT_RULE_NAME],
|
||||
meta: {
|
||||
ruleLink:
|
||||
alert.fields[ALERT_RULE_UUID] &&
|
||||
http.basePath.prepend(paths.observability.ruleDetails(alert.fields[ALERT_RULE_UUID])),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ColumnIDs.RULE_TYPE,
|
||||
key: i18n.translate('xpack.observability.alertFlyout.overviewTab.ruleType', {
|
||||
defaultMessage: 'Rule type',
|
||||
}),
|
||||
value: alert.fields[ALERT_RULE_CATEGORY],
|
||||
},
|
||||
{
|
||||
id: ColumnIDs.CASES,
|
||||
key: i18n.translate('xpack.observability.alertFlyout.overviewTab.cases', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
value: [],
|
||||
meta: {
|
||||
cases,
|
||||
navigateToCaseView,
|
||||
isLoading,
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [
|
||||
alert,
|
||||
alertEnd,
|
||||
cases,
|
||||
dateFormat,
|
||||
http.basePath,
|
||||
isLoading,
|
||||
navigateToCaseView,
|
||||
ruleCriteria,
|
||||
timeRange,
|
||||
]);
|
||||
return <EuiInMemoryTable width={'80%'} columns={overviewColumns} itemId="key" items={items} />;
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { CaseTooltipContentProps } from '@kbn/cases-components';
|
||||
import { Case } from '@kbn/cases-plugin/common';
|
||||
|
||||
export const formatCase = (theCase: Case): CaseTooltipContentProps => ({
|
||||
title: theCase.title,
|
||||
description: theCase.description,
|
||||
createdAt: theCase.created_at,
|
||||
createdBy: {
|
||||
username: theCase.created_by.username ?? undefined,
|
||||
fullName: theCase.created_by.full_name ?? undefined,
|
||||
},
|
||||
status: theCase.status,
|
||||
totalComments: theCase.totalComment,
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { ALERT_GROUP_FIELD, ALERT_GROUP_VALUE } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
apmSources,
|
||||
infraSources,
|
||||
} from '../../../../../common/custom_threshold_rule/helpers/get_alert_source_links';
|
||||
import { TopAlert } from '../../../..';
|
||||
|
||||
interface AlertFields {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const getSources = (alert: TopAlert) => {
|
||||
const groupsFromGroupFields = alert.fields[ALERT_GROUP_FIELD]?.map((field, index) => {
|
||||
const values = alert.fields[ALERT_GROUP_VALUE];
|
||||
if (values?.length && values[index]) {
|
||||
return { field, value: values[index] };
|
||||
}
|
||||
});
|
||||
|
||||
if (groupsFromGroupFields?.length) return groupsFromGroupFields;
|
||||
|
||||
// Not all rules has group.fields, in that case we search in the alert fields.
|
||||
const matchedSources: Array<{ field: string; value: any }> = [];
|
||||
const ALL_SOURCES = [...infraSources, ...apmSources];
|
||||
const alertFields = alert.fields as AlertFields;
|
||||
ALL_SOURCES.forEach((source: string) => {
|
||||
Object.keys(alertFields).forEach((field: any) => {
|
||||
if (source === field) {
|
||||
const fieldValue = alertFields[field];
|
||||
matchedSources.push({
|
||||
field: source,
|
||||
value: fieldValue[0],
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return matchedSources;
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export function isFieldsSameType(fields: string[]): boolean {
|
||||
if (fields.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const referenceType = fields[0].slice(fields[0].lastIndexOf('.') + 1);
|
||||
for (let i = 1; i < fields.length; i++) {
|
||||
const type = fields[i].slice(fields[i].lastIndexOf('.') + 1);
|
||||
if (type !== referenceType) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
|
@ -0,0 +1,385 @@
|
|||
/*
|
||||
* 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 { TopAlert } from '../../../../typings/alerts';
|
||||
import { mapRuleParamsWithFlyout } from './map_rules_params_with_flyout';
|
||||
|
||||
describe('Map rules params with flyout', () => {
|
||||
const testData = [
|
||||
{
|
||||
ruleType: 'observability.rules.custom_threshold',
|
||||
alert: {
|
||||
fields: {
|
||||
'kibana.alert.rule.rule_type_id': 'observability.rules.custom_threshold',
|
||||
'kibana.alert.rule.parameters': {
|
||||
criteria: [
|
||||
{
|
||||
comparator: '>',
|
||||
metrics: [
|
||||
{
|
||||
name: 'A',
|
||||
field: 'system.memory.usage',
|
||||
aggType: 'avg',
|
||||
},
|
||||
],
|
||||
threshold: [1000000000],
|
||||
timeSize: 1,
|
||||
timeUnit: 'm',
|
||||
},
|
||||
],
|
||||
alertOnNoData: true,
|
||||
alertOnGroupDisappear: true,
|
||||
searchConfiguration: {
|
||||
query: {
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
},
|
||||
index: 'apm_static_data_view_id_default',
|
||||
},
|
||||
},
|
||||
'kibana.alert.evaluation.threshold': 1000000000,
|
||||
'kibana.alert.evaluation.values': [1347892565.33],
|
||||
},
|
||||
},
|
||||
results: [
|
||||
{
|
||||
observedValue: '1,347,892,565.33',
|
||||
threshold: '1,000,000,000',
|
||||
comparator: '>',
|
||||
pctAboveThreshold: ' (34.79% above the threshold)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ruleType: '.es-query',
|
||||
alert: {
|
||||
fields: {
|
||||
'kibana.alert.rule.rule_type_id': '.es-query',
|
||||
'kibana.alert.rule.parameters': {
|
||||
searchConfiguration: {
|
||||
query: {
|
||||
language: 'kuery',
|
||||
query: '',
|
||||
},
|
||||
index: 'apm_static_data_view_id_default',
|
||||
},
|
||||
timeField: '@timestamp',
|
||||
searchType: 'searchSource',
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
threshold: [1],
|
||||
thresholdComparator: '>',
|
||||
size: 100,
|
||||
aggType: 'avg',
|
||||
aggField: 'system.disk.io',
|
||||
groupBy: 'all',
|
||||
termSize: 5,
|
||||
sourceFields: [
|
||||
{
|
||||
label: 'container.id',
|
||||
searchPath: 'container.id',
|
||||
},
|
||||
{
|
||||
label: 'host.hostname',
|
||||
searchPath: 'host.hostname',
|
||||
},
|
||||
{
|
||||
label: 'host.id',
|
||||
searchPath: 'host.id',
|
||||
},
|
||||
{
|
||||
label: 'host.name',
|
||||
searchPath: 'host.name',
|
||||
},
|
||||
{
|
||||
label: 'kubernetes.pod.uid',
|
||||
searchPath: 'kubernetes.pod.uid',
|
||||
},
|
||||
],
|
||||
excludeHitsFromPreviousRun: false,
|
||||
},
|
||||
'kibana.alert.evaluation.value': 100870655162.18182,
|
||||
'kibana.alert.evaluation.threshold': 1,
|
||||
},
|
||||
},
|
||||
results: [
|
||||
{
|
||||
observedValue: [100870655162.18182],
|
||||
threshold: [1],
|
||||
comparator: '>',
|
||||
pctAboveThreshold: ' (10087065516118.18% above the threshold)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ruleType: 'logs.alert.document.count',
|
||||
alert: {
|
||||
fields: {
|
||||
'kibana.alert.rule.rule_type_id': 'logs.alert.document.count',
|
||||
'kibana.alert.rule.parameters': {
|
||||
timeSize: 5,
|
||||
timeUnit: 'm',
|
||||
logView: {
|
||||
type: 'log-view-reference',
|
||||
logViewId: 'default',
|
||||
},
|
||||
count: {
|
||||
value: 100,
|
||||
comparator: 'more than',
|
||||
},
|
||||
criteria: [
|
||||
{
|
||||
field: 'host.name',
|
||||
comparator: 'does not equal',
|
||||
value: 'test',
|
||||
},
|
||||
],
|
||||
groupBy: ['host.name'],
|
||||
},
|
||||
'kibana.alert.evaluation.value': 4577,
|
||||
'kibana.alert.evaluation.threshold': 100,
|
||||
},
|
||||
},
|
||||
results: [
|
||||
{
|
||||
observedValue: [4577],
|
||||
threshold: [100],
|
||||
comparator: 'more than',
|
||||
pctAboveThreshold: ' (4477.00% above the threshold)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ruleType: 'metrics.alert.threshold',
|
||||
alert: {
|
||||
fields: {
|
||||
'kibana.alert.rule.rule_type_id': 'metrics.alert.threshold',
|
||||
'kibana.alert.rule.parameters': {
|
||||
criteria: [
|
||||
{
|
||||
aggType: 'avg',
|
||||
comparator: '>',
|
||||
threshold: [0.01],
|
||||
timeSize: 1,
|
||||
timeUnit: 'm',
|
||||
metric: 'system.process.cpu.total.pct',
|
||||
},
|
||||
],
|
||||
},
|
||||
'kibana.alert.evaluation.value': [0.06],
|
||||
'kibana.alert.evaluation.threshold': 0.01,
|
||||
},
|
||||
},
|
||||
results: [
|
||||
{
|
||||
observedValue: '6%',
|
||||
threshold: '1%',
|
||||
comparator: '>',
|
||||
pctAboveThreshold: ' (500.00% above the threshold)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ruleType: 'metrics.alert.inventory.threshold',
|
||||
alert: {
|
||||
fields: {
|
||||
'kibana.alert.rule.rule_type_id': 'metrics.alert.inventory.threshold',
|
||||
'kibana.alert.rule.parameters': {
|
||||
nodeType: 'host',
|
||||
criteria: [
|
||||
{
|
||||
metric: 'rx',
|
||||
comparator: '>',
|
||||
threshold: [3000000],
|
||||
timeSize: 1,
|
||||
timeUnit: 'm',
|
||||
customMetric: {
|
||||
type: 'custom',
|
||||
id: 'alert-custom-metric',
|
||||
field: '',
|
||||
aggregation: 'avg',
|
||||
},
|
||||
},
|
||||
],
|
||||
sourceId: 'default',
|
||||
},
|
||||
|
||||
'kibana.alert.evaluation.value': [1303266.4],
|
||||
'kibana.alert.evaluation.threshold': 3000000,
|
||||
},
|
||||
},
|
||||
results: [
|
||||
{
|
||||
observedValue: '10.4 Mbit',
|
||||
threshold: ['3 Mbit'],
|
||||
comparator: '>',
|
||||
pctAboveThreshold: ' (247.54% above the threshold)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ruleType: 'apm.error_rate',
|
||||
alert: {
|
||||
fields: {
|
||||
'kibana.alert.rule.rule_type_id': 'apm.error_rate',
|
||||
'kibana.alert.rule.parameters': {
|
||||
threshold: 1,
|
||||
windowSize: 5,
|
||||
windowUnit: 'm',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
},
|
||||
|
||||
'kibana.alert.evaluation.value': 1,
|
||||
'kibana.alert.evaluation.threshold': 1,
|
||||
},
|
||||
},
|
||||
results: [
|
||||
{
|
||||
observedValue: [1],
|
||||
threshold: [1],
|
||||
comparator: '>',
|
||||
pctAboveThreshold: ' (0.00% above the threshold)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ruleType: 'apm.transaction_duration',
|
||||
alert: {
|
||||
fields: {
|
||||
'kibana.alert.rule.rule_type_id': 'apm.transaction_duration',
|
||||
'kibana.alert.rule.parameters': {
|
||||
aggregationType: 'avg',
|
||||
threshold: 1500,
|
||||
windowSize: 5,
|
||||
windowUnit: 'm',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
},
|
||||
'kibana.alert.evaluation.value': 22872063,
|
||||
'kibana.alert.evaluation.threshold': 1500000,
|
||||
},
|
||||
},
|
||||
results: [
|
||||
{
|
||||
observedValue: ['23 s'],
|
||||
threshold: ['1.5 s'],
|
||||
comparator: '>',
|
||||
pctAboveThreshold: ' (1424.80% above the threshold)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ruleType: 'apm.transaction_error_rate',
|
||||
alert: {
|
||||
fields: {
|
||||
'kibana.alert.rule.rule_type_id': 'apm.transaction_error_rate',
|
||||
'kibana.alert.rule.parameters': {
|
||||
threshold: 1,
|
||||
windowSize: 5,
|
||||
windowUnit: 'm',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
},
|
||||
'kibana.alert.evaluation.value': 25,
|
||||
'kibana.alert.evaluation.threshold': 1,
|
||||
},
|
||||
},
|
||||
results: [
|
||||
{
|
||||
observedValue: ['25%'],
|
||||
threshold: ['1.0%'],
|
||||
comparator: '>',
|
||||
pctAboveThreshold: ' (2400.00% above the threshold)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ruleType: 'slo.rules.burnRate',
|
||||
alert: {
|
||||
fields: {
|
||||
'kibana.alert.rule.rule_type_id': 'slo.rules.burnRate',
|
||||
'kibana.alert.evaluation.value': 66.02,
|
||||
'kibana.alert.evaluation.threshold': 0.07,
|
||||
'kibana.alert.rule.parameters': {
|
||||
windows: [
|
||||
{
|
||||
id: 'adf3db9b-7362-4941-8433-f6c285b1226a',
|
||||
burnRateThreshold: 0.07200000000000001,
|
||||
maxBurnRateThreshold: 720,
|
||||
longWindow: {
|
||||
value: 1,
|
||||
unit: 'h',
|
||||
},
|
||||
shortWindow: {
|
||||
value: 5,
|
||||
unit: 'm',
|
||||
},
|
||||
actionGroup: 'slo.burnRate.low',
|
||||
},
|
||||
{
|
||||
id: 'cff3b7d1-741b-42ca-882c-a4a39084c503',
|
||||
burnRateThreshold: 4.4639999999999995,
|
||||
maxBurnRateThreshold: 720,
|
||||
longWindow: {
|
||||
value: 1,
|
||||
unit: 'h',
|
||||
},
|
||||
shortWindow: {
|
||||
value: 5,
|
||||
unit: 'm',
|
||||
},
|
||||
actionGroup: 'slo.burnRate.low',
|
||||
},
|
||||
{
|
||||
id: '5be0fc9b-1376-48bd-b583-117a5472759a',
|
||||
burnRateThreshold: 7.127999999999999,
|
||||
maxBurnRateThreshold: 720,
|
||||
longWindow: {
|
||||
value: 1,
|
||||
unit: 'h',
|
||||
},
|
||||
shortWindow: {
|
||||
value: 5,
|
||||
unit: 'm',
|
||||
},
|
||||
actionGroup: 'slo.burnRate.low',
|
||||
},
|
||||
{
|
||||
id: '17b6002a-806f-415b-839c-66aea0f76da6',
|
||||
burnRateThreshold: 0.1,
|
||||
maxBurnRateThreshold: 10,
|
||||
longWindow: {
|
||||
value: 1,
|
||||
unit: 'h',
|
||||
},
|
||||
shortWindow: {
|
||||
value: 5,
|
||||
unit: 'm',
|
||||
},
|
||||
actionGroup: 'slo.burnRate.low',
|
||||
},
|
||||
],
|
||||
sloId: 'd463f5cf-e1d8-4a2b-ab5d-e750625e4599',
|
||||
},
|
||||
},
|
||||
},
|
||||
results: [
|
||||
{
|
||||
observedValue: [66.02],
|
||||
threshold: [0.07],
|
||||
comparator: '>',
|
||||
pctAboveThreshold: ' (94214.29% above the threshold)',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testData)(
|
||||
'Map rules type ($ruleType) with the alert flyout is OK',
|
||||
({ alert, results }) => {
|
||||
expect(mapRuleParamsWithFlyout(alert as unknown as TopAlert)).toMatchObject(results);
|
||||
}
|
||||
);
|
||||
});
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* 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 {
|
||||
ALERT_EVALUATION_VALUE,
|
||||
ALERT_EVALUATION_VALUES,
|
||||
ALERT_RULE_PARAMETERS,
|
||||
ALERT_RULE_TYPE_ID,
|
||||
METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
|
||||
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
|
||||
LOG_THRESHOLD_ALERT_TYPE_ID,
|
||||
ALERT_EVALUATION_THRESHOLD,
|
||||
ApmRuleType,
|
||||
SLO_BURN_RATE_RULE_TYPE_ID,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { EsQueryRuleParams } from '@kbn/stack-alerts-plugin/public/rule_types/es_query/types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { asDuration, asPercent } from '../../../../../common';
|
||||
import { createFormatter } from '../../../../../common/custom_threshold_rule/formatters';
|
||||
import { metricValueFormatter } from '../../../../../common/custom_threshold_rule/metric_value_formatter';
|
||||
import { METRIC_FORMATTERS } from '../../../../../common/custom_threshold_rule/formatters/snapshot_metric_formats';
|
||||
import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../pages/alert_details/alert_details';
|
||||
import {
|
||||
BaseMetricExpressionParams,
|
||||
CustomMetricExpressionParams,
|
||||
} from '../../../../../common/custom_threshold_rule/types';
|
||||
import { TopAlert } from '../../../../typings/alerts';
|
||||
import { isFieldsSameType } from './is_fields_same_type';
|
||||
export interface FlyoutThresholdData {
|
||||
observedValue: string;
|
||||
threshold: string[];
|
||||
comparator: string;
|
||||
pctAboveThreshold: string;
|
||||
}
|
||||
|
||||
const getPctAboveThreshold = (observedValue?: number, threshold?: number[]): string => {
|
||||
if (!observedValue || !threshold || threshold.length > 1 || threshold[0] <= 0) return '';
|
||||
return i18n.translate('xpack.observability.alertFlyout.overview.aboveThresholdLabel', {
|
||||
defaultMessage: ' ({pctValue}% above the threshold)',
|
||||
values: {
|
||||
pctValue: (((observedValue - threshold[0]) * 100) / threshold[0]).toFixed(2),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const mapRuleParamsWithFlyout = (alert: TopAlert): FlyoutThresholdData[] | undefined => {
|
||||
const ruleParams = alert.fields[ALERT_RULE_PARAMETERS];
|
||||
if (!ruleParams) return;
|
||||
const ruleCriteria = ruleParams?.criteria as Array<Record<string, any>>;
|
||||
const ruleId = alert.fields[ALERT_RULE_TYPE_ID];
|
||||
const observedValues: number[] = alert.fields[ALERT_EVALUATION_VALUES]! || [
|
||||
alert.fields[ALERT_EVALUATION_VALUE]!,
|
||||
];
|
||||
|
||||
switch (ruleId) {
|
||||
case OBSERVABILITY_THRESHOLD_RULE_TYPE_ID:
|
||||
return observedValues.map((observedValue, metricIndex) => {
|
||||
const criteria = ruleCriteria[metricIndex] as CustomMetricExpressionParams;
|
||||
const fields = criteria.metrics.map((metric) => metric.field || 'COUNT_AGG');
|
||||
const comparator = criteria.comparator;
|
||||
const threshold = criteria.threshold;
|
||||
const isSameFieldsType = isFieldsSameType(fields);
|
||||
const formattedValue = metricValueFormatter(
|
||||
observedValue as number,
|
||||
isSameFieldsType ? fields[0] : 'noType'
|
||||
);
|
||||
const thresholdFormattedAsString = threshold
|
||||
.map((thresholdWithRange) =>
|
||||
metricValueFormatter(thresholdWithRange, isSameFieldsType ? fields[0] : 'noType')
|
||||
)
|
||||
.join(' AND ');
|
||||
|
||||
return {
|
||||
observedValue: formattedValue,
|
||||
threshold: thresholdFormattedAsString,
|
||||
comparator,
|
||||
pctAboveThreshold: getPctAboveThreshold(observedValue, threshold),
|
||||
} as unknown as FlyoutThresholdData;
|
||||
});
|
||||
|
||||
case METRIC_THRESHOLD_ALERT_TYPE_ID:
|
||||
return observedValues.map((observedValue, metricIndex) => {
|
||||
const criteria = ruleCriteria[metricIndex] as BaseMetricExpressionParams & {
|
||||
metric: string;
|
||||
customMetrics: Array<{
|
||||
field?: string;
|
||||
}>;
|
||||
};
|
||||
let fields: string[] = [];
|
||||
const metric = criteria.metric;
|
||||
const customMetric = criteria.customMetrics;
|
||||
if (metric) fields = [metric];
|
||||
if (customMetric && customMetric.length)
|
||||
fields = customMetric.map((cMetric) => cMetric.field as string);
|
||||
const comparator = criteria.comparator;
|
||||
const threshold = criteria.threshold;
|
||||
const isSameFieldsType = isFieldsSameType(fields);
|
||||
const formattedValue = metricValueFormatter(
|
||||
observedValue as number,
|
||||
isSameFieldsType ? fields[0] : 'noType'
|
||||
);
|
||||
const thresholdFormattedAsString = threshold
|
||||
.map((thresholdWithRange) =>
|
||||
metricValueFormatter(thresholdWithRange, isSameFieldsType ? fields[0] : 'noType')
|
||||
)
|
||||
.join(' AND ');
|
||||
|
||||
return {
|
||||
observedValue: formattedValue,
|
||||
threshold: thresholdFormattedAsString,
|
||||
comparator,
|
||||
pctAboveThreshold: getPctAboveThreshold(observedValue, threshold),
|
||||
} as unknown as FlyoutThresholdData;
|
||||
});
|
||||
|
||||
case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID:
|
||||
return observedValues.map((observedValue, metricIndex) => {
|
||||
const criteria = ruleCriteria[metricIndex] as BaseMetricExpressionParams & {
|
||||
metric: string;
|
||||
};
|
||||
const infraType = METRIC_FORMATTERS[criteria.metric].formatter;
|
||||
const formatter = createFormatter(infraType);
|
||||
const comparator = criteria.comparator;
|
||||
const threshold = criteria.threshold;
|
||||
const formatThreshold = threshold.map((v: number) => {
|
||||
if (infraType === 'percent') {
|
||||
v = Number(v) / 100;
|
||||
}
|
||||
if (infraType === 'bits') {
|
||||
v = Number(v) / 8;
|
||||
}
|
||||
return v;
|
||||
});
|
||||
|
||||
return {
|
||||
observedValue: formatter(observedValue),
|
||||
threshold: formatThreshold.map(formatter),
|
||||
comparator,
|
||||
pctAboveThreshold: getPctAboveThreshold(observedValue, formatThreshold),
|
||||
} as unknown as FlyoutThresholdData;
|
||||
});
|
||||
|
||||
case LOG_THRESHOLD_ALERT_TYPE_ID:
|
||||
const { comparator } = ruleParams?.count as { comparator: string };
|
||||
const flyoutMap = {
|
||||
observedValue: [alert.fields[ALERT_EVALUATION_VALUE]],
|
||||
threshold: [alert.fields[ALERT_EVALUATION_THRESHOLD]],
|
||||
comparator,
|
||||
pctAboveThreshold: getPctAboveThreshold(alert.fields[ALERT_EVALUATION_VALUE], [
|
||||
alert.fields[ALERT_EVALUATION_THRESHOLD]!,
|
||||
]),
|
||||
} as unknown as FlyoutThresholdData;
|
||||
return [flyoutMap];
|
||||
|
||||
case ApmRuleType.ErrorCount:
|
||||
const APMFlyoutMapErrorCount = {
|
||||
observedValue: [alert.fields[ALERT_EVALUATION_VALUE]],
|
||||
threshold: [alert.fields[ALERT_EVALUATION_THRESHOLD]],
|
||||
comparator: '>',
|
||||
pctAboveThreshold: getPctAboveThreshold(alert.fields[ALERT_EVALUATION_VALUE], [
|
||||
alert.fields[ALERT_EVALUATION_THRESHOLD]!,
|
||||
]),
|
||||
} as unknown as FlyoutThresholdData;
|
||||
return [APMFlyoutMapErrorCount];
|
||||
|
||||
case ApmRuleType.TransactionErrorRate:
|
||||
const APMFlyoutMapTransactionErrorRate = {
|
||||
observedValue: [asPercent(alert.fields[ALERT_EVALUATION_VALUE], 100)],
|
||||
threshold: [asPercent(alert.fields[ALERT_EVALUATION_THRESHOLD], 100)],
|
||||
comparator: '>',
|
||||
pctAboveThreshold: getPctAboveThreshold(alert.fields[ALERT_EVALUATION_VALUE], [
|
||||
alert.fields[ALERT_EVALUATION_THRESHOLD]!,
|
||||
]),
|
||||
} as unknown as FlyoutThresholdData;
|
||||
return [APMFlyoutMapTransactionErrorRate];
|
||||
|
||||
case ApmRuleType.TransactionDuration:
|
||||
const APMFlyoutMapTransactionDuration = {
|
||||
observedValue: [asDuration(alert.fields[ALERT_EVALUATION_VALUE])],
|
||||
threshold: [asDuration(alert.fields[ALERT_EVALUATION_THRESHOLD])],
|
||||
comparator: '>',
|
||||
pctAboveThreshold: getPctAboveThreshold(alert.fields[ALERT_EVALUATION_VALUE], [
|
||||
alert.fields[ALERT_EVALUATION_THRESHOLD]!,
|
||||
]),
|
||||
} as unknown as FlyoutThresholdData;
|
||||
return [APMFlyoutMapTransactionDuration];
|
||||
|
||||
case '.es-query':
|
||||
const { thresholdComparator } = ruleParams as EsQueryRuleParams;
|
||||
const ESQueryFlyoutMap = {
|
||||
observedValue: [alert.fields[ALERT_EVALUATION_VALUE]],
|
||||
threshold: [alert.fields[ALERT_EVALUATION_THRESHOLD]],
|
||||
comparator: thresholdComparator,
|
||||
pctAboveThreshold: getPctAboveThreshold(alert.fields[ALERT_EVALUATION_VALUE], [
|
||||
alert.fields[ALERT_EVALUATION_THRESHOLD]!,
|
||||
]),
|
||||
} as unknown as FlyoutThresholdData;
|
||||
return [ESQueryFlyoutMap];
|
||||
|
||||
case SLO_BURN_RATE_RULE_TYPE_ID:
|
||||
const SLOBurnRateFlyoutMap = {
|
||||
observedValue: [alert.fields[ALERT_EVALUATION_VALUE]],
|
||||
threshold: [alert.fields[ALERT_EVALUATION_THRESHOLD]],
|
||||
comparator: '>',
|
||||
pctAboveThreshold: getPctAboveThreshold(alert.fields[ALERT_EVALUATION_VALUE], [
|
||||
alert.fields[ALERT_EVALUATION_THRESHOLD]!,
|
||||
]),
|
||||
} as unknown as FlyoutThresholdData;
|
||||
return [SLOBurnRateFlyoutMap];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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 { EuiBasicTableColumn, EuiCallOut, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui';
|
||||
import { AlertLifecycleStatusBadge } from '@kbn/alerts-ui-shared';
|
||||
import { Cases } from '@kbn/cases-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AlertStatus } from '@kbn/rule-data-utils';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { Tooltip as CaseTooltip } from '@kbn/cases-components';
|
||||
import type { Group } from '../../../../common/custom_threshold_rule/types';
|
||||
import { NavigateToCaseView } from '../../../hooks/use_case_view_navigation';
|
||||
import { Groups } from '../../custom_threshold/components/alert_details_app_section/groups';
|
||||
import { formatCase } from './helpers/format_cases';
|
||||
import { FlyoutThresholdData } from './helpers/map_rules_params_with_flyout';
|
||||
|
||||
interface AlertOverviewField {
|
||||
id: string;
|
||||
key: string;
|
||||
value?: string | string[] | number | number[] | Record<string, any>;
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
export const ColumnIDs = {
|
||||
STATUS: 'status',
|
||||
SOURCE: 'source',
|
||||
TRIGGERED: 'triggered',
|
||||
DURATION: 'duration',
|
||||
OBSERVED_VALUE: 'observed_value',
|
||||
THRESHOLD: 'threshold',
|
||||
RULE_NAME: 'rule_name',
|
||||
RULE_TYPE: 'rule_type',
|
||||
CASES: 'cases',
|
||||
} as const;
|
||||
|
||||
export const overviewColumns: Array<EuiBasicTableColumn<AlertOverviewField>> = [
|
||||
{
|
||||
field: 'key',
|
||||
name: '',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
name: '',
|
||||
render: (value: AlertOverviewField['value'], { id, meta }: AlertOverviewField) => {
|
||||
if (!value && value !== 0 && !meta) return <>{'-'}</>;
|
||||
const ruleCriteria = meta?.ruleCriteria as FlyoutThresholdData[];
|
||||
switch (id) {
|
||||
case ColumnIDs.STATUS:
|
||||
const alertStatus = value as string;
|
||||
const flapping = meta?.flapping;
|
||||
return (
|
||||
<AlertLifecycleStatusBadge
|
||||
alertStatus={alertStatus as AlertStatus}
|
||||
flapping={flapping}
|
||||
/>
|
||||
);
|
||||
|
||||
case ColumnIDs.SOURCE:
|
||||
const groups = meta?.groups as Group[];
|
||||
if (!groups.length) return <>{'-'}</>;
|
||||
const alertEnd = meta?.alertEnd;
|
||||
const timeRange = meta?.timeRange;
|
||||
return (
|
||||
<div>
|
||||
<Groups
|
||||
groups={groups}
|
||||
timeRange={alertEnd ? timeRange : { ...timeRange, to: 'now' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case ColumnIDs.TRIGGERED:
|
||||
const triggeredDate = value as string;
|
||||
return <EuiText size="s">{moment(triggeredDate).format(meta?.dateFormat)}</EuiText>;
|
||||
case ColumnIDs.DURATION:
|
||||
const duration = value as number;
|
||||
return (
|
||||
<EuiText size="s">
|
||||
{/* duration is in μs so divide by 1000 */}
|
||||
<h4>{moment.duration(duration / 1000).humanize()}</h4>
|
||||
</EuiText>
|
||||
);
|
||||
case ColumnIDs.RULE_NAME:
|
||||
const ruleName = value as string;
|
||||
const ruleLink = meta?.ruleLink as string;
|
||||
return (
|
||||
<EuiLink data-test-subj="alertFlyoutOverview" href={ruleLink ? ruleLink : '#'}>
|
||||
{ruleName}
|
||||
</EuiLink>
|
||||
);
|
||||
case ColumnIDs.OBSERVED_VALUE:
|
||||
if (!ruleCriteria) return <>{'-'}</>;
|
||||
return (
|
||||
<div>
|
||||
{ruleCriteria.map((criteria, criteriaIndex) => {
|
||||
const observedValue = criteria.observedValue;
|
||||
const pctAboveThreshold = criteria.pctAboveThreshold;
|
||||
return (
|
||||
<EuiText size="s" key={`${observedValue}-${criteriaIndex}`}>
|
||||
<h4 style={{ display: 'inline' }}>{observedValue}</h4>
|
||||
<span>{pctAboveThreshold}</span>
|
||||
</EuiText>
|
||||
);
|
||||
})}
|
||||
{ruleCriteria.length > 1 && (
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
title={i18n.translate(
|
||||
'xpack.observability.columns.euiCallOut.multipleConditionsLabel',
|
||||
{ defaultMessage: 'Multiple conditions' }
|
||||
)}
|
||||
iconType="alert"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case ColumnIDs.THRESHOLD:
|
||||
if (!ruleCriteria) return <>{'-'}</>;
|
||||
return (
|
||||
<div>
|
||||
{ruleCriteria.map((criteria, criticalIndex) => {
|
||||
const threshold = criteria.threshold;
|
||||
const comparator = criteria.comparator;
|
||||
return (
|
||||
<EuiText size="s" key={`${threshold}-${criticalIndex}`}>
|
||||
<h4>{`${comparator.toUpperCase()} ${threshold}`}</h4>
|
||||
</EuiText>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
case ColumnIDs.RULE_TYPE:
|
||||
const ruleType = value as string;
|
||||
return <EuiText size="s">{ruleType}</EuiText>;
|
||||
|
||||
case ColumnIDs.CASES:
|
||||
const cases = meta?.cases as Cases;
|
||||
const isLoading = meta?.isLoading;
|
||||
if (isLoading) return <EuiLoadingSpinner size="m" />;
|
||||
if (!cases || !cases.length) return <>{'-'}</>;
|
||||
const navigateToCaseView = meta?.navigateToCaseView as NavigateToCaseView;
|
||||
return cases.map((caseInfo, index) => {
|
||||
return [
|
||||
index > 0 && index < cases.length && ', ',
|
||||
<CaseTooltip loading={false} content={formatCase(caseInfo)} key={caseInfo.id}>
|
||||
<EuiLink
|
||||
key={caseInfo.id}
|
||||
onClick={() => navigateToCaseView({ caseId: caseInfo.id })}
|
||||
data-test-subj="o11yAlertFlyoutOverviewTabCasesLink"
|
||||
>
|
||||
{caseInfo.title}
|
||||
</EuiLink>
|
||||
</CaseTooltip>,
|
||||
];
|
||||
});
|
||||
default:
|
||||
return <>{'-'}</>;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
|
@ -8,12 +8,19 @@
|
|||
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 { kibanaStartMock } from '../../utils/kibana_react.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'];
|
||||
|
||||
const mockUseKibanaReturnValue = kibanaStartMock.startContract();
|
||||
jest.mock('../../utils/kibana_react', () => ({
|
||||
__esModule: true,
|
||||
useKibana: jest.fn(() => mockUseKibanaReturnValue),
|
||||
}));
|
||||
|
||||
describe('AlertsFlyout', () => {
|
||||
jest
|
||||
.spyOn(useUiSettingHook, 'useUiSetting')
|
||||
|
@ -82,6 +89,26 @@ const activeAlert: TopAlert = {
|
|||
'kibana.space_ids': ['default'],
|
||||
'kibana.version': '8.0.0',
|
||||
'event.kind': 'signal',
|
||||
'kibana.alert.rule.parameters': {
|
||||
timeSize: 5,
|
||||
timeUnit: 'm',
|
||||
logView: {
|
||||
type: 'log-view-reference',
|
||||
logViewId: 'default',
|
||||
},
|
||||
count: {
|
||||
value: 100.25,
|
||||
comparator: 'more than',
|
||||
},
|
||||
criteria: [
|
||||
{
|
||||
field: 'host.name',
|
||||
comparator: 'does not equal',
|
||||
value: 'test',
|
||||
},
|
||||
],
|
||||
groupBy: ['host.name'],
|
||||
},
|
||||
'kibana.alert.evaluation.threshold': 100.25,
|
||||
},
|
||||
active: true,
|
||||
|
@ -113,6 +140,25 @@ const recoveredAlert: TopAlert = {
|
|||
'kibana.version': '8.0.0',
|
||||
'event.kind': 'signal',
|
||||
'kibana.alert.end': '2021-09-02T13:08:45.729Z',
|
||||
'kibana.alert.rule.parameters': {
|
||||
nodeType: 'host',
|
||||
criteria: [
|
||||
{
|
||||
metric: 'cpu',
|
||||
comparator: '>',
|
||||
threshold: [1],
|
||||
timeSize: 1,
|
||||
timeUnit: 'm',
|
||||
customMetric: {
|
||||
type: 'custom',
|
||||
id: 'alert-custom-metric',
|
||||
field: '',
|
||||
aggregation: 'avg',
|
||||
},
|
||||
},
|
||||
],
|
||||
sourceId: 'default',
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
start: 1630587936699,
|
||||
|
|
|
@ -13,12 +13,19 @@ 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';
|
||||
import { kibanaStartMock } from '../../utils/kibana_react.mock';
|
||||
|
||||
const tabsData = [
|
||||
{ name: 'Overview', subj: 'overviewTab' },
|
||||
{ name: 'Table', subj: 'tableTab' },
|
||||
{ name: 'Metadata', subj: 'metadataTab' },
|
||||
];
|
||||
|
||||
const mockUseKibanaReturnValue = kibanaStartMock.startContract();
|
||||
jest.mock('../../utils/kibana_react', () => ({
|
||||
__esModule: true,
|
||||
useKibana: jest.fn(() => mockUseKibanaReturnValue),
|
||||
}));
|
||||
|
||||
describe('AlertsFlyoutBody', () => {
|
||||
jest
|
||||
.spyOn(useUiSettingHook, 'useUiSetting')
|
||||
|
|
|
@ -7,41 +7,25 @@
|
|||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiLink,
|
||||
EuiHorizontalRule,
|
||||
EuiDescriptionList,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTabbedContentTab,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
AlertStatus,
|
||||
ALERT_DURATION,
|
||||
ALERT_EVALUATION_THRESHOLD,
|
||||
ALERT_EVALUATION_VALUE,
|
||||
ALERT_FLAPPING,
|
||||
ALERT_RULE_CATEGORY,
|
||||
ALERT_RULE_TYPE_ID,
|
||||
ALERT_RULE_UUID,
|
||||
ALERT_STATUS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { ALERT_RULE_UUID } from '@kbn/rule-data-utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
AlertFieldsTable,
|
||||
AlertLifecycleStatusBadge,
|
||||
ScrollableFlyoutTabbedContent,
|
||||
} from '@kbn/alerts-ui-shared';
|
||||
import moment from 'moment-timezone';
|
||||
import { useUiSetting } from '@kbn/kibana-react-plugin/public';
|
||||
import { AlertFieldsTable, ScrollableFlyoutTabbedContent } from '@kbn/alerts-ui-shared';
|
||||
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';
|
||||
import { formatAlertEvaluationValue } from '../../utils/format_alert_evaluation_value';
|
||||
|
||||
import { RULE_DETAILS_PAGE_ID } from '../../pages/rule_details/constants';
|
||||
import type { TopAlert } from '../../typings/alerts';
|
||||
import { Overview } from './alert_flyout_overview/alerts_flyout_overview';
|
||||
|
||||
interface FlyoutProps {
|
||||
rawAlert: AlertsTableFlyoutBaseProps['alert'];
|
||||
|
@ -58,8 +42,6 @@ export function AlertsFlyoutBody({ alert, rawAlert, id: pageId }: FlyoutProps) {
|
|||
},
|
||||
} = useKibana().services;
|
||||
|
||||
const dateFormat = useUiSetting<string>('dateFormat');
|
||||
|
||||
const ruleId = get(alert.fields, ALERT_RULE_UUID) ?? null;
|
||||
const linkToRule =
|
||||
pageId !== RULE_DETAILS_PAGE_ID && ruleId && prepend
|
||||
|
@ -67,68 +49,6 @@ export function AlertsFlyoutBody({ alert, rawAlert, id: pageId }: FlyoutProps) {
|
|||
: null;
|
||||
|
||||
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',
|
||||
|
@ -163,29 +83,21 @@ export function AlertsFlyoutBody({ alert, rawAlert, id: pageId }: FlyoutProps) {
|
|||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiDescriptionList
|
||||
compressed={true}
|
||||
type="responsiveColumn"
|
||||
listItems={overviewListItems}
|
||||
titleProps={{
|
||||
'data-test-subj': 'alertsFlyoutDescriptionListTitle',
|
||||
}}
|
||||
descriptionProps={{
|
||||
'data-test-subj': 'alertsFlyoutDescriptionListDescription',
|
||||
}}
|
||||
/>
|
||||
<Overview alert={alert} />
|
||||
</EuiPanel>
|
||||
),
|
||||
};
|
||||
}, [alert.fields, alert.lastUpdated, alert.reason, alert.start, dateFormat, linkToRule]);
|
||||
}, [alert, linkToRule]);
|
||||
|
||||
const tableTab = useMemo(
|
||||
const metadataTab = useMemo(
|
||||
() => ({
|
||||
id: 'table',
|
||||
'data-test-subj': 'tableTab',
|
||||
name: i18n.translate('xpack.observability.alertsFlyout.table', { defaultMessage: 'Table' }),
|
||||
id: 'metadata',
|
||||
'data-test-subj': 'metadataTab',
|
||||
name: i18n.translate('xpack.observability.alertsFlyout.metadata', {
|
||||
defaultMessage: 'Metadata',
|
||||
}),
|
||||
content: (
|
||||
<EuiPanel hasShadow={false} data-test-subj="tableTabPanel">
|
||||
<EuiPanel hasShadow={false} data-test-subj="metadataTabPanel">
|
||||
<AlertFieldsTable alert={rawAlert} />
|
||||
</EuiPanel>
|
||||
),
|
||||
|
@ -193,7 +105,7 @@ export function AlertsFlyoutBody({ alert, rawAlert, id: pageId }: FlyoutProps) {
|
|||
[rawAlert]
|
||||
);
|
||||
|
||||
const tabs = useMemo(() => [overviewTab, tableTab], [overviewTab, tableTab]);
|
||||
const tabs = useMemo(() => [overviewTab, metadataTab], [overviewTab, metadataTab]);
|
||||
const [selectedTabId, setSelectedTabId] = useState<TabId>('overview');
|
||||
const handleTabClick = useCallback(
|
||||
(tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as TabId),
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { generatePath } from 'react-router-dom';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { useCallback } from 'react';
|
||||
import { useKibana } from '../utils/kibana_react';
|
||||
|
||||
export type NavigateToCaseView = (pathParams: { caseId: string }) => void;
|
||||
|
||||
const CASE_DEEP_LINK_ID = 'cases';
|
||||
|
||||
const generateCaseViewPath = (caseId: string): string => {
|
||||
return generatePath('/:caseId', { caseId });
|
||||
};
|
||||
|
||||
export const useCaseViewNavigation = () => {
|
||||
const {
|
||||
application: { navigateToApp, currentAppId$ },
|
||||
} = useKibana().services;
|
||||
|
||||
const currentAppId = useObservable(currentAppId$) ?? '';
|
||||
|
||||
const navigateToCaseView = useCallback<NavigateToCaseView>(
|
||||
(pathParams) =>
|
||||
navigateToApp(currentAppId, {
|
||||
deepLinkId: CASE_DEEP_LINK_ID,
|
||||
path: generateCaseViewPath(pathParams.caseId),
|
||||
}),
|
||||
[navigateToApp, currentAppId]
|
||||
);
|
||||
|
||||
return { navigateToCaseView };
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { useFetchBulkCases } from './use_fetch_bulk_cases';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { kibanaStartMock } from '../utils/kibana_react.mock';
|
||||
|
||||
const mockUseKibanaReturnValue = kibanaStartMock.startContract();
|
||||
|
||||
jest.mock('../utils/kibana_react', () => ({
|
||||
__esModule: true,
|
||||
useKibana: jest.fn(() => mockUseKibanaReturnValue),
|
||||
}));
|
||||
|
||||
describe('Bulk Get Cases API hook', () => {
|
||||
it('initially is not loading and does not have data', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useFetchBulkCases({ ids: [] }));
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.cases).toEqual([]);
|
||||
expect(result.current.error).toEqual(undefined);
|
||||
expect(result.current.isLoading).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 type { Cases } from '@kbn/cases-plugin/common';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useKibana } from '../utils/kibana_react';
|
||||
|
||||
export const useFetchBulkCases = ({
|
||||
ids = [],
|
||||
}: {
|
||||
ids: string[];
|
||||
}): { cases: Cases; isLoading: boolean; error?: Record<string, any> } => {
|
||||
const [cases, setCases] = useState<Cases>([]);
|
||||
const [error, setError] = useState();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const { cases: casesService } = useKibana().services;
|
||||
|
||||
useEffect(() => {
|
||||
const getBulkCasesByIds = async () => {
|
||||
return casesService.api.cases.bulkGet({ ids });
|
||||
};
|
||||
if (ids.length) {
|
||||
setIsLoading(true);
|
||||
getBulkCasesByIds()
|
||||
.then((res) => setCases(res.cases))
|
||||
.catch((resError) => setError(resError))
|
||||
.finally(() => setIsLoading(false));
|
||||
}
|
||||
}, [casesService.api.cases, ids]);
|
||||
|
||||
return { cases, isLoading, error };
|
||||
};
|
|
@ -97,6 +97,7 @@
|
|||
"@kbn/field-formats-plugin",
|
||||
"@kbn/event-annotation-common",
|
||||
"@kbn/data-view-field-editor-plugin",
|
||||
"@kbn/cases-components",
|
||||
"@kbn/aiops-log-rate-analysis",
|
||||
],
|
||||
"exclude": [
|
||||
|
|
|
@ -30120,17 +30120,9 @@
|
|||
"xpack.observability.alerts.ruleStats.muted": "Répété",
|
||||
"xpack.observability.alerts.ruleStats.ruleCount": "Nombre de règles",
|
||||
"xpack.observability.alerts.searchBar.invalidQueryTitle": "Chaîne de requête non valide",
|
||||
"xpack.observability.alertsFlyout.actualValueLabel": "Valeur réelle",
|
||||
"xpack.observability.alertsFlyout.alertsDetailsButtonText": "Détails de l'alerte",
|
||||
"xpack.observability.alertsFlyout.documentSummaryTitle": "Résumé du document",
|
||||
"xpack.observability.alertsFlyout.durationLabel": "Durée",
|
||||
"xpack.observability.alertsFlyout.expectedValueLabel": "Valeur attendue",
|
||||
"xpack.observability.alertsFlyout.lastUpdatedLabel": "Dernière mise à jour",
|
||||
"xpack.observability.alertsFlyout.reasonTitle": "Raison",
|
||||
"xpack.observability.alertsFlyout.ruleTypeLabel": "Type de règle",
|
||||
"xpack.observability.alertsFlyout.startedAtLabel": "Démarré à",
|
||||
"xpack.observability.alertsFlyout.statusLabel": "Statut",
|
||||
"xpack.observability.alertsFlyout.table": "Tableau",
|
||||
"xpack.observability.alertsFlyout.viewInAppButtonText": "Afficher dans l'application",
|
||||
"xpack.observability.alertsFlyout.viewRulesDetailsLinkText": "Afficher les détails de la règle",
|
||||
"xpack.observability.alertsLinkTitle": "Alertes",
|
||||
|
|
|
@ -30092,17 +30092,9 @@
|
|||
"xpack.observability.alerts.ruleStats.muted": "スヌーズ済み",
|
||||
"xpack.observability.alerts.ruleStats.ruleCount": "ルール数",
|
||||
"xpack.observability.alerts.searchBar.invalidQueryTitle": "無効なクエリ文字列",
|
||||
"xpack.observability.alertsFlyout.actualValueLabel": "実際の値",
|
||||
"xpack.observability.alertsFlyout.alertsDetailsButtonText": "アラートの詳細",
|
||||
"xpack.observability.alertsFlyout.documentSummaryTitle": "ドキュメント概要",
|
||||
"xpack.observability.alertsFlyout.durationLabel": "期間",
|
||||
"xpack.observability.alertsFlyout.expectedValueLabel": "想定された値",
|
||||
"xpack.observability.alertsFlyout.lastUpdatedLabel": "最終更新",
|
||||
"xpack.observability.alertsFlyout.reasonTitle": "理由",
|
||||
"xpack.observability.alertsFlyout.ruleTypeLabel": "ルールタイプ",
|
||||
"xpack.observability.alertsFlyout.startedAtLabel": "開始日時",
|
||||
"xpack.observability.alertsFlyout.statusLabel": "ステータス",
|
||||
"xpack.observability.alertsFlyout.table": "表",
|
||||
"xpack.observability.alertsFlyout.viewInAppButtonText": "アプリで表示",
|
||||
"xpack.observability.alertsFlyout.viewRulesDetailsLinkText": "ルール詳細を表示",
|
||||
"xpack.observability.alertsLinkTitle": "アラート",
|
||||
|
|
|
@ -30132,17 +30132,9 @@
|
|||
"xpack.observability.alerts.ruleStats.muted": "已暂停",
|
||||
"xpack.observability.alerts.ruleStats.ruleCount": "规则计数",
|
||||
"xpack.observability.alerts.searchBar.invalidQueryTitle": "字符串查询无效",
|
||||
"xpack.observability.alertsFlyout.actualValueLabel": "实际值",
|
||||
"xpack.observability.alertsFlyout.alertsDetailsButtonText": "告警详情",
|
||||
"xpack.observability.alertsFlyout.documentSummaryTitle": "文档摘要",
|
||||
"xpack.observability.alertsFlyout.durationLabel": "持续时间",
|
||||
"xpack.observability.alertsFlyout.expectedValueLabel": "预期值",
|
||||
"xpack.observability.alertsFlyout.lastUpdatedLabel": "上次更新时间",
|
||||
"xpack.observability.alertsFlyout.reasonTitle": "原因",
|
||||
"xpack.observability.alertsFlyout.ruleTypeLabel": "规则类型",
|
||||
"xpack.observability.alertsFlyout.startedAtLabel": "启动时间",
|
||||
"xpack.observability.alertsFlyout.statusLabel": "状态",
|
||||
"xpack.observability.alertsFlyout.table": "表",
|
||||
"xpack.observability.alertsFlyout.viewInAppButtonText": "在应用中查看",
|
||||
"xpack.observability.alertsFlyout.viewRulesDetailsLinkText": "查看规则详情",
|
||||
"xpack.observability.alertsLinkTitle": "告警",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue