[AO] Add data view to the new threshold rule (#159479)

Closes #158840

## Summary

This PR adds selecting a persisted data view and using it in the new
threshold rule.

|Flyout|Rule saved object|
|---|---|

|![image](6f1115e2-e1f1-4348-b380-18b7ce2cacba)|

## 🧪 How to test
- Create a threshold rule with a persisted data view
- Make sure the related feature flag is configured:
`xpack.observability.unsafe.thresholdRule.enabled: true`
- Check whether the triggered alert matches the expectation related to
that data view
- Check the rule saved object to ensure data is saved there correctly

## What is not covered in this PR
I will follow up on the following list in future PRs:
- [Temporary data view](https://github.com/elastic/kibana/issues/159774)
- [Initial loading](https://github.com/elastic/kibana/issues/159779)
- [Setting a timeField beside the
timestamp](https://github.com/elastic/kibana/issues/159777)
- [Error handling](https://github.com/elastic/kibana/issues/159776)
- [Testing](https://github.com/elastic/kibana/issues/159778)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Maryam Saeidi 2023-06-21 13:25:51 +02:00 committed by GitHub
parent a705225f6f
commit 87b80cb21b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 231 additions and 659 deletions

View file

@ -6,7 +6,6 @@
*/
import * as rt from 'io-ts';
import { indexPatternRt } from '@kbn/io-ts-utils';
import { ML_ANOMALY_THRESHOLD } from '@kbn/ml-anomaly-utils/anomaly_threshold';
import { values } from 'lodash';
import { Color } from './color_palette';
@ -94,68 +93,14 @@ export const logDataViewReferenceRT = rt.type({
dataViewId: rt.string,
});
export type LogDataViewReference = rt.TypeOf<typeof logDataViewReferenceRT>;
// Index name
export const logIndexNameReferenceRT = rt.type({
type: rt.literal('index_name'),
indexName: rt.string,
});
export type LogIndexNameReference = rt.TypeOf<typeof logIndexNameReferenceRT>;
export const logIndexReferenceRT = rt.union([logDataViewReferenceRT, logIndexNameReferenceRT]);
/**
* Properties that represent a full source configuration, which is the result of merging static values with
* saved values.
*/
const SourceConfigurationFieldsRT = rt.type({
message: rt.array(rt.string),
});
export const SourceConfigurationRT = rt.type({
name: rt.string,
description: rt.string,
metricAlias: rt.string,
logIndices: logIndexReferenceRT,
inventoryDefaultView: rt.string,
metricsExplorerDefaultView: rt.string,
fields: SourceConfigurationFieldsRT,
logColumns: rt.array(SourceConfigurationColumnRuntimeType),
anomalyThreshold: rt.number,
});
export const metricsSourceConfigurationPropertiesRT = rt.strict({
name: SourceConfigurationRT.props.name,
description: SourceConfigurationRT.props.description,
metricAlias: SourceConfigurationRT.props.metricAlias,
inventoryDefaultView: SourceConfigurationRT.props.inventoryDefaultView,
metricsExplorerDefaultView: SourceConfigurationRT.props.metricsExplorerDefaultView,
anomalyThreshold: rt.number,
});
export type MetricsSourceConfigurationProperties = rt.TypeOf<
typeof metricsSourceConfigurationPropertiesRT
>;
export const partialMetricsSourceConfigurationReqPayloadRT = rt.partial({
...metricsSourceConfigurationPropertiesRT.type.props,
metricAlias: indexPatternRt,
});
export const partialMetricsSourceConfigurationPropertiesRT = rt.partial({
...metricsSourceConfigurationPropertiesRT.type.props,
});
export type PartialMetricsSourceConfigurationProperties = rt.TypeOf<
typeof partialMetricsSourceConfigurationPropertiesRT
>;
const metricsSourceConfigurationOriginRT = rt.keyof({
fallback: null,
internal: null,
stored: null,
});
/**
* Source status
*/
@ -180,32 +125,6 @@ export const metricsSourceStatusRT = rt.strict({
export type MetricsSourceStatus = rt.TypeOf<typeof metricsSourceStatusRT>;
export const metricsSourceConfigurationRT = rt.exact(
rt.intersection([
rt.type({
id: rt.string,
origin: metricsSourceConfigurationOriginRT,
configuration: metricsSourceConfigurationPropertiesRT,
}),
rt.partial({
updatedAt: rt.number,
version: rt.string,
status: metricsSourceStatusRT,
}),
])
);
export type MetricsSourceConfiguration = rt.TypeOf<typeof metricsSourceConfigurationRT>;
export type PartialMetricsSourceConfiguration = DeepPartial<MetricsSourceConfiguration>;
export const metricsSourceConfigurationResponseRT = rt.type({
source: metricsSourceConfigurationRT,
});
export type MetricsSourceConfigurationResponse = rt.TypeOf<
typeof metricsSourceConfigurationResponseRT
>;
export enum Comparator {
GT = '>',
LT = '<',

View file

@ -13,6 +13,7 @@
"charts",
"data",
"dataViews",
"dataViewEditor",
"embeddable",
"exploratoryView",
"features",
@ -29,7 +30,7 @@
"visualizations"
],
"optionalPlugins": ["discover", "home", "licensing", "usageCollection", "cloud", "spaces"],
"requiredBundles": ["data", "kibanaReact", "kibanaUtils", "unifiedSearch", "cloudChat", "spaces"],
"requiredBundles": ["data", "kibanaReact", "kibanaUtils", "unifiedSearch", "cloudChat", "stackAlerts", "spaces"],
"extraPublicDirs": ["common"]
}
}

View file

@ -19,7 +19,7 @@ Array [
"chartType": "line",
"derivedIndexPattern": Object {
"fields": Array [],
"title": "metricbeat-*",
"title": "unknown-index",
},
"expression": Object {
"aggType": "count",
@ -35,9 +35,6 @@ Array [
"host.hostname",
],
"hideTitle": true,
"source": Object {
"id": "default",
},
"timeRange": Object {
"from": "2023-03-28T10:43:13.802Z",
"to": "2023-03-29T13:14:09.581Z",

View file

@ -16,7 +16,7 @@ import {
buildMetricThresholdAlert,
buildMetricThresholdRule,
} from '../mocks/metric_threshold_rule';
import { AlertDetailsAppSection } from './alert_details_app_section';
import AlertDetailsAppSection from './alert_details_app_section';
import { ExpressionChart } from './expression_chart';
const mockedChartStartContract = chartPluginMock.createStartContract();
@ -43,14 +43,6 @@ jest.mock('../../../utils/kibana_react', () => ({
}),
}));
jest.mock('../helpers/source', () => ({
withSourceProvider: () => jest.fn,
useSourceContext: () => ({
source: { id: 'default' },
createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }),
}),
}));
describe('AlertDetailsAppSection', () => {
const queryClient = new QueryClient();
const mockedSetAlertSummaryFields = jest.fn();

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { DataViewBase } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useEffect, useMemo } from 'react';
@ -35,7 +36,6 @@ import { ExpressionChart } from './expression_chart';
import { TIME_LABELS } from './criterion_preview_chart/criterion_preview_chart';
import { Threshold } from './threshold';
import { MetricsExplorerChartType } from '../hooks/use_metrics_explorer_options';
import { useSourceContext, withSourceProvider } from '../helpers/source';
import { MetricThresholdRuleTypeParams } from '../types';
// TODO Use a generic props for app sections https://github.com/elastic/kibana/issues/152690
@ -58,19 +58,23 @@ interface AppSectionProps {
setAlertSummaryFields: React.Dispatch<React.SetStateAction<AlertSummaryField[] | undefined>>;
}
export function AlertDetailsAppSection({
// eslint-disable-next-line import/no-default-export
export default function AlertDetailsAppSection({
alert,
rule,
ruleLink,
setAlertSummaryFields,
}: AppSectionProps) {
const { uiSettings, charts } = useKibana().services;
const { source, createDerivedIndexPattern } = useSourceContext();
const { euiTheme } = useEuiTheme();
const derivedIndexPattern = useMemo(
() => createDerivedIndexPattern(),
[createDerivedIndexPattern]
// TODO Use rule data view
const derivedIndexPattern = useMemo<DataViewBase>(
() => ({
fields: [],
title: 'unknown-index',
}),
[]
);
const chartProps = {
theme: charts.theme.useChartsTheme(),
@ -162,7 +166,6 @@ export function AlertDetailsAppSection({
filterQuery={rule.params.filterQueryText}
groupBy={rule.params.groupBy}
hideTitle
source={source}
timeRange={timeRange}
/>
</EuiFlexItem>
@ -173,5 +176,3 @@ export function AlertDetailsAppSection({
</EuiFlexGroup>
) : null;
}
// eslint-disable-next-line import/no-default-export
export default withSourceProvider<AppSectionProps>(AlertDetailsAppSection)('default');

View file

@ -5,34 +5,35 @@
* 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';
// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock`
import { coreMock as mockCoreMock } from '@kbn/core/public/mocks';
import { Expressions } from './expression';
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';
jest.mock('../helpers/source', () => ({
withSourceProvider: () => jest.fn,
useSourceContext: () => ({
source: { id: 'default' },
createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }),
}),
}));
jest.mock('../../../utils/kibana_react');
jest.mock('../../../utils/kibana_react', () => ({
useKibana: () => ({
services: mockCoreMock.createStart(),
}),
}));
const useKibanaMock = useKibana as jest.Mock;
const mockKibana = () => {
useKibanaMock.mockReturnValue({
...kibanaStartMock.startContract(),
});
};
const dataViewMock = dataViewPluginMocks.createStartContract();
describe('Expression', () => {
beforeEach(() => {
jest.clearAllMocks();
mockKibana();
});
async function setup(currentOptions: {
metrics?: MetricsExplorerMetric[];
filterQuery?: string;
@ -43,6 +44,7 @@ describe('Expression', () => {
groupBy: undefined,
filterQueryText: '',
sourceId: 'default',
searchConfiguration: {},
};
const wrapper = mountWithIntl(
<Expressions
@ -55,8 +57,10 @@ describe('Expression', () => {
setRuleProperty={() => {}}
metadata={{
currentOptions,
adHocDataViewList: [],
}}
dataViews={dataViewMock}
onChangeMetaData={jest.fn()}
/>
);

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
import {
EuiAccordion,
@ -18,6 +19,10 @@ import {
EuiText,
EuiToolTip,
} from '@elastic/eui';
import { ISearchSource } from '@kbn/data-plugin/common';
import { DataView } from '@kbn/data-views-plugin/common';
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';
@ -36,13 +41,12 @@ import { ExpressionRow } from './expression_row';
import { MetricsExplorerKueryBar } from './kuery_bar';
import { MetricsExplorerOptions } from '../hooks/use_metrics_explorer_options';
import { convertKueryToElasticSearchQuery } from '../helpers/kuery';
import { useSourceContext, withSourceProvider } from '../helpers/source';
import { MetricsExplorerGroupBy } from './group_by';
const FILTER_TYPING_DEBOUNCE_MS = 500;
type Props = Omit<
RuleTypeParamsExpressionProps<RuleTypeParams & AlertParams, AlertContextMeta>,
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch' | 'onChangeMetaData'
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch'
>;
export const defaultExpression = {
@ -53,18 +57,55 @@ export const defaultExpression = {
timeUnit: 'm',
} as MetricExpression;
export function Expressions(props: Props) {
const { setRuleParams, ruleParams, errors, metadata } = props;
const { docLinks } = useKibana().services;
const { source, createDerivedIndexPattern } = useSourceContext();
// eslint-disable-next-line import/no-default-export
export default function Expressions(props: Props) {
const { setRuleParams, ruleParams, errors, metadata, onChangeMetaData } = props;
const { data, dataViews, dataViewEditor, docLinks } = useKibana().services;
const [timeSize, setTimeSize] = useState<number | undefined>(1);
const [timeUnit, setTimeUnit] = useState<TimeUnitChar | undefined>('m');
const derivedIndexPattern = useMemo(
() => createDerivedIndexPattern(),
[createDerivedIndexPattern]
const [dataView, setDataView] = useState<DataView>();
const [searchSource, setSearchSource] = useState<ISearchSource>();
const derivedIndexPattern = useMemo<DataViewBase>(
() => ({
fields: dataView?.fields || [],
title: dataView?.getIndexPattern() || 'unknown-index',
}),
[dataView]
);
useEffect(() => {
const initSearchSource = async () => {
let initialSearchConfiguration = ruleParams.searchConfiguration;
if (!ruleParams.searchConfiguration) {
const newSearchSource = data.search.searchSource.createEmpty();
newSearchSource.setField('query', data.query.queryString.getDefaultQuery());
const defaultDataView = await data.dataViews.getDefaultDataView();
if (defaultDataView) {
newSearchSource.setField('index', defaultDataView);
setDataView(defaultDataView);
}
initialSearchConfiguration = newSearchSource.getSerializedFields();
}
try {
const createdSearchSource = await data.search.searchSource.create(
initialSearchConfiguration
);
setRuleParams('searchConfiguration', initialSearchConfiguration);
setSearchSource(createdSearchSource);
setDataView(createdSearchSource.getField('index'));
} catch (error) {
// TODO Handle error
console.log('error:', error);
}
};
initSearchSource();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data.search.searchSource, data.dataViews]);
const options = useMemo<MetricsExplorerOptions>(() => {
if (metadata?.currentOptions?.metrics) {
return metadata.currentOptions as MetricsExplorerOptions;
@ -76,31 +117,49 @@ export function Expressions(props: Props) {
}
}, [metadata]);
const onSelectDataView = useCallback(
(newDataView: DataView) => {
const ruleCriteria = (ruleParams.criteria ? ruleParams.criteria.slice() : []).map(
(criterion) => {
criterion.customMetrics?.forEach((metric) => {
metric.field = undefined;
});
return criterion;
}
);
setRuleParams('criteria', ruleCriteria);
searchSource?.setParent(undefined).setField('index', newDataView);
setRuleParams('searchConfiguration', searchSource?.getSerializedFields());
setDataView(newDataView);
},
[ruleParams.criteria, searchSource, setRuleParams]
);
const updateParams = useCallback(
(id, e: MetricExpression) => {
const exp = ruleParams.criteria ? ruleParams.criteria.slice() : [];
exp[id] = e;
setRuleParams('criteria', exp);
const ruleCriteria = ruleParams.criteria ? ruleParams.criteria.slice() : [];
ruleCriteria[id] = e;
setRuleParams('criteria', ruleCriteria);
},
[setRuleParams, ruleParams.criteria]
);
const addExpression = useCallback(() => {
const exp = ruleParams.criteria?.slice() || [];
exp.push({
const ruleCriteria = ruleParams.criteria?.slice() || [];
ruleCriteria.push({
...defaultExpression,
timeSize: timeSize ?? defaultExpression.timeSize,
timeUnit: timeUnit ?? defaultExpression.timeUnit,
});
setRuleParams('criteria', exp);
setRuleParams('criteria', ruleCriteria);
}, [setRuleParams, ruleParams.criteria, timeSize, timeUnit]);
const removeExpression = useCallback(
(id: number) => {
const exp = ruleParams.criteria?.slice() || [];
if (exp.length > 1) {
exp.splice(id, 1);
setRuleParams('criteria', exp);
const ruleCriteria = ruleParams.criteria?.slice() || [];
if (ruleCriteria.length > 1) {
ruleCriteria.splice(id, 1);
setRuleParams('criteria', ruleCriteria);
}
},
[setRuleParams, ruleParams.criteria]
@ -143,26 +202,25 @@ export function Expressions(props: Props) {
const updateTimeSize = useCallback(
(ts: number | undefined) => {
const criteria =
const ruleCriteria =
ruleParams.criteria?.map((c) => ({
...c,
timeSize: ts,
})) || [];
setTimeSize(ts || undefined);
setRuleParams('criteria', criteria);
setRuleParams('criteria', ruleCriteria);
},
[ruleParams.criteria, setRuleParams]
);
const updateTimeUnit = useCallback(
(tu: string) => {
const criteria =
ruleParams.criteria?.map((c) => ({
const ruleCriteria = (ruleParams.criteria?.map((c) => ({
...c,
timeUnit: tu,
})) || [];
})) || []) as AlertParams['criteria'];
setTimeUnit(tu as TimeUnitChar);
setRuleParams('criteria', criteria as AlertParams['criteria']);
setRuleParams('criteria', ruleCriteria);
},
[ruleParams.criteria, setRuleParams]
);
@ -230,17 +288,13 @@ export function Expressions(props: Props) {
preFillAlertGroupBy();
}
if (!ruleParams.sourceId) {
setRuleParams('sourceId', source?.id || 'default');
}
if (typeof ruleParams.alertOnNoData === 'undefined') {
setRuleParams('alertOnNoData', true);
}
if (typeof ruleParams.alertOnGroupDisappear === 'undefined') {
setRuleParams('alertOnGroupDisappear', true);
}
}, [metadata, source]); // eslint-disable-line react-hooks/exhaustive-deps
}, [metadata]); // eslint-disable-line react-hooks/exhaustive-deps
const handleFieldSearchChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => onFilterChange(e.target.value),
@ -283,7 +337,15 @@ export function Expressions(props: Props) {
return (
<>
<EuiSpacer size={'m'} />
<DataViewSelectPopover
dependencies={{ dataViews, dataViewEditor }}
dataView={dataView}
onSelectDataView={onSelectDataView}
onChangeMetaData={({ adHocDataViewList }) => {
onChangeMetaData({ ...metadata, adHocDataViewList });
}}
/>
<EuiSpacer size={'s'} />
<EuiText size="xs">
<h4>
<FormattedMessage
@ -298,7 +360,7 @@ export function Expressions(props: Props) {
return (
<ExpressionRow
canDelete={(ruleParams.criteria && ruleParams.criteria.length > 1) || false}
fields={derivedIndexPattern.fields}
fields={derivedIndexPattern.fields as any}
remove={removeExpression}
addExpression={addExpression}
key={idx} // idx's don't usually make good key's but here the index has semantic meaning
@ -308,17 +370,16 @@ export function Expressions(props: Props) {
expression={e || {}}
dataView={derivedIndexPattern}
>
{/* Preview */}
<ExpressionChart
expression={e}
derivedIndexPattern={derivedIndexPattern}
source={source}
filterQuery={ruleParams.filterQueryText}
groupBy={ruleParams.groupBy}
/>
</ExpressionRow>
);
})}
<div style={{ marginLeft: 28 }}>
<ForLastExpression
timeWindowSize={timeSize}
@ -328,7 +389,6 @@ export function Expressions(props: Props) {
onChangeWindowUnit={updateTimeUnit}
/>
</div>
<EuiSpacer size={'m'} />
<div>
<EuiButtonEmpty
@ -345,7 +405,6 @@ export function Expressions(props: Props) {
/>
</EuiButtonEmpty>
</div>
<EuiSpacer size={'m'} />
<EuiAccordion
id="advanced-options-accordion"
@ -387,7 +446,6 @@ export function Expressions(props: Props) {
</EuiPanel>
</EuiAccordion>
<EuiSpacer size={'m'} />
<EuiFormRow
label={i18n.translate('xpack.observability.threshold.rule.alertFlyout.filterLabel', {
defaultMessage: 'Filter (optional)',
@ -398,7 +456,7 @@ export function Expressions(props: Props) {
fullWidth
display="rowCompressed"
>
{(metadata && (
{(metadata && derivedIndexPattern && (
<MetricsExplorerKueryBar
derivedIndexPattern={derivedIndexPattern}
onChange={debouncedOnFilterChange}
@ -414,7 +472,6 @@ export function Expressions(props: Props) {
/>
)}
</EuiFormRow>
<EuiSpacer size={'m'} />
<EuiFormRow
label={i18n.translate('xpack.observability.threshold.rule.alertFlyout.createAlertPerText', {
@ -432,7 +489,7 @@ export function Expressions(props: Props) {
>
<MetricsExplorerGroupBy
onChange={onGroupByChange}
fields={derivedIndexPattern.fields}
fields={derivedIndexPattern.fields as any}
options={{
...options,
groupBy: ruleParams.groupBy || undefined,
@ -508,7 +565,3 @@ const docCountNoDataDisabledHelpText = i18n.translate(
defaultMessage: '[This setting is not applicable to the Document Count aggregator.]',
}
);
// required for dynamic import
// eslint-disable-next-line import/no-default-export
export default withSourceProvider<Props>(Expressions)('default');

View file

@ -14,11 +14,7 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { coreMock as mockCoreMock } from '@kbn/core/public/mocks';
import { MetricExpression } from '../types';
import { ExpressionChart } from './expression_chart';
import {
Aggregators,
Comparator,
MetricsSourceConfiguration,
} from '../../../../common/threshold_rule/types';
import { Aggregators, Comparator } from '../../../../common/threshold_rule/types';
const mockStartServices = mockCoreMock.createStart();
@ -57,24 +53,10 @@ describe('ExpressionChart', () => {
fields: [],
};
const source: MetricsSourceConfiguration = {
id: 'default',
origin: 'fallback',
configuration: {
name: 'default',
description: 'The default configuration',
metricAlias: 'metricbeat-*',
inventoryDefaultView: 'host',
metricsExplorerDefaultView: 'host',
anomalyThreshold: 20,
},
};
const wrapper = mountWithIntl(
<ExpressionChart
expression={expression}
derivedIndexPattern={derivedIndexPattern}
source={source}
filterQuery={filterQuery}
groupBy={groupBy}
annotations={annotations}

View file

@ -31,7 +31,6 @@ import { Color } from '../../../../common/threshold_rule/color_palette';
import {
MetricsExplorerChartType,
MetricsExplorerOptionsMetric,
MetricsSourceConfiguration,
} from '../../../../common/threshold_rule/types';
import { MetricExpression, TimeRange } from '../types';
import { createFormatterForMetric } from '../helpers/create_formatter_for_metric';
@ -57,7 +56,6 @@ interface Props {
filterQuery?: string;
groupBy?: string | string[];
hideTitle?: boolean;
source?: MetricsSourceConfiguration;
timeRange?: TimeRange;
}
@ -69,14 +67,12 @@ export function ExpressionChart({
filterQuery,
groupBy,
hideTitle = false,
source,
timeRange,
}: Props) {
const { charts, uiSettings } = useKibana().services;
const { isLoading, data } = useMetricsExplorerChartData(
expression,
derivedIndexPattern,
source,
filterQuery,
groupBy,
timeRange

View file

@ -13,14 +13,6 @@ import { act } from 'react-dom/test-utils';
import { MetricExpression } from '../types';
import { ExpressionRow } from './expression_row';
jest.mock('../helpers/source', () => ({
withSourceProvider: () => jest.fn,
useSourceContext: () => ({
source: { id: 'default' },
createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }),
}),
}));
describe('ExpressionRow', () => {
async function setup(expression: MetricExpression) {
const wrapper = mountWithIntl(

View file

@ -144,20 +144,6 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
name: f.name,
}));
// for v8.9 we want to show only the Custom Equation. Use EuiExpression instead of WhenExpression */
// const updateAggType = useCallback(
// (at: string) => {
// setRuleParams(expressionId, {
// ...expression,
// aggType: at as MetricExpression['aggType'],
// metric: ['custom', 'count'].includes(at) ? undefined : expression.metric,
// customMetrics: at === 'custom' ? expression.customMetrics : undefined,
// equation: at === 'custom' ? expression.equation : undefined,
// label: at === 'custom' ? expression.label : undefined,
// });
// },
// [expressionId, expression, setRuleParams]
// );
return (
<>
<EuiFlexGroup gutterSize="xs">
@ -177,12 +163,6 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
<EuiFlexItem grow>
<StyledExpressionRow style={{ gap: aggType !== 'custom' ? 24 : 12 }}>
<StyledExpression>
{/* for v8.9 we want to show only the Custom Equation. Use EuiExpression instead of WhenExpression */}
{/* <WhenExpression
customAggTypesOptions={aggregationType}
aggType={aggType}
onChangeSelectedAggType={updateAggType}
/> */}
<EuiExpression
data-test-subj="customEquationWhen"
description={i18n.translate(

View file

@ -9,10 +9,7 @@ import DateMath from '@kbn/datemath';
import { DataViewBase } from '@kbn/es-query';
import { useMemo } from 'react';
import { MetricExplorerCustomMetricAggregations } from '../../../../common/threshold_rule/metrics_explorer';
import {
MetricExpressionCustomMetric,
MetricsSourceConfiguration,
} from '../../../../common/threshold_rule/types';
import { MetricExpressionCustomMetric } from '../../../../common/threshold_rule/types';
import { MetricExpression, TimeRange } from '../types';
import { useMetricsExplorerData } from './use_metrics_explorer_data';
@ -26,7 +23,6 @@ const DEFAULT_TIME_RANGE = {};
export const useMetricsExplorerChartData = (
expression: MetricExpression,
derivedIndexPattern: DataViewBase,
source?: MetricsSourceConfiguration,
filterQuery?: string,
groupBy?: string | string[],
timeRange: TimeRange = DEFAULT_TIME_RANGE
@ -53,11 +49,13 @@ export const useMetricsExplorerChartData = (
],
aggregation: expression.aggType || 'avg',
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
expression.aggType,
expression.equation,
expression.metric,
expression.customMetrics,
// eslint-disable-next-line react-hooks/exhaustive-deps
JSON.stringify(expression.customMetrics),
filterQuery,
groupBy,
]
@ -74,7 +72,7 @@ export const useMetricsExplorerChartData = (
};
}, [timeRange, timeSize, timeUnit]);
return useMetricsExplorerData(options, source?.configuration, derivedIndexPattern, timestamps);
return useMetricsExplorerData(options, derivedIndexPattern, timestamps);
};
const mapMetricThresholdMetricToMetricsExplorerMetric = (metric: MetricExpressionCustomMetric) => {

View file

@ -17,13 +17,11 @@ import {
MetricsExplorerTimestampsRT,
} from './use_metrics_explorer_options';
import { DataViewBase } from '@kbn/es-query';
import { MetricsSourceConfigurationProperties } from '../../../../common/threshold_rule/types';
import {
createSeries,
derivedIndexPattern,
options,
resp,
source,
timestamps,
} from '../../../utils/metrics_explorer';
@ -54,20 +52,12 @@ const renderUseMetricsExplorerDataHook = () => {
return renderHook(
(props: {
options: MetricsExplorerOptions;
source: MetricsSourceConfigurationProperties | undefined;
derivedIndexPattern: DataViewBase;
timestamps: MetricsExplorerTimestampsRT;
}) =>
useMetricsExplorerData(
props.options,
props.source,
props.derivedIndexPattern,
props.timestamps
),
}) => useMetricsExplorerData(props.options, props.derivedIndexPattern, props.timestamps),
{
initialProps: {
options,
source,
derivedIndexPattern,
timestamps,
},
@ -163,7 +153,6 @@ describe('useMetricsExplorerData Hook', () => {
aggregation: 'count',
metrics: [{ aggregation: 'count' }],
},
source,
derivedIndexPattern,
timestamps,
});
@ -187,7 +176,6 @@ describe('useMetricsExplorerData Hook', () => {
mockedFetch.mockResolvedValue(resp as any);
rerender({
options,
source,
derivedIndexPattern,
timestamps: { fromTimestamp: 1678378092225, toTimestamp: 1678381693477, interval: '>=10s' },
});

View file

@ -9,7 +9,6 @@ import { DataViewBase } from '@kbn/es-query';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { MetricsSourceConfigurationProperties } from '../../../../common/threshold_rule/types';
import {
MetricsExplorerResponse,
metricsExplorerResponseRT,
@ -24,7 +23,6 @@ import { decodeOrThrow } from '../helpers/runtime_types';
export function useMetricsExplorerData(
options: MetricsExplorerOptions,
source: MetricsSourceConfigurationProperties | undefined,
derivedIndexPattern: DataViewBase,
{ fromTimestamp, toTimestamp, interval }: MetricsExplorerTimestampsRT,
enabled = true
@ -35,7 +33,7 @@ export function useMetricsExplorerData(
MetricsExplorerResponse,
Error
>({
queryKey: ['metricExplorer', options, fromTimestamp, toTimestamp],
queryKey: ['metricExplorer', options, fromTimestamp, toTimestamp, derivedIndexPattern.title],
queryFn: async ({ signal, pageParam = { afterKey: null } }) => {
if (!fromTimestamp || !toTimestamp) {
throw new Error('Unable to parse timerange');
@ -43,8 +41,8 @@ export function useMetricsExplorerData(
if (!http) {
throw new Error('HTTP service is unavailable');
}
if (!source) {
throw new Error('Source is unavailable');
if (!derivedIndexPattern.title) {
throw new Error('Data view is unavailable');
}
const { afterKey } = pageParam;
@ -57,7 +55,7 @@ export function useMetricsExplorerData(
groupBy: options.groupBy,
afterKey,
limit: options.limit,
indexPattern: source.metricAlias,
indexPattern: derivedIndexPattern.title,
filterQuery:
(options.filterQuery &&
convertKueryToElasticSearchQuery(options.filterQuery, derivedIndexPattern)) ||
@ -74,7 +72,7 @@ export function useMetricsExplorerData(
return decodeOrThrow(metricsExplorerResponseRT)(response);
},
getNextPageParam: (lastPage) => lastPage.pageInfo,
enabled: enabled && !!fromTimestamp && !!toTimestamp && !!http && !!source,
enabled: enabled && !!fromTimestamp && !!toTimestamp && !!http && !!derivedIndexPattern.title,
refetchOnWindowFocus: false,
});

View file

@ -7,8 +7,8 @@
import * as rt from 'io-ts';
import { CasesUiStart } from '@kbn/cases-plugin/public';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { DataPublicPluginStart, SerializedSearchSourceFields } from '@kbn/data-plugin/public';
import { DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { DiscoverStart } from '@kbn/discover-plugin/public';
import { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
@ -38,6 +38,7 @@ import { ObservabilityPublicStart } from '../../plugin';
import { MetricsExplorerOptions } from './hooks/use_metrics_explorer_options';
export interface AlertContextMeta {
adHocDataViewList: DataView[];
currentOptions?: Partial<MetricsExplorerOptions>;
series?: MetricsExplorerSeries;
}
@ -94,6 +95,7 @@ export interface AlertParams {
filterQueryText?: string;
alertOnNoData?: boolean;
alertOnGroupDisappear?: boolean;
searchConfiguration: SerializedSearchSourceFields;
shouldDropPartialBuckets?: boolean;
}

View file

@ -1,142 +0,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 createContainer from 'constate';
import React, { useEffect, useState } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { IHttpFetchError } from '@kbn/core-http-browser';
import {
MetricsSourceConfiguration,
MetricsSourceConfigurationResponse,
PartialMetricsSourceConfigurationProperties,
} from '../../../../common/threshold_rule/types';
import { MissingHttpClientException } from './source_errors';
import { useTrackedPromise } from '../hooks/use_tracked_promise';
import { useSourceNotifier } from './notifications';
export const pickIndexPattern = (
source: MetricsSourceConfiguration | undefined,
type: 'metrics'
) => {
if (!source) {
return 'unknown-index';
}
if (type === 'metrics') {
return source.configuration.metricAlias;
}
return `${source.configuration.metricAlias}`;
};
export const useSource = ({ sourceId }: { sourceId: string }) => {
const { services } = useKibana();
const notify = useSourceNotifier();
const fetchService = services.http;
const API_URL = `/api/metrics/source/${sourceId}`;
const [source, setSource] = useState<MetricsSourceConfiguration | undefined>(undefined);
const [loadSourceRequest, loadSource] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: () => {
if (!fetchService) {
throw new MissingHttpClientException();
}
return fetchService.fetch<MetricsSourceConfigurationResponse>(API_URL, { method: 'GET' });
},
onResolve: (response) => {
if (response) {
setSource(response.source);
}
},
},
[fetchService, sourceId]
);
const [createSourceConfigurationRequest, createSourceConfiguration] = useTrackedPromise(
{
createPromise: async (sourceProperties: PartialMetricsSourceConfigurationProperties) => {
if (!fetchService) {
throw new MissingHttpClientException();
}
return await fetchService.patch<MetricsSourceConfigurationResponse>(API_URL, {
method: 'PATCH',
body: JSON.stringify(sourceProperties),
});
},
onResolve: (response) => {
if (response) {
notify.updateSuccess();
setSource(response.source);
}
},
onReject: (error) => {
notify.updateFailure((error as IHttpFetchError<{ message: string }>).body?.message);
},
},
[fetchService, sourceId]
);
useEffect(() => {
loadSource();
}, [loadSource, sourceId]);
const createDerivedIndexPattern = () => {
return {
fields: source?.status ? source.status.indexFields : [],
title: pickIndexPattern(source, 'metrics'),
};
};
const hasFailedLoadingSource = loadSourceRequest.state === 'rejected';
const isUninitialized = loadSourceRequest.state === 'uninitialized';
const isLoadingSource = loadSourceRequest.state === 'pending';
const isLoading = isLoadingSource || createSourceConfigurationRequest.state === 'pending';
const sourceExists = source ? !!source.version : undefined;
const metricIndicesExist = Boolean(source?.status?.metricIndicesExist);
const version = source?.version;
return {
createSourceConfiguration,
createDerivedIndexPattern,
isLoading,
isLoadingSource,
isUninitialized,
hasFailedLoadingSource,
loadSource,
loadSourceRequest,
loadSourceFailureMessage: hasFailedLoadingSource ? `${loadSourceRequest.value}` : undefined,
metricIndicesExist,
source,
sourceExists,
sourceId,
updateSourceConfiguration: createSourceConfiguration,
version,
};
};
export const [SourceProvider, useSourceContext] = createContainer(useSource);
export const withSourceProvider =
<ComponentProps,>(Component: React.FunctionComponent<ComponentProps>) =>
(sourceId = 'default') => {
// eslint-disable-next-line react/function-component-definition
return function ComponentWithSourceProvider(props: ComponentProps) {
return (
<SourceProvider sourceId={sourceId}>
<Component {...props} />
</SourceProvider>
);
};
};

View file

@ -71,6 +71,33 @@ const data = {
timefilter: jest.fn(),
},
},
search: {
searchSource: jest.fn(),
},
};
},
};
const dataViewEditor = {
createStart() {
return {
userPermissions: {
editDataView: jest.fn(),
},
};
},
};
const dataViews = {
createStart() {
return {
getIds: jest.fn().mockImplementation(() => []),
get: jest.fn(),
create: jest.fn().mockImplementation(() => ({
fields: {
getByName: jest.fn(),
},
})),
};
},
};
@ -81,6 +108,8 @@ export const observabilityPublicPluginsStartMock = {
cases: mockCasesContract(),
triggersActionsUi: triggersActionsUiStartMock.createStart(),
data: data.createStart(),
dataViews: dataViews.createStart(),
dataViewEditor: dataViewEditor.createStart(),
lens: null,
discover: null,
};

View file

@ -8,6 +8,7 @@
import { i18n } from '@kbn/i18n';
import { BehaviorSubject, from } from 'rxjs';
import { map } from 'rxjs/operators';
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import {
AppDeepLink,
@ -103,6 +104,7 @@ export interface ObservabilityPublicPluginsStart {
charts: ChartsPluginStart;
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
dataViewEditor: DataViewEditorStart;
discover: DiscoverStart;
embeddable: EmbeddableStart;
exploratoryView: ExploratoryViewPublicStart;

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import { ALERT_REASON } from '@kbn/rule-data-utils';
@ -16,8 +17,8 @@ import {
SLO_BURN_RATE_RULE_TYPE_ID,
} from '../../common/constants';
import { validateBurnRateRule } from '../components/burn_rate_rule_editor/validation';
import { validateMetricThreshold } from '../pages/threshold/components/validation';
import { formatReason } from '../pages/threshold/rule_data_formatters';
import { validateMetricThreshold } from '../components/threshold/components/validation';
import { formatReason } from '../components/threshold/rule_data_formatters';
const sloBurnRateDefaultActionMessage = i18n.translate(
'xpack.observability.slo.rules.burnRate.defaultActionMessage',
@ -91,7 +92,7 @@ export const registerObservabilityRuleTypes = (
documentationUrl(docLinks) {
return `${docLinks.links.observability.threshold}`;
},
ruleParamsExpression: lazy(() => import('../pages/threshold/components/expression')),
ruleParamsExpression: lazy(() => import('../components/threshold/components/expression')),
validate: validateMetricThreshold,
defaultActionMessage: i18n.translate(
'xpack.observability.threshold.rule.alerting.threshold.defaultActionMessage',
@ -106,7 +107,7 @@ export const registerObservabilityRuleTypes = (
requiresAppContext: false,
format: formatReason,
alertDetailsAppSection: lazy(
() => import('../pages/threshold/components/alert_details_app_section')
() => import('../components/threshold/components/alert_details_app_section')
),
});
}

View file

@ -16,7 +16,7 @@ import {
MetricsExplorerTimeOptions,
MetricsExplorerTimestampsRT,
MetricsExplorerYAxisMode,
} from '../pages/threshold/hooks/use_metrics_explorer_options';
} from '../components/threshold/hooks/use_metrics_explorer_options';
export const options: MetricsExplorerOptions = {
limit: 3,

View file

@ -6,6 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { extractReferences, injectReferences } from '@kbn/data-plugin/common';
import { i18n } from '@kbn/i18n';
import { IRuleTypeAlerts } from '@kbn/alerting-plugin/server';
import { IBasePath, Logger } from '@kbn/core/server';
@ -16,10 +17,11 @@ import {
IRuleDataClient,
} from '@kbn/rule-registry-plugin/server';
import { LicenseType } from '@kbn/licensing-plugin/server';
import { THRESHOLD_RULE_REGISTRATION_CONTEXT } from '../../../common/constants';
import { EsQueryRuleParamsExtractedParams } from '@kbn/stack-alerts-plugin/server/rule_types/es_query/rule_type_params';
import { observabilityFeatureId } from '../../../../common';
import { Comparator } from '../../../../common/threshold_rule/types';
import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '../../../../common/constants';
import { THRESHOLD_RULE_REGISTRATION_CONTEXT } from '../../../common/constants';
import {
alertDetailUrlActionVariableDescription,
@ -148,7 +150,6 @@ export function thresholdRuleType(
validate: validateIsStringElasticsearchJSONFilter,
})
),
sourceId: schema.string(),
alertOnNoData: schema.maybe(schema.boolean()),
alertOnGroupDisappear: schema.maybe(schema.boolean()),
},
@ -203,6 +204,21 @@ export function thresholdRuleType(
},
],
},
useSavedObjectReferences: {
// TODO revisit types https://github.com/elastic/kibana/issues/159714
extractReferences: (params: any) => {
const [searchConfiguration, references] = extractReferences(params.searchConfiguration);
const newParams = { ...params, searchConfiguration } as EsQueryRuleParamsExtractedParams;
return { params: newParams, references };
},
injectReferences: (params: any, references: any) => {
return {
...params,
searchConfiguration: injectReferences(params.searchConfiguration, references),
};
},
},
producer: observabilityFeatureId,
getSummarizedAlerts: getSummarizedAlerts(),
alerts: MetricsRulesTypeAlertDefinition,

View file

@ -1,237 +0,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.
*/
const bucketsA = (from: number) => [
{
doc_count: null,
aggregatedValue: { value: null, values: [{ key: 95.0, value: null }] },
from_as_string: new Date(from).toISOString(),
},
{
doc_count: 2,
aggregatedValue: { value: 0.5, values: [{ key: 95.0, value: 0.5 }] },
from_as_string: new Date(from + 60000).toISOString(),
},
{
doc_count: 2,
aggregatedValue: { value: 0.5, values: [{ key: 95.0, value: 0.5 }] },
from_as_string: new Date(from + 120000).toISOString(),
},
{
doc_count: 2,
aggregatedValue: { value: 0.5, values: [{ key: 95.0, value: 0.5 }] },
from_as_string: new Date(from + 180000).toISOString(),
},
{
doc_count: 3,
aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] },
from_as_string: new Date(from + 240000).toISOString(),
},
{
doc_count: 1,
aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] },
from_as_string: new Date(from + 300000).toISOString(),
},
];
const bucketsB = (from: number) => [
{
doc_count: 0,
aggregatedValue: { value: null, values: [{ key: 99.0, value: null }] },
from_as_string: new Date(from).toISOString(),
},
{
doc_count: 4,
aggregatedValue: { value: 2.5, values: [{ key: 99.0, value: 2.5 }] },
from_as_string: new Date(from + 60000).toISOString(),
},
{
doc_count: 4,
aggregatedValue: { value: 2.5, values: [{ key: 99.0, value: 2.5 }] },
from_as_string: new Date(from + 120000).toISOString(),
},
{
doc_count: 4,
aggregatedValue: { value: 2.5, values: [{ key: 99.0, value: 2.5 }] },
from_as_string: new Date(from + 180000).toISOString(),
},
{
doc_count: 5,
aggregatedValue: { value: 3.5, values: [{ key: 99.0, value: 3.5 }] },
from_as_string: new Date(from + 240000).toISOString(),
},
{
doc_count: 1,
aggregatedValue: { value: 3, values: [{ key: 99.0, value: 3 }] },
from_as_string: new Date(from + 300000).toISOString(),
},
];
const bucketsC = (from: number) => [
{
doc_count: 0,
aggregatedValue: { value: null },
from_as_string: new Date(from).toISOString(),
},
{
doc_count: 2,
aggregatedValue: { value: 0.5 },
from_as_string: new Date(from + 60000).toISOString(),
},
{
doc_count: 2,
aggregatedValue: { value: 0.5 },
from_as_string: new Date(from + 120000).toISOString(),
},
{
doc_count: 2,
aggregatedValue: { value: 0.5 },
from_as_string: new Date(from + 180000).toISOString(),
},
{
doc_count: 3,
aggregatedValue: { value: 16 },
from_as_string: new Date(from + 240000).toISOString(),
},
{
doc_count: 1,
aggregatedValue: { value: 3 },
from_as_string: new Date(from + 300000).toISOString(),
},
];
export const basicMetricResponse = () => ({
hits: {
total: {
value: 1,
},
},
aggregations: {
aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] },
},
});
export const alternateMetricResponse = () => ({
hits: {
total: {
value: 1,
},
},
aggregations: {
aggregatedValue: { value: 3, values: [{ key: 99.0, value: 3 }] },
},
});
export const emptyMetricResponse = {
aggregations: {
aggregatedIntervals: {
buckets: [],
},
},
};
export const emptyRateResponse = (from: number) => ({
aggregations: {
aggregatedIntervals: {
buckets: [
{
doc_count: 2,
aggregatedValueMax: { value: null },
from_as_string: new Date(from).toISOString(),
},
],
},
},
});
export const basicCompositeResponse = (from: number) => ({
aggregations: {
groupings: {
after_key: { groupBy0: 'foo' },
buckets: [
{
key: {
groupBy0: 'a',
},
aggregatedIntervals: {
buckets: bucketsA(from),
},
doc_count: 1,
},
{
key: {
groupBy0: 'b',
},
aggregatedIntervals: {
buckets: bucketsB(from),
},
doc_count: 1,
},
],
},
},
hits: {
total: {
value: 2,
},
},
});
export const alternateCompositeResponse = (from: number) => ({
aggregations: {
groupings: {
after_key: { groupBy0: 'foo' },
buckets: [
{
key: {
groupBy0: 'a',
},
aggregatedIntervals: {
buckets: bucketsB(from),
},
doc_count: 1,
},
{
key: {
groupBy0: 'b',
},
aggregatedIntervals: {
buckets: bucketsA(from),
},
doc_count: 1,
},
{
key: {
groupBy0: 'c',
},
aggregatedIntervals: {
buckets: bucketsC(from),
},
doc_count: 1,
},
],
},
},
hits: {
total: {
value: 3,
},
},
});
export const compositeEndResponse = {
aggregations: {},
hits: { total: { value: 0 } },
};
export const changedSourceIdResponse = (from: number) => ({
aggregations: {
aggregatedIntervals: {
buckets: bucketsC(from),
},
},
});

View file

@ -120,7 +120,7 @@ export const createMetricThresholdExecutor = ({
});
// TODO: check if we need to use "savedObjectsClient"=> https://github.com/elastic/kibana/issues/159340
const { alertWithLifecycle, getAlertUuid, getAlertByAlertUuid, dataViews } = services;
const { alertWithLifecycle, getAlertUuid, getAlertByAlertUuid, searchSourceClient } = services;
const alertFactory: MetricThresholdAlertFactory = (
id,
@ -138,9 +138,8 @@ export const createMetricThresholdExecutor = ({
...flattenAdditionalContext(additionalContext),
},
});
// TODO: check if we need to use "sourceId"
const { alertOnNoData, alertOnGroupDisappear: _alertOnGroupDisappear } = params as {
sourceId?: string;
alertOnNoData: boolean;
alertOnGroupDisappear: boolean | undefined;
};
@ -188,9 +187,9 @@ export const createMetricThresholdExecutor = ({
alertOnGroupDisappear && filterQueryIsSame && groupByIsSame && state.missingGroups
? state.missingGroups
: [];
// TODO: check the DATA VIEW
const defaultDataView = await dataViews.getDefaultDataView();
const dataView = defaultDataView?.getIndexPattern();
const initialSearchSource = await searchSourceClient.create(params.searchConfiguration!);
const dataView = initialSearchSource.getField('index')!.getIndexPattern();
if (!dataView) {
throw new Error('No matched data view');
}

View file

@ -37,7 +37,6 @@ export interface MetricAnomalyParams {
nodeType: rt.TypeOf<typeof metricAnomalyNodeTypeRT>;
metric: rt.TypeOf<typeof metricAnomalyMetricRT>;
alertInterval?: string;
sourceId?: string;
spaceId?: string;
threshold: Exclude<ML_ANOMALY_THRESHOLD, ML_ANOMALY_THRESHOLD.LOW>;
influencerFilter: rt.TypeOf<typeof metricAnomalyInfluencerFilterRT> | undefined;
@ -48,7 +47,6 @@ export interface MetricAnomalyParams {
interface BaseMetricExpressionParams {
timeSize: number;
timeUnit: TimeUnitChar;
sourceId?: string;
threshold: number[];
comparator: Comparator;
warningComparator?: Comparator;

View file

@ -77,7 +77,9 @@
"@kbn/safer-lodash-set",
"@kbn/core-http-server",
"@kbn/cloud-chat-plugin",
"@kbn/cloud-plugin"
"@kbn/cloud-plugin",
"@kbn/stack-alerts-plugin",
"@kbn/data-view-editor-plugin"
],
"exclude": [
"target/**/*"

View file

@ -7,4 +7,6 @@
import { StackAlertsPublicPlugin } from './plugin';
export { DataViewSelectPopover } from './rule_types/components/data_view_select_popover';
export const plugin = () => new StackAlertsPublicPlugin();

View file

@ -8,7 +8,6 @@
import React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { DataViewSelectPopover, DataViewSelectPopoverProps } from './data_view_select_popover';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import type { DataView } from '@kbn/data-views-plugin/public';
import { indexPatternEditorPluginMock as dataViewEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
@ -24,12 +23,6 @@ const selectedDataView = {
getName: () => 'kibana_sample_data_logs',
} as unknown as DataView;
const props: DataViewSelectPopoverProps = {
onSelectDataView: () => {},
onChangeMetaData: () => {},
dataView: selectedDataView,
};
const dataViewIds = ['mock-data-logs-id', 'mock-ecommerce-id', 'mock-test-id', 'mock-ad-hoc-id'];
const dataViewOptions = [
@ -80,15 +73,15 @@ const mount = () => {
Promise.resolve(dataViewOptions.find((current) => current.id === id))
);
const dataViewEditorMock = dataViewEditorPluginMock.createStartContract();
const props: DataViewSelectPopoverProps = {
dependencies: { dataViews: dataViewsMock, dataViewEditor: dataViewEditorMock },
onSelectDataView: () => {},
onChangeMetaData: () => {},
dataView: selectedDataView,
};
return {
wrapper: mountWithIntl(
<KibanaContextProvider
services={{ dataViews: dataViewsMock, dataViewEditor: dataViewEditorMock }}
>
<DataViewSelectPopover {...props} />
</KibanaContextProvider>
),
wrapper: mountWithIntl(<DataViewSelectPopover {...props} />),
dataViewsMock,
};
};

View file

@ -20,13 +20,17 @@ import {
EuiText,
useEuiPaddingCSS,
} from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/public';
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { DataViewSelector } from '@kbn/unified-search-plugin/public';
import type { DataViewListItemEnhanced } from '@kbn/unified-search-plugin/public/dataview_picker/dataview_list';
import { useTriggerUiActionServices } from '../es_query/util';
import { EsQueryRuleMetaData } from '../es_query/types';
export interface DataViewSelectPopoverProps {
dependencies: {
dataViews: DataViewsPublicPluginStart;
dataViewEditor: DataViewEditorStart;
};
dataView?: DataView;
metadata?: EsQueryRuleMetaData;
onSelectDataView: (selectedDataView: DataView) => void;
@ -43,12 +47,12 @@ const toDataViewListItem = (dataView: DataView): DataViewListItemEnhanced => {
};
export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopoverProps> = ({
dependencies: { dataViews, dataViewEditor },
metadata = { adHocDataViewList: [], isManagementPage: true },
dataView,
onSelectDataView,
onChangeMetaData,
}) => {
const { dataViews, dataViewEditor } = useTriggerUiActionServices();
const [dataViewItems, setDataViewsItems] = useState<DataViewListItemEnhanced[]>([]);
const [dataViewPopoverOpen, setDataViewPopoverOpen] = useState(false);

View file

@ -78,6 +78,7 @@ const isSearchSourceParam = (action: LocalStateAction): action is SearchSourcePa
export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProps) => {
const services = useTriggerUiActionServices();
const unifiedSearch = services.unifiedSearch;
const { dataViews, dataViewEditor } = useTriggerUiActionServices();
const { searchSource, errors, initialSavedQuery, setParam, ruleParams } = props;
const [savedQuery, setSavedQuery] = useState<SavedQuery>();
@ -117,7 +118,7 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
);
const { index: dataView, query, filter: filters } = ruleConfiguration;
const dataViews = useMemo(() => (dataView ? [dataView] : []), [dataView]);
const indexPatterns = useMemo(() => (dataView ? [dataView] : []), [dataView]);
const [esFields, setEsFields] = useState<FieldOption[]>(
dataView ? convertFieldSpecToFieldOption(dataView.fields.map((field) => field.toSpec())) : []
@ -296,6 +297,7 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
</EuiTitle>
<EuiSpacer size="s" />
<DataViewSelectPopover
dependencies={{ dataViews, dataViewEditor }}
dataView={dataView}
metadata={props.metadata}
onSelectDataView={onSelectDataView}
@ -320,7 +322,7 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
suggestionsSize="s"
displayStyle="inPage"
query={query}
indexPatterns={dataViews}
indexPatterns={indexPatterns}
savedQuery={savedQuery}
filters={filters}
onFiltersUpdated={onUpdateFilters}