Skip to content

Commit ae19412

Browse files
committed
[Metrics UI] Allow users to create alerts from the central Alerts UI (elastic#63803)
1 parent 488e12f commit ae19412

File tree

13 files changed

+402
-243
lines changed

13 files changed

+402
-243
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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+
/* eslint-disable @typescript-eslint/no-empty-interface */
8+
9+
import * as rt from 'io-ts';
10+
import moment from 'moment';
11+
import { pipe } from 'fp-ts/lib/pipeable';
12+
import { chain } from 'fp-ts/lib/Either';
13+
14+
export const TimestampFromString = new rt.Type<number, string>(
15+
'TimestampFromString',
16+
(input): input is number => typeof input === 'number',
17+
(input, context) =>
18+
pipe(
19+
rt.string.validate(input, context),
20+
chain(stringInput => {
21+
const momentValue = moment(stringInput);
22+
return momentValue.isValid()
23+
? rt.success(momentValue.valueOf())
24+
: rt.failure(stringInput, context);
25+
})
26+
),
27+
output => new Date(output).toISOString()
28+
);
29+
30+
/**
31+
* Stored source configuration as read from and written to saved objects
32+
*/
33+
34+
const SavedSourceConfigurationFieldsRuntimeType = rt.partial({
35+
container: rt.string,
36+
host: rt.string,
37+
pod: rt.string,
38+
tiebreaker: rt.string,
39+
timestamp: rt.string,
40+
});
41+
42+
export const SavedSourceConfigurationTimestampColumnRuntimeType = rt.type({
43+
timestampColumn: rt.type({
44+
id: rt.string,
45+
}),
46+
});
47+
48+
export const SavedSourceConfigurationMessageColumnRuntimeType = rt.type({
49+
messageColumn: rt.type({
50+
id: rt.string,
51+
}),
52+
});
53+
54+
export const SavedSourceConfigurationFieldColumnRuntimeType = rt.type({
55+
fieldColumn: rt.type({
56+
id: rt.string,
57+
field: rt.string,
58+
}),
59+
});
60+
61+
export const SavedSourceConfigurationColumnRuntimeType = rt.union([
62+
SavedSourceConfigurationTimestampColumnRuntimeType,
63+
SavedSourceConfigurationMessageColumnRuntimeType,
64+
SavedSourceConfigurationFieldColumnRuntimeType,
65+
]);
66+
67+
export const SavedSourceConfigurationRuntimeType = rt.partial({
68+
name: rt.string,
69+
description: rt.string,
70+
metricAlias: rt.string,
71+
logAlias: rt.string,
72+
fields: SavedSourceConfigurationFieldsRuntimeType,
73+
logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType),
74+
});
75+
76+
export interface InfraSavedSourceConfiguration
77+
extends rt.TypeOf<typeof SavedSourceConfigurationRuntimeType> {}
78+
79+
export const pickSavedSourceConfiguration = (
80+
value: InfraSourceConfiguration
81+
): InfraSavedSourceConfiguration => {
82+
const { name, description, metricAlias, logAlias, fields, logColumns } = value;
83+
const { container, host, pod, tiebreaker, timestamp } = fields;
84+
85+
return {
86+
name,
87+
description,
88+
metricAlias,
89+
logAlias,
90+
fields: { container, host, pod, tiebreaker, timestamp },
91+
logColumns,
92+
};
93+
};
94+
95+
/**
96+
* Static source configuration as read from the configuration file
97+
*/
98+
99+
const StaticSourceConfigurationFieldsRuntimeType = rt.partial({
100+
...SavedSourceConfigurationFieldsRuntimeType.props,
101+
message: rt.array(rt.string),
102+
});
103+
104+
export const StaticSourceConfigurationRuntimeType = rt.partial({
105+
name: rt.string,
106+
description: rt.string,
107+
metricAlias: rt.string,
108+
logAlias: rt.string,
109+
fields: StaticSourceConfigurationFieldsRuntimeType,
110+
logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType),
111+
});
112+
113+
export interface InfraStaticSourceConfiguration
114+
extends rt.TypeOf<typeof StaticSourceConfigurationRuntimeType> {}
115+
116+
/**
117+
* Full source configuration type after all cleanup has been done at the edges
118+
*/
119+
120+
const SourceConfigurationFieldsRuntimeType = rt.type({
121+
...StaticSourceConfigurationFieldsRuntimeType.props,
122+
});
123+
124+
export const SourceConfigurationRuntimeType = rt.type({
125+
...SavedSourceConfigurationRuntimeType.props,
126+
fields: SourceConfigurationFieldsRuntimeType,
127+
logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType),
128+
});
129+
130+
export const SourceRuntimeType = rt.intersection([
131+
rt.type({
132+
id: rt.string,
133+
origin: rt.keyof({
134+
fallback: null,
135+
internal: null,
136+
stored: null,
137+
}),
138+
configuration: SourceConfigurationRuntimeType,
139+
}),
140+
rt.partial({
141+
version: rt.string,
142+
updatedAt: rt.number,
143+
}),
144+
]);
145+
146+
export interface InfraSourceConfiguration
147+
extends rt.TypeOf<typeof SourceConfigurationRuntimeType> {}
148+
149+
export interface InfraSource extends rt.TypeOf<typeof SourceRuntimeType> {}
150+
151+
const SourceStatusFieldRuntimeType = rt.type({
152+
name: rt.string,
153+
type: rt.string,
154+
searchable: rt.boolean,
155+
aggregatable: rt.boolean,
156+
displayable: rt.boolean,
157+
});
158+
159+
const SourceStatusRuntimeType = rt.type({
160+
logIndicesExist: rt.boolean,
161+
metricIndicesExist: rt.boolean,
162+
indexFields: rt.array(SourceStatusFieldRuntimeType),
163+
});
164+
165+
export const SourceResponseRuntimeType = rt.type({
166+
source: SourceRuntimeType,
167+
status: SourceStatusRuntimeType,
168+
});
169+
170+
export type SourceResponse = rt.TypeOf<typeof SourceResponseRuntimeType>;
171+
172+
/**
173+
* Saved object type with metadata
174+
*/
175+
176+
export const SourceConfigurationSavedObjectRuntimeType = rt.intersection([
177+
rt.type({
178+
id: rt.string,
179+
attributes: SavedSourceConfigurationRuntimeType,
180+
}),
181+
rt.partial({
182+
version: rt.string,
183+
updated_at: TimestampFromString,
184+
}),
185+
]);
186+
187+
export interface SourceConfigurationSavedObject
188+
extends rt.TypeOf<typeof SourceConfigurationSavedObjectRuntimeType> {}

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

Lines changed: 32 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ import {
1717
import { IFieldType } from 'src/plugins/data/public';
1818
import { FormattedMessage } from '@kbn/i18n/react';
1919
import { i18n } from '@kbn/i18n';
20-
import { EuiExpression } from '@elastic/eui';
21-
import { EuiCallOut } from '@elastic/eui';
22-
import { EuiLink } from '@elastic/eui';
2320
import {
2421
MetricExpressionParams,
2522
Comparator,
@@ -41,8 +38,8 @@ import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/ap
4138
import { MetricsExplorerOptions } from '../../../containers/metrics_explorer/use_metrics_explorer_options';
4239
import { MetricsExplorerKueryBar } from '../../metrics_explorer/kuery_bar';
4340
import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer';
44-
import { useSource } from '../../../containers/source';
4541
import { MetricsExplorerGroupBy } from '../../metrics_explorer/group_by';
42+
import { useSourceViaHttp } from '../../../containers/source/use_source_via_http';
4643

4744
interface AlertContextMeta {
4845
currentOptions?: Partial<MetricsExplorerOptions>;
@@ -87,7 +84,12 @@ const defaultExpression = {
8784

8885
export const Expressions: React.FC<Props> = props => {
8986
const { setAlertParams, alertParams, errors, alertsContext } = props;
90-
const { source, createDerivedIndexPattern } = useSource({ sourceId: 'default' });
87+
const { source, createDerivedIndexPattern } = useSourceViaHttp({
88+
sourceId: 'default',
89+
type: 'metrics',
90+
fetch: alertsContext.http.fetch,
91+
toastWarning: alertsContext.toastNotifications.addWarning,
92+
});
9193
const [timeSize, setTimeSize] = useState<number | undefined>(1);
9294
const [timeUnit, setTimeUnit] = useState<TimeUnit>('m');
9395

@@ -208,40 +210,11 @@ export const Expressions: React.FC<Props> = props => {
208210
setAlertParams('groupBy', md.currentOptions.groupBy);
209211
}
210212
setAlertParams('sourceId', source?.id);
213+
} else {
214+
setAlertParams('criteria', [defaultExpression]);
211215
}
212216
}, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps
213217

214-
// INFO: If there is metadata, you're in the metrics explorer context
215-
const canAddConditions = !!alertsContext.metadata;
216-
217-
if (!canAddConditions && !alertParams.criteria) {
218-
return (
219-
<>
220-
<EuiSpacer size={'m'} />
221-
<EuiCallOut
222-
title={
223-
<>
224-
<FormattedMessage
225-
id="xpack.infra.metrics.alertFlyout.createAlertWarningBody"
226-
defaultMessage="Create new metric threshold alerts from"
227-
/>{' '}
228-
<EuiLink href={'../app/metrics/explorer'}>
229-
<FormattedMessage
230-
id="xpack.infra.homePage.metricsExplorerTabTitle"
231-
defaultMessage="Metrics Explorer"
232-
/>
233-
</EuiLink>
234-
.
235-
</>
236-
}
237-
color="warning"
238-
iconType="help"
239-
/>
240-
<EuiSpacer size={'m'} />
241-
</>
242-
);
243-
}
244-
245218
return (
246219
<>
247220
<EuiSpacer size={'m'} />
@@ -258,7 +231,6 @@ export const Expressions: React.FC<Props> = props => {
258231
alertParams.criteria.map((e, idx) => {
259232
return (
260233
<ExpressionRow
261-
canEditAggField={canAddConditions}
262234
canDelete={alertParams.criteria.length > 1}
263235
fields={derivedIndexPattern.fields}
264236
remove={removeExpression}
@@ -281,20 +253,18 @@ export const Expressions: React.FC<Props> = props => {
281253
/>
282254

283255
<div>
284-
{canAddConditions && (
285-
<EuiButtonEmpty
286-
color={'primary'}
287-
iconSide={'left'}
288-
flush={'left'}
289-
iconType={'plusInCircleFilled'}
290-
onClick={addExpression}
291-
>
292-
<FormattedMessage
293-
id="xpack.infra.metrics.alertFlyout.addCondition"
294-
defaultMessage="Add condition"
295-
/>
296-
</EuiButtonEmpty>
297-
)}
256+
<EuiButtonEmpty
257+
color={'primary'}
258+
iconSide={'left'}
259+
flush={'left'}
260+
iconType={'plusInCircleFilled'}
261+
onClick={addExpression}
262+
>
263+
<FormattedMessage
264+
id="xpack.infra.metrics.alertFlyout.addCondition"
265+
defaultMessage="Add condition"
266+
/>
267+
</EuiButtonEmpty>
298268
</div>
299269

300270
<EuiSpacer size={'m'} />
@@ -347,7 +317,6 @@ export const Expressions: React.FC<Props> = props => {
347317

348318
interface ExpressionRowProps {
349319
fields: IFieldType[];
350-
canEditAggField: boolean;
351320
expressionId: number;
352321
expression: MetricExpression;
353322
errors: IErrorObject;
@@ -420,20 +389,17 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = props => {
420389
</StyledExpression>
421390
{aggType !== 'count' && (
422391
<StyledExpression>
423-
{!props.canEditAggField && <DisabledAggField text={metric || ''} />}
424-
{props.canEditAggField && (
425-
<OfExpression
426-
customAggTypesOptions={aggregationType}
427-
aggField={metric}
428-
fields={fields.map(f => ({
429-
normalizedType: f.type,
430-
name: f.name,
431-
}))}
432-
aggType={aggType}
433-
errors={errors}
434-
onChangeSelectedAggField={updateMetric}
435-
/>
436-
)}
392+
<OfExpression
393+
customAggTypesOptions={aggregationType}
394+
aggField={metric}
395+
fields={fields.map(f => ({
396+
normalizedType: f.type,
397+
name: f.name,
398+
}))}
399+
aggType={aggType}
400+
errors={errors}
401+
onChangeSelectedAggField={updateMetric}
402+
/>
437403
</StyledExpression>
438404
)}
439405
<StyledExpression>
@@ -465,19 +431,6 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = props => {
465431
);
466432
};
467433

468-
export const DisabledAggField = ({ text }: { text: string }) => {
469-
return (
470-
<EuiExpression
471-
description={i18n.translate('xpack.infra.metrics.alertFlyout.of.buttonLabel', {
472-
defaultMessage: 'of',
473-
})}
474-
value={text}
475-
isActive={false}
476-
color={'secondary'}
477-
/>
478-
);
479-
};
480-
481434
export const aggregationType: { [key: string]: any } = {
482435
avg: {
483436
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { updateSourceMutation } from './update_source.gql_query';
2121

2222
type Source = SourceQuery.Query['source'];
2323

24-
const pickIndexPattern = (source: Source | undefined, type: 'logs' | 'metrics' | 'both') => {
24+
export const pickIndexPattern = (source: Source | undefined, type: 'logs' | 'metrics' | 'both') => {
2525
if (!source) {
2626
return 'unknown-index';
2727
}

0 commit comments

Comments
 (0)