Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Custom threshold rule] Use lens chart with annotations in alert details page #175513

Merged
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2913a41
reuse lens chart, add annotations
benakansara Jan 25, 2024
c50a95c
Merge branch 'main' into feat/use-lens-chart-for-alert-details
benakansara Jan 25, 2024
5dbc95c
resize actions menu
benakansara Jan 25, 2024
54385bc
fix filter query issue, error loading alert details page
benakansara Jan 25, 2024
7a596ad
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jan 25, 2024
ac70b09
Merge branch 'main' into feat/use-lens-chart-for-alert-details
benakansara Jan 25, 2024
a8dd3f9
fix tests, fix bug
benakansara Jan 26, 2024
edc8a8f
Merge branch 'main' into feat/use-lens-chart-for-alert-details
benakansara Jan 26, 2024
2ef2081
refactoring
benakansara Jan 26, 2024
f24c421
Merge branch 'main' into feat/use-lens-chart-for-alert-details
benakansara Jan 26, 2024
f99f5fb
make timerange non-optional
benakansara Jan 26, 2024
fd364fa
change xy_annotation_layer to xy_by_value_annotation_layer
benakansara Jan 30, 2024
1bf452a
Merge branch 'main' into feat/use-lens-chart-for-alert-details
benakansara Jan 30, 2024
c31fc97
Merge branch 'main' into feat/use-lens-chart-for-alert-details
benakansara Feb 1, 2024
a6fb1a7
PR feedback
benakansara Jan 31, 2024
68b92cb
fix annotation issue, refactoring
benakansara Feb 2, 2024
51eadcc
refactoring
benakansara Feb 2, 2024
2de9e0e
typo
benakansara Feb 2, 2024
b1d8c91
fix tests
benakansara Feb 2, 2024
fa3df0d
pr feedback
benakansara Feb 2, 2024
71753ca
Merge branch 'main' into feat/use-lens-chart-for-alert-details
benakansara Feb 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export const METRIC_ID = 'lnsMetric';

export const METRIC_TREND_LINE_ID = 'metricTrendline';
export const XY_REFERENCE_LINE_ID = 'referenceLine';
export const XY_ANNOTATIONS_ID = 'annotations';
export const XY_DATA_ID = 'data';
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export {
XYReferenceLinesLayer,
type XYReferenceLinesLayerConfig,
} from './xy_reference_lines_layer';

export {
XYByValueAnnotationsLayer,
type XYByValueAnnotationsLayerConfig,
} from './xy_by_value_annotation_layer';
export { FormulaColumn } from './columns/formula';
export { StaticColumn } from './columns/static';
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The attribute_builder will be deprecated in the future in favor of the config_builder created recently by the visualizations team.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@crespocarlos thanks for the heads up. I will create a separate issue to replace attribute_builder usage with config_builder in observability plugin.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created #176146

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @benakansara !

* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { SavedObjectReference } from '@kbn/core/server';
import type { DataView } from '@kbn/data-views-plugin/common';
import { EventAnnotationConfig } from '@kbn/event-annotation-common';
import type { FormBasedPersistedState, PersistedIndexPatternLayer } from '@kbn/lens-plugin/public';
import type { XYByValueAnnotationLayerConfig } from '@kbn/lens-plugin/public/visualizations/xy/types';
import type { ChartLayer } from '../../types';
import { getDefaultReferences } from '../../utils';
import { XY_ANNOTATIONS_ID } from '../constants';

export interface XYByValueAnnotationsLayerConfig {
annotations: EventAnnotationConfig[];
layerType?: typeof XY_ANNOTATIONS_ID;
/**
* It is possible to define a specific dataView for the layer. It will override the global chart one
**/
dataView?: DataView;
ignoreGlobalFilters?: boolean;
}

export class XYByValueAnnotationsLayer implements ChartLayer<XYByValueAnnotationLayerConfig> {
private layerConfig: XYByValueAnnotationsLayerConfig;

constructor(layerConfig: XYByValueAnnotationsLayerConfig) {
this.layerConfig = {
...layerConfig,
layerType: layerConfig.layerType ?? 'annotations',
};
}

getName(): string | undefined {
return this.layerConfig.annotations[0].label;
}

getLayer(layerId: string): FormBasedPersistedState['layers'] {
const baseLayer = { columnOrder: [], columns: {} } as PersistedIndexPatternLayer;
return {
[`${layerId}_annotation`]: baseLayer,
};
}

getReference(layerId: string, chartDataView: DataView): SavedObjectReference[] {
return getDefaultReferences(this.layerConfig.dataView ?? chartDataView, `${layerId}_reference`);
}

getLayerConfig(layerId: string): XYByValueAnnotationLayerConfig {
return {
layerId: `${layerId}_annotation`,
layerType: 'annotations',
annotations: this.layerConfig.annotations,
ignoreGlobalFilters: this.layerConfig.ignoreGlobalFilters || false,
indexPatternId: this.layerConfig.dataView?.id || '',
};
}

getDataView(): DataView | undefined {
return this.layerConfig.dataView;
}
}
1 change: 1 addition & 0 deletions packages/kbn-lens-embeddable-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export {
XYChart,
XYDataLayer,
XYReferenceLinesLayer,
XYByValueAnnotationsLayer,
METRIC_ID,
METRIC_TREND_LINE_ID,
XY_ID,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
buildCustomThresholdRule,
} from '../../mocks/custom_threshold_rule';
import { CustomThresholdAlertFields } from '../../types';
import { ExpressionChart } from '../expression_chart';
import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart';
import AlertDetailsAppSection, { CustomThresholdAlert } from './alert_details_app_section';
import { Groups } from './groups';
import { Tags } from './tags';
Expand All @@ -37,8 +37,8 @@ jest.mock('@kbn/observability-get-padded-alert-time-range-util', () => ({
}),
}));

jest.mock('../expression_chart', () => ({
ExpressionChart: jest.fn(() => <div data-test-subj="ExpressionChart" />),
jest.mock('../rule_condition_chart/rule_condition_chart', () => ({
RuleConditionChart: jest.fn(() => <div data-test-subj="RuleConditionChart" />),
}));

jest.mock('../../../../utils/kibana_react', () => ({
Expand Down Expand Up @@ -141,11 +141,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);
const alertDetailsAppSectionComponent = renderComponent();

expect(alertDetailsAppSectionComponent.getAllByTestId('ExpressionChart').length).toBe(3);
expect(mockedExpressionChart.mock.calls[0]).toMatchSnapshot();
expect(alertDetailsAppSectionComponent.getAllByTestId('RuleConditionChart').length).toBe(3);
expect(mockedRuleConditionChart.mock.calls[0]).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
* 2.0.
*/

import moment from 'moment';
import { DataViewBase, Query } from '@kbn/es-query';
import { Query } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
Expand All @@ -18,10 +17,8 @@ import {
EuiSpacer,
EuiText,
EuiTitle,
useEuiTheme,
} from '@elastic/eui';
import { Rule, RuleTypeParams } from '@kbn/alerting-plugin/common';
import { AlertAnnotation, AlertActiveTimeRangeAnnotation } from '@kbn/observability-alert-details';
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
import {
ALERT_END,
Expand All @@ -31,7 +28,6 @@ import {
TAGS,
} from '@kbn/rule-data-utils';
import { DataView } from '@kbn/data-views-plugin/common';
import { MetricsExplorerChartType } from '../../../../../common/custom_threshold_rule/types';
import { useLicense } from '../../../../hooks/use_license';
import { useKibana } from '../../../../utils/kibana_react';
import { metricValueFormatter } from '../../../../../common/custom_threshold_rule/metric_value_formatter';
Expand All @@ -41,21 +37,17 @@ import {
CustomThresholdAlertFields,
CustomThresholdRuleTypeParams,
} from '../../types';
import { ExpressionChart } from '../expression_chart';
import { TIME_LABELS } from '../criterion_preview_chart/criterion_preview_chart';
import { Threshold } from '../custom_threshold';
import { LogRateAnalysis } from './log_rate_analysis';
import { Groups } from './groups';
import { Tags } from './tags';
import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart';

// TODO Use a generic props for app sections https://github.com/elastic/kibana/issues/152690
export type CustomThresholdRule = Rule<CustomThresholdRuleTypeParams>;
export type CustomThresholdAlert = TopAlert<CustomThresholdAlertFields>;

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: CustomThresholdAlert;
rule: CustomThresholdRule;
Expand All @@ -71,38 +63,20 @@ export default function AlertDetailsAppSection({
setAlertSummaryFields,
}: AppSectionProps) {
const services = useKibana().services;
const { uiSettings, charts, data } = services;
const { euiTheme } = useEuiTheme();
const { charts, data } = services;
const { hasAtLeast } = useLicense();
const hasLogRateAnalysisLicense = hasAtLeast('platinum');
const [dataView, setDataView] = useState<DataView>();
const [filterQuery, setFilterQuery] = useState<string>('');
const [, setDataViewError] = useState<Error>();
const ruleParams = rule.params as RuleTypeParams & AlertParams;
const chartProps = {
baseTheme: charts.theme.useChartsBaseTheme(),
};
const timeRange = getPaddedAlertTimeRange(alert.fields[ALERT_START]!, alert.fields[ALERT_END]);
const alertEnd = alert.fields[ALERT_END] ? moment(alert.fields[ALERT_END]).valueOf() : undefined;
const groups = alert.fields[ALERT_GROUP];
const tags = alert.fields[TAGS];

const annotations = [
maryam-saeidi marked this conversation as resolved.
Show resolved Hide resolved
<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}
/>,
];

useEffect(() => {
const alertSummaryFields = [];
if (groups) {
Expand Down Expand Up @@ -144,13 +118,14 @@ export default function AlertDetailsAppSection({
setAlertSummaryFields(alertSummaryFields);
}, [groups, tags, rule, ruleLink, setAlertSummaryFields]);

const derivedIndexPattern = useMemo<DataViewBase>(
() => ({
fields: dataView?.fields || [],
title: dataView?.getIndexPattern() || 'unknown-index',
}),
[dataView]
);
useEffect(() => {
let query = `${(ruleParams.searchConfiguration?.query as Query)?.query as string}`;
maryam-saeidi marked this conversation as resolved.
Show resolved Hide resolved
if (groups) {
const groupQueries = groups?.map(({ field, value }) => `${field}: ${value}`).join(' and ');
query = query ? `(${query}) and ${groupQueries}` : groupQueries;
}
setFilterQuery(query);
}, [groups, ruleParams.searchConfiguration]);

useEffect(() => {
const initDataView = async () => {
Expand Down Expand Up @@ -209,14 +184,15 @@ export default function AlertDetailsAppSection({
/>
</EuiFlexItem>
<EuiFlexItem grow={5}>
<ExpressionChart
annotations={annotations}
chartType={MetricsExplorerChartType.line}
derivedIndexPattern={derivedIndexPattern}
expression={criterion}
filterQuery={(ruleParams.searchConfiguration?.query as Query)?.query as string}
<RuleConditionChart
metricExpression={criterion}
dataView={dataView}
filterQuery={filterQuery}
groupBy={ruleParams.groupBy}
hideTitle
annotation={{
timestamp: alert.fields[ALERT_START],
maryam-saeidi marked this conversation as resolved.
Show resolved Hide resolved
endTimestamp: alert.fields[ALERT_END],
}}
timeRange={timeRange}
/>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Comparator, Aggregators } from '../../../../../common/custom_threshold_
import { useKibana } from '../../../../utils/kibana_react';
import { kibanaStartMock } from '../../../../utils/kibana_react.mock';
import { MetricExpression } from '../../types';
import { getBufferThreshold, PreviewChart } from './preview_chart';
import { getBufferThreshold, RuleConditionChart } from './rule_condition_chart';

jest.mock('../../../../utils/kibana_react');

Expand All @@ -25,19 +25,20 @@ const mockKibana = () => {
});
};

describe('Preview chart', () => {
describe('Rule condition chart', () => {
beforeEach(() => {
jest.clearAllMocks();
mockKibana();
});
async function setup(expression: MetricExpression, dataView?: DataView) {
const wrapper = mountWithIntl(
<PreviewChart
<RuleConditionChart
metricExpression={expression}
dataView={dataView}
filterQuery={''}
groupBy={[]}
error={{}}
timeRange={{ from: 'now-15m', to: 'now' }}
/>
);

Expand Down
Loading