Skip to content

Commit

Permalink
adding support for >=, <=, and between for threshold alerts (#35614)
Browse files Browse the repository at this point in the history
* adding support for >=, <=, and between for threshold alerts

* adding i18n for AND label

* making prettier happy

* appeasing the linter

* more linting issues

* more linting

* fixing test

* fixing error state for threshold expression

* fixing alignment for treshold inputs

* addressing PR feedback

* one more PR review fix

* eslint fixes

* fixing a couple of watch viz bugs
  • Loading branch information
bmcconaghy committed Apr 26, 2019
1 parent 6b17a37 commit 2f79b06
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 91 deletions.
4 changes: 3 additions & 1 deletion x-pack/plugins/watcher/common/constants/comparators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

export const COMPARATORS: { [key: string]: string } = {
GREATER_THAN: '>',

GREATER_THAN_OR_EQUALS: '>=',
BETWEEN: 'between',
LESS_THAN: '<',
LESS_THAN_OR_EQUALS: '<=',
};
1 change: 0 additions & 1 deletion x-pack/plugins/watcher/public/components/form_errors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import { EuiFormRow } from '@elastic/eui';
import React, { Children, cloneElement, Fragment, ReactElement } from 'react';

export const ErrableFormRow = ({
errorKey,
isShowingErrors,
Expand Down
58 changes: 58 additions & 0 deletions x-pack/plugins/watcher/public/models/watch/comparators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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.
*/

import { i18n } from '@kbn/i18n';

import { COMPARATORS } from '../../../common/constants';

export interface Comparator {
text: string;
value: string;
requiredValues: number;
}
export const comparators: { [key: string]: Comparator } = {
[COMPARATORS.GREATER_THAN]: {
text: i18n.translate('xpack.watcher.thresholdWatchExpression.comparators.isAboveLabel', {
defaultMessage: 'Is above',
}),
value: COMPARATORS.GREATER_THAN,
requiredValues: 1,
},
[COMPARATORS.GREATER_THAN_OR_EQUALS]: {
text: i18n.translate(
'xpack.watcher.thresholdWatchExpression.comparators.isAboveOrEqualsLabel',
{
defaultMessage: 'Is above or equals',
}
),
value: COMPARATORS.GREATER_THAN_OR_EQUALS,
requiredValues: 1,
},
[COMPARATORS.LESS_THAN]: {
text: i18n.translate('xpack.watcher.thresholdWatchExpression.comparators.isBelowLabel', {
defaultMessage: 'Is below',
}),
value: COMPARATORS.LESS_THAN,
requiredValues: 1,
},
[COMPARATORS.LESS_THAN_OR_EQUALS]: {
text: i18n.translate(
'xpack.watcher.thresholdWatchExpression.comparators.isBelowOrEqualsLabel',
{
defaultMessage: 'Is below or equals',
}
),
value: COMPARATORS.LESS_THAN_OR_EQUALS,
requiredValues: 1,
},
[COMPARATORS.BETWEEN]: {
text: i18n.translate('xpack.watcher.thresholdWatchExpression.comparators.isBetweenLabel', {
defaultMessage: 'Is between',
}),
value: COMPARATORS.BETWEEN,
requiredValues: 2,
},
};
31 changes: 23 additions & 8 deletions x-pack/plugins/watcher/public/models/watch/threshold_watch.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { getTimeUnitsLabel } from 'plugins/watcher/lib/get_time_units_label';
import { i18n } from '@kbn/i18n';
import { aggTypes } from './agg_types';
import { groupByTypes } from './group_by_types';
import { comparators } from './comparators';
const { BETWEEN } = COMPARATORS;
const DEFAULT_VALUES = {
AGG_TYPE: 'count',
TERM_SIZE: 5,
Expand All @@ -19,7 +21,7 @@ const DEFAULT_VALUES = {
TIME_WINDOW_UNIT: 'm',
TRIGGER_INTERVAL_SIZE: 1,
TRIGGER_INTERVAL_UNIT: 'm',
THRESHOLD: 1000,
THRESHOLD: [1000, 5000],
GROUP_BY: 'all',
};

Expand Down Expand Up @@ -102,7 +104,6 @@ export class ThresholdWatch extends BaseWatch {
aggField: [],
termSize: [],
termField: [],
threshold: [],
timeWindowSize: [],
};
validationResult.errors = errors;
Expand Down Expand Up @@ -176,15 +177,29 @@ export class ThresholdWatch extends BaseWatch {
);
}
}
if (!this.threshold) {
errors.threshold.push(
i18n.translate(

Array.from(Array(comparators[this.thresholdComparator].requiredValues)).forEach((value, i) => {
const key = `threshold${i}`;
errors[key] = [];
if (!this.threshold[i]) {
errors[key].push(i18n.translate(
'xpack.watcher.thresholdWatchExpression.thresholdLevel.valueIsRequiredValidationMessage',
{
defaultMessage: 'A value is required.',
}
)
);
));
}
});
if (this.thresholdComparator === BETWEEN && this.threshold[0] && this.threshold[1] && !(this.threshold[1] > this.threshold[0])) {
errors.threshold1.push(i18n.translate(
'xpack.watcher.thresholdWatchExpression.thresholdLevel.secondValueMustBeGreaterMessage',
{
defaultMessage: 'This value must be greater than {lowerBound}.',
values: {
lowerBound: this.threshold[0]
}
}
));
}
if (!this.timeWindowSize) {
errors.timeWindowSize.push(
Expand Down Expand Up @@ -212,7 +227,7 @@ export class ThresholdWatch extends BaseWatch {
thresholdComparator: this.thresholdComparator,
timeWindowSize: this.timeWindowSize,
timeWindowUnit: this.timeWindowUnit,
threshold: this.threshold,
threshold: comparators[this.thresholdComparator].requiredValues > 1 ? this.threshold : this.threshold[0],
});

return result;
Expand Down
28 changes: 0 additions & 28 deletions x-pack/plugins/watcher/public/sections/watch_edit/comparators.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/

import React, { Fragment, useContext, useEffect, useState } from 'react';

import {
EuiButton,
EuiButtonEmpty,
Expand All @@ -32,7 +31,7 @@ import { ErrableFormRow } from '../../../components/form_errors';
import { fetchFields, getMatchingIndices, loadIndexPatterns } from '../../../lib/api';
import { aggTypes } from '../../../models/watch/agg_types';
import { groupByTypes } from '../../../models/watch/group_by_types';
import { comparators } from '../comparators';
import { comparators } from '../../../models/watch/comparators';
import { timeUnits } from '../time_units';
import { onWatchSave, saveWatch } from '../watch_edit_actions';
import { WatchContext } from './watch_context';
Expand Down Expand Up @@ -156,11 +155,21 @@ const ThresholdWatchEditUi = ({ intl, pageTitle }: { intl: InjectedIntl; pageTit
defaultMessage: 'Please fix the errors in the expression below.',
}
);
const expressionFields = ['aggField', 'termSize', 'termField', 'threshold', 'timeWindowSize'];
const expressionFields = [
'aggField',
'termSize',
'termField',
'threshold0',
'threshold1',
'timeWindowSize',
];
const hasExpressionErrors = !!Object.keys(errors).find(
errorKey => expressionFields.includes(errorKey) && errors[errorKey].length >= 1
);
const shouldShowThresholdExpression = watch.index && watch.index.length > 0 && watch.timeField;
const andThresholdText = i18n.translate('xpack.watcher.sections.watchEdit.threshold.andLabel', {
defaultMessage: 'AND',
});
return (
<EuiPageContent>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexEnd">
Expand Down Expand Up @@ -630,12 +639,22 @@ const ThresholdWatchEditUi = ({ intl, pageTitle }: { intl: InjectedIntl; pageTit
button={
<EuiExpression
description={comparators[watch.thresholdComparator].text}
value={watch.threshold}
isActive={watchThresholdPopoverOpen || !watch.threshold}
value={watch.threshold
.slice(0, comparators[watch.thresholdComparator].requiredValues)
.join(` ${andThresholdText} `)}
isActive={
watchThresholdPopoverOpen ||
errors.threshold0.length ||
(errors.threshold1 && errors.threshold1.length)
}
onClick={() => {
setWatchThresholdPopoverOpen(true);
}}
color={watch.threshold ? 'secondary' : 'danger'}
color={
errors.threshold0.length || (errors.threshold1 && errors.threshold1.length)
? 'danger'
: 'secondary'
}
/>
}
isOpen={watchThresholdPopoverOpen}
Expand All @@ -648,33 +667,50 @@ const ThresholdWatchEditUi = ({ intl, pageTitle }: { intl: InjectedIntl; pageTit
>
<div>
<EuiPopoverTitle>{comparators[watch.thresholdComparator].text}</EuiPopoverTitle>
<EuiFlexGroup>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiSelect
value={watch.thresholdComparator}
onChange={e => {
setWatchProperty('thresholdComparator', e.target.value);
}}
options={Object.values(comparators)}
options={Object.values(comparators).map(({ text, value }) => {
return { text, value };
})}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ErrableFormRow
errorKey="threshold"
isShowingErrors={hasErrors}
errors={errors}
>
<EuiFieldNumber
value={watch.threshold}
min={1}
onChange={e => {
const { value } = e.target;
const threshold = value !== '' ? parseInt(value, 10) : value;
setWatchProperty('threshold', threshold);
}}
/>
</ErrableFormRow>
</EuiFlexItem>
{Array.from(Array(comparators[watch.thresholdComparator].requiredValues)).map(
(notUsed, i) => {
return (
<Fragment key={`threshold${i}`}>
{i > 0 ? (
<EuiFlexItem grow={false}>
<EuiText>{andThresholdText}</EuiText>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<ErrableFormRow
errorKey={`threshold${i}`}
isShowingErrors={hasErrors}
errors={errors}
>
<EuiFieldNumber
value={watch.threshold[i]}
min={1}
onChange={e => {
const { value } = e.target;
const threshold = value !== '' ? parseInt(value, 10) : value;
const newThreshold = [...watch.threshold];
newThreshold[i] = threshold;
setWatchProperty('threshold', newThreshold);
}}
/>
</ErrableFormRow>
</EuiFlexItem>
</Fragment>
);
}
)}
</EuiFlexGroup>
</div>
</EuiPopover>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { VisualizeOptions } from 'plugins/watcher/models/visualize_options';
import { getWatchVisualizationData } from '../../../lib/api';
import { WatchContext } from './watch_context';
import { aggTypes } from '../../../models/watch/agg_types';
import { comparators } from '../../../models/watch/comparators';
const getChartTheme = () => {
const isDarkTheme = chrome.getUiSettingsClient().get('theme:darkMode');
const baseTheme = isDarkTheme ? DARK_THEME : LIGHT_THEME;
Expand Down Expand Up @@ -82,7 +83,9 @@ const getDomain = (watch: any) => {
max: visualizeTimeWindowTo,
};
};

const getThreshold = (watch: any) => {
return watch.threshold.slice(0, comparators[watch.thresholdComparator].requiredValues);
};
const getTimeBuckets = (watch: any) => {
const domain = getDomain(watch);
const timeBuckets = new TimeBuckets();
Expand Down Expand Up @@ -123,12 +126,17 @@ const WatchVisualizationUi = () => {
.format(getTimeBuckets(watch).getScaledDateFormat());
};
const aggLabel = aggTypes[watch.aggType].text;
const thresholdCustomSeriesColors: CustomSeriesColorsMap = new Map();
const thresholdDataSeriesColorValues: DataSeriesColorsValues = {
colorValues: [],
specId: getSpecId('threshold'),

const getCustomColors = (specId: string) => {
const customSeriesColors: CustomSeriesColorsMap = new Map();
const dataSeriesColorValues: DataSeriesColorsValues = {
colorValues: [],
specId: getSpecId(specId),
};
customSeriesColors.set(dataSeriesColorValues, '#BD271E');
return customSeriesColors;
};
thresholdCustomSeriesColors.set(thresholdDataSeriesColorValues, '#BD271E');

const domain = getDomain(watch);
const watchVisualizationDataKeys = Object.keys(watchVisualizationData);
return (
Expand Down Expand Up @@ -163,17 +171,23 @@ const WatchVisualizationUi = () => {
/>
);
})}
<LineSeries
id={getSpecId('threshold')}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
data={[[domain.min, watch.threshold], [domain.max, watch.threshold]]}
xAccessor={0}
yAccessors={[1]}
timeZone={timezone}
yScaleToDataExtent={true}
customSeriesColors={thresholdCustomSeriesColors}
/>
{getThreshold(watch).map((value: any, i: number) => {
const specId = i === 0 ? 'threshold' : `threshold${i}`;
return (
<LineSeries
key={specId}
id={getSpecId(specId)}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
data={[[domain.min, watch.threshold[i]], [domain.max, watch.threshold[i]]]}
xAccessor={0}
yAccessors={[1]}
timeZone={timezone}
yScaleToDataExtent={true}
customSeriesColors={getCustomColors(specId)}
/>
);
})}
</Chart>
) : (
<EuiCallOut
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ describe('ThresholdWatch', () => {
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold'
threshold: ['threshold']
};

expect(actual).to.eql(expected);
Expand Down
Loading

0 comments on commit 2f79b06

Please sign in to comment.