diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.js b/src/legacy/core_plugins/kibana/migrations/migrations.js index 4951c2f06f4db..ec66e2401e918 100644 --- a/src/legacy/core_plugins/kibana/migrations/migrations.js +++ b/src/legacy/core_plugins/kibana/migrations/migrations.js @@ -114,8 +114,9 @@ const migrateDateHistogramAggregation = doc => { delete agg.params.customInterval; } - if (get(agg, 'params.customBucket.type', null) === 'date_histogram' - && agg.params.customBucket.params + if ( + get(agg, 'params.customBucket.type', null) === 'date_histogram' && + agg.params.customBucket.params ) { if (agg.params.customBucket.params.interval === 'custom') { agg.params.customBucket.params.interval = agg.params.customBucket.params.customInterval; @@ -128,7 +129,7 @@ const migrateDateHistogramAggregation = doc => { attributes: { ...doc.attributes, visState: JSON.stringify(visState), - } + }, }; } } @@ -152,7 +153,10 @@ function removeDateHistogramTimeZones(doc) { delete agg.params.time_zone; } - if (get(agg, 'params.customBucket.type', null) === 'date_histogram' && agg.params.customBucket.params) { + if ( + get(agg, 'params.customBucket.type', null) === 'date_histogram' && + agg.params.customBucket.params + ) { delete agg.params.customBucket.params.time_zone; } }); @@ -164,15 +168,16 @@ function removeDateHistogramTimeZones(doc) { // migrate gauge verticalSplit to alignment // https://github.com/elastic/kibana/issues/34636 -function migrateGaugeVerticalSplitToAlignment(doc, logger) { +function migrateGaugeVerticalSplitToAlignment(doc, logger) { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { try { const visState = JSON.parse(visStateJSON); if (visState && visState.type === 'gauge' && !visState.params.gauge.alignment) { - - visState.params.gauge.alignment = visState.params.gauge.verticalSplit ? 'vertical' : 'horizontal'; + visState.params.gauge.alignment = visState.params.gauge.verticalSplit + ? 'vertical' + : 'horizontal'; delete visState.params.gauge.verticalSplit; return { ...doc, @@ -229,7 +234,7 @@ function transformFilterStringToQueryObject(doc) { // migrate the annotations query string: const annotations = get(visState, 'params.annotations') || []; - annotations.forEach((item) => { + annotations.forEach(item => { if (!item.query_string) { // we don't need to transform anything if there isn't a filter at all return; @@ -244,7 +249,7 @@ function transformFilterStringToQueryObject(doc) { }); // migrate the series filters const series = get(visState, 'params.series') || []; - series.forEach((item) => { + series.forEach(item => { if (!item.filter) { // we don't need to transform anything if there isn't a filter at all return; @@ -260,7 +265,7 @@ function transformFilterStringToQueryObject(doc) { // series item split filters filter if (item.split_filters) { const splitFilters = get(item, 'split_filters') || []; - splitFilters.forEach((filter) => { + splitFilters.forEach(filter => { if (!filter.filter) { // we don't need to transform anything if there isn't a filter at all return; @@ -288,10 +293,10 @@ function migrateFiltersAggQuery(doc) { try { const visState = JSON.parse(visStateJSON); if (visState && visState.aggs) { - visState.aggs.forEach((agg) => { + visState.aggs.forEach(agg => { if (agg.type !== 'filters') return; - agg.params.filters.forEach((filter) => { + agg.params.filters.forEach(filter => { if (filter.input.language) return filter; filter.input.language = 'lucene'; }); @@ -312,16 +317,72 @@ function migrateFiltersAggQuery(doc) { return doc; } -const executeMigrations720 = flow(migratePercentileRankAggregation, migrateDateHistogramAggregation); -const executeMigrations730 = flow(migrateGaugeVerticalSplitToAlignment, transformFilterStringToQueryObject, migrateFiltersAggQuery); +function replaceMovAvgToMovFn(doc, logger) { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + + if (visState && visState.type === 'metrics') { + const series = get(visState, 'params.series', []); + + series.forEach(part => { + if (part.metrics && Array.isArray(part.metrics)) { + part.metrics.forEach(metric => { + if (metric.type === 'moving_average') { + metric.model_type = metric.model; + metric.alpha = get(metric, 'settings.alpha', 0.3); + metric.beta = get(metric, 'settings.beta', 0.1); + metric.gamma = get(metric, 'settings.gamma', 0.3); + metric.period = get(metric, 'settings.period', 1); + metric.multiplicative = get(metric, 'settings.type') === 'mult'; + + delete metric.minimize; + delete metric.model; + delete metric.settings; + delete metric.predict; + } + }); + } + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } catch (e) { + logger.warning(`Exception @ replaceMovAvgToMovFn! ${e}`); + logger.warning(`Exception @ replaceMovAvgToMovFn! Payload: ${visStateJSON}`); + } + } + + return doc; +} + +const executeMigrations720 = flow( + migratePercentileRankAggregation, + migrateDateHistogramAggregation +); +const executeMigrations730 = flow( + migrateGaugeVerticalSplitToAlignment, + transformFilterStringToQueryObject, + migrateFiltersAggQuery, + replaceMovAvgToMovFn +); export const migrations = { 'index-pattern': { - '6.5.0': (doc) => { + '6.5.0': doc => { doc.attributes.type = doc.attributes.type || undefined; doc.attributes.typeMeta = doc.attributes.typeMeta || undefined; return doc; - } + }, }, visualization: { /** @@ -335,7 +396,7 @@ export const migrations = { * only contained the 6.7.2 migration and not the 7.0.1 migration. */ '6.7.2': removeDateHistogramTimeZones, - '7.0.0': (doc) => { + '7.0.0': doc => { // Set new "references" attribute doc.references = doc.references || []; @@ -412,15 +473,17 @@ export const migrations = { newDoc.attributes.visState = JSON.stringify(visState); return newDoc; } catch (e) { - throw new Error(`Failure attempting to migrate saved object '${doc.attributes.title}' - ${e}`); + throw new Error( + `Failure attempting to migrate saved object '${doc.attributes.title}' - ${e}` + ); } }, '7.0.1': removeDateHistogramTimeZones, '7.2.0': doc => executeMigrations720(doc), - '7.3.0': executeMigrations730 + '7.3.0': executeMigrations730, }, dashboard: { - '7.0.0': (doc) => { + '7.0.0': doc => { // Set new "references" attribute doc.references = doc.references || []; @@ -460,7 +523,7 @@ export const migrations = { '7.3.0': dashboardMigrations730 }, search: { - '7.0.0': (doc) => { + '7.0.0': doc => { // Set new "references" attribute doc.references = doc.references || []; // Migrate index pattern diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.test.js b/src/legacy/core_plugins/kibana/migrations/migrations.test.js index 2b9fd2b03bdd0..12ede94894c73 100644 --- a/src/legacy/core_plugins/kibana/migrations/migrations.test.js +++ b/src/legacy/core_plugins/kibana/migrations/migrations.test.js @@ -989,6 +989,51 @@ Array [ }); }); }); + + describe('replaceMovAvgToMovFn()', () => { + let doc; + + beforeEach(() => { + doc = { + attributes: { + title: 'VIS', + visState: `{"title":"VIS","type":"metrics","params":{"id":"61ca57f0-469d-11e7-af02-69e470af7417", + "type":"timeseries","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(0,156,224,1)", + "split_mode":"terms","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"count", + "numerator":"FlightDelay:true"},{"settings":"","minimize":0,"window":5,"model": + "holt_winters","id":"23054fe0-8915-11e9-9b86-d3f94982620f","type":"moving_average","field": + "61ca57f2-469d-11e7-af02-69e470af7417","predict":1}],"separate_axis":0,"axis_position":"right", + "formatter":"number","chart_type":"line","line_width":"2","point_size":"0","fill":0.5,"stacked":"none", + "label":"Percent Delays","terms_size":"2","terms_field":"OriginCityName"}],"time_field":"timestamp", + "index_pattern":"kibana_sample_data_flights","interval":">=12h","axis_position":"left","axis_formatter": + "number","show_legend":1,"show_grid":1,"annotations":[{"fields":"FlightDelay,Cancelled,Carrier", + "template":"{{Carrier}}: Flight Delayed and Cancelled!","index_pattern":"kibana_sample_data_flights", + "query_string":"FlightDelay:true AND Cancelled:true","id":"53b7dff0-4c89-11e8-a66a-6989ad5a0a39", + "color":"rgba(0,98,177,1)","time_field":"timestamp","icon":"fa-exclamation-triangle", + "ignore_global_filters":1,"ignore_panel_filters":1,"hidden":true}],"legend_position":"bottom", + "axis_scale":"normal","default_index_pattern":"kibana_sample_data_flights","default_timefield":"timestamp"}, + "aggs":[]}`, + }, + migrationVersion: { + visualization: '7.2.0', + }, + type: 'visualization', + }; + }); + + test('should add some necessary moving_fn fields', () => { + const migratedDoc = migrate(doc); + const visState = JSON.parse(migratedDoc.attributes.visState); + const metric = visState.params.series[0].metrics[1]; + + expect(metric).toHaveProperty('model_type'); + expect(metric).toHaveProperty('alpha'); + expect(metric).toHaveProperty('beta'); + expect(metric).toHaveProperty('gamma'); + expect(metric).toHaveProperty('period'); + expect(metric).toHaveProperty('multiplicative'); + }); + }); }); describe('7.3.0 tsvb', () => { const migrate = doc => migrations.visualization['7.3.0'](doc); @@ -1019,15 +1064,18 @@ Array [ { filter: 'Filter Bytes Test:>1000', split_filters: [{ filter: 'bytes:>1000' }], - } - ] + }, + ], }; const markdownDoc = generateDoc({ params: markdownParams }); const migratedMarkdownDoc = migrate(markdownDoc); const markdownSeries = JSON.parse(migratedMarkdownDoc.attributes.visState).params.series; - expect(markdownSeries[0].filter.query).toBe(JSON.parse(markdownDoc.attributes.visState).params.series[0].filter); - expect(markdownSeries[0].split_filters[0].filter.query) - .toBe(JSON.parse(markdownDoc.attributes.visState).params.series[0].split_filters[0].filter); + expect(markdownSeries[0].filter.query).toBe( + JSON.parse(markdownDoc.attributes.visState).params.series[0].filter + ); + expect(markdownSeries[0].split_filters[0].filter.query).toBe( + JSON.parse(markdownDoc.attributes.visState).params.series[0].split_filters[0].filter + ); }); it('should change series item filters from a string into an object for all filters', () => { const params = { @@ -1037,16 +1085,22 @@ Array [ { filter: 'Filter Bytes Test:>1000', split_filters: [{ filter: 'bytes:>1000' }], - } + }, ], annotations: [{ query_string: 'bytes:>1000' }], }; const timeSeriesDoc = generateDoc({ params: params }); const migratedtimeSeriesDoc = migrate(timeSeriesDoc); const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; - expect(Object.keys(timeSeriesParams.series[0].filter)).toEqual(expect.arrayContaining(['query', 'language'])); - expect(Object.keys(timeSeriesParams.series[0].split_filters[0].filter)).toEqual(expect.arrayContaining(['query', 'language'])); - expect(Object.keys(timeSeriesParams.annotations[0].query_string)).toEqual(expect.arrayContaining(['query', 'language'])); + expect(Object.keys(timeSeriesParams.series[0].filter)).toEqual( + expect.arrayContaining(['query', 'language']) + ); + expect(Object.keys(timeSeriesParams.series[0].split_filters[0].filter)).toEqual( + expect.arrayContaining(['query', 'language']) + ); + expect(Object.keys(timeSeriesParams.annotations[0].query_string)).toEqual( + expect.arrayContaining(['query', 'language']) + ); }); it('should not fail on a metric visualization without a filter in a series item', () => { const params = { type: 'metric', series: [{}, {}, {}] }; diff --git a/src/legacy/core_plugins/metrics/common/__snapshots__/model_options.test.js.snap b/src/legacy/core_plugins/metrics/common/__snapshots__/model_options.test.js.snap new file mode 100644 index 0000000000000..0fca2a017b911 --- /dev/null +++ b/src/legacy/core_plugins/metrics/common/__snapshots__/model_options.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`src/legacy/core_plugins/metrics/common/model_options.js MODEL_TYPES should match a snapshot of constants 1`] = ` +Object { + "UNWEIGHTED": "simple", + "WEIGHTED_EXPONENTIAL": "ewma", + "WEIGHTED_EXPONENTIAL_DOUBLE": "holt", + "WEIGHTED_EXPONENTIAL_TRIPLE": "holt_winters", + "WEIGHTED_LINEAR": "linear", +} +`; diff --git a/src/legacy/core_plugins/metrics/common/model_options.js b/src/legacy/core_plugins/metrics/common/model_options.js new file mode 100644 index 0000000000000..22fe7a0abc842 --- /dev/null +++ b/src/legacy/core_plugins/metrics/common/model_options.js @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const MODEL_TYPES = { + UNWEIGHTED: 'simple', + WEIGHTED_EXPONENTIAL: 'ewma', + WEIGHTED_EXPONENTIAL_DOUBLE: 'holt', + WEIGHTED_EXPONENTIAL_TRIPLE: 'holt_winters', + WEIGHTED_LINEAR: 'linear', +}; diff --git a/src/legacy/core_plugins/metrics/common/model_options.test.js b/src/legacy/core_plugins/metrics/common/model_options.test.js new file mode 100644 index 0000000000000..7d01226bdc040 --- /dev/null +++ b/src/legacy/core_plugins/metrics/common/model_options.test.js @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MODEL_TYPES } from './model_options'; + +describe('src/legacy/core_plugins/metrics/common/model_options.js', () => { + describe('MODEL_TYPES', () => { + test('should match a snapshot of constants', () => { + expect(MODEL_TYPES).toMatchSnapshot(); + }); + }); +}); diff --git a/src/legacy/core_plugins/metrics/public/components/aggs/moving_average.js b/src/legacy/core_plugins/metrics/public/components/aggs/moving_average.js index 5d059c2477edf..f3dbb9266befc 100644 --- a/src/legacy/core_plugins/metrics/public/components/aggs/moving_average.js +++ b/src/legacy/core_plugins/metrics/public/components/aggs/moving_average.js @@ -18,13 +18,12 @@ */ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { Fragment } from 'react'; import { AggRow } from './agg_row'; import { AggSelect } from './agg_select'; import { MetricSelect } from './metric_select'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; -import { createTextHandler } from '../lib/create_text_handler'; import { createNumberHandler } from '../lib/create_number_handler'; import { htmlIdGenerator, @@ -34,69 +33,85 @@ import { EuiComboBox, EuiSpacer, EuiFormRow, - EuiCode, - EuiTextArea, + EuiFieldNumber, } from '@elastic/eui'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { MODEL_TYPES } from '../../../common/model_options'; -const MovingAverageAggUi = props => { - const { siblings, intl } = props; - const defaults = { - settings: '', - minimize: 0, - window: '', - model: 'simple', - }; - const model = { ...defaults, ...props.model }; - const handleChange = createChangeHandler(props.onChange, model); - const handleSelectChange = createSelectHandler(handleChange); - const handleTextChange = createTextHandler(handleChange); - const handleNumberChange = createNumberHandler(handleChange); +const DEFAULTS = { + model_type: MODEL_TYPES.UNWEIGHTED, + alpha: 0.3, + beta: 0.1, + gamma: 0.3, + period: 1, + multiplicative: true, + window: 5, +}; + +const shouldShowHint = ({ model_type: type, window, period }) => + type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE && period * 2 > window; + +export const MovingAverageAgg = props => { + const { siblings } = props; + + const model = { ...DEFAULTS, ...props.model }; const modelOptions = [ { - label: intl.formatMessage({ - id: 'tsvb.movingAverage.modelOptions.simpleLabel', + label: i18n.translate('tsvb.movingAverage.modelOptions.simpleLabel', { defaultMessage: 'Simple', }), - value: 'simple', + value: MODEL_TYPES.UNWEIGHTED, }, { - label: intl.formatMessage({ - id: 'tsvb.movingAverage.modelOptions.linearLabel', + label: i18n.translate('tsvb.movingAverage.modelOptions.linearLabel', { defaultMessage: 'Linear', }), - value: 'linear', + value: MODEL_TYPES.WEIGHTED_LINEAR, }, { - label: intl.formatMessage({ - id: 'tsvb.movingAverage.modelOptions.exponentiallyWeightedLabel', + label: i18n.translate('tsvb.movingAverage.modelOptions.exponentiallyWeightedLabel', { defaultMessage: 'Exponentially Weighted', }), - value: 'ewma', + value: MODEL_TYPES.WEIGHTED_EXPONENTIAL, }, { - label: intl.formatMessage({ - id: 'tsvb.movingAverage.modelOptions.holtLinearLabel', + label: i18n.translate('tsvb.movingAverage.modelOptions.holtLinearLabel', { defaultMessage: 'Holt-Linear', }), - value: 'holt', + value: MODEL_TYPES.WEIGHTED_EXPONENTIAL_DOUBLE, }, { - label: intl.formatMessage({ - id: 'tsvb.movingAverage.modelOptions.holtWintersLabel', + label: i18n.translate('tsvb.movingAverage.modelOptions.holtWintersLabel', { defaultMessage: 'Holt-Winters', }), - value: 'holt_winters', + value: MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE, }, ]; - const minimizeOptions = [{ label: 'True', value: 1 }, { label: 'False', value: 0 }]; + + const handleChange = createChangeHandler(props.onChange, model); + const handleSelectChange = createSelectHandler(handleChange); + const handleNumberChange = createNumberHandler(handleChange); + const htmlId = htmlIdGenerator(); - const selectedModelOption = modelOptions.find(option => { - return model.model === option.value; - }); - const selectedMinimizeOption = minimizeOptions.find(option => { - return model.minimize === option.value; - }); + const selectedModelOption = modelOptions.find(({ value }) => model.model_type === value); + + const multiplicativeOptions = [ + { + label: i18n.translate('tsvb.movingAverage.multiplicativeOptions.true', { + defaultMessage: 'True', + }), + value: true, + }, + { + label: i18n.translate('tsvb.movingAverage.multiplicativeOptions.false', { + defaultMessage: 'False', + }), + value: false, + }, + ]; + const selectedMultiplicative = multiplicativeOptions.find( + ({ value }) => model.multiplicative === value + ); return ( { - + {i18n.translate('tsvb.movingAverage.aggregationLabel', { + defaultMessage: 'Aggregation', + })} { } + label={i18n.translate('tsvb.movingAverage.metricLabel', { + defaultMessage: 'Metric', + })} > { } + id={htmlId('model_type')} + label={i18n.translate('tsvb.movingAverage.modelLabel', { + defaultMessage: 'Model', + })} > @@ -162,11 +179,14 @@ const MovingAverageAggUi = props => { + label={i18n.translate('tsvb.movingAverage.windowSizeLabel', { + defaultMessage: 'Window Size', + })} + helpText={ + shouldShowHint(model) && + i18n.translate('tsvb.movingAverage.windowSizeHint', { + defaultMessage: 'Window must always be at least twice the size of your period', + }) } > {/* @@ -181,73 +201,109 @@ const MovingAverageAggUi = props => { /> - - - } - > - - - - - - } - > - {/* - EUITODO: The following input couldn't be converted to EUI because of type mis-match. - Should it be text or number? - */} - - - - + {(model.model_type === MODEL_TYPES.WEIGHTED_EXPONENTIAL || + model.model_type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_DOUBLE || + model.model_type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE) && ( + + - - - } - helpText={ - - Key=Value }} - /> - - } - > - - - + + { + + + + + + } + {(model.model_type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_DOUBLE || + model.model_type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE) && ( + + + + + + )} + {model.model_type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE && ( + + + + + + + + + + + + + + + + + + )} + + + )} ); }; -MovingAverageAggUi.propTypes = { +MovingAverageAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, model: PropTypes.object, @@ -258,5 +314,3 @@ MovingAverageAggUi.propTypes = { series: PropTypes.object, siblings: PropTypes.array, }; - -export const MovingAverageAgg = injectI18n(MovingAverageAggUi); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/bucket_transform.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/bucket_transform.js index 475870f7829db..b4425bc8571c1 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/bucket_transform.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/bucket_transform.js @@ -311,77 +311,6 @@ describe('bucketTransform', () => { }); }); - describe('moving_average', () => { - it('returns moving_average agg with defaults', () => { - const metric = { id: '2', type: 'moving_average', field: '1' }; - const metrics = [{ id: '1', type: 'avg', field: 'cpu.pct' }, metric]; - const fn = bucketTransform.moving_average; - expect(fn(metric, metrics, '10s')).is.eql({ - moving_avg: { - buckets_path: '1', - model: 'simple', - gap_policy: 'skip', - }, - }); - }); - - it('returns moving_average agg with predict', () => { - const metric = { - id: '2', - type: 'moving_average', - field: '1', - predict: 10, - }; - const metrics = [{ id: '1', type: 'avg', field: 'cpu.pct' }, metric]; - const fn = bucketTransform.moving_average; - expect(fn(metric, metrics, '10s')).is.eql({ - moving_avg: { - buckets_path: '1', - model: 'simple', - gap_policy: 'skip', - predict: 10, - }, - }); - }); - - it('returns moving_average agg with options', () => { - const metric = { - id: '2', - type: 'moving_average', - field: '1', - model: 'holt_winters', - window: 10, - minimize: 1, - settings: 'alpha=0.9 beta=0.5', - }; - const metrics = [{ id: '1', type: 'avg', field: 'cpu.pct' }, metric]; - const fn = bucketTransform.moving_average; - expect(fn(metric, metrics, '10s')).is.eql({ - moving_avg: { - buckets_path: '1', - model: 'holt_winters', - gap_policy: 'skip', - window: 10, - minimize: true, - settings: { - alpha: 0.9, - beta: 0.5, - }, - }, - }); - }); - - it('throws error if type is missing', () => { - const run = () => bucketTransform.moving_average({ id: 'test', field: 'cpu.pct' }); - expect(run).to.throw(Error, 'Metric missing type'); - }); - - it('throws error if field is missing', () => { - const run = () => bucketTransform.moving_average({ id: 'test', type: 'moving_average' }); - expect(run).to.throw(Error, 'Metric missing field'); - }); - }); - describe('calculation', () => { it('returns calculation(bucket_script)', () => { const metric = { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/__snapshots__/bucket_transform.test.js.snap b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/__snapshots__/bucket_transform.test.js.snap new file mode 100644 index 0000000000000..cb377871cee28 --- /dev/null +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/__snapshots__/bucket_transform.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js bucketTransform moving_average should return a moving function aggregation API and match a snapshot 1`] = ` +Object { + "moving_fn": Object { + "buckets_path": "61ca57f2-469d-11e7-af02-69e470af7417", + "script": "if (values.length > 1*2) {MovingFunctions.holtWinters(values, 0.6, 0.3, 0.3, 1, true)}", + "window": 10, + }, +} +`; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js index 3e315934b7ad9..95550e2115efb 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js @@ -17,11 +17,11 @@ * under the License. */ -import { parseSettings } from './parse_settings'; import { getBucketsPath } from './get_buckets_path'; import { parseInterval } from './parse_interval'; import { set, isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { MODEL_SCRIPTS } from './moving_fn_scripts'; function checkMetric(metric, fields) { fields.forEach(field => { @@ -199,21 +199,14 @@ export const bucketTransform = { moving_average: (bucket, metrics) => { checkMetric(bucket, ['type', 'field']); - const body = { - moving_avg: { + + return { + moving_fn: { buckets_path: getBucketsPath(bucket.field, metrics), - model: bucket.model || 'simple', - gap_policy: 'skip', // seems sane + window: bucket.window, + script: MODEL_SCRIPTS[bucket.model_type](bucket), }, }; - if (bucket.gap_policy) body.moving_avg.gap_policy = bucket.gap_policy; - if (bucket.window) body.moving_avg.window = Number(bucket.window); - if (bucket.minimize) body.moving_avg.minimize = Boolean(bucket.minimize); - if (bucket.predict) body.moving_avg.predict = Number(bucket.predict); - if (bucket.settings) { - body.moving_avg.settings = parseSettings(bucket.settings); - } - return body; }, calculation: (bucket, metrics, bucketSize) => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.test.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.test.js new file mode 100644 index 0000000000000..186baf73a86c7 --- /dev/null +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.test.js @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { bucketTransform } from './bucket_transform'; + +describe('src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js', () => { + describe('bucketTransform', () => { + let bucket; + let metrics; + + beforeEach(() => { + bucket = { + model_type: 'holt_winters', + alpha: 0.6, + beta: 0.3, + gamma: 0.3, + period: 1, + multiplicative: true, + window: 10, + field: '61ca57f2-469d-11e7-af02-69e470af7417', + id: 'e815ae00-7881-11e9-9392-cbca66a4cf76', + type: 'moving_average', + }; + metrics = [ + { + id: '61ca57f2-469d-11e7-af02-69e470af7417', + numerator: 'FlightDelay:true', + type: 'count', + }, + { + model_type: 'holt_winters', + alpha: 0.6, + beta: 0.3, + gamma: 0.3, + period: 1, + multiplicative: true, + window: 10, + field: '61ca57f2-469d-11e7-af02-69e470af7417', + id: 'e815ae00-7881-11e9-9392-cbca66a4cf76', + type: 'moving_average', + }, + ]; + }); + + describe('moving_average', () => { + test('should return a moving function aggregation API and match a snapshot', () => { + expect(bucketTransform.moving_average(bucket, metrics)).toMatchSnapshot(); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.js new file mode 100644 index 0000000000000..32bec6aea93ae --- /dev/null +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.js @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MODEL_TYPES } from '../../../../common/model_options'; + +export const MODEL_SCRIPTS = { + [MODEL_TYPES.UNWEIGHTED]: () => 'MovingFunctions.unweightedAvg(values)', + [MODEL_TYPES.WEIGHTED_EXPONENTIAL]: ({ alpha }) => `MovingFunctions.ewma(values, ${alpha})`, + [MODEL_TYPES.WEIGHTED_EXPONENTIAL_DOUBLE]: ({ alpha, beta }) => + `MovingFunctions.holt(values, ${alpha}, ${beta})`, + [MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE]: ({ alpha, beta, gamma, period, multiplicative }) => + `if (values.length > ${period}*2) {MovingFunctions.holtWinters(values, ${alpha}, ${beta}, ${gamma}, ${period}, ${multiplicative})}`, + [MODEL_TYPES.WEIGHTED_LINEAR]: () => 'MovingFunctions.linearWeightedAvg(values)', +}; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.test.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.test.js new file mode 100644 index 0000000000000..858e3d30b8ebc --- /dev/null +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.test.js @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MODEL_TYPES } from '../../../../common/model_options'; +import { MODEL_SCRIPTS } from './moving_fn_scripts'; + +describe('src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.js', () => { + describe('MODEL_SCRIPTS', () => { + let bucket; + + beforeEach(() => { + bucket = { + alpha: 0.1, + beta: 0.2, + gamma: 0.3, + period: 5, + multiplicative: true, + }; + }); + + test('should return an expected result of the UNWEIGHTED model type', () => { + expect(MODEL_SCRIPTS[MODEL_TYPES.UNWEIGHTED](bucket)).toBe( + 'MovingFunctions.unweightedAvg(values)' + ); + }); + + test('should return an expected result of the WEIGHTED_LINEAR model type', () => { + expect(MODEL_SCRIPTS[MODEL_TYPES.WEIGHTED_LINEAR](bucket)).toBe( + 'MovingFunctions.linearWeightedAvg(values)' + ); + }); + + test('should return an expected result of the WEIGHTED_EXPONENTIAL model type', () => { + const { alpha } = bucket; + + expect(MODEL_SCRIPTS[MODEL_TYPES.WEIGHTED_EXPONENTIAL](bucket)).toBe( + `MovingFunctions.ewma(values, ${alpha})` + ); + }); + + test('should return an expected result of the WEIGHTED_EXPONENTIAL_DOUBLE model type', () => { + const { alpha, beta } = bucket; + + expect(MODEL_SCRIPTS[MODEL_TYPES.WEIGHTED_EXPONENTIAL_DOUBLE](bucket)).toBe( + `MovingFunctions.holt(values, ${alpha}, ${beta})` + ); + }); + + test('should return an expected result of the WEIGHTED_EXPONENTIAL_TRIPLE model type', () => { + const { alpha, beta, gamma, period, multiplicative } = bucket; + + expect(MODEL_SCRIPTS[MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE](bucket)).toBe( + `if (values.length > ${period}*2) {MovingFunctions.holtWinters(values, ${alpha}, ${beta}, ${gamma}, ${period}, ${multiplicative})}` + ); + }); + }); +}); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fe57f4d4aed8e..09324740481ff 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3291,8 +3291,6 @@ "tsvb.missingPanelConfigDescription": "「{modelType}」にパネル構成が欠けています", "tsvb.movingAverage.aggregationLabel": "集約", "tsvb.movingAverage.metricLabel": "メトリック", - "tsvb.movingAverage.minimize.selectPlaceholder": "選択", - "tsvb.movingAverage.minimizeLabel": "最小化", "tsvb.movingAverage.model.selectPlaceholder": "選択", "tsvb.movingAverage.modelLabel": "モデル", "tsvb.movingAverage.modelOptions.exponentiallyWeightedLabel": "指数加重", @@ -3300,9 +3298,6 @@ "tsvb.movingAverage.modelOptions.holtWintersLabel": "Holt-Winters", "tsvb.movingAverage.modelOptions.linearLabel": "直線", "tsvb.movingAverage.modelOptions.simpleLabel": "シンプル", - "tsvb.movingAverage.predictLabel": "予想", - "tsvb.movingAverage.settingsDescription": "{keyValue} スペース区切り", - "tsvb.movingAverage.settingsLabel": "設定", "tsvb.movingAverage.windowSizeLabel": "ウィンドウサイズ", "tsvb.multivalueRow.valueLabel": "値:", "tsvb.noButtonLabel": "いいえ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6e5d13edf30b8..63e6d0f93bca8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2848,8 +2848,6 @@ "tsvb.missingPanelConfigDescription": "缺少 “{modelType}” 的面板配置", "tsvb.movingAverage.aggregationLabel": "聚合", "tsvb.movingAverage.metricLabel": "指标", - "tsvb.movingAverage.minimize.selectPlaceholder": "选择", - "tsvb.movingAverage.minimizeLabel": "最小化", "tsvb.movingAverage.model.selectPlaceholder": "选择", "tsvb.movingAverage.modelLabel": "模型", "tsvb.movingAverage.modelOptions.exponentiallyWeightedLabel": "指数加权", @@ -2857,9 +2855,6 @@ "tsvb.movingAverage.modelOptions.holtWintersLabel": "Holt-Winters", "tsvb.movingAverage.modelOptions.linearLabel": "线性", "tsvb.movingAverage.modelOptions.simpleLabel": "简单", - "tsvb.movingAverage.predictLabel": "预测", - "tsvb.movingAverage.settingsDescription": "{keyValue} 空格分隔", - "tsvb.movingAverage.settingsLabel": "设置", "tsvb.movingAverage.windowSizeLabel": "窗口大小", "tsvb.noButtonLabel": "否", "tsvb.noDataDescription": "所选指标没有可显示的数据",