mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Introduce date histogram time base configuration to EditorConfig (#22344)
* Add `default` and `baseInterval` configuration ability to date histogram EditorConfig * Change EditorConfig `warning` to `help`, show `help` below date histogram and histogram interval inputs
This commit is contained in:
parent
44475231a6
commit
0ed2623ce3
15 changed files with 518 additions and 42 deletions
|
@ -5,8 +5,9 @@
|
|||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"main": "target/index.js",
|
||||
"typings": "target/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "babel src --out-dir target",
|
||||
"build": "babel src --out-dir target --copy-files",
|
||||
"kbn:bootstrap": "yarn build",
|
||||
"kbn:watch": "yarn build --watch"
|
||||
},
|
||||
|
|
29
packages/kbn-datemath/src/index.d.ts
vendored
Normal file
29
packages/kbn-datemath/src/index.d.ts
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare module '@kbn/datemath' {
|
||||
const dateMath: {
|
||||
parse: any;
|
||||
unitsMap: any;
|
||||
units: string[];
|
||||
unitsAsc: string[];
|
||||
unitsDesc: string[];
|
||||
};
|
||||
export default dateMath;
|
||||
}
|
|
@ -19,9 +19,20 @@
|
|||
|
||||
import moment from 'moment';
|
||||
|
||||
const units = ['y', 'M', 'w', 'd', 'h', 'm', 's', 'ms'];
|
||||
const unitsDesc = units;
|
||||
const unitsAsc = [...unitsDesc].reverse();
|
||||
const unitsMap = {
|
||||
ms: { weight: 1, type: 'fixed', base: 1 },
|
||||
s: { weight: 2, type: 'fixed', base: 1000 },
|
||||
m: { weight: 3, type: 'mixed', base: 1000 * 60 },
|
||||
h: { weight: 4, type: 'mixed', base: 1000 * 60 * 60 },
|
||||
d: { weight: 5, type: 'mixed', base: 1000 * 60 * 60 * 24 },
|
||||
w: { weight: 6, type: 'calendar' },
|
||||
M: { weight: 7, type: 'calendar' },
|
||||
// q: { weight: 8, type: 'calendar' }, // TODO: moment duration does not support quarter
|
||||
y: { weight: 9, type: 'calendar' },
|
||||
};
|
||||
const units = Object.keys(unitsMap).sort((a, b) => unitsMap[b].weight - unitsMap[a].weight);
|
||||
const unitsDesc = [...units];
|
||||
const unitsAsc = [...units].reverse();
|
||||
|
||||
const isDate = d => Object.prototype.toString.call(d) === '[object Date]';
|
||||
|
||||
|
@ -142,6 +153,7 @@ function parseDateMath(mathString, time, roundUp) {
|
|||
|
||||
export default {
|
||||
parse: parse,
|
||||
unitsMap: Object.freeze(unitsMap),
|
||||
units: Object.freeze(units),
|
||||
unitsAsc: Object.freeze(unitsAsc),
|
||||
unitsDesc: Object.freeze(unitsDesc),
|
||||
|
|
10
packages/kbn-datemath/tsconfig.json
Normal file
10
packages/kbn-datemath/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"outDir": "./target"
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts"
|
||||
]
|
||||
}
|
|
@ -6,15 +6,6 @@
|
|||
position="'right'"
|
||||
content="'Interval will be automatically scaled in the event that the provided value creates more buckets than specified by Advanced Setting\'s histogram:maxBars'"
|
||||
></icon-tip>
|
||||
|
||||
<icon-tip
|
||||
ng-if="editorConfig.interval.warning"
|
||||
position="'right'"
|
||||
content="editorConfig.interval.warning"
|
||||
type="'alert'"
|
||||
color="'warning'"
|
||||
style="float: right"
|
||||
></icon-tip>
|
||||
</label>
|
||||
<input
|
||||
id="visEditorInterval{{agg.id}}"
|
||||
|
@ -27,4 +18,11 @@
|
|||
step="{{editorConfig.interval.base}}"
|
||||
input-number
|
||||
>
|
||||
<div
|
||||
ng-if="editorConfig.interval.help"
|
||||
class="kuiSubText kuiSubduedText kuiVerticalRhythmSmall"
|
||||
style="margin-top: 5px"
|
||||
>
|
||||
<span>{{editorConfig.interval.help}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
></icon-tip>
|
||||
</label>
|
||||
<select
|
||||
ng-if="!editorConfig.customInterval.timeBase"
|
||||
id="visEditorInterval{{agg.id}}"
|
||||
ng-model="agg.params.interval"
|
||||
ng-change="agg.write()"
|
||||
|
@ -23,9 +24,16 @@
|
|||
type="text"
|
||||
name="customInterval"
|
||||
ng-model="agg.params.customInterval"
|
||||
validate-date-interval
|
||||
validate-date-interval="{{editorConfig.customInterval.timeBase}}"
|
||||
ng-change="aggForm.customInterval.$valid && agg.write()"
|
||||
ng-if="agg.params.interval.val == 'custom'"
|
||||
class="form-control"
|
||||
required />
|
||||
<div
|
||||
ng-if="editorConfig.customInterval.help"
|
||||
class="kuiSubText kuiSubduedText kuiVerticalRhythmSmall"
|
||||
style="margin-top: 5px"
|
||||
>
|
||||
<span>{{editorConfig.customInterval.help}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
54
src/ui/public/utils/parse_es_interval.test.ts
Normal file
54
src/ui/public/utils/parse_es_interval.test.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { parseEsInterval } from './parse_es_interval';
|
||||
|
||||
describe('parseEsInterval', () => {
|
||||
it('should correctly parse an interval containing unit and single value', () => {
|
||||
expect(parseEsInterval('1ms')).toEqual({ value: 1, unit: 'ms', type: 'fixed' });
|
||||
expect(parseEsInterval('1s')).toEqual({ value: 1, unit: 's', type: 'fixed' });
|
||||
expect(parseEsInterval('1m')).toEqual({ value: 1, unit: 'm', type: 'calendar' });
|
||||
expect(parseEsInterval('1h')).toEqual({ value: 1, unit: 'h', type: 'calendar' });
|
||||
expect(parseEsInterval('1d')).toEqual({ value: 1, unit: 'd', type: 'calendar' });
|
||||
expect(parseEsInterval('1w')).toEqual({ value: 1, unit: 'w', type: 'calendar' });
|
||||
expect(parseEsInterval('1M')).toEqual({ value: 1, unit: 'M', type: 'calendar' });
|
||||
expect(parseEsInterval('1y')).toEqual({ value: 1, unit: 'y', type: 'calendar' });
|
||||
});
|
||||
|
||||
it('should correctly parse an interval containing unit and multiple value', () => {
|
||||
expect(parseEsInterval('250ms')).toEqual({ value: 250, unit: 'ms', type: 'fixed' });
|
||||
expect(parseEsInterval('90s')).toEqual({ value: 90, unit: 's', type: 'fixed' });
|
||||
expect(parseEsInterval('60m')).toEqual({ value: 60, unit: 'm', type: 'fixed' });
|
||||
expect(parseEsInterval('12h')).toEqual({ value: 12, unit: 'h', type: 'fixed' });
|
||||
expect(parseEsInterval('7d')).toEqual({ value: 7, unit: 'd', type: 'fixed' });
|
||||
});
|
||||
|
||||
it('should throw an error for intervals containing calendar unit and multiple value', () => {
|
||||
expect(() => parseEsInterval('4w')).toThrowError();
|
||||
expect(() => parseEsInterval('12M')).toThrowError();
|
||||
expect(() => parseEsInterval('10y')).toThrowError();
|
||||
});
|
||||
|
||||
it('should throw an error for invalid interval formats', () => {
|
||||
expect(() => parseEsInterval('1')).toThrowError();
|
||||
expect(() => parseEsInterval('h')).toThrowError();
|
||||
expect(() => parseEsInterval('0m')).toThrowError();
|
||||
expect(() => parseEsInterval('0.5h')).toThrowError();
|
||||
});
|
||||
});
|
66
src/ui/public/utils/parse_es_interval.ts
Normal file
66
src/ui/public/utils/parse_es_interval.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 dateMath from '@kbn/datemath';
|
||||
|
||||
const ES_INTERVAL_STRING_REGEX = new RegExp(
|
||||
'^([1-9][0-9]*)\\s*(' + dateMath.units.join('|') + ')$'
|
||||
);
|
||||
|
||||
/**
|
||||
* Extracts interval properties from an ES interval string. Disallows unrecognized interval formats
|
||||
* and fractional values. Converts some intervals from "calendar" to "fixed" when the number of
|
||||
* units is larger than 1, and throws an error for others.
|
||||
*
|
||||
* Conversion rules:
|
||||
*
|
||||
* | Interval | Single unit type | Multiple units type |
|
||||
* | -------- | ---------------- | ------------------- |
|
||||
* | ms | fixed | fixed |
|
||||
* | s | fixed | fixed |
|
||||
* | m | fixed | fixed |
|
||||
* | h | calendar | fixed |
|
||||
* | d | calendar | fixed |
|
||||
* | w | calendar | N/A - disallowed |
|
||||
* | M | calendar | N/A - disallowed |
|
||||
* | y | calendar | N/A - disallowed |
|
||||
*
|
||||
*/
|
||||
export function parseEsInterval(interval: string): { value: number; unit: string; type: string } {
|
||||
const matches = String(interval)
|
||||
.trim()
|
||||
.match(ES_INTERVAL_STRING_REGEX);
|
||||
|
||||
if (!matches) {
|
||||
throw Error(`Invalid interval format: ${interval}`);
|
||||
}
|
||||
|
||||
const value = matches && parseFloat(matches[1]);
|
||||
const unit = matches && matches[2];
|
||||
const type = unit && dateMath.unitsMap[unit].type;
|
||||
|
||||
if (type === 'calendar' && value !== 1) {
|
||||
throw Error(`Invalid calendar interval: ${interval}, value must be 1`);
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
unit,
|
||||
type: (type === 'mixed' && value === 1) || type === 'calendar' ? 'calendar' : 'fixed',
|
||||
};
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import { parseInterval } from './utils/parse_interval';
|
||||
import { uiModules } from './modules';
|
||||
import { leastCommonInterval } from './vis/lib/least_common_interval';
|
||||
|
||||
uiModules
|
||||
.get('kibana')
|
||||
|
@ -27,14 +28,31 @@ uiModules
|
|||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function ($scope, $el, attrs, ngModelCntrl) {
|
||||
const baseInterval = attrs.validateDateInterval || null;
|
||||
|
||||
ngModelCntrl.$parsers.push(check);
|
||||
ngModelCntrl.$formatters.push(check);
|
||||
|
||||
function check(value) {
|
||||
ngModelCntrl.$setValidity('dateInterval', parseInterval(value) != null);
|
||||
if(baseInterval) {
|
||||
ngModelCntrl.$setValidity('dateInterval', parseWithBase(value) === true);
|
||||
} else {
|
||||
ngModelCntrl.$setValidity('dateInterval', parseInterval(value) != null);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// When base interval is set, check for least common interval and allow
|
||||
// input the value is the same. This means that the input interval is a
|
||||
// multiple of the base interval.
|
||||
function parseWithBase(value) {
|
||||
try {
|
||||
const interval = leastCommonInterval(baseInterval, value);
|
||||
return interval === value.replace(/\s/g, '');
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { EditorConfigProviderRegistry } from './editor_config_providers';
|
||||
import { EditorParamConfig, FixedParam, NumericIntervalParam } from './types';
|
||||
import { EditorParamConfig, FixedParam, NumericIntervalParam, TimeIntervalParam } from './types';
|
||||
|
||||
describe('EditorConfigProvider', () => {
|
||||
let registry: EditorConfigProviderRegistry;
|
||||
|
@ -111,6 +111,49 @@ describe('EditorConfigProvider', () => {
|
|||
}).toThrowError();
|
||||
});
|
||||
|
||||
it('should allow same timeBase values', () => {
|
||||
registry.register(singleConfig({ timeBase: '2h', default: '2h' }));
|
||||
registry.register(singleConfig({ timeBase: '2h', default: '2h' }));
|
||||
const config = getOutputConfig(registry) as TimeIntervalParam;
|
||||
expect(config).toHaveProperty('timeBase');
|
||||
expect(config).toHaveProperty('default');
|
||||
expect(config.timeBase).toBe('2h');
|
||||
expect(config.default).toBe('2h');
|
||||
});
|
||||
|
||||
it('should merge multiple compatible timeBase values, using least common interval', () => {
|
||||
registry.register(singleConfig({ timeBase: '2h', default: '2h' }));
|
||||
registry.register(singleConfig({ timeBase: '3h', default: '3h' }));
|
||||
registry.register(singleConfig({ timeBase: '4h', default: '4h' }));
|
||||
const config = getOutputConfig(registry) as TimeIntervalParam;
|
||||
expect(config).toHaveProperty('timeBase');
|
||||
expect(config).toHaveProperty('default');
|
||||
expect(config.timeBase).toBe('12h');
|
||||
expect(config.default).toBe('12h');
|
||||
});
|
||||
|
||||
it('should throw on combining incompatible timeBase values', () => {
|
||||
registry.register(singleConfig({ timeBase: '2h', default: '2h' }));
|
||||
registry.register(singleConfig({ timeBase: '1d', default: '1d' }));
|
||||
expect(() => {
|
||||
getOutputConfig(registry);
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
it('should throw on invalid timeBase values', () => {
|
||||
registry.register(singleConfig({ timeBase: '2w', default: '2w' }));
|
||||
expect(() => {
|
||||
getOutputConfig(registry);
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
it('should throw if timeBase and default are different', () => {
|
||||
registry.register(singleConfig({ timeBase: '1h', default: '2h' }));
|
||||
expect(() => {
|
||||
getOutputConfig(registry);
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
it('should merge hidden together with fixedValue', () => {
|
||||
registry.register(singleConfig({ fixedValue: 'foo', hidden: true }));
|
||||
registry.register(singleConfig({ fixedValue: 'foo', hidden: false }));
|
||||
|
@ -131,12 +174,24 @@ describe('EditorConfigProvider', () => {
|
|||
expect(config.hidden).toBe(false);
|
||||
});
|
||||
|
||||
it('should merge warnings together into one string', () => {
|
||||
registry.register(singleConfig({ warning: 'Warning' }));
|
||||
registry.register(singleConfig({ warning: 'Another warning' }));
|
||||
it('should merge hidden together with timeBase', () => {
|
||||
registry.register(singleConfig({ timeBase: '2h', default: '2h', hidden: false }));
|
||||
registry.register(singleConfig({ timeBase: '4h', default: '4h', hidden: false }));
|
||||
const config = getOutputConfig(registry) as TimeIntervalParam;
|
||||
expect(config).toHaveProperty('timeBase');
|
||||
expect(config).toHaveProperty('default');
|
||||
expect(config).toHaveProperty('hidden');
|
||||
expect(config.timeBase).toBe('4h');
|
||||
expect(config.default).toBe('4h');
|
||||
expect(config.hidden).toBe(false);
|
||||
});
|
||||
|
||||
it('should merge helps together into one string', () => {
|
||||
registry.register(singleConfig({ help: 'Warning' }));
|
||||
registry.register(singleConfig({ help: 'Another help' }));
|
||||
const config = getOutputConfig(registry);
|
||||
expect(config).toHaveProperty('warning');
|
||||
expect(config.warning).toBe('Warning\n\nAnother warning');
|
||||
expect(config).toHaveProperty('help');
|
||||
expect(config.help).toBe('Warning\n\nAnother help');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,10 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { TimeIntervalParam } from 'ui/vis/editors/config/types';
|
||||
import { AggConfig } from '../..';
|
||||
import { AggType } from '../../../agg_types';
|
||||
import { IndexPattern } from '../../../index_patterns';
|
||||
import { leastCommonMultiple } from '../../../utils/math';
|
||||
import { parseEsInterval } from '../../../utils/parse_es_interval';
|
||||
import { leastCommonInterval } from '../../lib/least_common_interval';
|
||||
import { EditorConfig, EditorParamConfig, FixedParam, NumericIntervalParam } from './types';
|
||||
|
||||
type EditorConfigProvider = (
|
||||
|
@ -47,6 +50,10 @@ class EditorConfigProviderRegistry {
|
|||
return this.mergeConfigs(configs);
|
||||
}
|
||||
|
||||
private isTimeBaseParam(config: EditorParamConfig): config is TimeIntervalParam {
|
||||
return config.hasOwnProperty('default') && config.hasOwnProperty('timeBase');
|
||||
}
|
||||
|
||||
private isBaseParam(config: EditorParamConfig): config is NumericIntervalParam {
|
||||
return config.hasOwnProperty('base');
|
||||
}
|
||||
|
@ -59,12 +66,12 @@ class EditorConfigProviderRegistry {
|
|||
return Boolean(current.hidden || merged.hidden);
|
||||
}
|
||||
|
||||
private mergeWarning(current: EditorParamConfig, merged: EditorParamConfig): string | undefined {
|
||||
if (!current.warning) {
|
||||
return merged.warning;
|
||||
private mergeHelp(current: EditorParamConfig, merged: EditorParamConfig): string | undefined {
|
||||
if (!current.help) {
|
||||
return merged.help;
|
||||
}
|
||||
|
||||
return merged.warning ? `${merged.warning}\n\n${current.warning}` : current.warning;
|
||||
return merged.help ? `${merged.help}\n\n${current.help}` : current.help;
|
||||
}
|
||||
|
||||
private mergeFixedAndBase(
|
||||
|
@ -95,7 +102,7 @@ class EditorConfigProviderRegistry {
|
|||
}
|
||||
|
||||
if (this.isBaseParam(current) && this.isBaseParam(merged)) {
|
||||
// In case both had where interval values, just use the least common multiple between both interval
|
||||
// In case where both had interval values, just use the least common multiple between both interval
|
||||
return {
|
||||
base: leastCommonMultiple(current.base, merged.base),
|
||||
};
|
||||
|
@ -108,7 +115,6 @@ class EditorConfigProviderRegistry {
|
|||
fixedValue: current.fixedValue,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.isBaseParam(current)) {
|
||||
return {
|
||||
base: current.base,
|
||||
|
@ -118,18 +124,57 @@ class EditorConfigProviderRegistry {
|
|||
return {};
|
||||
}
|
||||
|
||||
private mergeTimeBase(
|
||||
current: TimeIntervalParam,
|
||||
merged: EditorParamConfig,
|
||||
paramName: string
|
||||
): { timeBase?: string; default?: string } {
|
||||
if (current.default !== current.timeBase) {
|
||||
throw new Error(`Tried to provide differing default and timeBase values for ${paramName}.`);
|
||||
}
|
||||
|
||||
if (this.isTimeBaseParam(current) && this.isTimeBaseParam(merged)) {
|
||||
// In case both had where interval values, just use the least common multiple between both intervals
|
||||
try {
|
||||
const timeBase = leastCommonInterval(current.timeBase, merged.timeBase);
|
||||
return {
|
||||
default: timeBase,
|
||||
timeBase,
|
||||
};
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isTimeBaseParam(current)) {
|
||||
try {
|
||||
parseEsInterval(current.timeBase);
|
||||
return {
|
||||
default: current.timeBase,
|
||||
timeBase: current.timeBase,
|
||||
};
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private mergeConfigs(configs: EditorConfig[]): EditorConfig {
|
||||
return configs.reduce((output, conf) => {
|
||||
Object.entries(conf).forEach(([paramName, paramConfig]) => {
|
||||
if (!output[paramName]) {
|
||||
output[paramName] = { ...paramConfig };
|
||||
} else {
|
||||
output[paramName] = {
|
||||
hidden: this.mergeHidden(paramConfig, output[paramName]),
|
||||
warning: this.mergeWarning(paramConfig, output[paramName]),
|
||||
...this.mergeFixedAndBase(paramConfig, output[paramName], paramName),
|
||||
};
|
||||
output[paramName] = {};
|
||||
}
|
||||
|
||||
output[paramName] = {
|
||||
hidden: this.mergeHidden(paramConfig, output[paramName]),
|
||||
help: this.mergeHelp(paramConfig, output[paramName]),
|
||||
...(this.isTimeBaseParam(paramConfig)
|
||||
? this.mergeTimeBase(paramConfig, output[paramName], paramName)
|
||||
: this.mergeFixedAndBase(paramConfig, output[paramName], paramName)),
|
||||
};
|
||||
});
|
||||
return output;
|
||||
}, {});
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
*/
|
||||
interface Param {
|
||||
hidden?: boolean;
|
||||
warning?: string;
|
||||
help?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -41,7 +41,16 @@ export type NumericIntervalParam = Partial<Param> & {
|
|||
base: number;
|
||||
};
|
||||
|
||||
export type EditorParamConfig = NumericIntervalParam | FixedParam | Param;
|
||||
/**
|
||||
* Time interval parameters must always be set in the editor to a multiple of
|
||||
* the specified base. It can optionally also be hidden.
|
||||
*/
|
||||
export type TimeIntervalParam = Partial<Param> & {
|
||||
default: string;
|
||||
timeBase: string;
|
||||
};
|
||||
|
||||
export type EditorParamConfig = NumericIntervalParam | TimeIntervalParam | FixedParam | Param;
|
||||
|
||||
export interface EditorConfig {
|
||||
[paramName: string]: EditorParamConfig;
|
||||
|
|
|
@ -50,9 +50,12 @@ uiModules
|
|||
|
||||
// We set up this watch prior to adding the controls below, because when the controls are added,
|
||||
// there is a possibility that the agg type can be automatically selected (if there is only one)
|
||||
$scope.$watch('agg.type', updateAggParamEditor);
|
||||
$scope.$watch('agg.type', () => {
|
||||
updateAggParamEditor();
|
||||
updateEditorConfig('default');
|
||||
});
|
||||
|
||||
function updateEditorConfig() {
|
||||
function updateEditorConfig(property = 'fixedValue') {
|
||||
$scope.editorConfig = editorConfigProviders.getConfigForAgg(
|
||||
aggTypes.byType[$scope.groupName],
|
||||
$scope.indexPattern,
|
||||
|
@ -61,17 +64,21 @@ uiModules
|
|||
|
||||
Object.keys($scope.editorConfig).forEach(param => {
|
||||
const config = $scope.editorConfig[param];
|
||||
const paramOptions = $scope.agg.type.params.find((paramOption) => paramOption.name === param);
|
||||
// If the parameter has a fixed value in the config, set this value.
|
||||
// Also for all supported configs we should freeze the editor for this param.
|
||||
if (config.hasOwnProperty('fixedValue')) {
|
||||
$scope.agg.params[param] = config.fixedValue;
|
||||
if (config.hasOwnProperty(property)) {
|
||||
if(paramOptions && paramOptions.deserialize) {
|
||||
$scope.agg.params[param] = paramOptions.deserialize(config[property]);
|
||||
} else {
|
||||
$scope.agg.params[param] = config[property];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.$watchCollection('agg.params', updateEditorConfig);
|
||||
|
||||
updateEditorConfig();
|
||||
$scope.$watchCollection('agg.params', updateEditorConfig);
|
||||
|
||||
// this will contain the controls for the schema (rows or columns?), which are unrelated to
|
||||
// controls for the agg, which is why they are first
|
||||
|
|
82
src/ui/public/vis/lib/least_common_interval.test.ts
Normal file
82
src/ui/public/vis/lib/least_common_interval.test.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { leastCommonInterval } from './least_common_interval';
|
||||
|
||||
describe('leastCommonInterval', () => {
|
||||
it('should correctly return lowest common interval for fixed units', () => {
|
||||
expect(leastCommonInterval('1ms', '1s')).toBe('1s');
|
||||
expect(leastCommonInterval('500ms', '1s')).toBe('1s');
|
||||
expect(leastCommonInterval('1000ms', '1s')).toBe('1s');
|
||||
expect(leastCommonInterval('1500ms', '1s')).toBe('3s');
|
||||
expect(leastCommonInterval('1234ms', '1s')).toBe('617s');
|
||||
expect(leastCommonInterval('1s', '2m')).toBe('2m');
|
||||
expect(leastCommonInterval('300s', '2m')).toBe('10m');
|
||||
expect(leastCommonInterval('1234ms', '7m')).toBe('4319m');
|
||||
expect(leastCommonInterval('45m', '2h')).toBe('6h');
|
||||
expect(leastCommonInterval('12h', '4d')).toBe('4d');
|
||||
expect(leastCommonInterval(' 20 h', '7d')).toBe('35d');
|
||||
});
|
||||
|
||||
it('should correctly return lowest common interval for calendar units', () => {
|
||||
expect(leastCommonInterval('1m', '1h')).toBe('1h');
|
||||
expect(leastCommonInterval('1h', '1d')).toBe('1d');
|
||||
expect(leastCommonInterval('1d', '1w')).toBe('1w');
|
||||
expect(leastCommonInterval('1w', '1M')).toBe('1M');
|
||||
expect(leastCommonInterval('1M', '1y')).toBe('1y');
|
||||
expect(leastCommonInterval('1M', '1m')).toBe('1M');
|
||||
expect(leastCommonInterval('1y', '1w')).toBe('1y');
|
||||
});
|
||||
|
||||
it('should throw an error for intervals of different types', () => {
|
||||
expect(() => {
|
||||
leastCommonInterval('60 s', '1m');
|
||||
}).toThrowError();
|
||||
expect(() => {
|
||||
leastCommonInterval('1d', '7d');
|
||||
}).toThrowError();
|
||||
expect(() => {
|
||||
leastCommonInterval('1h', '3d');
|
||||
}).toThrowError();
|
||||
expect(() => {
|
||||
leastCommonInterval('7d', '1w');
|
||||
}).toThrowError();
|
||||
expect(() => {
|
||||
leastCommonInterval('1M', '1000ms');
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
it('should throw an error for invalid intervals', () => {
|
||||
expect(() => {
|
||||
leastCommonInterval('foo', 'bar');
|
||||
}).toThrowError();
|
||||
expect(() => {
|
||||
leastCommonInterval('0h', '1h');
|
||||
}).toThrowError();
|
||||
expect(() => {
|
||||
leastCommonInterval('0.5h', '1h');
|
||||
}).toThrowError();
|
||||
expect(() => {
|
||||
leastCommonInterval('5w', '1h');
|
||||
}).toThrowError();
|
||||
expect(() => {
|
||||
leastCommonInterval('2M', '4w');
|
||||
}).toThrowError();
|
||||
});
|
||||
});
|
82
src/ui/public/vis/lib/least_common_interval.ts
Normal file
82
src/ui/public/vis/lib/least_common_interval.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 dateMath from '@kbn/datemath';
|
||||
import { leastCommonMultiple } from '../../utils/math';
|
||||
import { parseEsInterval } from '../../utils/parse_es_interval';
|
||||
|
||||
/**
|
||||
* Finds the lowest common interval between two given ES date histogram intervals
|
||||
* in the format of (value)(unit)
|
||||
*
|
||||
* - `ms, s` units are fixed-length intervals
|
||||
* - `m, h, d` units are fixed-length intervals when value > 1 (i.e. 2m, 24h, 7d),
|
||||
* but calendar interval when value === 1
|
||||
* - `w, M, q, y` units are calendar intervals and do not support multiple, aka
|
||||
* value must === 1
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function leastCommonInterval(a: string, b: string): string {
|
||||
const { unitsMap, unitsDesc } = dateMath;
|
||||
const aInt = parseEsInterval(a);
|
||||
const bInt = parseEsInterval(b);
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
const aUnit = unitsMap[aInt.unit];
|
||||
const bUnit = unitsMap[bInt.unit];
|
||||
|
||||
// If intervals aren't the same type, throw error
|
||||
if (aInt.type !== bInt.type) {
|
||||
throw Error(`Incompatible intervals: ${a} (${aInt.type}), ${b} (${bInt.type})`);
|
||||
}
|
||||
|
||||
// If intervals are calendar units, pick the larger one (calendar value is always 1)
|
||||
if (aInt.type === 'calendar') {
|
||||
return aUnit.weight > bUnit.weight ? `${aInt.value}${aInt.unit}` : `${bInt.value}${bInt.unit}`;
|
||||
}
|
||||
|
||||
// Otherwise if intervals are fixed units, find least common multiple in milliseconds
|
||||
const aMs = aInt.value * aUnit.base;
|
||||
const bMs = bInt.value * bUnit.base;
|
||||
const lcmMs = leastCommonMultiple(aMs, bMs);
|
||||
|
||||
// Return original interval string if it matches one of the original milliseconds
|
||||
if (lcmMs === bMs) {
|
||||
return b.replace(/\s/g, '');
|
||||
}
|
||||
if (lcmMs === aMs) {
|
||||
return a.replace(/\s/g, '');
|
||||
}
|
||||
|
||||
// Otherwise find the biggest unit that divides evenly
|
||||
const lcmUnit = unitsDesc.find(unit => unitsMap[unit].base && lcmMs % unitsMap[unit].base === 0);
|
||||
|
||||
// Throw error in case we couldn't divide evenly, theoretically we never get here as everything is
|
||||
// divisible by 1 millisecond
|
||||
if (!lcmUnit) {
|
||||
throw Error(`Unable to find common interval for: ${a}, ${b}`);
|
||||
}
|
||||
|
||||
// Return the interval string
|
||||
return `${lcmMs / unitsMap[lcmUnit].base}${lcmUnit}`;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue