[AO] Fix showing temporary data view in the list of data views for the new threshold rule (#161462)

Closes #159774, closes #159778, closes #159779, closes #159776

## Summary

This PR fixes the data view list in the new metric threshold. Also, it
adds an API integration test to check the reference in the rule saved
object.

Also, this PR improves the error handling and loading of the data view,
similar to what we have for elasticsearch rule data view.
|Threshold rule error|Elasticsearch query error|
|---|---|

|![image](1f65ff6b-18f3-4a4d-941b-cafe35c4d145)|

## How to test
- Make sure `xpack.observability.unsafe.thresholdRule.enabled` is set to
true in kibana yml config

Error handling
- Throw an error
[here](https://github.com/elastic/kibana/pull/161462/files#diff-4f65f6debaf6457d4b0400a27c1ea57ba52bfe4426ee40460d43a857c5bd165eL98)
and make sure the message is shown correctly in the rule.

Temporary data view
- Create a temporary data view
- Check the list of data views and make sure the new temporary one is
added to the list

|Adding a new temporary data view|Temporary data view in the list|
|---|---|

|![image](9109308e-88d4-49f7-98b1-19f635804c48)|
This commit is contained in:
Maryam Saeidi 2023-07-13 09:26:50 +02:00 committed by GitHub
parent 7c333cdc33
commit ac4635417f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 228 additions and 29 deletions

View file

@ -5,18 +5,18 @@
* 2.0.
*/
import { useKibana } from '../../../utils/kibana_react';
import { kibanaStartMock } from '../../../utils/kibana_react.mock';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import React from 'react';
import { act } from 'react-dom/test-utils';
import Expressions from './expression';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { MetricsExplorerMetric } from '../../../../common/threshold_rule/metrics_explorer';
import { Comparator } from '../../../../common/threshold_rule/types';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
jest.mock('../../../utils/kibana_react');
import { Comparator } from '../../../common/threshold_rule/types';
import { MetricsExplorerMetric } from '../../../common/threshold_rule/metrics_explorer';
import { useKibana } from '../../utils/kibana_react';
import { kibanaStartMock } from '../../utils/kibana_react.mock';
import Expressions from './threshold_rule_expression';
jest.mock('../../utils/kibana_react');
const useKibanaMock = useKibana as jest.Mock;
@ -106,4 +106,44 @@ describe('Expression', () => {
},
]);
});
it('should show the error message', async () => {
const currentOptions = {
groupBy: 'host.hostname',
filterQuery: 'foo',
metrics: [
{ aggregation: 'avg', field: 'system.load.1' },
{ aggregation: 'cardinality', field: 'system.cpu.user.pct' },
] as MetricsExplorerMetric[],
};
const errorMessage = 'Error in searchSource create';
const kibanaMock = kibanaStartMock.startContract();
useKibanaMock.mockReturnValue({
...kibanaMock,
services: {
...kibanaMock.services,
data: {
dataViews: {
create: jest.fn(),
},
query: {
timefilter: {
timefilter: jest.fn(),
},
},
search: {
searchSource: {
create: jest.fn(() => {
throw new Error(errorMessage);
}),
},
},
},
},
});
const { wrapper } = await setup(currentOptions);
expect(wrapper.find(`[data-test-subj="thresholdRuleExpressionError"]`).first().text()).toBe(
errorMessage
);
});
});

View file

@ -6,14 +6,18 @@
*/
import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { debounce } from 'lodash';
import {
EuiAccordion,
EuiButtonEmpty,
EuiCallOut,
EuiCheckbox,
EuiEmptyPrompt,
EuiFieldSearch,
EuiFormRow,
EuiIcon,
EuiLink,
EuiLoadingSpinner,
EuiPanel,
EuiSpacer,
EuiText,
@ -25,23 +29,24 @@ import { DataViewBase } from '@kbn/es-query';
import { DataViewSelectPopover } from '@kbn/stack-alerts-plugin/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { debounce } from 'lodash';
import {
ForLastExpression,
IErrorObject,
RuleTypeParams,
RuleTypeParamsExpressionProps,
} from '@kbn/triggers-actions-ui-plugin/public';
import { useKibana } from '../../../utils/kibana_react';
import { Aggregators, Comparator, QUERY_INVALID } from '../../../../common/threshold_rule/types';
import { TimeUnitChar } from '../../../../common/utils/formatters/duration';
import { AlertContextMeta, AlertParams, MetricExpression } from '../types';
import { ExpressionChart } from './expression_chart';
import { ExpressionRow } from './expression_row';
import { MetricsExplorerKueryBar } from './kuery_bar';
import { MetricsExplorerOptions } from '../hooks/use_metrics_explorer_options';
import { convertKueryToElasticSearchQuery } from '../helpers/kuery';
import { MetricsExplorerGroupBy } from './group_by';
import { useKibana } from '../../utils/kibana_react';
import { Aggregators, Comparator, QUERY_INVALID } from '../../../common/threshold_rule/types';
import { TimeUnitChar } from '../../../common/utils/formatters/duration';
import { AlertContextMeta, AlertParams, MetricExpression } from './types';
import { ExpressionChart } from './components/expression_chart';
import { ExpressionRow } from './components/expression_row';
import { MetricsExplorerKueryBar } from './components/kuery_bar';
import { MetricsExplorerGroupBy } from './components/group_by';
import { MetricsExplorerOptions } from './hooks/use_metrics_explorer_options';
import { convertKueryToElasticSearchQuery } from './helpers/kuery';
const FILTER_TYPING_DEBOUNCE_MS = 500;
type Props = Omit<
@ -66,6 +71,7 @@ export default function Expressions(props: Props) {
const [timeUnit, setTimeUnit] = useState<TimeUnitChar | undefined>('m');
const [dataView, setDataView] = useState<DataView>();
const [searchSource, setSearchSource] = useState<ISearchSource>();
const [paramsError, setParamsError] = useState<Error>();
const derivedIndexPattern = useMemo<DataViewBase>(
() => ({
fields: dataView?.fields || [],
@ -97,8 +103,7 @@ export default function Expressions(props: Props) {
setSearchSource(createdSearchSource);
setDataView(createdSearchSource.getField('index'));
} catch (error) {
// TODO Handle error
console.log('error:', error);
setParamsError(error);
}
};
@ -335,11 +340,32 @@ export default function Expressions(props: Props) {
.filter((g) => typeof g === 'string') as string[];
}, [ruleParams, groupByFilterTestPatterns]);
if (paramsError) {
return (
<>
<EuiCallOut color="danger" iconType="warning" data-test-subj="thresholdRuleExpressionError">
<p>{paramsError.message}</p>
</EuiCallOut>
<EuiSpacer size={'m'} />
</>
);
}
if (!searchSource) {
return (
<>
<EuiEmptyPrompt title={<EuiLoadingSpinner size="xl" />} />
<EuiSpacer size={'m'} />
</>
);
}
return (
<>
<DataViewSelectPopover
dependencies={{ dataViews, dataViewEditor }}
dataView={dataView}
metadata={{ adHocDataViewList: metadata?.adHocDataViewList || [] }}
onSelectDataView={onSelectDataView}
onChangeMetaData={({ adHocDataViewList }) => {
onChangeMetaData({ ...metadata, adHocDataViewList });

View file

@ -72,7 +72,9 @@ const data = {
},
},
search: {
searchSource: jest.fn(),
searchSource: {
create: jest.fn(),
},
},
};
},

View file

@ -93,7 +93,7 @@ export const registerObservabilityRuleTypes = (
documentationUrl(docLinks) {
return `${docLinks.links.observability.threshold}`;
},
ruleParamsExpression: lazy(() => import('../components/threshold/components/expression')),
ruleParamsExpression: lazy(() => import('../components/threshold/threshold_rule_expression')),
validate: validateMetricThreshold,
defaultActionMessage: i18n.translate(
'xpack.observability.threshold.rule.alerting.threshold.defaultActionMessage',

View file

@ -11,6 +11,7 @@ export default function ({ loadTestFile }: any) {
describe('MetricsUI Endpoints', () => {
loadTestFile(require.resolve('./metric_threshold_rule'));
loadTestFile(require.resolve('./threshold_rule'));
loadTestFile(require.resolve('./threshold_rule_data_view'));
});
describe('Synthetics', () => {

View file

@ -4,12 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/*
* 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 moment from 'moment';
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';

View file

@ -0,0 +1,136 @@
/*
* 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 expect from '@kbn/expect';
import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types';
import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants';
import { FtrProviderContext } from '../common/ftr_provider_context';
import { getUrlPrefix, ObjectRemover } from '../common/lib';
import { createRule } from './helpers/alerting_api_helper';
import { createDataView, deleteDataView } from './helpers/data_view';
// eslint-disable-next-line import/no-default-export
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const objectRemover = new ObjectRemover(supertest);
const es = getService('es');
describe('Threshold rule data view >', () => {
const DATA_VIEW_ID = 'data-view-id';
let ruleId: string;
const searchRule = () =>
es.search<{ references: unknown; alert: { params: any } }>({
index: '.kibana*',
query: {
bool: {
filter: [
{
term: {
_id: `alert:${ruleId}`,
},
},
],
},
},
fields: ['alert.params', 'references'],
});
before(async () => {
await createDataView({
supertest,
name: 'test-data-view',
id: DATA_VIEW_ID,
title: 'random-index*',
});
});
after(async () => {
objectRemover.removeAll();
await deleteDataView({
supertest,
id: DATA_VIEW_ID,
});
});
describe('save data view in rule correctly', () => {
it('create a threshold rule', async () => {
const createdRule = await createRule({
supertest,
tags: ['observability'],
consumer: 'alerts',
name: 'Threshold rule',
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
params: {
criteria: [
{
aggType: Aggregators.CUSTOM,
comparator: Comparator.GT,
threshold: [7500000],
timeSize: 5,
timeUnit: 'm',
customMetrics: [
{ name: 'A', field: 'span.self_time.sum.us', aggType: Aggregators.AVERAGE },
],
},
],
alertOnNoData: true,
alertOnGroupDisappear: true,
searchConfiguration: {
query: {
query: '',
language: 'kuery',
},
index: DATA_VIEW_ID,
},
},
actions: [],
});
ruleId = createdRule.id;
expect(ruleId).not.to.be(undefined);
});
it('should have correct data view reference before and after edit', async () => {
const {
hits: { hits: alertHitsV1 },
} = await searchRule();
await supertest
.post(`${getUrlPrefix('default')}/internal/alerting/rules/_bulk_edit`)
.set('kbn-xsrf', 'foo')
.send({
ids: [ruleId],
operations: [{ operation: 'set', field: 'apiKey' }],
})
.expect(200);
objectRemover.add('default', ruleId, 'rule', 'alerting');
const {
hits: { hits: alertHitsV2 },
} = await searchRule();
expect(alertHitsV1[0]?._source?.references).to.eql([
{
name: 'param:kibanaSavedObjectMeta.searchSourceJSON.index',
type: 'index-pattern',
id: 'data-view-id',
},
]);
expect(alertHitsV1[0]?._source?.alert?.params?.searchConfiguration).to.eql({
query: { query: '', language: 'kuery' },
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
});
expect(alertHitsV1[0].fields).to.eql(alertHitsV2[0].fields);
expect(alertHitsV1[0]?._source?.references ?? true).to.eql(
alertHitsV2[0]?._source?.references ?? false
);
});
});
});
}