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

[7.7] [Metrics UI] Allow users to create alerts from the central Alerts UI (#63803) #63907

Merged
merged 2 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
188 changes: 188 additions & 0 deletions x-pack/plugins/infra/common/http_api/source_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

/* eslint-disable @typescript-eslint/no-empty-interface */

import * as rt from 'io-ts';
import moment from 'moment';
import { pipe } from 'fp-ts/lib/pipeable';
import { chain } from 'fp-ts/lib/Either';

export const TimestampFromString = new rt.Type<number, string>(
'TimestampFromString',
(input): input is number => typeof input === 'number',
(input, context) =>
pipe(
rt.string.validate(input, context),
chain(stringInput => {
const momentValue = moment(stringInput);
return momentValue.isValid()
? rt.success(momentValue.valueOf())
: rt.failure(stringInput, context);
})
),
output => new Date(output).toISOString()
);

/**
* Stored source configuration as read from and written to saved objects
*/

const SavedSourceConfigurationFieldsRuntimeType = rt.partial({
container: rt.string,
host: rt.string,
pod: rt.string,
tiebreaker: rt.string,
timestamp: rt.string,
});

export const SavedSourceConfigurationTimestampColumnRuntimeType = rt.type({
timestampColumn: rt.type({
id: rt.string,
}),
});

export const SavedSourceConfigurationMessageColumnRuntimeType = rt.type({
messageColumn: rt.type({
id: rt.string,
}),
});

export const SavedSourceConfigurationFieldColumnRuntimeType = rt.type({
fieldColumn: rt.type({
id: rt.string,
field: rt.string,
}),
});

export const SavedSourceConfigurationColumnRuntimeType = rt.union([
SavedSourceConfigurationTimestampColumnRuntimeType,
SavedSourceConfigurationMessageColumnRuntimeType,
SavedSourceConfigurationFieldColumnRuntimeType,
]);

export const SavedSourceConfigurationRuntimeType = rt.partial({
name: rt.string,
description: rt.string,
metricAlias: rt.string,
logAlias: rt.string,
fields: SavedSourceConfigurationFieldsRuntimeType,
logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType),
});

export interface InfraSavedSourceConfiguration
extends rt.TypeOf<typeof SavedSourceConfigurationRuntimeType> {}

export const pickSavedSourceConfiguration = (
value: InfraSourceConfiguration
): InfraSavedSourceConfiguration => {
const { name, description, metricAlias, logAlias, fields, logColumns } = value;
const { container, host, pod, tiebreaker, timestamp } = fields;

return {
name,
description,
metricAlias,
logAlias,
fields: { container, host, pod, tiebreaker, timestamp },
logColumns,
};
};

/**
* Static source configuration as read from the configuration file
*/

const StaticSourceConfigurationFieldsRuntimeType = rt.partial({
...SavedSourceConfigurationFieldsRuntimeType.props,
message: rt.array(rt.string),
});

export const StaticSourceConfigurationRuntimeType = rt.partial({
name: rt.string,
description: rt.string,
metricAlias: rt.string,
logAlias: rt.string,
fields: StaticSourceConfigurationFieldsRuntimeType,
logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType),
});

export interface InfraStaticSourceConfiguration
extends rt.TypeOf<typeof StaticSourceConfigurationRuntimeType> {}

/**
* Full source configuration type after all cleanup has been done at the edges
*/

const SourceConfigurationFieldsRuntimeType = rt.type({
...StaticSourceConfigurationFieldsRuntimeType.props,
});

export const SourceConfigurationRuntimeType = rt.type({
...SavedSourceConfigurationRuntimeType.props,
fields: SourceConfigurationFieldsRuntimeType,
logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType),
});

export const SourceRuntimeType = rt.intersection([
rt.type({
id: rt.string,
origin: rt.keyof({
fallback: null,
internal: null,
stored: null,
}),
configuration: SourceConfigurationRuntimeType,
}),
rt.partial({
version: rt.string,
updatedAt: rt.number,
}),
]);

export interface InfraSourceConfiguration
extends rt.TypeOf<typeof SourceConfigurationRuntimeType> {}

export interface InfraSource extends rt.TypeOf<typeof SourceRuntimeType> {}

const SourceStatusFieldRuntimeType = rt.type({
name: rt.string,
type: rt.string,
searchable: rt.boolean,
aggregatable: rt.boolean,
displayable: rt.boolean,
});

const SourceStatusRuntimeType = rt.type({
logIndicesExist: rt.boolean,
metricIndicesExist: rt.boolean,
indexFields: rt.array(SourceStatusFieldRuntimeType),
});

export const SourceResponseRuntimeType = rt.type({
source: SourceRuntimeType,
status: SourceStatusRuntimeType,
});

export type SourceResponse = rt.TypeOf<typeof SourceResponseRuntimeType>;

/**
* Saved object type with metadata
*/

export const SourceConfigurationSavedObjectRuntimeType = rt.intersection([
rt.type({
id: rt.string,
attributes: SavedSourceConfigurationRuntimeType,
}),
rt.partial({
version: rt.string,
updated_at: TimestampFromString,
}),
]);

export interface SourceConfigurationSavedObject
extends rt.TypeOf<typeof SourceConfigurationSavedObjectRuntimeType> {}
111 changes: 32 additions & 79 deletions x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ import {
import { IFieldType } from 'src/plugins/data/public';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiExpression } from '@elastic/eui';
import { EuiCallOut } from '@elastic/eui';
import { EuiLink } from '@elastic/eui';
import {
MetricExpressionParams,
Comparator,
Expand All @@ -41,8 +38,8 @@ import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/ap
import { MetricsExplorerOptions } from '../../../containers/metrics_explorer/use_metrics_explorer_options';
import { MetricsExplorerKueryBar } from '../../metrics_explorer/kuery_bar';
import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer';
import { useSource } from '../../../containers/source';
import { MetricsExplorerGroupBy } from '../../metrics_explorer/group_by';
import { useSourceViaHttp } from '../../../containers/source/use_source_via_http';

interface AlertContextMeta {
currentOptions?: Partial<MetricsExplorerOptions>;
Expand Down Expand Up @@ -87,7 +84,12 @@ const defaultExpression = {

export const Expressions: React.FC<Props> = props => {
const { setAlertParams, alertParams, errors, alertsContext } = props;
const { source, createDerivedIndexPattern } = useSource({ sourceId: 'default' });
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
type: 'metrics',
fetch: alertsContext.http.fetch,
toastWarning: alertsContext.toastNotifications.addWarning,
});
const [timeSize, setTimeSize] = useState<number | undefined>(1);
const [timeUnit, setTimeUnit] = useState<TimeUnit>('m');

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

// INFO: If there is metadata, you're in the metrics explorer context
const canAddConditions = !!alertsContext.metadata;

if (!canAddConditions && !alertParams.criteria) {
return (
<>
<EuiSpacer size={'m'} />
<EuiCallOut
title={
<>
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.createAlertWarningBody"
defaultMessage="Create new metric threshold alerts from"
/>{' '}
<EuiLink href={'../app/metrics/explorer'}>
<FormattedMessage
id="xpack.infra.homePage.metricsExplorerTabTitle"
defaultMessage="Metrics Explorer"
/>
</EuiLink>
.
</>
}
color="warning"
iconType="help"
/>
<EuiSpacer size={'m'} />
</>
);
}

return (
<>
<EuiSpacer size={'m'} />
Expand All @@ -258,7 +231,6 @@ export const Expressions: React.FC<Props> = props => {
alertParams.criteria.map((e, idx) => {
return (
<ExpressionRow
canEditAggField={canAddConditions}
canDelete={alertParams.criteria.length > 1}
fields={derivedIndexPattern.fields}
remove={removeExpression}
Expand All @@ -281,20 +253,18 @@ export const Expressions: React.FC<Props> = props => {
/>

<div>
{canAddConditions && (
<EuiButtonEmpty
color={'primary'}
iconSide={'left'}
flush={'left'}
iconType={'plusInCircleFilled'}
onClick={addExpression}
>
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.addCondition"
defaultMessage="Add condition"
/>
</EuiButtonEmpty>
)}
<EuiButtonEmpty
color={'primary'}
iconSide={'left'}
flush={'left'}
iconType={'plusInCircleFilled'}
onClick={addExpression}
>
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.addCondition"
defaultMessage="Add condition"
/>
</EuiButtonEmpty>
</div>

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

interface ExpressionRowProps {
fields: IFieldType[];
canEditAggField: boolean;
expressionId: number;
expression: MetricExpression;
errors: IErrorObject;
Expand Down Expand Up @@ -424,20 +393,17 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = props => {
</StyledExpression>
{aggType !== 'count' && (
<StyledExpression>
{!props.canEditAggField && <DisabledAggField text={metric || ''} />}
{props.canEditAggField && (
<OfExpression
customAggTypesOptions={aggregationType}
aggField={metric}
fields={fields.map(f => ({
normalizedType: f.type,
name: f.name,
}))}
aggType={aggType}
errors={errors}
onChangeSelectedAggField={updateMetric}
/>
)}
<OfExpression
customAggTypesOptions={aggregationType}
aggField={metric}
fields={fields.map(f => ({
normalizedType: f.type,
name: f.name,
}))}
aggType={aggType}
errors={errors}
onChangeSelectedAggField={updateMetric}
/>
</StyledExpression>
)}
<StyledExpression>
Expand Down Expand Up @@ -469,19 +435,6 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = props => {
);
};

export const DisabledAggField = ({ text }: { text: string }) => {
return (
<EuiExpression
description={i18n.translate('xpack.infra.metrics.alertFlyout.of.buttonLabel', {
defaultMessage: 'of',
})}
value={text}
isActive={false}
color={'secondary'}
/>
);
};

export const aggregationType: { [key: string]: any } = {
avg: {
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', {
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/infra/public/containers/source/source.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { updateSourceMutation } from './update_source.gql_query';

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

const pickIndexPattern = (source: Source | undefined, type: 'logs' | 'metrics' | 'both') => {
export const pickIndexPattern = (source: Source | undefined, type: 'logs' | 'metrics' | 'both') => {
if (!source) {
return 'unknown-index';
}
Expand Down
Loading