Allow setting offset relative to overall chart time (#19709) (#20331)

* Allow setting offset relative to kibana time

* remove changes to kibana.yml

* use timerange instead of kibana_time, do not allow default value, better help text

* throw error when zero is provided, round number to avoid decimals - which are not allowed

* do not allow offsets larger than zero
This commit is contained in:
Nathan Reese 2018-06-28 19:29:41 -06:00 committed by GitHub
parent 73d3883188
commit 0ab5e56e87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 101 additions and 7 deletions

View file

@ -20,7 +20,7 @@
import loadFunctions from '../load_functions.js';
const fitFunctions = loadFunctions('fit_functions');
import TimelionFunction from './timelion_function';
import offsetTime from '../offset_time';
import { offsetTime, preprocessOffset } from '../offset_time';
import _ from 'lodash';
@ -41,7 +41,9 @@ export default class Datasource extends TimelionFunction {
name: 'offset',
types: ['string', 'null'],
help: 'Offset the series retrieval by a date expression, ' +
'e.g., -1M to make events from one month ago appear as if they are happening now'
'e.g., -1M to make events from one month ago appear as if they are happening now. ' +
'Offset the series relative to the charts overall time range, by using the value "timerange", ' +
'e.g. "timerange:-2" will specify an offset that is twice the overall chart time range to the past.'
});
config.args.push({
@ -54,16 +56,18 @@ export default class Datasource extends TimelionFunction {
const originalFunction = config.fn;
config.fn = function (args, tlConfig) {
const config = _.clone(tlConfig);
if (args.byName.offset) {
let offset = args.byName.offset;
if (offset) {
offset = preprocessOffset(offset, tlConfig.time.from, tlConfig.time.to);
config.time = _.cloneDeep(tlConfig.time);
config.time.from = offsetTime(config.time.from, args.byName.offset);
config.time.to = offsetTime(config.time.to, args.byName.offset);
config.time.from = offsetTime(config.time.from, offset);
config.time.to = offsetTime(config.time.to, offset);
}
return Promise.resolve(originalFunction(args, config)).then(function (seriesList) {
seriesList.list = _.map(seriesList.list, function (series) {
if (series.data.length === 0) throw new Error(name + '() returned no results');
series.data = offsetSeries(series.data, args.byName.offset);
series.data = offsetSeries(series.data, offset);
series.fit = args.byName.fit || series.fit || 'nearest';
return series;
});

View file

@ -20,7 +20,7 @@
import moment from 'moment';
// usually reverse = false on the request, true on the response
export default function offsetTime(milliseconds, offset, reverse) {
export function offsetTime(milliseconds, offset, reverse) {
if (!offset.match(/[-+][0-9]+[mshdwMy]/g)) {
throw new Error ('Malformed `offset` at ' + offset);
}
@ -34,3 +34,37 @@ export default function offsetTime(milliseconds, offset, reverse) {
const momentObj = moment(milliseconds)[mode](parts[1], parts[2]);
return momentObj.valueOf();
}
function timeRangeErrorMsg(offset) {
return `Malformed timerange offset, expecting "timerange:<number>", received: ${offset}`;
}
/*
* Calculate offset when parameter is requesting a relative offset based on requested time range.
*
* @param {string} offset - offset parameter value
* @param {number} from - kibana global time 'from' in milliseconds
* @param {number} to - kibana global time 'to' in milliseconds
*/
export function preprocessOffset(offset, from, to) {
if (!offset.startsWith('timerange')) {
return offset;
}
const parts = offset.split(':');
if (parts.length === 1) {
throw new Error(timeRangeErrorMsg(offset));
}
const factor = parseFloat(parts[1]);
if (isNaN(factor)) {
throw new Error(timeRangeErrorMsg(offset));
}
if (factor >= 0) {
throw new Error('Malformed timerange offset, factor must be negative number.');
}
const deltaSeconds = (to - from) / 1000;
const processedOffset = Math.round(deltaSeconds * factor);
return `${processedOffset}s`;
}

View file

@ -0,0 +1,56 @@
/*
* 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 expect from 'expect.js';
import moment from 'moment';
import { preprocessOffset } from './offset_time';
describe('offset', () => {
describe('preprocessOffset', () => {
const from = moment('2018-01-01T00:00:00.000Z').valueOf();
const to = moment('2018-01-01T00:15:00.000Z').valueOf();
test('throws error when no number is provided', () => {
expect(() => preprocessOffset('timerange', from, to)).to.throwError();
});
test('throws error when zero is provided', () => {
expect(() => preprocessOffset('timerange:0', from, to)).to.throwError();
});
test('throws error when factor is larger than zero', () => {
expect(() => preprocessOffset('timerange:1', from, to)).to.throwError();
});
test('throws error with malformed', () => {
expect(() => preprocessOffset('timerange:notANumber', from, to)).to.throwError();
});
test('does not modify offset when value is not requesting relative offset', () => {
const offset = '-1d';
expect(preprocessOffset(offset, from, to)).to.eql(offset);
});
test('converts offset when value is requesting relative offset with multiplier', () => {
const offset = 'timerange:-2';
expect(preprocessOffset(offset, from, to)).to.eql('-1800s');
});
});
});