[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
![Screenshot 2024-06-10 at 16 12
43](9d88d9b9-fe5d-4f8d-9e5a-538c52c58692)

- Rule creation flyout
![Screenshot 2024-06-10 at 16 13
18](8c9ca3b3-2fbf-4cfa-83c9-00278c5e8e77)
This commit is contained in:
Faisal Kanout 2024-06-20 14:16:16 +02:00 committed by GitHub
parent 1e1e35ba08
commit 85f12800bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 354 additions and 135 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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: {

View file

@ -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/**/*"

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = [
[

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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