Skip to content

Commit 35fde53

Browse files
[7.x] [Metrics UI] Prefill alerts from the global dropdown (#68967) (#69981)
* [Metrics UI] Prefill alerts from the global dropdown (#68967) Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> * Fix uncaught typecheck merge conflict Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent b408a9f commit 35fde53

File tree

18 files changed

+539
-135
lines changed

18 files changed

+539
-135
lines changed

x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import React, { useState, useCallback, useMemo } from 'react';
88
import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui';
99
import { FormattedMessage } from '@kbn/i18n/react';
10+
import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill';
1011
import { AlertFlyout } from './alert_flyout';
1112
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
1213

@@ -15,6 +16,9 @@ export const InventoryAlertDropdown = () => {
1516
const [flyoutVisible, setFlyoutVisible] = useState(false);
1617
const kibana = useKibana();
1718

19+
const { inventoryPrefill } = useAlertPrefillContext();
20+
const { nodeType, metric, filterQuery } = inventoryPrefill;
21+
1822
const closePopover = useCallback(() => {
1923
setPopoverOpen(false);
2024
}, [setPopoverOpen]);
@@ -57,7 +61,13 @@ export const InventoryAlertDropdown = () => {
5761
>
5862
<EuiContextMenuPanel items={menuItems} />
5963
</EuiPopover>
60-
<AlertFlyout setVisible={setFlyoutVisible} visible={flyoutVisible} />
64+
<AlertFlyout
65+
setVisible={setFlyoutVisible}
66+
visible={flyoutVisible}
67+
nodeType={nodeType}
68+
options={{ metric }}
69+
filter={filterQuery}
70+
/>
6171
</>
6272
);
6373
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { useState } from 'react';
8+
import { SnapshotMetricInput } from '../../../../common/http_api/snapshot_api';
9+
import { InventoryItemType } from '../../../../common/inventory_models/types';
10+
11+
export const useInventoryAlertPrefill = () => {
12+
const [nodeType, setNodeType] = useState<InventoryItemType>('host');
13+
const [filterQuery, setFilterQuery] = useState<string | undefined>();
14+
const [metric, setMetric] = useState<SnapshotMetricInput>({ type: 'cpu' });
15+
16+
return {
17+
nodeType,
18+
filterQuery,
19+
metric,
20+
setNodeType,
21+
setFilterQuery,
22+
setMetric,
23+
};
24+
};

x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@
77
import React, { useState, useCallback, useMemo } from 'react';
88
import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui';
99
import { FormattedMessage } from '@kbn/i18n/react';
10-
import { AlertFlyout } from './alert_flyout';
1110
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
11+
import { useAlertPrefillContext } from '../../use_alert_prefill';
12+
import { AlertFlyout } from './alert_flyout';
1213

1314
export const MetricsAlertDropdown = () => {
1415
const [popoverOpen, setPopoverOpen] = useState(false);
1516
const [flyoutVisible, setFlyoutVisible] = useState(false);
1617
const kibana = useKibana();
1718

19+
const { metricThresholdPrefill } = useAlertPrefillContext();
20+
const { groupBy, filterQuery, metrics } = metricThresholdPrefill;
21+
1822
const closePopover = useCallback(() => {
1923
setPopoverOpen(false);
2024
}, [setPopoverOpen]);
@@ -57,7 +61,11 @@ export const MetricsAlertDropdown = () => {
5761
>
5862
<EuiContextMenuPanel items={menuItems} />
5963
</EuiPopover>
60-
<AlertFlyout setVisible={setFlyoutVisible} visible={flyoutVisible} />
64+
<AlertFlyout
65+
setVisible={setFlyoutVisible}
66+
visible={flyoutVisible}
67+
options={{ groupBy, filterQuery, metrics }}
68+
/>
6169
</>
6270
);
6371
};
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
8+
import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock';
9+
import { alertTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/alert_type_registry.mock';
10+
import { coreMock } from '../../../../../../../src/core/public/mocks';
11+
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
12+
import { AlertContextMeta } from '../types';
13+
import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer';
14+
import React from 'react';
15+
import { Expressions } from './expression';
16+
import { act } from 'react-dom/test-utils';
17+
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
18+
import { Comparator } from '../../../../server/lib/alerting/metric_threshold/types';
19+
20+
jest.mock('../../../containers/source/use_source_via_http', () => ({
21+
useSourceViaHttp: () => ({
22+
source: { id: 'default' },
23+
createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }),
24+
}),
25+
}));
26+
27+
describe('Expression', () => {
28+
async function setup(currentOptions: {
29+
metrics?: MetricsExplorerMetric[];
30+
filterQuery?: string;
31+
groupBy?: string;
32+
}) {
33+
const alertParams = {
34+
criteria: [],
35+
groupBy: undefined,
36+
filterQueryText: '',
37+
};
38+
39+
const mocks = coreMock.createSetup();
40+
const startMocks = coreMock.createStart();
41+
const [
42+
{
43+
application: { capabilities },
44+
},
45+
] = await mocks.getStartServices();
46+
47+
const context: AlertsContextValue<AlertContextMeta> = {
48+
http: mocks.http,
49+
toastNotifications: mocks.notifications.toasts,
50+
actionTypeRegistry: actionTypeRegistryMock.create() as any,
51+
alertTypeRegistry: alertTypeRegistryMock.create() as any,
52+
docLinks: startMocks.docLinks,
53+
capabilities: {
54+
...capabilities,
55+
actions: {
56+
delete: true,
57+
save: true,
58+
show: true,
59+
},
60+
},
61+
metadata: {
62+
currentOptions,
63+
},
64+
};
65+
66+
const wrapper = mountWithIntl(
67+
<Expressions
68+
alertsContext={context}
69+
alertInterval="1m"
70+
alertParams={alertParams}
71+
errors={[]}
72+
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}
73+
setAlertProperty={() => {}}
74+
/>
75+
);
76+
77+
const update = async () =>
78+
await act(async () => {
79+
await nextTick();
80+
wrapper.update();
81+
});
82+
83+
await update();
84+
85+
return { wrapper, update, alertParams };
86+
}
87+
88+
it('should prefill the alert using the context metadata', async () => {
89+
const currentOptions = {
90+
groupBy: 'host.hostname',
91+
filterQuery: 'foo',
92+
metrics: [
93+
{ aggregation: 'avg', field: 'system.load.1' },
94+
{ aggregation: 'cardinality', field: 'system.cpu.user.pct' },
95+
] as MetricsExplorerMetric[],
96+
};
97+
const { alertParams } = await setup(currentOptions);
98+
expect(alertParams.groupBy).toBe('host.hostname');
99+
expect(alertParams.filterQueryText).toBe('foo');
100+
expect(alertParams.criteria).toEqual([
101+
{
102+
metric: 'system.load.1',
103+
comparator: Comparator.GT,
104+
threshold: [],
105+
timeSize: 1,
106+
timeUnit: 'm',
107+
aggType: 'avg',
108+
},
109+
{
110+
metric: 'system.cpu.user.pct',
111+
comparator: Comparator.GT,
112+
threshold: [],
113+
timeSize: 1,
114+
timeUnit: 'm',
115+
aggType: 'cardinality',
116+
},
117+
]);
118+
});
119+
});

x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { debounce, pick } from 'lodash';
7+
import { debounce, pick, omit } from 'lodash';
88
import { Unit } from '@elastic/datemath';
99
import * as rt from 'io-ts';
1010
import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react';
@@ -52,22 +52,15 @@ import { useSourceViaHttp } from '../../../containers/source/use_source_via_http
5252
import { convertKueryToElasticSearchQuery } from '../../../utils/kuery';
5353

5454
import { ExpressionRow } from './expression_row';
55-
import { AlertContextMeta, TimeUnit, MetricExpression } from '../types';
55+
import { AlertContextMeta, TimeUnit, MetricExpression, AlertParams } from '../types';
5656
import { ExpressionChart } from './expression_chart';
5757
import { validateMetricThreshold } from './validation';
5858

5959
const FILTER_TYPING_DEBOUNCE_MS = 500;
6060

6161
interface Props {
6262
errors: IErrorObject[];
63-
alertParams: {
64-
criteria: MetricExpression[];
65-
groupBy?: string;
66-
filterQuery?: string;
67-
sourceId?: string;
68-
filterQueryText?: string;
69-
alertOnNoData?: boolean;
70-
};
63+
alertParams: AlertParams;
7164
alertsContext: AlertsContextValue<AlertContextMeta>;
7265
alertInterval: string;
7366
setAlertParams(key: string, value: any): void;
@@ -81,6 +74,7 @@ const defaultExpression = {
8174
timeSize: 1,
8275
timeUnit: 'm',
8376
} as MetricExpression;
77+
export { defaultExpression };
8478

8579
export const Expressions: React.FC<Props> = (props) => {
8680
const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props;
@@ -247,6 +241,13 @@ export const Expressions: React.FC<Props> = (props) => {
247241
}
248242
}, [alertsContext.metadata, derivedIndexPattern, setAlertParams]);
249243

244+
const preFillAlertGroupBy = useCallback(() => {
245+
const md = alertsContext.metadata;
246+
if (md && md.currentOptions?.groupBy && !md.series) {
247+
setAlertParams('groupBy', md.currentOptions.groupBy);
248+
}
249+
}, [alertsContext.metadata, setAlertParams]);
250+
250251
const onSelectPreviewLookbackInterval = useCallback((e) => {
251252
setPreviewLookbackInterval(e.target.value);
252253
setPreviewResult(null);
@@ -286,6 +287,10 @@ export const Expressions: React.FC<Props> = (props) => {
286287
preFillAlertFilter();
287288
}
288289

290+
if (!alertParams.groupBy) {
291+
preFillAlertGroupBy();
292+
}
293+
289294
if (!alertParams.sourceId) {
290295
setAlertParams('sourceId', source?.id || 'default');
291296
}
@@ -465,7 +470,7 @@ export const Expressions: React.FC<Props> = (props) => {
465470
id="selectPreviewLookbackInterval"
466471
value={previewLookbackInterval}
467472
onChange={onSelectPreviewLookbackInterval}
468-
options={previewOptions}
473+
options={previewDOMOptions}
469474
/>
470475
</EuiFlexItem>
471476
<EuiFlexItem grow={false}>
@@ -588,6 +593,10 @@ export const Expressions: React.FC<Props> = (props) => {
588593
);
589594
};
590595

596+
const previewDOMOptions: Array<{ text: string; value: string }> = previewOptions.map((o) =>
597+
omit(o, 'shortText')
598+
);
599+
591600
// required for dynamic import
592601
// eslint-disable-next-line import/no-default-export
593602
export default Expressions;

x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function validateMetricThreshold({
5050
if (!c.aggType) {
5151
errors[id].aggField.push(
5252
i18n.translate('xpack.infra.metrics.alertFlyout.error.aggregationRequired', {
53-
defaultMessage: 'Aggreation is required.',
53+
defaultMessage: 'Aggregation is required.',
5454
})
5555
);
5656
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { isEqual } from 'lodash';
8+
import { useState } from 'react';
9+
import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer';
10+
11+
interface MetricThresholdPrefillOptions {
12+
groupBy: string | string[] | undefined;
13+
filterQuery: string | undefined;
14+
metrics: MetricsExplorerMetric[];
15+
}
16+
17+
export const useMetricThresholdAlertPrefill = () => {
18+
const [prefillOptionsState, setPrefillOptionsState] = useState<MetricThresholdPrefillOptions>({
19+
groupBy: undefined,
20+
filterQuery: undefined,
21+
metrics: [],
22+
});
23+
24+
const { groupBy, filterQuery, metrics } = prefillOptionsState;
25+
26+
return {
27+
groupBy,
28+
filterQuery,
29+
metrics,
30+
setPrefillOptions(newState: MetricThresholdPrefillOptions) {
31+
if (!isEqual(newState, prefillOptionsState)) setPrefillOptionsState(newState);
32+
},
33+
};
34+
};

x-pack/plugins/infra/public/alerting/metric_threshold/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,12 @@ export interface ExpressionChartData {
5151
id: string;
5252
series: ExpressionChartSeries;
5353
}
54+
55+
export interface AlertParams {
56+
criteria: MetricExpression[];
57+
groupBy?: string;
58+
filterQuery?: string;
59+
sourceId?: string;
60+
filterQueryText?: string;
61+
alertOnNoData?: boolean;
62+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import createContainer from 'constate';
8+
import { useMetricThresholdAlertPrefill } from './metric_threshold/hooks/use_metric_threshold_alert_prefill';
9+
import { useInventoryAlertPrefill } from './inventory/hooks/use_inventory_alert_prefill';
10+
11+
const useAlertPrefill = () => {
12+
const metricThresholdPrefill = useMetricThresholdAlertPrefill();
13+
const inventoryPrefill = useInventoryAlertPrefill();
14+
15+
return { metricThresholdPrefill, inventoryPrefill };
16+
};
17+
18+
export const [AlertPrefillProvider, useAlertPrefillContext] = createContainer(useAlertPrefill);

x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class WithKueryAutocompletionComponent extends React.Component<
5959
) => {
6060
const { indexPattern } = this.props;
6161
const language = 'kuery';
62-
const hasQuerySuggestions = this.props.kibana.services.data.autocomplete.hasQuerySuggestions(
62+
const hasQuerySuggestions = this.props.kibana.services.data?.autocomplete.hasQuerySuggestions(
6363
language
6464
);
6565

0 commit comments

Comments
 (0)