[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:
![Screenshot 2024-03-27 at 14 12
01](fd893c1a-2890-4da7-b76f-dc2d0d19f054)
![Screenshot 2024-03-27 at 14 11
46](b80b4e33-66b7-42d4-af89-c9efd0531720)
![Screenshot 2024-03-27 at 14 11
26](2a814dd7-09a9-4cb9-a5ae-6fd7c80b4e70)

![Screenshot 2024-03-27 at 14 13
57](368b57ee-37d8-4644-8c4c-8c7c7c5784ad)
![Screenshot 2024-03-27 at 14 12
32](835ca10b-c9bb-452e-a874-930006df62c1)
This commit is contained in:
Faisal Kanout 2024-04-05 18:23:36 +02:00 committed by GitHub
parent 4ca52b7549
commit 0fe636f745
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1201 additions and 134 deletions

View file

@ -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`,

View file

@ -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} />;
});

View file

@ -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,
});

View file

@ -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;
};

View file

@ -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;
}

View file

@ -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);
}
);
});

View file

@ -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 [];
}
};

View file

@ -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 <>{'-'}</>;
}
},
},
];

View file

@ -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,

View file

@ -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')

View file

@ -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),

View file

@ -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 };
};

View file

@ -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);
});
});
});

View file

@ -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 };
};

View file

@ -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": [

View file

@ -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",

View file

@ -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": "アラート",

View file

@ -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": "告警",