Skip to content

Commit

Permalink
[Metrics UI] Add Charts to Alert Conditions (#64384)
Browse files Browse the repository at this point in the history
* [Metrics UI] Add Charts to Alert Conditions

- Reorganize files under public/alerting
- Change Metrics Explorer API to force interval
- Add charts to expression rows
- Allow expression rows to be collapsable
- Adding sum aggregation to Metrics Explorer for parity

* Adding interval information to Metrics Eexplorer API

* Moving data hook into the expression charts component

* Revert "Adding interval information to Metrics Eexplorer API"

This reverts commit f6e2fc1.

* Reducing the opacity for the threshold areas

* Changing darkMode to use alertsContext.uiSettings
  • Loading branch information
simianhacker committed Apr 30, 2020
1 parent c8b9bdd commit 0399f70
Show file tree
Hide file tree
Showing 22 changed files with 770 additions and 251 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const METRIC_EXPLORER_AGGREGATIONS = [
'cardinality',
'rate',
'count',
'sum',
] as const;

type MetricExplorerAggregations = typeof METRIC_EXPLORER_AGGREGATIONS[number];
Expand Down Expand Up @@ -54,6 +55,7 @@ export const metricsExplorerRequestBodyOptionalFieldsRT = rt.partial({
afterKey: rt.union([rt.string, rt.null, rt.undefined]),
limit: rt.union([rt.number, rt.null, rt.undefined]),
filterQuery: rt.union([rt.string, rt.null, rt.undefined]),
forceInterval: rt.boolean,
});

export const metricsExplorerRequestBodyRT = rt.intersection([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export const metricsExplorerViewSavedObjectMappings: {
},
options: {
properties: {
forceInterval: {
type: 'boolean',
},
metrics: {
type: 'nested',
properties: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@

import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiButtonIcon,
EuiSpacer,
EuiText,
EuiFormRow,
Expand All @@ -18,40 +15,28 @@ import {
EuiIcon,
EuiFieldSearch,
} from '@elastic/eui';
import { IFieldType } from 'src/plugins/data/public';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
MetricExpressionParams,
Comparator,
Aggregators,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../server/lib/alerting/metric_threshold/types';
import { euiStyled } from '../../../../../observability/public';
import {
WhenExpression,
OfExpression,
ThresholdExpression,
ForLastExpression,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../triggers_actions_ui/public/common';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { builtInComparators } from '../../../../../triggers_actions_ui/public/common/constants';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IErrorObject } from '../../../../../triggers_actions_ui/public/types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer';
import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar';
import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
import { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by';
import { useSourceViaHttp } from '../../../containers/source/use_source_via_http';

interface AlertContextMeta {
currentOptions?: Partial<MetricsExplorerOptions>;
series?: MetricsExplorerSeries;
}
import { ExpressionRow } from './expression_row';
import { AlertContextMeta, TimeUnit, MetricExpression } from '../types';
import { ExpressionChart } from './expression_chart';

interface Props {
errors: IErrorObject[];
Expand All @@ -67,11 +52,6 @@ interface Props {
setAlertProperty(key: string, value: any): void;
}

type TimeUnit = 's' | 'm' | 'h' | 'd';
type MetricExpression = Omit<MetricExpressionParams, 'metric'> & {
metric?: string;
};

const defaultExpression = {
aggType: Aggregators.AVERAGE,
comparator: Comparator.GT,
Expand All @@ -80,17 +60,6 @@ const defaultExpression = {
timeUnit: 'm',
} as MetricExpression;

const customComparators = {
...builtInComparators,
[Comparator.OUTSIDE_RANGE]: {
text: i18n.translate('xpack.infra.metrics.alertFlyout.outsideRangeLabel', {
defaultMessage: 'Is not between',
}),
value: Comparator.OUTSIDE_RANGE,
requiredValues: 2,
},
};

export const Expressions: React.FC<Props> = props => {
const { setAlertParams, alertParams, errors, alertsContext } = props;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
Expand All @@ -101,7 +70,6 @@ export const Expressions: React.FC<Props> = props => {
});
const [timeSize, setTimeSize] = useState<number | undefined>(1);
const [timeUnit, setTimeUnit] = useState<TimeUnit>('m');

const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [
createDerivedIndexPattern,
]);
Expand All @@ -127,14 +95,14 @@ export const Expressions: React.FC<Props> = props => {
);

const addExpression = useCallback(() => {
const exp = alertParams.criteria.slice();
const exp = alertParams.criteria?.slice() || [];
exp.push(defaultExpression);
setAlertParams('criteria', exp);
}, [setAlertParams, alertParams.criteria]);

const removeExpression = useCallback(
(id: number) => {
const exp = alertParams.criteria.slice();
const exp = alertParams.criteria?.slice() || [];
if (exp.length > 1) {
exp.splice(id, 1);
setAlertParams('criteria', exp);
Expand Down Expand Up @@ -167,10 +135,11 @@ export const Expressions: React.FC<Props> = props => {

const updateTimeSize = useCallback(
(ts: number | undefined) => {
const criteria = alertParams.criteria.map(c => ({
...c,
timeSize: ts,
}));
const criteria =
alertParams.criteria?.map(c => ({
...c,
timeSize: ts,
})) || [];
setTimeSize(ts || undefined);
setAlertParams('criteria', criteria);
},
Expand All @@ -179,10 +148,11 @@ export const Expressions: React.FC<Props> = props => {

const updateTimeUnit = useCallback(
(tu: string) => {
const criteria = alertParams.criteria.map(c => ({
...c,
timeUnit: tu,
}));
const criteria =
alertParams.criteria?.map(c => ({
...c,
timeUnit: tu,
})) || [];
setTimeUnit(tu as TimeUnit);
setAlertParams('criteria', criteria);
},
Expand Down Expand Up @@ -250,7 +220,7 @@ export const Expressions: React.FC<Props> = props => {
alertParams.criteria.map((e, idx) => {
return (
<ExpressionRow
canDelete={alertParams.criteria.length > 1}
canDelete={(alertParams.criteria && alertParams.criteria.length > 1) || false}
fields={derivedIndexPattern.fields}
remove={removeExpression}
addExpression={addExpression}
Expand All @@ -259,17 +229,28 @@ export const Expressions: React.FC<Props> = props => {
setAlertParams={updateParams}
errors={errors[idx] || emptyError}
expression={e || {}}
/>
>
<ExpressionChart
expression={e}
context={alertsContext}
derivedIndexPattern={derivedIndexPattern}
source={source}
filterQuery={alertParams.filterQuery}
groupBy={alertParams.groupBy}
/>
</ExpressionRow>
);
})}

<ForLastExpression
timeWindowSize={timeSize}
timeWindowUnit={timeUnit}
errors={emptyError}
onChangeWindowSize={updateTimeSize}
onChangeWindowUnit={updateTimeUnit}
/>
<div style={{ marginLeft: 28 }}>
<ForLastExpression
timeWindowSize={timeSize}
timeWindowUnit={timeUnit}
errors={emptyError}
onChangeWindowSize={updateTimeSize}
onChangeWindowUnit={updateTimeUnit}
/>
</div>

<div>
<EuiButtonEmpty
Expand Down Expand Up @@ -359,175 +340,3 @@ export const Expressions: React.FC<Props> = props => {
</>
);
};

interface ExpressionRowProps {
fields: IFieldType[];
expressionId: number;
expression: MetricExpression;
errors: IErrorObject;
canDelete: boolean;
addExpression(): void;
remove(id: number): void;
setAlertParams(id: number, params: MetricExpression): void;
}

const StyledExpressionRow = euiStyled(EuiFlexGroup)`
display: flex;
flex-wrap: wrap;
margin: 0 -4px;
`;

const StyledExpression = euiStyled.div`
padding: 0 4px;
`;

export const ExpressionRow: React.FC<ExpressionRowProps> = props => {
const { setAlertParams, expression, errors, expressionId, remove, fields, canDelete } = props;
const {
aggType = Aggregators.MAX,
metric,
comparator = Comparator.GT,
threshold = [],
} = expression;

const updateAggType = useCallback(
(at: string) => {
setAlertParams(expressionId, {
...expression,
aggType: at as MetricExpression['aggType'],
metric: at === 'count' ? undefined : expression.metric,
});
},
[expressionId, expression, setAlertParams]
);

const updateMetric = useCallback(
(m?: MetricExpression['metric']) => {
setAlertParams(expressionId, { ...expression, metric: m });
},
[expressionId, expression, setAlertParams]
);

const updateComparator = useCallback(
(c?: string) => {
setAlertParams(expressionId, { ...expression, comparator: c as Comparator });
},
[expressionId, expression, setAlertParams]
);

const updateThreshold = useCallback(
t => {
if (t.join() !== expression.threshold.join()) {
setAlertParams(expressionId, { ...expression, threshold: t });
}
},
[expressionId, expression, setAlertParams]
);

return (
<>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow>
<StyledExpressionRow>
<StyledExpression>
<WhenExpression
customAggTypesOptions={aggregationType}
aggType={aggType}
onChangeSelectedAggType={updateAggType}
/>
</StyledExpression>
{aggType !== 'count' && (
<StyledExpression>
<OfExpression
customAggTypesOptions={aggregationType}
aggField={metric}
fields={fields.map(f => ({
normalizedType: f.type,
name: f.name,
}))}
aggType={aggType}
errors={errors}
onChangeSelectedAggField={updateMetric}
/>
</StyledExpression>
)}
<StyledExpression>
<ThresholdExpression
thresholdComparator={comparator || Comparator.GT}
threshold={threshold}
customComparators={customComparators}
onChangeSelectedThresholdComparator={updateComparator}
onChangeSelectedThreshold={updateThreshold}
errors={errors}
/>
</StyledExpression>
</StyledExpressionRow>
</EuiFlexItem>
{canDelete && (
<EuiFlexItem grow={false}>
<EuiButtonIcon
aria-label={i18n.translate('xpack.infra.metrics.alertFlyout.removeCondition', {
defaultMessage: 'Remove condition',
})}
color={'danger'}
iconType={'trash'}
onClick={() => remove(expressionId)}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer size={'s'} />
</>
);
};

export const aggregationType: { [key: string]: any } = {
avg: {
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', {
defaultMessage: 'Average',
}),
fieldRequired: true,
validNormalizedTypes: ['number'],
value: Aggregators.AVERAGE,
},
max: {
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.max', {
defaultMessage: 'Max',
}),
fieldRequired: true,
validNormalizedTypes: ['number', 'date'],
value: Aggregators.MAX,
},
min: {
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.min', {
defaultMessage: 'Min',
}),
fieldRequired: true,
validNormalizedTypes: ['number', 'date'],
value: Aggregators.MIN,
},
cardinality: {
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.cardinality', {
defaultMessage: 'Cardinality',
}),
fieldRequired: false,
value: Aggregators.CARDINALITY,
validNormalizedTypes: ['number'],
},
rate: {
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.rate', {
defaultMessage: 'Rate',
}),
fieldRequired: false,
value: Aggregators.RATE,
validNormalizedTypes: ['number'],
},
count: {
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.count', {
defaultMessage: 'Document count',
}),
fieldRequired: false,
value: Aggregators.COUNT,
validNormalizedTypes: ['number'],
},
};
Loading

0 comments on commit 0399f70

Please sign in to comment.