mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[OBX-UI-MNGMT] Align the Metric rule charts by using Lens in Alert details page and Creation Rule flyout (#184950)
## Summary Fixes #184922 Fixes #184574 It uses the `RuleConditionChart`, a.k.a Lens chart, for the Metric Threshold rule. ### Implemented in both places: - Metric Alert Details page  - Rule creation flyout 
This commit is contained in:
parent
1e1e35ba08
commit
85f12800bb
20 changed files with 354 additions and 135 deletions
|
@ -3,37 +3,61 @@
|
|||
exports[`AlertDetailsAppSection should render annotations 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"additionalFilters": undefined,
|
||||
"annotations": Array [
|
||||
<AlertAnnotation
|
||||
alertStart={1678716383695}
|
||||
color="#BD271E"
|
||||
dateFormat="YYYY-MM-DD HH:mm"
|
||||
id="alert_start_annotation"
|
||||
/>,
|
||||
<AlertActiveTimeRangeAnnotation
|
||||
alertStart={1678716383695}
|
||||
color="#BD271E"
|
||||
id="alert_time_range_annotation"
|
||||
/>,
|
||||
Object {
|
||||
"color": "#BD271E",
|
||||
"icon": "alert",
|
||||
"id": "metric_threshold_alert_start_annotation",
|
||||
"key": Object {
|
||||
"timestamp": "2023-03-28T13:40:00.000Z",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"label": "Alert",
|
||||
"type": "manual",
|
||||
},
|
||||
Object {
|
||||
"color": "#F04E9833",
|
||||
"id": "metric_threshold_active_alert_range_annotation",
|
||||
"key": Object {
|
||||
"endTimestamp": "2024-06-13T07:00:33.381Z",
|
||||
"timestamp": "2023-03-28T13:40:00.000Z",
|
||||
"type": "range",
|
||||
},
|
||||
"label": "Active alert",
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"chartType": "line",
|
||||
"expression": Object {
|
||||
"aggType": "count",
|
||||
"chartOptions": Object {
|
||||
"seriesType": "bar_stacked",
|
||||
},
|
||||
"dataView": "index",
|
||||
"groupBy": Array [
|
||||
"host.hostname",
|
||||
],
|
||||
"metricExpression": Object {
|
||||
"comparator": ">",
|
||||
"metrics": Array [
|
||||
Object {
|
||||
"aggType": "count",
|
||||
"field": "",
|
||||
"name": "A",
|
||||
},
|
||||
],
|
||||
"threshold": Array [
|
||||
2000,
|
||||
],
|
||||
"timeSize": 15,
|
||||
"timeUnit": "m",
|
||||
"warningComparator": undefined,
|
||||
"warningThreshold": undefined,
|
||||
},
|
||||
"searchConfiguration": Object {
|
||||
"query": Object {
|
||||
"language": "",
|
||||
"query": "",
|
||||
},
|
||||
},
|
||||
"filterQuery": undefined,
|
||||
"groupBy": Array [
|
||||
"host.hostname",
|
||||
],
|
||||
"groupInstance": Array [
|
||||
"host-1",
|
||||
],
|
||||
"hideTitle": true,
|
||||
"timeRange": Object {
|
||||
"from": "2023-03-28T10:43:13.802Z",
|
||||
"to": "2023-03-29T13:14:09.581Z",
|
||||
|
|
|
@ -16,15 +16,35 @@ import {
|
|||
buildMetricThresholdRule,
|
||||
} from '../mocks/metric_threshold_rule';
|
||||
import { AlertDetailsAppSection } from './alert_details_app_section';
|
||||
import { ExpressionChart } from './expression_chart';
|
||||
import { RuleConditionChart } from '@kbn/observability-plugin/public';
|
||||
import { lensPluginMock } from '@kbn/lens-plugin/public/mocks';
|
||||
|
||||
const mockedChartStartContract = chartPluginMock.createStartContract();
|
||||
const mockedLensStartContract = lensPluginMock.createStartContract();
|
||||
|
||||
Date.now = jest.fn(() => new Date('2024-06-13T07:00:33.381Z').getTime());
|
||||
|
||||
jest.mock('../../../containers/metrics_source', () => ({
|
||||
useMetricsDataViewContext: () => ({
|
||||
metricsView: { dataViewReference: 'index' },
|
||||
}),
|
||||
withSourceProvider:
|
||||
<ComponentProps extends {}>(Component: React.FC<ComponentProps>) =>
|
||||
() => {
|
||||
return function ComponentWithSourceProvider(props: ComponentProps) {
|
||||
return <div />;
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/observability-alert-details', () => ({
|
||||
AlertAnnotation: () => {},
|
||||
AlertActiveTimeRangeAnnotation: () => {},
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/observability-alert-details', () => ({
|
||||
AlertAnnotation: () => {},
|
||||
AlertActiveTimeRangeAnnotation: () => {},
|
||||
}));
|
||||
jest.mock('@kbn/observability-get-padded-alert-time-range-util', () => ({
|
||||
getPaddedAlertTimeRange: () => ({
|
||||
from: '2023-03-28T10:43:13.802Z',
|
||||
|
@ -32,8 +52,9 @@ jest.mock('@kbn/observability-get-padded-alert-time-range-util', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('./expression_chart', () => ({
|
||||
ExpressionChart: jest.fn(() => <div data-test-subj="ExpressionChart" />),
|
||||
jest.mock('@kbn/observability-plugin/public', () => ({
|
||||
RuleConditionChart: jest.fn(() => <div data-test-subj="RuleConditionChart" />),
|
||||
getGroupFilters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../hooks/use_kibana', () => ({
|
||||
|
@ -41,6 +62,7 @@ jest.mock('../../../hooks/use_kibana', () => ({
|
|||
services: {
|
||||
...mockCoreMock.createStart(),
|
||||
charts: mockedChartStartContract,
|
||||
lens: mockedLensStartContract,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
@ -74,11 +96,11 @@ describe('AlertDetailsAppSection', () => {
|
|||
});
|
||||
|
||||
it('should render annotations', async () => {
|
||||
const mockedExpressionChart = jest.fn(() => <div data-test-subj="ExpressionChart" />);
|
||||
(ExpressionChart as jest.Mock).mockImplementation(mockedExpressionChart);
|
||||
const mockedRuleConditionChart = jest.fn(() => <div data-test-subj="RuleConditionChart" />);
|
||||
(RuleConditionChart as jest.Mock).mockImplementation(mockedRuleConditionChart);
|
||||
renderComponent();
|
||||
|
||||
expect(mockedExpressionChart).toHaveBeenCalledTimes(3);
|
||||
expect(mockedExpressionChart.mock.calls[0]).toMatchSnapshot();
|
||||
expect(mockedRuleConditionChart).toHaveBeenCalledTimes(3);
|
||||
expect(mockedRuleConditionChart.mock.calls[0]).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,29 +15,32 @@ import {
|
|||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
transparentize,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { AlertSummaryField, TopAlert } from '@kbn/observability-plugin/public';
|
||||
import chroma from 'chroma-js';
|
||||
|
||||
import { AlertSummaryField, RuleConditionChart, TopAlert } from '@kbn/observability-plugin/public';
|
||||
import { ALERT_END, ALERT_START, ALERT_EVALUATION_VALUES, ALERT_GROUP } from '@kbn/rule-data-utils';
|
||||
import { Rule } from '@kbn/alerting-plugin/common';
|
||||
import { AlertAnnotation, AlertActiveTimeRangeAnnotation } from '@kbn/observability-alert-details';
|
||||
import { Rule, RuleTypeParams } from '@kbn/alerting-plugin/common';
|
||||
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
|
||||
import type {
|
||||
EventAnnotationConfig,
|
||||
PointInTimeEventAnnotationConfig,
|
||||
RangeEventAnnotationConfig,
|
||||
} from '@kbn/event-annotation-common';
|
||||
|
||||
import { getGroupFilters } from '@kbn/observability-plugin/public';
|
||||
import type { GenericAggType } from '@kbn/observability-plugin/public';
|
||||
import { metricValueFormatter } from '../../../../common/alerting/metrics/metric_value_formatter';
|
||||
import { Threshold } from '../../common/components/threshold';
|
||||
import { withSourceProvider } from '../../../containers/metrics_source';
|
||||
import { useMetricsDataViewContext, withSourceProvider } from '../../../containers/metrics_source';
|
||||
import { generateUniqueKey } from '../lib/generate_unique_key';
|
||||
import { MetricsExplorerChartType } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
|
||||
import { MetricThresholdRuleTypeParams } from '..';
|
||||
import { ExpressionChart } from './expression_chart';
|
||||
import { AlertParams } from '../types';
|
||||
|
||||
// TODO Use a generic props for app sections https://github.com/elastic/kibana/issues/152690
|
||||
export type MetricThresholdRule = Rule<
|
||||
MetricThresholdRuleTypeParams & {
|
||||
filterQueryText?: string;
|
||||
groupBy?: string | string[];
|
||||
}
|
||||
>;
|
||||
export type MetricThresholdRule = Rule<RuleTypeParams & AlertParams>;
|
||||
|
||||
interface Group {
|
||||
field: string;
|
||||
|
@ -51,41 +54,49 @@ interface MetricThresholdAlertField {
|
|||
|
||||
export type MetricThresholdAlert = TopAlert<MetricThresholdAlertField>;
|
||||
|
||||
const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD HH:mm';
|
||||
const ALERT_START_ANNOTATION_ID = 'alert_start_annotation';
|
||||
const ALERT_TIME_RANGE_ANNOTATION_ID = 'alert_time_range_annotation';
|
||||
|
||||
interface AppSectionProps {
|
||||
alert: MetricThresholdAlert;
|
||||
rule: MetricThresholdRule;
|
||||
setAlertSummaryFields: React.Dispatch<React.SetStateAction<AlertSummaryField[] | undefined>>;
|
||||
}
|
||||
|
||||
export function AlertDetailsAppSection({ alert, rule }: AppSectionProps) {
|
||||
const { uiSettings, charts } = useKibanaContextForPlugin().services;
|
||||
export function AlertDetailsAppSection({ alert, rule, setAlertSummaryFields }: AppSectionProps) {
|
||||
const { charts } = useKibanaContextForPlugin().services;
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const groupInstance = alert.fields[ALERT_GROUP]?.map((group: Group) => group.value);
|
||||
|
||||
const groups = alert.fields[ALERT_GROUP];
|
||||
const { metricsView } = useMetricsDataViewContext();
|
||||
const chartProps = {
|
||||
baseTheme: charts.theme.useChartsBaseTheme(),
|
||||
};
|
||||
const alertEnd = alert.fields[ALERT_END] ? moment(alert.fields[ALERT_END]).valueOf() : undefined;
|
||||
const annotations = [
|
||||
<AlertAnnotation
|
||||
alertStart={alert.start}
|
||||
color={euiTheme.colors.danger}
|
||||
dateFormat={uiSettings.get('dateFormat') || DEFAULT_DATE_FORMAT}
|
||||
id={ALERT_START_ANNOTATION_ID}
|
||||
key={ALERT_START_ANNOTATION_ID}
|
||||
/>,
|
||||
<AlertActiveTimeRangeAnnotation
|
||||
alertStart={alert.start}
|
||||
alertEnd={alertEnd}
|
||||
color={euiTheme.colors.danger}
|
||||
id={ALERT_TIME_RANGE_ANNOTATION_ID}
|
||||
key={ALERT_TIME_RANGE_ANNOTATION_ID}
|
||||
/>,
|
||||
];
|
||||
const alertEnd = alert.fields[ALERT_END];
|
||||
const alertStart = alert.fields[ALERT_START];
|
||||
|
||||
const alertStartAnnotation: PointInTimeEventAnnotationConfig = {
|
||||
label: 'Alert',
|
||||
type: 'manual',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
timestamp: alertStart!,
|
||||
},
|
||||
color: euiTheme.colors.danger,
|
||||
icon: 'alert',
|
||||
id: 'metric_threshold_alert_start_annotation',
|
||||
};
|
||||
|
||||
const alertRangeAnnotation: RangeEventAnnotationConfig = {
|
||||
label: `${alertEnd ? 'Alert duration' : 'Active alert'}`,
|
||||
type: 'manual',
|
||||
key: {
|
||||
type: 'range',
|
||||
timestamp: alertStart!,
|
||||
endTimestamp: alertEnd ?? moment().toISOString(),
|
||||
},
|
||||
color: chroma(transparentize('#F04E981A', 0.2)).hex().toUpperCase(),
|
||||
id: `metric_threshold_${alertEnd ? 'recovered' : 'active'}_alert_range_annotation`,
|
||||
};
|
||||
|
||||
const annotations: EventAnnotationConfig[] = [];
|
||||
annotations.push(alertStartAnnotation, alertRangeAnnotation);
|
||||
|
||||
return !!rule.params.criteria ? (
|
||||
<EuiFlexGroup direction="column" data-test-subj="metricThresholdAppSection">
|
||||
|
@ -94,10 +105,25 @@ export function AlertDetailsAppSection({ alert, rule }: AppSectionProps) {
|
|||
alert.fields[ALERT_START]!,
|
||||
alert.fields[ALERT_END],
|
||||
{
|
||||
size: criterion.timeSize,
|
||||
unit: criterion.timeUnit,
|
||||
size: criterion.timeSize!,
|
||||
unit: criterion.timeUnit!,
|
||||
}
|
||||
);
|
||||
let metricExpression = [
|
||||
{
|
||||
aggType: criterion.aggType as GenericAggType,
|
||||
name: String.fromCharCode('A'.charCodeAt(0) + index),
|
||||
field: criterion.metric || '',
|
||||
},
|
||||
];
|
||||
if (criterion.customMetrics) {
|
||||
metricExpression = criterion.customMetrics.map((metric) => ({
|
||||
name: metric.name,
|
||||
aggType: metric.aggType as GenericAggType,
|
||||
field: metric.field || '',
|
||||
filter: metric.filter,
|
||||
}));
|
||||
}
|
||||
return (
|
||||
<EuiFlexItem key={generateUniqueKey(criterion)}>
|
||||
<EuiPanel hasBorder hasShadow={false}>
|
||||
|
@ -135,16 +161,30 @@ export function AlertDetailsAppSection({ alert, rule }: AppSectionProps) {
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={5}>
|
||||
<ExpressionChart
|
||||
annotations={annotations}
|
||||
chartType={MetricsExplorerChartType.line}
|
||||
expression={criterion}
|
||||
filterQuery={rule.params.filterQueryText}
|
||||
groupBy={rule.params.groupBy}
|
||||
groupInstance={groupInstance}
|
||||
hideTitle
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
{metricsView && (
|
||||
<RuleConditionChart
|
||||
additionalFilters={getGroupFilters(groups)}
|
||||
metricExpression={{
|
||||
metrics: metricExpression,
|
||||
threshold: criterion.threshold,
|
||||
comparator: criterion.comparator,
|
||||
timeSize: criterion.timeSize,
|
||||
timeUnit: criterion.timeUnit,
|
||||
warningComparator: criterion.warningComparator,
|
||||
warningThreshold: criterion.warningThreshold,
|
||||
}}
|
||||
chartOptions={{
|
||||
// For alert details page, the series type needs to be changed to 'bar_stacked'
|
||||
// due to https://github.com/elastic/elastic-charts/issues/2323
|
||||
seriesType: 'bar_stacked',
|
||||
}}
|
||||
searchConfiguration={{ query: { query: '', language: '' } }}
|
||||
timeRange={timeRange}
|
||||
dataView={metricsView.dataViewReference}
|
||||
groupBy={rule.params.groupBy}
|
||||
annotations={annotations}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { TimeUnitChar } from '@kbn/observability-plugin/common/utils/formatters/duration';
|
||||
import { COMPARATORS } from '@kbn/alerting-comparators';
|
||||
import { GenericAggType, RuleConditionChart } from '@kbn/observability-plugin/public';
|
||||
import { Aggregators, QUERY_INVALID } from '../../../../common/alerting/metrics';
|
||||
import {
|
||||
useMetricsDataViewContext,
|
||||
|
@ -40,7 +41,6 @@ import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer
|
|||
import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import { convertKueryToElasticSearchQuery } from '../../../utils/kuery';
|
||||
import { AlertContextMeta, AlertParams, MetricExpression } from '../types';
|
||||
import { ExpressionChart } from './expression_chart';
|
||||
import { ExpressionRow } from './expression_row';
|
||||
const FILTER_TYPING_DEBOUNCE_MS = 500;
|
||||
|
||||
|
@ -69,7 +69,6 @@ export const Expressions: React.FC<Props> = (props) => {
|
|||
const { docLinks } = useKibanaContextForPlugin().services;
|
||||
const { source } = useSourceContext();
|
||||
const { metricsView } = useMetricsDataViewContext();
|
||||
|
||||
const [timeSize, setTimeSize] = useState<number | undefined>(1);
|
||||
const [timeUnit, setTimeUnit] = useState<TimeUnitChar | undefined>('m');
|
||||
|
||||
|
@ -304,8 +303,24 @@ export const Expressions: React.FC<Props> = (props) => {
|
|||
</h4>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xs" />
|
||||
{ruleParams.criteria &&
|
||||
{metricsView &&
|
||||
ruleParams.criteria.map((e, idx) => {
|
||||
let metricExpression = [
|
||||
{
|
||||
aggType: e.aggType as GenericAggType,
|
||||
// RuleConditionChart uses A,B,C etc in its parser to identify multiple conditions
|
||||
name: String.fromCharCode('A'.charCodeAt(0) + idx),
|
||||
field: e.metric || '',
|
||||
},
|
||||
];
|
||||
if (e.customMetrics) {
|
||||
metricExpression = e.customMetrics.map((metric) => ({
|
||||
name: metric.name,
|
||||
aggType: metric.aggType as GenericAggType,
|
||||
field: metric.field || '',
|
||||
filter: metric.filter,
|
||||
}));
|
||||
}
|
||||
return (
|
||||
<ExpressionRow
|
||||
canDelete={(ruleParams.criteria && ruleParams.criteria.length > 1) || false}
|
||||
|
@ -317,9 +332,26 @@ export const Expressions: React.FC<Props> = (props) => {
|
|||
errors={(errors[idx] as IErrorObject) || emptyError}
|
||||
expression={e || {}}
|
||||
>
|
||||
<ExpressionChart
|
||||
expression={e}
|
||||
filterQuery={ruleParams.filterQueryText}
|
||||
<RuleConditionChart
|
||||
metricExpression={{
|
||||
metrics: metricExpression,
|
||||
threshold: e.threshold,
|
||||
comparator: e.comparator,
|
||||
timeSize,
|
||||
timeUnit,
|
||||
warningComparator: e.warningComparator,
|
||||
warningThreshold: e.warningThreshold,
|
||||
}}
|
||||
searchConfiguration={{
|
||||
index: metricsView.dataViewReference.id,
|
||||
query: {
|
||||
query: ruleParams.filterQueryText || '',
|
||||
language: 'kuery',
|
||||
},
|
||||
}}
|
||||
timeRange={{ from: `now-${(timeSize ?? 1) * 20}${timeUnit}`, to: 'now' }}
|
||||
error={(errors[idx] as IErrorObject) || emptyError}
|
||||
dataView={metricsView.dataViewReference}
|
||||
groupBy={ruleParams.groupBy}
|
||||
/>
|
||||
</ExpressionRow>
|
||||
|
|
|
@ -87,6 +87,7 @@ export const buildMetricThresholdRule = (
|
|||
filterQuery:
|
||||
'{"bool":{"filter":[{"bool":{"should":[{"term":{"host.hostname":{"value":"Users-System.local"}}}],"minimum_should_match":1}},{"bool":{"should":[{"term":{"service.type":{"value":"system"}}}],"minimum_should_match":1}}]}}',
|
||||
groupBy: ['host.hostname'],
|
||||
sourceId: 'sourceId',
|
||||
},
|
||||
monitoring: {
|
||||
run: {
|
||||
|
|
|
@ -63,7 +63,6 @@
|
|||
"@kbn/shared-ux-router",
|
||||
"@kbn/shared-ux-link-redirect-app",
|
||||
"@kbn/discover-plugin",
|
||||
"@kbn/observability-alert-details",
|
||||
"@kbn/observability-shared-plugin",
|
||||
"@kbn/observability-ai-assistant-plugin",
|
||||
"@kbn/ui-theme",
|
||||
|
@ -105,7 +104,8 @@
|
|||
"@kbn/react-kibana-context-theme",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/presentation-containers",
|
||||
"@kbn/deeplinks-observability"
|
||||
"@kbn/deeplinks-observability",
|
||||
"@kbn/event-annotation-common"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
buildCustomThresholdRule,
|
||||
} from '../../mocks/custom_threshold_rule';
|
||||
import { CustomThresholdAlertFields } from '../../types';
|
||||
import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart';
|
||||
import { RuleConditionChart } from '../../../rule_condition_chart/rule_condition_chart';
|
||||
import { CustomThresholdAlert } from '../types';
|
||||
import AlertDetailsAppSection from './alert_details_app_section';
|
||||
|
||||
|
@ -47,7 +47,7 @@ jest.mock('@kbn/observability-get-padded-alert-time-range-util', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../rule_condition_chart/rule_condition_chart', () => ({
|
||||
jest.mock('../../../rule_condition_chart/rule_condition_chart', () => ({
|
||||
RuleConditionChart: jest.fn(() => <div data-test-subj="RuleConditionChart" />),
|
||||
}));
|
||||
|
||||
|
|
|
@ -31,16 +31,16 @@ import type {
|
|||
import moment from 'moment';
|
||||
import { LOGS_EXPLORER_LOCATOR_ID, LogsExplorerLocatorParams } from '@kbn/deeplinks-observability';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
import { getGroupFilters } from '../../../../../common/custom_threshold_rule/helpers/get_group';
|
||||
import { useLicense } from '../../../../hooks/use_license';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { getGroupFilters } from '../../../../../common/custom_threshold_rule/helpers/get_group';
|
||||
import { metricValueFormatter } from '../../../../../common/custom_threshold_rule/metric_value_formatter';
|
||||
import { AlertSummaryField } from '../../../..';
|
||||
import { AlertParams } from '../../types';
|
||||
import { Threshold } from '../custom_threshold';
|
||||
import { CustomThresholdRule, CustomThresholdAlert } from '../types';
|
||||
import { LogRateAnalysis } from './log_rate_analysis';
|
||||
import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart';
|
||||
import { RuleConditionChart } from '../../../rule_condition_chart/rule_condition_chart';
|
||||
import { getViewInAppUrl } from '../../../../../common/custom_threshold_rule/get_view_in_app_url';
|
||||
import { SearchConfigurationWithExtractedReferenceType } from '../../../../../common/custom_threshold_rule/types';
|
||||
import { generateChartTitleAndTooltip } from './helpers/generate_chart_title_and_tooltip';
|
||||
|
|
|
@ -21,7 +21,7 @@ import Expressions from './custom_threshold_rule_expression';
|
|||
import { AlertParams, CustomThresholdPrefillOptions } from './types';
|
||||
|
||||
jest.mock('../../utils/kibana_react');
|
||||
jest.mock('./components/rule_condition_chart/rule_condition_chart', () => ({
|
||||
jest.mock('../rule_condition_chart/rule_condition_chart', () => ({
|
||||
RuleConditionChart: jest.fn(() => <div data-test-subj="RuleConditionChart" />),
|
||||
}));
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ import { TimeUnitChar } from '../../../common/utils/formatters/duration';
|
|||
import { AlertContextMeta, AlertParams, MetricExpression } from './types';
|
||||
import { ExpressionRow } from './components/expression_row';
|
||||
import { MetricsExplorerFields, GroupBy } from './components/group_by';
|
||||
import { RuleConditionChart as PreviewChart } from './components/rule_condition_chart/rule_condition_chart';
|
||||
import { RuleConditionChart as PreviewChart } from '../rule_condition_chart/rule_condition_chart';
|
||||
import { getSearchConfiguration } from './helpers/get_search_configuration';
|
||||
|
||||
const FILTER_TYPING_DEBOUNCE_MS = 500;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import {
|
||||
Aggregators,
|
||||
CustomThresholdExpressionMetric,
|
||||
} from '../../../../../common/custom_threshold_rule/types';
|
||||
} from '../../../common/custom_threshold_rule/types';
|
||||
import { getBufferThreshold, getLensOperationFromRuleMetric, lensFieldFormatter } from './helpers';
|
||||
const useCases = [
|
||||
[
|
|
@ -5,12 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
Aggregators,
|
||||
CustomThresholdExpressionMetric,
|
||||
} from '../../../../../common/custom_threshold_rule/types';
|
||||
import { Aggregators } from '../../../common/custom_threshold_rule/types';
|
||||
import { GenericMetric } from './rule_condition_chart';
|
||||
|
||||
export const getLensOperationFromRuleMetric = (metric: CustomThresholdExpressionMetric): string => {
|
||||
export const getLensOperationFromRuleMetric = (metric: GenericMetric): string => {
|
||||
const { aggType, field, filter } = metric;
|
||||
let operation: string = aggType;
|
||||
const operationArgs: string[] = [];
|
||||
|
@ -56,7 +54,7 @@ export const LensFieldFormat = {
|
|||
} as const;
|
||||
|
||||
export const lensFieldFormatter = (
|
||||
metrics: CustomThresholdExpressionMetric[]
|
||||
metrics: GenericMetric[]
|
||||
): typeof LensFieldFormat[keyof typeof LensFieldFormat] => {
|
||||
if (metrics.length < 1 || !metrics[0].field) return LensFieldFormat.NUMBER;
|
||||
const firstMetricField = metrics[0].field;
|
||||
|
@ -65,5 +63,5 @@ export const lensFieldFormatter = (
|
|||
return LensFieldFormat.NUMBER;
|
||||
};
|
||||
|
||||
export const isRate = (metrics: CustomThresholdExpressionMetric[]): boolean =>
|
||||
export const isRate = (metrics: GenericMetric[]): boolean =>
|
||||
Boolean(metrics.length > 0 && metrics[0].aggType === Aggregators.RATE);
|
|
@ -13,13 +13,12 @@ import { COMPARATORS } from '@kbn/alerting-comparators';
|
|||
import {
|
||||
Aggregators,
|
||||
CustomThresholdSearchSourceFields,
|
||||
} from '../../../../../common/custom_threshold_rule/types';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { kibanaStartMock } from '../../../../utils/kibana_react.mock';
|
||||
import { MetricExpression } from '../../types';
|
||||
import { RuleConditionChart } from './rule_condition_chart';
|
||||
} from '../../../common/custom_threshold_rule/types';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { kibanaStartMock } from '../../utils/kibana_react.mock';
|
||||
import { RuleConditionChart, RuleConditionChartExpressions } from './rule_condition_chart';
|
||||
|
||||
jest.mock('../../../../utils/kibana_react');
|
||||
jest.mock('../../utils/kibana_react');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mock;
|
||||
|
||||
|
@ -34,7 +33,7 @@ describe('Rule condition chart', () => {
|
|||
jest.clearAllMocks();
|
||||
mockKibana();
|
||||
});
|
||||
async function setup(expression: MetricExpression, dataView?: DataView) {
|
||||
async function setup(expression: RuleConditionChartExpressions, dataView?: DataView) {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleConditionChart
|
||||
metricExpression={expression}
|
||||
|
@ -58,7 +57,7 @@ describe('Rule condition chart', () => {
|
|||
}
|
||||
|
||||
it('should display no data message', async () => {
|
||||
const expression: MetricExpression = {
|
||||
const expression: RuleConditionChartExpressions = {
|
||||
metrics: [
|
||||
{
|
||||
name: 'A',
|
||||
|
@ -67,7 +66,6 @@ describe('Rule condition chart', () => {
|
|||
],
|
||||
timeSize: 1,
|
||||
timeUnit: 'm',
|
||||
sourceId: 'default',
|
||||
threshold: [1],
|
||||
comparator: COMPARATORS.GREATER_THAN_OR_EQUALS,
|
||||
};
|
|
@ -26,10 +26,12 @@ import { i18n } from '@kbn/i18n';
|
|||
import { TimeRange } from '@kbn/es-query';
|
||||
import { EventAnnotationConfig } from '@kbn/event-annotation-common';
|
||||
import { COMPARATORS } from '@kbn/alerting-comparators';
|
||||
import { EventsAsUnit } from '../../../../../common/constants';
|
||||
import { CustomThresholdSearchSourceFields } from '../../../../../common/custom_threshold_rule/types';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { MetricExpression } from '../../types';
|
||||
import { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
|
||||
import { TimeUnitChar } from '../../../common';
|
||||
import { LEGACY_COMPARATORS } from '../../../common/utils/convert_legacy_outside_comparator';
|
||||
import { EventsAsUnit } from '../../../common/constants';
|
||||
import { Aggregators } from '../../../common/custom_threshold_rule/types';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { AggMap, PainlessTinyMathParser } from './painless_tinymath_parser';
|
||||
import {
|
||||
lensFieldFormatter,
|
||||
|
@ -38,15 +40,38 @@ import {
|
|||
isRate,
|
||||
LensFieldFormat,
|
||||
} from './helpers';
|
||||
|
||||
interface ChartOptions {
|
||||
seriesType?: SeriesType;
|
||||
interval?: string;
|
||||
}
|
||||
|
||||
interface GenericSearchSourceFields extends SerializedSearchSourceFields {
|
||||
query?: Query;
|
||||
filter?: Array<Pick<Filter, 'meta' | 'query'>>;
|
||||
}
|
||||
|
||||
export type GenericAggType = Aggregators | 'custom';
|
||||
|
||||
export interface GenericMetric {
|
||||
aggType: GenericAggType;
|
||||
name: string;
|
||||
field?: string;
|
||||
filter?: string;
|
||||
}
|
||||
|
||||
export interface RuleConditionChartExpressions {
|
||||
metrics: GenericMetric[];
|
||||
threshold: number[];
|
||||
comparator: COMPARATORS | LEGACY_COMPARATORS;
|
||||
warningThreshold?: number[];
|
||||
warningComparator?: COMPARATORS | LEGACY_COMPARATORS;
|
||||
timeSize?: number;
|
||||
timeUnit?: TimeUnitChar;
|
||||
equation?: string;
|
||||
}
|
||||
interface RuleConditionChartProps {
|
||||
metricExpression: MetricExpression;
|
||||
searchConfiguration: CustomThresholdSearchSourceFields;
|
||||
metricExpression: RuleConditionChartExpressions;
|
||||
searchConfiguration: GenericSearchSourceFields;
|
||||
dataView?: DataView;
|
||||
groupBy?: string | string[];
|
||||
error?: IErrorObject;
|
||||
|
@ -76,11 +101,22 @@ export function RuleConditionChart({
|
|||
services: { lens },
|
||||
} = useKibana();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { metrics, timeSize, timeUnit, threshold, comparator, equation } = metricExpression;
|
||||
const {
|
||||
metrics,
|
||||
timeSize,
|
||||
timeUnit,
|
||||
threshold,
|
||||
comparator,
|
||||
equation,
|
||||
warningComparator,
|
||||
warningThreshold,
|
||||
} = metricExpression;
|
||||
const [attributes, setAttributes] = useState<LensAttributes>();
|
||||
const [aggMap, setAggMap] = useState<AggMap>();
|
||||
const [formula, setFormula] = useState<string>('');
|
||||
const [thresholdReferenceLine, setThresholdReferenceLine] = useState<XYReferenceLinesLayer[]>();
|
||||
const [warningThresholdReferenceLine, setWarningThresholdReferenceLine] =
|
||||
useState<XYReferenceLinesLayer[]>();
|
||||
const [alertAnnotation, setAlertAnnotation] = useState<XYByValueAnnotationsLayer>();
|
||||
const [chartLoading, setChartLoading] = useState<boolean>(false);
|
||||
const filters = [...(searchConfiguration.filter || []), ...additionalFilters];
|
||||
|
@ -98,13 +134,13 @@ export function RuleConditionChart({
|
|||
const paragraphElements = errorDiv.querySelectorAll('p');
|
||||
if (!paragraphElements || paragraphElements.length < 2) return;
|
||||
paragraphElements[0].innerText = i18n.translate(
|
||||
'xpack.observability.customThreshold.rule..charts.error_equation.title',
|
||||
'xpack.observability.ruleCondition.chart.error_equation.title',
|
||||
{
|
||||
defaultMessage: 'An error occurred while rendering the chart',
|
||||
}
|
||||
);
|
||||
paragraphElements[1].innerText = i18n.translate(
|
||||
'xpack.observability.customThreshold.rule..charts.error_equation.description',
|
||||
'xpack.observability.ruleCondition.chart.error_equation.description',
|
||||
{
|
||||
defaultMessage: 'Check the rule equation.',
|
||||
}
|
||||
|
@ -113,6 +149,77 @@ export function RuleConditionChart({
|
|||
});
|
||||
}, [chartLoading, attributes]);
|
||||
|
||||
// Build the warning threshold reference line
|
||||
useEffect(() => {
|
||||
if (!warningThreshold) {
|
||||
if (warningThresholdReferenceLine?.length) {
|
||||
setWarningThresholdReferenceLine([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const refLayers = [];
|
||||
if (
|
||||
warningComparator === COMPARATORS.NOT_BETWEEN ||
|
||||
(warningComparator === COMPARATORS.BETWEEN && warningThreshold.length === 2)
|
||||
) {
|
||||
const refLineStart = new XYReferenceLinesLayer({
|
||||
data: [
|
||||
{
|
||||
value: (warningThreshold[0] || 0).toString(),
|
||||
color: euiTheme.colors.warning,
|
||||
fill: warningComparator === COMPARATORS.NOT_BETWEEN ? 'below' : 'none',
|
||||
},
|
||||
],
|
||||
});
|
||||
const refLineEnd = new XYReferenceLinesLayer({
|
||||
data: [
|
||||
{
|
||||
value: (warningThreshold[1] || 0).toString(),
|
||||
color: euiTheme.colors.warning,
|
||||
fill: warningComparator === COMPARATORS.NOT_BETWEEN ? 'above' : 'none',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
refLayers.push(refLineStart, refLineEnd);
|
||||
} else {
|
||||
let fill: FillStyle = 'above';
|
||||
if (
|
||||
warningComparator === COMPARATORS.LESS_THAN ||
|
||||
warningComparator === COMPARATORS.LESS_THAN_OR_EQUALS
|
||||
) {
|
||||
fill = 'below';
|
||||
}
|
||||
const warningThresholdRefLine = new XYReferenceLinesLayer({
|
||||
data: [
|
||||
{
|
||||
value: (warningThreshold[0] || 0).toString(),
|
||||
color: euiTheme.colors.warning,
|
||||
fill,
|
||||
},
|
||||
],
|
||||
});
|
||||
// A transparent line to add extra buffer at the top of threshold
|
||||
const bufferRefLine = new XYReferenceLinesLayer({
|
||||
data: [
|
||||
{
|
||||
value: getBufferThreshold(warningThreshold[0]),
|
||||
color: 'transparent',
|
||||
fill,
|
||||
},
|
||||
],
|
||||
});
|
||||
refLayers.push(warningThresholdRefLine, bufferRefLine);
|
||||
}
|
||||
setWarningThresholdReferenceLine(refLayers);
|
||||
}, [
|
||||
warningThreshold,
|
||||
warningComparator,
|
||||
euiTheme.colors.warning,
|
||||
metrics,
|
||||
warningThresholdReferenceLine?.length,
|
||||
]);
|
||||
|
||||
// Build the threshold reference line
|
||||
useEffect(() => {
|
||||
if (!threshold) return;
|
||||
|
@ -225,7 +332,7 @@ export function RuleConditionChart({
|
|||
const baseLayer = {
|
||||
type: 'formula',
|
||||
value: formula,
|
||||
label: 'Custom Threshold',
|
||||
label: formula,
|
||||
groupBy,
|
||||
format: {
|
||||
id: formatId,
|
||||
|
@ -272,6 +379,9 @@ export function RuleConditionChart({
|
|||
const layers: Array<XYDataLayer | XYReferenceLinesLayer | XYByValueAnnotationsLayer> = [
|
||||
xyDataLayer,
|
||||
];
|
||||
if (warningThresholdReferenceLine) {
|
||||
layers.push(...warningThresholdReferenceLine);
|
||||
}
|
||||
if (thresholdReferenceLine) {
|
||||
layers.push(...thresholdReferenceLine);
|
||||
}
|
||||
|
@ -311,13 +421,14 @@ export function RuleConditionChart({
|
|||
timeSize,
|
||||
timeUnit,
|
||||
seriesType,
|
||||
warningThresholdReferenceLine,
|
||||
]);
|
||||
|
||||
if (
|
||||
!dataView ||
|
||||
!attributes ||
|
||||
error?.equation ||
|
||||
Object.keys(error?.metrics || {}).length !== 0 ||
|
||||
Object.keys(error?.metrics || error?.metric || {}).length !== 0 ||
|
||||
!timeSize ||
|
||||
!timeRange
|
||||
) {
|
||||
|
@ -329,7 +440,7 @@ export function RuleConditionChart({
|
|||
data-test-subj="thresholdRuleNoChartData"
|
||||
body={
|
||||
<FormattedMessage
|
||||
id="xpack.observability.customThreshold.rule..charts.noData.title"
|
||||
id="xpack.observability.customThreshold.rule.charts.noData.title"
|
||||
defaultMessage="No chart data available, check the rule {errorSourceField}"
|
||||
values={{
|
||||
errorSourceField:
|
||||
|
@ -345,12 +456,11 @@ export function RuleConditionChart({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<lens.EmbeddableComponent
|
||||
onLoad={setChartLoading}
|
||||
id="customThresholdPreviewChart"
|
||||
id="ruleConditionChart"
|
||||
style={{ height: 180 }}
|
||||
timeRange={timeRange}
|
||||
attributes={attributes}
|
|
@ -99,3 +99,6 @@ export { formatAlertEvaluationValue } from './utils/format_alert_evaluation_valu
|
|||
export { WithKueryAutocompletion } from './components/rule_kql_filter/with_kuery_autocompletion';
|
||||
export { AutocompleteField } from './components/rule_kql_filter/autocomplete_field';
|
||||
export { RuleFlyoutKueryBar } from './components/rule_kql_filter/kuery_bar';
|
||||
export { RuleConditionChart } from './components/rule_condition_chart/rule_condition_chart';
|
||||
export { getGroupFilters } from '../common/custom_threshold_rule/helpers/get_group';
|
||||
export type { GenericAggType } from './components/rule_condition_chart/rule_condition_chart';
|
||||
|
|
|
@ -29570,7 +29570,6 @@
|
|||
"xpack.observability.apmProgressiveLoadingDescription": "{technicalPreviewLabel} S'il faut charger les données de façon progressive pour les vues APM. Les données peuvent être demandées d'abord avec un taux d'échantillonnage inférieur, avec une précision plus faible mais des temps de réponse plus rapides, pendant que les données non échantillonnées se chargent en arrière-plan",
|
||||
"xpack.observability.apmServiceInventoryOptimizedSortingDescription": "{technicalPreviewLabel} Tri par défaut des pages d'inventaire et de stockage des services APM (pour les services hors Machine Learning), en fonction du nom de service.",
|
||||
"xpack.observability.apmTraceExplorerTabDescription": "{technicalPreviewLabel} Activer la fonctionnalité Explorateur de traces APM, qui vous permet de rechercher et d'inspecter les traces avec KQL ou EQL. {link}",
|
||||
"xpack.observability.customThreshold.rule..charts.noData.title": "Aucune donnée du graphique n'est disponible, vérifiez la règle {errorSourceField}",
|
||||
"xpack.observability.customThreshold.rule.aggregators.average": "{metric} moyen",
|
||||
"xpack.observability.customThreshold.rule.aggregators.cardinality": "Cardinalité de {metric}",
|
||||
"xpack.observability.customThreshold.rule.aggregators.max": "{metric} max.",
|
||||
|
@ -29683,8 +29682,6 @@
|
|||
"xpack.observability.customThreshold.alertChartTitle": "Résultat de l'équation pour ",
|
||||
"xpack.observability.customThreshold.alertDetails.logRateAnalysis.sectionTitle": "Analyse du taux de log",
|
||||
"xpack.observability.customThreshold.alertDetails.logRateAnalysisTitle": "Causes possibles et résolutions",
|
||||
"xpack.observability.customThreshold.rule..charts.error_equation.description": "Vérifiez l'équation de la règle.",
|
||||
"xpack.observability.customThreshold.rule..charts.error_equation.title": "Une erreur s'est produite lors de l'affichage du graphique",
|
||||
"xpack.observability.customThreshold.rule..charts.errorMessage": "Oups, un problème est survenu",
|
||||
"xpack.observability.customThreshold.rule..charts.noDataMessage": "Aucune donnée graphique disponible",
|
||||
"xpack.observability.customThreshold.rule..timeLabels.days": "jours",
|
||||
|
|
|
@ -29547,7 +29547,6 @@
|
|||
"xpack.observability.apmProgressiveLoadingDescription": "{technicalPreviewLabel} APMビューでデータのプログレッシブ読み込みを行うかどうか。サンプリングされていないデータをバックグラウンドで読み込みながら、最初は低いサンプリングレート、低い精度、高速の応答時間でデータを要求できます",
|
||||
"xpack.observability.apmServiceInventoryOptimizedSortingDescription": "{technicalPreviewLabel} サービス名によるデフォルトAPMサービスインベントリおよびストレージエクスプローラーページの並べ替え(機械学習が適用されていないサービス)。",
|
||||
"xpack.observability.apmTraceExplorerTabDescription": "{technicalPreviewLabel} APMトレースエクスプローラー機能を有効にし、KQLまたはEQLでトレースを検索、検査できます。{link}",
|
||||
"xpack.observability.customThreshold.rule..charts.noData.title": "グラフデータはありません。ルール{errorSourceField}を確認してください",
|
||||
"xpack.observability.customThreshold.rule.aggregators.average": "平均{metric}",
|
||||
"xpack.observability.customThreshold.rule.aggregators.cardinality": "{metric}のカーディナリティ",
|
||||
"xpack.observability.customThreshold.rule.aggregators.max": "最大{metric}",
|
||||
|
@ -29661,8 +29660,6 @@
|
|||
"xpack.observability.customThreshold.alertChartTitle": "式の結果 ",
|
||||
"xpack.observability.customThreshold.alertDetails.logRateAnalysis.sectionTitle": "ログレート分析",
|
||||
"xpack.observability.customThreshold.alertDetails.logRateAnalysisTitle": "考えられる原因と修正方法",
|
||||
"xpack.observability.customThreshold.rule..charts.error_equation.description": "ルール式を確認してください。",
|
||||
"xpack.observability.customThreshold.rule..charts.error_equation.title": "グラフの表示中にエラーが発生しました",
|
||||
"xpack.observability.customThreshold.rule..charts.errorMessage": "問題が発生しました",
|
||||
"xpack.observability.customThreshold.rule..charts.noDataMessage": "グラフデータがありません",
|
||||
"xpack.observability.customThreshold.rule..timeLabels.days": "日",
|
||||
|
|
|
@ -29587,7 +29587,6 @@
|
|||
"xpack.observability.apmProgressiveLoadingDescription": "{technicalPreviewLabel} 是否以渐进方式为 APM 视图加载数据。可以先以较低的采样速率请求数据,这样的准确性较低,但响应时间更快,同时在后台加载未采样数据",
|
||||
"xpack.observability.apmServiceInventoryOptimizedSortingDescription": "{technicalPreviewLabel} 默认 APM 服务库存和 Storage Explorer 页面排序(对于未应用 Machine Learning 的服务)将按服务名称排序。",
|
||||
"xpack.observability.apmTraceExplorerTabDescription": "{technicalPreviewLabel} 启用 APM Trace Explorer 功能,它允许您通过 KQL 或 EQL 搜索和检查跟踪。{link}",
|
||||
"xpack.observability.customThreshold.rule..charts.noData.title": "没有可用图表数据,请检查规则 {errorSourceField}",
|
||||
"xpack.observability.customThreshold.rule.aggregators.average": "平均值 {metric}",
|
||||
"xpack.observability.customThreshold.rule.aggregators.cardinality": "{metric} 的基数",
|
||||
"xpack.observability.customThreshold.rule.aggregators.max": "{metric} 最大值",
|
||||
|
@ -29701,8 +29700,6 @@
|
|||
"xpack.observability.customThreshold.alertChartTitle": "方程结果用于 ",
|
||||
"xpack.observability.customThreshold.alertDetails.logRateAnalysis.sectionTitle": "日志速率分析",
|
||||
"xpack.observability.customThreshold.alertDetails.logRateAnalysisTitle": "可能的原因和补救措施",
|
||||
"xpack.observability.customThreshold.rule..charts.error_equation.description": "检查规则方程。",
|
||||
"xpack.observability.customThreshold.rule..charts.error_equation.title": "渲染图表时出错",
|
||||
"xpack.observability.customThreshold.rule..charts.errorMessage": "哇哦,出问题了",
|
||||
"xpack.observability.customThreshold.rule..charts.noDataMessage": "没有可用图表数据",
|
||||
"xpack.observability.customThreshold.rule..timeLabels.days": "天",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue