From 9e3843a80c67ebff79499b6dd4825bd44455cda7 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Wed, 4 Mar 2020 13:23:59 -0500 Subject: [PATCH] [Alerting] replace index threshold graph usage of watcher apis --- .../lib => common}/parse_duration.test.ts | 0 .../{server/lib => common}/parse_duration.ts | 1 + x-pack/plugins/alerting/server/lib/index.ts | 2 +- .../alert_types/index_threshold/index.ts} | 11 +- .../index_threshold/lib/time_series_types.ts | 18 +- .../plugins/triggers_actions_ui/kibana.json | 2 +- .../threshold/expression.tsx | 3 + .../builtin_alert_types/threshold/lib/api.ts | 39 +- .../time_buckets/calc_auto_interval.test.ts | 123 ------ .../lib/time_buckets/calc_auto_interval.ts | 132 ------ .../lib/time_buckets/calc_es_interval.js | 58 --- .../threshold/lib/time_buckets/index.d.ts | 7 - .../lib/time_buckets/time_buckets.js | 397 ------------------ .../builtin_alert_types/threshold/types.ts | 6 + .../threshold/visualization.tsx | 91 ++-- .../sections/alert_form/alert_form.tsx | 1 + 16 files changed, 101 insertions(+), 790 deletions(-) rename x-pack/plugins/alerting/{server/lib => common}/parse_duration.test.ts (100%) rename x-pack/plugins/alerting/{server/lib => common}/parse_duration.ts (95%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.js => alerting_builtins/common/alert_types/index_threshold/index.ts} (51%) delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.test.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_es_interval.js delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.d.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/time_buckets.js diff --git a/x-pack/plugins/alerting/server/lib/parse_duration.test.ts b/x-pack/plugins/alerting/common/parse_duration.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/parse_duration.test.ts rename to x-pack/plugins/alerting/common/parse_duration.test.ts diff --git a/x-pack/plugins/alerting/server/lib/parse_duration.ts b/x-pack/plugins/alerting/common/parse_duration.ts similarity index 95% rename from x-pack/plugins/alerting/server/lib/parse_duration.ts rename to x-pack/plugins/alerting/common/parse_duration.ts index 51f3d746a6869..4e35a4c4cb0cf 100644 --- a/x-pack/plugins/alerting/server/lib/parse_duration.ts +++ b/x-pack/plugins/alerting/common/parse_duration.ts @@ -8,6 +8,7 @@ const MINUTES_REGEX = /^[1-9][0-9]*m$/; const HOURS_REGEX = /^[1-9][0-9]*h$/; const DAYS_REGEX = /^[1-9][0-9]*d$/; +// parse an interval string '{digit*}{s|m|h|d}' into milliseconds export function parseDuration(duration: string): number { const parsed = parseInt(duration, 10); if (isSeconds(duration)) { diff --git a/x-pack/plugins/alerting/server/lib/index.ts b/x-pack/plugins/alerting/server/lib/index.ts index c84825cadbd16..2f610aafd8c31 100644 --- a/x-pack/plugins/alerting/server/lib/index.ts +++ b/x-pack/plugins/alerting/server/lib/index.ts @@ -4,6 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { parseDuration, validateDurationSchema } from './parse_duration'; +export { parseDuration, validateDurationSchema } from '../../common/parse_duration'; export { LicenseState } from './license_state'; export { validateAlertTypeParams } from './validate_alert_type_params'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.js b/x-pack/plugins/alerting_builtins/common/alert_types/index_threshold/index.ts similarity index 51% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.js rename to x-pack/plugins/alerting_builtins/common/alert_types/index_threshold/index.ts index 4f2cce5861424..63873918b0231 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.js +++ b/x-pack/plugins/alerting_builtins/common/alert_types/index_threshold/index.ts @@ -4,4 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -export { TimeBuckets } from './time_buckets'; +export interface TimeSeriesResult { + results: TimeSeriesResultRow[]; +} + +export interface TimeSeriesResultRow { + group: string; + metrics: MetricResult[]; +} + +export type MetricResult = [string, number]; // [iso date, value] diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts index 6cb21a1581113..abe5d562027eb 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts @@ -18,19 +18,11 @@ import { getDateStartAfterDateEndErrorMessage, } from './date_range_info'; -// The result is an object with a key for every field value aggregated -// via the `aggField` property. If `aggField` is not specified, the -// object will have a single key of `all documents`. The value associated -// with each key is an array of 2-tuples of `[ ISO-date, calculated-value ]` - -export interface TimeSeriesResult { - results: TimeSeriesResultRow[]; -} -export interface TimeSeriesResultRow { - group: string; - metrics: MetricResult[]; -} -export type MetricResult = [string, number]; // [iso date, value] +export { + TimeSeriesResult, + TimeSeriesResultRow, + MetricResult, +} from '../../../../common/alert_types/index_threshold'; // The parameters here are very similar to the alert parameters. // Missing are `comparator` and `threshold`, which aren't needed to generate diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index cf66883412edb..2652c0edbe381 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -3,5 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["management", "charts", "data"] + "requiredPlugins": ["management", "charts", "data", "alerting", "alertingBuiltins"] } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index a2ef67be7bca2..80746c3758d53 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -63,6 +63,7 @@ const expressionFieldsWithValidation = [ interface IndexThresholdProps { alertParams: IndexThresholdAlertParams; + alertInterval: string; setAlertParams: (property: string, value: any) => void; setAlertProperty: (key: string, value: any) => void; errors: { [key: string]: string[] }; @@ -71,6 +72,7 @@ interface IndexThresholdProps { export const IndexThresholdAlertTypeExpression: React.FunctionComponent = ({ alertParams, + alertInterval, setAlertParams, setAlertProperty, errors, @@ -476,6 +478,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent { return savedObjects; }; +const TimeSeriesQueryRoute = '/api/alerting_builtins/index_threshold/_time_series_query'; + +interface GetThresholdAlertVisualizationDataParams { + model: any; + visualizeOptions: any; + http: HttpSetup; +} + export async function getThresholdAlertVisualizationData({ model, visualizeOptions, http, -}: { - model: any; - visualizeOptions: any; - http: HttpSetup; -}): Promise> { - const { visualizeData } = await http.post(`${WATCHER_API_ROOT}/watch/visualize`, { - body: JSON.stringify({ - watch: model, - options: visualizeOptions, - }), +}: GetThresholdAlertVisualizationDataParams): Promise { + const timeSeriesQueryParams = { + index: model.index, + timeField: model.timeField, + aggType: model.aggType, + aggField: model.aggField, + groupBy: model.groupBy, + termField: model.termField, + termSize: model.termSize, + timeWindowSize: model.timeWindowSize, + timeWindowUnit: model.timeWindowUnit, + dateStart: new Date(visualizeOptions.rangeFrom).toISOString(), + dateEnd: new Date(visualizeOptions.rangeTo).toISOString(), + interval: visualizeOptions.interval, + }; + + return await http.post(TimeSeriesQueryRoute, { + body: JSON.stringify(timeSeriesQueryParams), }); - return visualizeData; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.test.ts deleted file mode 100644 index 34e435be152f6..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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 moment from 'moment'; - -import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval'; - -describe('calcAutoIntervalNear', () => { - test('1h/0 buckets = 0ms buckets', () => { - const interval = calcAutoIntervalNear(0, Number(moment.duration(1, 'h'))); - expect(interval.asMilliseconds()).toBe(0); - }); - - test('undefined/100 buckets = 0ms buckets', () => { - const interval = calcAutoIntervalNear(0, undefined as any); - expect(interval.asMilliseconds()).toBe(0); - }); - - test('1ms/100 buckets = 1ms buckets', () => { - const interval = calcAutoIntervalNear(100, Number(moment.duration(1, 'ms'))); - expect(interval.asMilliseconds()).toBe(1); - }); - - test('200ms/100 buckets = 2ms buckets', () => { - const interval = calcAutoIntervalNear(100, Number(moment.duration(200, 'ms'))); - expect(interval.asMilliseconds()).toBe(2); - }); - - test('1s/1000 buckets = 1ms buckets', () => { - const interval = calcAutoIntervalNear(1000, Number(moment.duration(1, 's'))); - expect(interval.asMilliseconds()).toBe(1); - }); - - test('1000h/1000 buckets = 1h buckets', () => { - const interval = calcAutoIntervalNear(1000, Number(moment.duration(1000, 'hours'))); - expect(interval.asHours()).toBe(1); - }); - - test('1h/100 buckets = 30s buckets', () => { - const interval = calcAutoIntervalNear(100, Number(moment.duration(1, 'hours'))); - expect(interval.asSeconds()).toBe(30); - }); - - test('1d/25 buckets = 1h buckets', () => { - const interval = calcAutoIntervalNear(25, Number(moment.duration(1, 'day'))); - expect(interval.asHours()).toBe(1); - }); - - test('1y/1000 buckets = 12h buckets', () => { - const interval = calcAutoIntervalNear(1000, Number(moment.duration(1, 'year'))); - expect(interval.asHours()).toBe(12); - }); - - test('1y/10000 buckets = 1h buckets', () => { - const interval = calcAutoIntervalNear(10000, Number(moment.duration(1, 'year'))); - expect(interval.asHours()).toBe(1); - }); - - test('1y/100000 buckets = 5m buckets', () => { - const interval = calcAutoIntervalNear(100000, Number(moment.duration(1, 'year'))); - expect(interval.asMinutes()).toBe(5); - }); -}); - -describe('calcAutoIntervalLessThan', () => { - test('1h/0 buckets = 0ms buckets', () => { - const interval = calcAutoIntervalLessThan(0, Number(moment.duration(1, 'h'))); - expect(interval.asMilliseconds()).toBe(0); - }); - - test('undefined/100 buckets = 0ms buckets', () => { - const interval = calcAutoIntervalLessThan(0, undefined as any); - expect(interval.asMilliseconds()).toBe(0); - }); - - test('1ms/100 buckets = 1ms buckets', () => { - const interval = calcAutoIntervalLessThan(100, Number(moment.duration(1, 'ms'))); - expect(interval.asMilliseconds()).toBe(1); - }); - - test('200ms/100 buckets = 2ms buckets', () => { - const interval = calcAutoIntervalLessThan(100, Number(moment.duration(200, 'ms'))); - expect(interval.asMilliseconds()).toBe(2); - }); - - test('1s/1000 buckets = 1ms buckets', () => { - const interval = calcAutoIntervalLessThan(1000, Number(moment.duration(1, 's'))); - expect(interval.asMilliseconds()).toBe(1); - }); - - test('1000h/1000 buckets = 1h buckets', () => { - const interval = calcAutoIntervalLessThan(1000, Number(moment.duration(1000, 'hours'))); - expect(interval.asHours()).toBe(1); - }); - - test('1h/100 buckets = 30s buckets', () => { - const interval = calcAutoIntervalLessThan(100, Number(moment.duration(1, 'hours'))); - expect(interval.asSeconds()).toBe(30); - }); - - test('1d/25 buckets = 30m buckets', () => { - const interval = calcAutoIntervalLessThan(25, Number(moment.duration(1, 'day'))); - expect(interval.asMinutes()).toBe(30); - }); - - test('1y/1000 buckets = 3h buckets', () => { - const interval = calcAutoIntervalLessThan(1000, Number(moment.duration(1, 'year'))); - expect(interval.asHours()).toBe(3); - }); - - test('1y/10000 buckets = 30m buckets', () => { - const interval = calcAutoIntervalLessThan(10000, Number(moment.duration(1, 'year'))); - expect(interval.asMinutes()).toBe(30); - }); - - test('1y/100000 buckets = 5m buckets', () => { - const interval = calcAutoIntervalLessThan(100000, Number(moment.duration(1, 'year'))); - expect(interval.asMinutes()).toBe(5); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.ts deleted file mode 100644 index c910f1e6752d4..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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 moment from 'moment'; - -const boundsDescending = [ - { - bound: Infinity, - interval: Number(moment.duration(1, 'year')), - }, - { - bound: Number(moment.duration(1, 'year')), - interval: Number(moment.duration(1, 'month')), - }, - { - bound: Number(moment.duration(3, 'week')), - interval: Number(moment.duration(1, 'week')), - }, - { - bound: Number(moment.duration(1, 'week')), - interval: Number(moment.duration(1, 'd')), - }, - { - bound: Number(moment.duration(24, 'hour')), - interval: Number(moment.duration(12, 'hour')), - }, - { - bound: Number(moment.duration(6, 'hour')), - interval: Number(moment.duration(3, 'hour')), - }, - { - bound: Number(moment.duration(2, 'hour')), - interval: Number(moment.duration(1, 'hour')), - }, - { - bound: Number(moment.duration(45, 'minute')), - interval: Number(moment.duration(30, 'minute')), - }, - { - bound: Number(moment.duration(20, 'minute')), - interval: Number(moment.duration(10, 'minute')), - }, - { - bound: Number(moment.duration(9, 'minute')), - interval: Number(moment.duration(5, 'minute')), - }, - { - bound: Number(moment.duration(3, 'minute')), - interval: Number(moment.duration(1, 'minute')), - }, - { - bound: Number(moment.duration(45, 'second')), - interval: Number(moment.duration(30, 'second')), - }, - { - bound: Number(moment.duration(15, 'second')), - interval: Number(moment.duration(10, 'second')), - }, - { - bound: Number(moment.duration(7.5, 'second')), - interval: Number(moment.duration(5, 'second')), - }, - { - bound: Number(moment.duration(5, 'second')), - interval: Number(moment.duration(1, 'second')), - }, - { - bound: Number(moment.duration(500, 'ms')), - interval: Number(moment.duration(100, 'ms')), - }, -]; - -function getPerBucketMs(count: number, duration: number) { - const ms = duration / count; - return isFinite(ms) ? ms : NaN; -} - -function normalizeMinimumInterval(targetMs: number) { - const value = isNaN(targetMs) ? 0 : Math.max(Math.floor(targetMs), 1); - return moment.duration(value); -} - -/** - * Using some simple rules we pick a "pretty" interval that will - * produce around the number of buckets desired given a time range. - * - * @param targetBucketCount desired number of buckets - * @param duration time range the agg covers - */ -export function calcAutoIntervalNear(targetBucketCount: number, duration: number) { - const targetPerBucketMs = getPerBucketMs(targetBucketCount, duration); - - // Find the first bound which is smaller than our target. - const lowerBoundIndex = boundsDescending.findIndex(({ bound }) => { - const boundMs = Number(bound); - return boundMs <= targetPerBucketMs; - }); - - // The bound immediately preceeding that lower bound contains the - // interval most closely matching our target. - if (lowerBoundIndex !== -1) { - const nearestInterval = boundsDescending[lowerBoundIndex - 1].interval; - return moment.duration(nearestInterval); - } - - // If the target is smaller than any of our bounds, then we'll use it for the interval as-is. - return normalizeMinimumInterval(targetPerBucketMs); -} - -/** - * Pick a "pretty" interval that produces no more than the maxBucketCount - * for the given time range. - * - * @param maxBucketCount maximum number of buckets to create - * @param duration amount of time covered by the agg - */ -export function calcAutoIntervalLessThan(maxBucketCount: number, duration: number) { - const maxPerBucketMs = getPerBucketMs(maxBucketCount, duration); - - for (const { interval } of boundsDescending) { - // Find the highest interval which meets our per bucket limitation. - if (interval <= maxPerBucketMs) { - return moment.duration(interval); - } - } - - // If the max is smaller than any of our intervals, then we'll use it for the interval as-is. - return normalizeMinimumInterval(maxPerBucketMs); -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_es_interval.js b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_es_interval.js deleted file mode 100644 index bb5725c567b1f..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_es_interval.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 dateMath from '@elastic/datemath'; - -import { parseEsInterval } from '../../../../../../../../../../src/plugins/data/public'; - -const unitsDesc = dateMath.unitsDesc; -const largeMax = unitsDesc.indexOf('M'); - -/** - * Convert a moment.duration into an es - * compatible expression, and provide - * associated metadata - * - * @param {moment.duration} duration - * @return {object} - */ -export function convertDurationToNormalizedEsInterval(duration) { - for (let i = 0; i < unitsDesc.length; i++) { - const unit = unitsDesc[i]; - const val = duration.as(unit); - // find a unit that rounds neatly - if (val >= 1 && Math.floor(val) === val) { - // if the unit is "large", like years, but - // isn't set to 1 ES will puke. So keep going until - // we get out of the "large" units - if (i <= largeMax && val !== 1) { - continue; - } - - return { - value: val, - unit: unit, - expression: val + unit, - }; - } - } - - const ms = duration.as('ms'); - return { - value: ms, - unit: 'ms', - expression: ms + 'ms', - }; -} - -export function convertIntervalToEsInterval(interval) { - const { value, unit } = parseEsInterval(interval); - return { - value, - unit, - expression: interval, - }; -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.d.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.d.ts deleted file mode 100644 index d62655518f44a..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * 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. - */ - -export const TimeBuckets: any; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/time_buckets.js b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/time_buckets.js deleted file mode 100644 index f49e85ddefea8..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/time_buckets.js +++ /dev/null @@ -1,397 +0,0 @@ -/* - * 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 _ from 'lodash'; -import moment from 'moment'; -import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval'; -import { - convertDurationToNormalizedEsInterval, - convertIntervalToEsInterval, -} from './calc_es_interval'; -import { fieldFormats, parseInterval } from '../../../../../../../../../../src/plugins/data/public'; - -function isValidMoment(m) { - return m && 'isValid' in m && m.isValid(); -} - -/** - * Helper class for wrapping the concept of an "Interval", - * which describes a timespan that will separate moments. - * - * @param {state} object - one of "" - * @param {[type]} display [description] - */ -function TimeBuckets(uiSettings, dataFieldsFormats) { - this.uiSettings = uiSettings; - this.dataFieldsFormats = dataFieldsFormats; - return TimeBuckets.__cached__(this); -} - -/**** - * PUBLIC API - ****/ - -/** - * Set the bounds that these buckets are expected to cover. - * This is required to support interval "auto" as well - * as interval scaling. - * - * @param {object} input - an object with properties min and max, - * representing the edges for the time span - * we should cover - * - * @returns {undefined} - */ -TimeBuckets.prototype.setBounds = function(input) { - if (!input) return this.clearBounds(); - - let bounds; - if (_.isPlainObject(input)) { - // accept the response from timefilter.getActiveBounds() - bounds = [input.min, input.max]; - } else { - bounds = Array.isArray(input) ? input : []; - } - - const moments = _(bounds) - .map(_.ary(moment, 1)) - .sortBy(Number); - - const valid = moments.size() === 2 && moments.every(isValidMoment); - if (!valid) { - this.clearBounds(); - throw new Error('invalid bounds set: ' + input); - } - - this._lb = moments.shift(); - this._ub = moments.pop(); - if (this.getDuration().asSeconds() < 0) { - throw new TypeError('Intervals must be positive'); - } -}; - -/** - * Clear the stored bounds - * - * @return {undefined} - */ -TimeBuckets.prototype.clearBounds = function() { - this._lb = this._ub = null; -}; - -/** - * Check to see if we have received bounds yet - * - * @return {Boolean} - */ -TimeBuckets.prototype.hasBounds = function() { - return isValidMoment(this._ub) && isValidMoment(this._lb); -}; - -/** - * Return the current bounds, if we have any. - * - * THIS DOES NOT CLONE THE BOUNDS, so editing them - * may have unexpected side-effects. Always - * call bounds.min.clone() before editing - * - * @return {object|undefined} - If bounds are not defined, this - * returns undefined, else it returns the bounds - * for these buckets. This object has two props, - * min and max. Each property will be a moment() - * object - * - */ -TimeBuckets.prototype.getBounds = function() { - if (!this.hasBounds()) return; - return { - min: this._lb, - max: this._ub, - }; -}; - -/** - * Get a moment duration object representing - * the distance between the bounds, if the bounds - * are set. - * - * @return {moment.duration|undefined} - */ -TimeBuckets.prototype.getDuration = function() { - if (!this.hasBounds()) return; - return moment.duration(this._ub - this._lb, 'ms'); -}; - -/** - * Update the interval at which buckets should be - * generated. - * - * Input can be one of the following: - * - Any object from src/legacy/ui/agg_types/buckets/_interval_options.js - * - "auto" - * - Pass a valid moment unit - * - a moment.duration object. - * - * @param {object|string|moment.duration} input - see desc - */ -TimeBuckets.prototype.setInterval = function(input) { - // Preserve the original units because they're lost when the interval is converted to a - // moment duration object. - this.originalInterval = input; - - let interval = input; - - // selection object -> val - if (_.isObject(input)) { - interval = input.val; - } - - if (!interval || interval === 'auto') { - this._i = 'auto'; - return; - } - - if (_.isString(interval)) { - input = interval; - interval = parseInterval(interval); - if (+interval === 0) { - interval = null; - } - } - - // if the value wasn't converted to a duration, and isn't - // already a duration, we have a problem - if (!moment.isDuration(interval)) { - throw new TypeError('"' + input + '" is not a valid interval.'); - } - - this._i = interval; -}; - -/** - * Get the interval for the buckets. If the - * number of buckets created by the interval set - * is larger than config:histogram:maxBars then the - * interval will be scaled up. If the number of buckets - * created is less than one, the interval is scaled back. - * - * The interval object returned is a moment.duration - * object that has been decorated with the following - * properties. - * - * interval.description: a text description of the interval. - * designed to be used list "field per {{ desc }}". - * - "minute" - * - "10 days" - * - "3 years" - * - * interval.expr: the elasticsearch expression that creates this - * interval. If the interval does not properly form an elasticsearch - * expression it will be forced into one. - * - * interval.scaled: the interval was adjusted to - * accommodate the maxBars setting. - * - * interval.scale: the number that y-values should be - * multiplied by - * - * interval.scaleDescription: a description that reflects - * the values which will be produced by using the - * interval.scale. - * - * - * @return {[type]} [description] - */ -TimeBuckets.prototype.getInterval = function(useNormalizedEsInterval = true) { - const self = this; - const duration = self.getDuration(); - const parsedInterval = readInterval(); - - if (useNormalizedEsInterval) { - return decorateInterval(maybeScaleInterval(parsedInterval)); - } else { - return decorateInterval(parsedInterval); - } - - // either pull the interval from state or calculate the auto-interval - function readInterval() { - const interval = self._i; - if (moment.isDuration(interval)) return interval; - return calcAutoIntervalNear(self.uiSettings.get('histogram:barTarget'), Number(duration)); - } - - // check to see if the interval should be scaled, and scale it if so - function maybeScaleInterval(interval) { - if (!self.hasBounds()) return interval; - - const maxLength = self.uiSettings.get('histogram:maxBars'); - const approxLen = duration / interval; - let scaled; - - if (approxLen > maxLength) { - scaled = calcAutoIntervalLessThan(maxLength, Number(duration)); - } else { - return interval; - } - - if (+scaled === +interval) return interval; - - decorateInterval(interval); - return _.assign(scaled, { - preScaled: interval, - scale: interval / scaled, - scaled: true, - }); - } - - // append some TimeBuckets specific props to the interval - function decorateInterval(interval) { - const esInterval = useNormalizedEsInterval - ? convertDurationToNormalizedEsInterval(interval) - : convertIntervalToEsInterval(self.originalInterval); - interval.esValue = esInterval.value; - interval.esUnit = esInterval.unit; - interval.expression = esInterval.expression; - interval.overflow = duration > interval ? moment.duration(interval - duration) : false; - - const prettyUnits = moment.normalizeUnits(esInterval.unit); - if (esInterval.value === 1) { - interval.description = prettyUnits; - } else { - interval.description = esInterval.value + ' ' + prettyUnits + 's'; - } - - return interval; - } -}; - -/** - * Get a date format string that will represent dates that - * progress at our interval. - * - * Since our interval can be as small as 1ms, the default - * date format is usually way too much. with `dateFormat:scaled` - * users can modify how dates are formatted within series - * produced by TimeBuckets - * - * @return {string} - */ -TimeBuckets.prototype.getScaledDateFormat = function() { - const interval = this.getInterval(); - const rules = this.uiSettings.get('dateFormat:scaled'); - - for (let i = rules.length - 1; i >= 0; i--) { - const rule = rules[i]; - if (!rule[0] || interval >= moment.duration(rule[0])) { - return rule[1]; - } - } - - return this.uiSettings.get('dateFormat'); -}; - -TimeBuckets.prototype.getScaledDateFormatter = function() { - const fieldFormatsService = this.dataFieldsFormats; - const DateFieldFormat = fieldFormatsService.getType(fieldFormats.FIELD_FORMAT_IDS.DATE); - - return new DateFieldFormat( - { - pattern: this.getScaledDateFormat(), - }, - configPath => this.uiSettings.get(configPath) - ); -}; - -TimeBuckets.__cached__ = function(self) { - let cache = {}; - const sameMoment = same(moment.isMoment); - const sameDuration = same(moment.isDuration); - - const desc = { - __cached__: { - value: self, - }, - }; - - const breakers = { - setBounds: 'bounds', - clearBounds: 'bounds', - setInterval: 'interval', - }; - - const resources = { - bounds: { - setup: function() { - return [self._lb, self._ub]; - }, - changes: function(prev) { - return !sameMoment(prev[0], self._lb) || !sameMoment(prev[1], self._ub); - }, - }, - interval: { - setup: function() { - return self._i; - }, - changes: function(prev) { - return !sameDuration(prev, this._i); - }, - }, - }; - - function cachedGetter(prop) { - return { - value: function cachedGetter(...rest) { - if (cache.hasOwnProperty(prop)) { - return cache[prop]; - } - - return (cache[prop] = self[prop](...rest)); - }, - }; - } - - function cacheBreaker(prop) { - const resource = resources[breakers[prop]]; - const setup = resource.setup; - const changes = resource.changes; - const fn = self[prop]; - - return { - value: function cacheBreaker() { - const prev = setup.call(self); - const ret = fn.apply(self, arguments); - - if (changes.call(self, prev)) { - cache = {}; - } - - return ret; - }, - }; - } - - function same(checkType) { - return function(a, b) { - if (a === b) return true; - if (checkType(a) === checkType(b)) return +a === +b; - return false; - }; - } - - _.forOwn(TimeBuckets.prototype, function(fn, prop) { - if (prop[0] === '_') return; - - if (breakers.hasOwnProperty(prop)) { - desc[prop] = cacheBreaker(prop); - } else { - desc[prop] = cachedGetter(prop); - } - }); - - return Object.create(self, desc); -}; - -export { TimeBuckets }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts index 356b0fbbc0845..d5b64f1489b8d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +export { + TimeSeriesResult, + TimeSeriesResultRow, + MetricResult, +} from '../../../../../../alerting_builtins/common/alert_types/index_threshold'; + export interface Comparator { text: string; value: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx index 4d97a59e36320..530e3b6116479 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx @@ -5,7 +5,7 @@ */ import React, { Fragment, useEffect, useState } from 'react'; -import { IUiSettingsClient } from 'kibana/public'; +import { IUiSettingsClient, HttpSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { AnnotationDomainTypes, @@ -18,17 +18,16 @@ import { Position, ScaleType, Settings, + niceTimeFormatter, } from '@elastic/charts'; -import dateMath from '@elastic/datemath'; import moment from 'moment-timezone'; import { EuiCallOut, EuiLoadingChart, EuiSpacer, EuiEmptyPrompt, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { getThresholdAlertVisualizationData } from './lib/api'; import { AggregationType, Comparator } from '../../../../common/types'; -/* TODO: This file was copied from ui/time_buckets for NP migration. We should clean this up and add TS support */ -import { TimeBuckets } from './lib/time_buckets'; import { AlertsContextValue } from '../../../context/alerts_context'; import { IndexThresholdAlertParams } from './types'; +import { parseDuration } from '../../../../../../alerting/common/parse_duration'; const customTheme = () => { return { @@ -60,35 +59,26 @@ const getTimezone = (uiSettings: IUiSettingsClient) => { return tzOffset; }; -const getDomain = (alertParams: any) => { - const VISUALIZE_TIME_WINDOW_MULTIPLIER = 5; - const fromExpression = `now-${alertParams.timeWindowSize * VISUALIZE_TIME_WINDOW_MULTIPLIER}${ - alertParams.timeWindowUnit - }`; - const toExpression = 'now'; - const fromMoment = dateMath.parse(fromExpression); - const toMoment = dateMath.parse(toExpression); - const visualizeTimeWindowFrom = fromMoment ? fromMoment.valueOf() : 0; - const visualizeTimeWindowTo = toMoment ? toMoment.valueOf() : 0; +const getDomain = (alertInterval: string) => { + const VISUALIZE_INTERVALS = 30; + let intervalMillis: number; + + try { + intervalMillis = parseDuration(alertInterval); + } catch (err) { + intervalMillis = 1000 * 60; // default to one minute if not parseable + } + + const now = Date.now(); return { - min: visualizeTimeWindowFrom, - max: visualizeTimeWindowTo, + min: now - intervalMillis * VISUALIZE_INTERVALS, + max: now, }; }; -const getTimeBuckets = ( - uiSettings: IUiSettingsClient, - dataFieldsFormats: any, - alertParams: any -) => { - const domain = getDomain(alertParams); - const timeBuckets = new TimeBuckets(uiSettings, dataFieldsFormats); - timeBuckets.setBounds(domain); - return timeBuckets; -}; - interface Props { alertParams: IndexThresholdAlertParams; + alertInterval: string; aggregationTypes: { [key: string]: AggregationType }; comparators: { [key: string]: Comparator; @@ -96,8 +86,10 @@ interface Props { alertsContext: AlertsContextValue; } +type MetricResult = [number, number]; // [epochMillis, value] export const ThresholdVisualization: React.FunctionComponent = ({ alertParams, + alertInterval, aggregationTypes, comparators, alertsContext, @@ -119,18 +111,14 @@ export const ThresholdVisualization: React.FunctionComponent = ({ const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); - const [visualizationData, setVisualizationData] = useState>([]); + const [visualizationData, setVisualizationData] = useState>(); useEffect(() => { (async () => { try { setIsLoading(true); setVisualizationData( - await getThresholdAlertVisualizationData({ - model: alertWithoutActions, - visualizeOptions, - http, - }) + await getVisualizationData(alertWithoutActions, visualizeOptions, http) ); } catch (e) { if (toastNotifications) { @@ -167,15 +155,11 @@ export const ThresholdVisualization: React.FunctionComponent = ({ } const chartsTheme = charts.theme.useChartsTheme(); - const domain = getDomain(alertParams); - const timeBuckets = new TimeBuckets(uiSettings, dataFieldsFormats); - timeBuckets.setBounds(domain); - const interval = timeBuckets.getInterval().expression; + const domain = getDomain(alertInterval); const visualizeOptions = { - rangeFrom: domain.min, - rangeTo: domain.max, - interval, - timezone: getTimezone(uiSettings), + rangeFrom: new Date(domain.min).toISOString(), + rangeTo: new Date(domain.max).toISOString(), + interval: alertInterval, }; // Fetching visualization data is independent of alert actions @@ -237,11 +221,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ } }); }); - const dateFormatter = (d: number) => { - return moment(d) - .tz(timezone) - .format(getTimeBuckets(uiSettings, dataFieldsFormats, alertParams).getScaledDateFormat()); - }; + const dateFormatter = niceTimeFormatter([domain.min, domain.max]); const aggLabel = aggregationTypes[aggType].text; return (
@@ -316,3 +296,22 @@ export const ThresholdVisualization: React.FunctionComponent = ({ return null; }; + +// convert the data from the visualization API into something easier to digest with charts +async function getVisualizationData(model: any, visualizeOptions: any, http: HttpSetup) { + const vizData = await getThresholdAlertVisualizationData({ + model, + visualizeOptions, + http, + }); + const result: Record> = {}; + + for (const groupMetrics of vizData.results) { + result[groupMetrics.group] = groupMetrics.metrics.map(metricResult => [ + Date.parse(metricResult[0]), + metricResult[1], + ]); + } + + return result; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index b875fae75c7df..c38422b5a8381 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -632,6 +632,7 @@ export const AlertForm = ({ {AlertParamsExpressionComponent ? (