Introduce EditorConfigProvider to customize vis editor (#20519)

* Add possibility to specify base for histogram interval

* Move AggParamsWriter to ES6

* Add param for time_zone in date_histogram

* Prevent writing intervalBase to DSL

* Add basic EditorConfig providers

* Merge multiple configs together

* Allow hiding parameters

* Improve config merging and add tests

* Remove TODO

* Implement review feedback

* Add warning to parameter

* Remove unneeded self
This commit is contained in:
Tim Roes 2018-08-10 20:10:07 +02:00 committed by GitHub
parent 9646850306
commit 133d25aef1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 665 additions and 82 deletions

View file

@ -46,55 +46,57 @@ export default function AggParamWriterHelper(Private) {
* @param {object} opts - describe the properties of this paramWriter
* @param {string} opts.aggType - the name of the aggType we want to test. ('histogram', 'filter', etc.)
*/
function AggParamWriter(opts) {
const self = this;
class AggParamWriter {
self.aggType = opts.aggType;
if (_.isString(self.aggType)) {
self.aggType = aggTypes.byName[self.aggType];
constructor(opts) {
this.aggType = opts.aggType;
if (_.isString(this.aggType)) {
this.aggType = aggTypes.byName[this.aggType];
}
// not configurable right now, but totally required
this.indexPattern = stubbedLogstashIndexPattern;
// the schema that the aggType satisfies
this.visAggSchema = null;
this.vis = new Vis(this.indexPattern, {
type: 'histogram',
aggs: [{
id: 1,
type: this.aggType.name,
params: {}
}]
});
}
// not configurable right now, but totally required
self.indexPattern = stubbedLogstashIndexPattern;
write(paramValues, modifyAggConfig = null) {
paramValues = _.clone(paramValues);
// the schema that the aggType satisfies
self.visAggSchema = null;
if (this.aggType.params.byName.field && !paramValues.field) {
// pick a field rather than force a field to be specified everywhere
if (this.aggType.type === 'metrics') {
paramValues.field = _.sample(this.indexPattern.fields.byType.number);
} else {
const type = this.aggType.params.byName.field.filterFieldTypes || 'string';
let field;
do {
field = _.sample(this.indexPattern.fields.byType[type]);
} while (!field.aggregatable);
paramValues.field = field.name;
}
}
self.vis = new Vis(self.indexPattern, {
type: 'histogram',
aggs: [{
id: 1,
type: self.aggType.name,
params: {}
}]
});
const aggConfig = this.vis.aggs[0];
aggConfig.setParams(paramValues);
if (modifyAggConfig) {
modifyAggConfig(aggConfig);
}
return aggConfig.write(this.vis.aggs);
}
}
AggParamWriter.prototype.write = function (paramValues) {
const self = this;
paramValues = _.clone(paramValues);
if (self.aggType.params.byName.field && !paramValues.field) {
// pick a field rather than force a field to be specified everywhere
if (self.aggType.type === 'metrics') {
paramValues.field = _.sample(self.indexPattern.fields.byType.number);
} else {
const type = self.aggType.params.byName.field.filterFieldTypes || 'string';
let field;
do {
field = _.sample(self.indexPattern.fields.byType[type]);
} while (!field.aggregatable);
paramValues.field = field.name;
}
}
const aggConfig = self.vis.aggs[0];
aggConfig.setParams(paramValues);
return aggConfig.write(self.vis.aggs);
};
return AggParamWriter;
}

View file

@ -18,10 +18,13 @@
*/
import expect from 'expect.js';
import sinon from 'sinon';
import ngMock from 'ng_mock';
import { aggTypes } from '../..';
import chrome from '../../../chrome';
import AggParamWriterProvider from '../agg_param_writer';
const config = chrome.getUiSettingsClient();
const histogram = aggTypes.byName.histogram;
describe('Histogram Agg', function () {
@ -46,6 +49,13 @@ describe('Histogram Agg', function () {
paramWriter = new AggParamWriter({ aggType: 'histogram' });
}));
describe('intervalBase', () => {
it('should not be written to the DSL', () => {
const output = paramWriter.write({ intervalBase: 100 });
expect(output.params).not.to.have.property('intervalBase');
});
});
describe('interval', function () {
// reads aggConfig.params.interval, writes to dsl.interval
@ -74,6 +84,57 @@ describe('Histogram Agg', function () {
const output = paramWriter.write({ interval: [] });
expect(isNaN(output.params.interval)).to.be.ok();
});
describe('interval scaling', () => {
beforeEach(() => {
sinon.stub(config, 'get');
});
it('will respect the histogram:maxBars setting', () => {
config.get.withArgs('histogram:maxBars').returns(5);
const output = paramWriter.write({ interval: 5 },
aggConfig => aggConfig.setAutoBounds({ min: 0, max: 10000 }));
expect(output.params).to.have.property('interval', 2000);
});
it('will return specified interval, if bars are below histogram:maxBars config', () => {
config.get.withArgs('histogram:maxBars').returns(10000);
const output = paramWriter.write({ interval: 5 },
aggConfig => aggConfig.setAutoBounds({ min: 0, max: 10000 }));
expect(output.params).to.have.property('interval', 5);
});
it('will set to intervalBase if interval is below base', () => {
const output = paramWriter.write({ interval: 3, intervalBase: 8 });
expect(output.params).to.have.property('interval', 8);
});
it('will round to nearest intervalBase multiple if interval is above base', () => {
const roundUp = paramWriter.write({ interval: 46, intervalBase: 10 });
expect(roundUp.params).to.have.property('interval', 50);
const roundDown = paramWriter.write({ interval: 43, intervalBase: 10 });
expect(roundDown.params).to.have.property('interval', 40);
});
it('will not change interval if it is a multiple of base', () => {
const output = paramWriter.write({ interval: 35, intervalBase: 5 });
expect(output.params).to.have.property('interval', 35);
});
it('will round to intervalBase after scaling histogram:maxBars', () => {
config.get.withArgs('histogram:maxBars').returns(100);
const output = paramWriter.write({ interval: 5, intervalBase: 6 },
aggConfig => aggConfig.setAutoBounds({ min: 0, max: 1000 }));
// 100 buckets in 0 to 1000 would result in an interval of 10, so we should
// round to the next multiple of 6 -> 12
expect(output.params).to.have.property('interval', 12);
});
afterEach(() => {
config.get.restore();
});
});
});
describe('min_doc_count', function () {

View file

@ -20,13 +20,17 @@
import _ from 'lodash';
import moment from 'moment';
import expect from 'expect.js';
import sinon from 'sinon';
import ngMock from 'ng_mock';
import AggParamWriterProvider from '../../agg_param_writer';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import chrome from '../../../../chrome';
import { aggTypes } from '../../..';
import { AggConfig } from '../../../../vis/agg_config';
import { timefilter } from 'ui/timefilter';
const config = chrome.getUiSettingsClient();
describe('params', function () {
let paramWriter;
@ -132,6 +136,30 @@ describe('params', function () {
});
});
describe('time_zone', () => {
beforeEach(() => {
sinon.stub(config, 'get');
sinon.stub(config, 'isDefault');
});
it('should use the specified time_zone', () => {
const output = paramWriter.write({ time_zone: 'Europe/Kiev' });
expect(output.params).to.have.property('time_zone', 'Europe/Kiev');
});
it('should use the Kibana time_zone if no parameter specified', () => {
config.isDefault.withArgs('dateFormat:tz').returns(false);
config.get.withArgs('dateFormat:tz').returns('Europe/Riga');
const output = paramWriter.write({});
expect(output.params).to.have.property('time_zone', 'Europe/Riga');
});
afterEach(() => {
config.get.restore();
config.isDefault.restore();
});
});
describe('extended_bounds', function () {
it('should write a long value if a moment passed in', function () {
const then = moment(0);

View file

@ -128,13 +128,6 @@ export const dateHistogramBucketAgg = new BucketAggType({
output.bucketInterval = interval;
output.params.interval = interval.expression;
const isDefaultTimezone = config.isDefault('dateFormat:tz');
if (isDefaultTimezone) {
output.params.time_zone = detectedTimezone || tzOffset;
} else {
output.params.time_zone = config.get('dateFormat:tz');
}
const scaleMetrics = interval.scaled && interval.scale < 1;
if (scaleMetrics && aggs) {
const all = _.every(aggs.bySchemaGroup.metrics, function (agg) {
@ -147,13 +140,18 @@ export const dateHistogramBucketAgg = new BucketAggType({
}
}
},
{
name: 'time_zone',
default: () => {
const isDefaultTimezone = config.isDefault('dateFormat:tz');
return isDefaultTimezone ? detectedTimezone || tzOffset : config.get('dateFormat:tz');
},
},
{
name: 'customInterval',
default: '2h',
write: _.noop
},
{
name: 'format'
},

View file

@ -59,7 +59,15 @@ export const histogramBucketAgg = new BucketAggType({
name: 'field',
filterFieldTypes: 'number'
},
{
/*
* This parameter can be set if you want the auto scaled interval to always
* be a multiple of a specific base.
*/
name: 'intervalBase',
default: null,
write: () => {},
},
{
name: 'interval',
editor: intervalTemplate,
@ -111,6 +119,17 @@ export const histogramBucketAgg = new BucketAggType({
}
}
const base = aggConfig.params.intervalBase;
if (base) {
if (interval < base) {
// In case the specified interval is below the base, just increase it to it's base
interval = base;
} else if (interval % base !== 0) {
// In case the interval is not a multiple of the base round it to the next base
interval = Math.round(interval / base) * base;
}
}
output.params.interval = interval;
}
},

View file

@ -6,6 +6,15 @@
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}}"
@ -14,7 +23,8 @@
type="number"
class="form-control"
name="interval"
min="0"
min="{{editorConfig.interval.base || 0}}"
step="{{editorConfig.interval.base}}"
input-number
>
</div>

View file

@ -43,6 +43,6 @@ app.directive('icon', reactDirective => reactDirective(EuiIcon));
app.directive('colorPicker', reactDirective => reactDirective(EuiColorPicker));
app.directive('iconTip', reactDirective => reactDirective(EuiIconTip, ['content', 'type', 'position', 'title']));
app.directive('iconTip', reactDirective => reactDirective(EuiIconTip, ['content', 'type', 'position', 'title', 'color']));
app.directive('callOut', reactDirective => reactDirective(EuiCallOut, ['title', 'color', 'size', 'iconType', 'children']));

View 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 { greatestCommonDivisor, leastCommonMultiple } from './math';
describe('math utils', () => {
describe('greatestCommonDivisor', () => {
const tests: Array<[number, number, number]> = [
[3, 5, 1],
[30, 36, 6],
[5, 1, 1],
[9, 9, 9],
[40, 20, 20],
[3, 0, 3],
[0, 5, 5],
[0, 0, 0],
[-9, -3, 3],
[-24, 8, 8],
[22, -7, 1],
];
tests.map(([a, b, expected]) => {
it(`should return ${expected} for greatestCommonDivisor(${a}, ${b})`, () => {
expect(greatestCommonDivisor(a, b)).toBe(expected);
});
});
});
describe('leastCommonMultiple', () => {
const tests: Array<[number, number, number]> = [
[3, 5, 15],
[1, 1, 1],
[5, 6, 30],
[3, 9, 9],
[8, 20, 40],
[5, 5, 5],
[0, 5, 0],
[-4, -5, 20],
[-2, -3, 6],
[-8, 2, 8],
[-8, 5, 40],
];
tests.map(([a, b, expected]) => {
it(`should return ${expected} for leastCommonMultiple(${a}, ${b})`, () => {
expect(leastCommonMultiple(a, b)).toBe(expected);
});
});
});
});

View file

@ -0,0 +1,42 @@
/*
* 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.
*/
/**
* Calculates the greates common divisor of two numbers. This will be the
* greatest positive integer number, that both input values share as a divisor.
*
* This method does not properly work for fractional (non integer) numbers. If you
* pass in fractional numbers there usually will be an output, but that's not necessarily
* the greatest common divisor of those two numbers.
*/
export function greatestCommonDivisor(a: number, b: number): number {
return a === 0 ? Math.abs(b) : greatestCommonDivisor(b % a, a);
}
/**
* Calculates the least common multiple of two numbers. The least common multiple
* is the smallest positive integer number, that is divisible by both input parameters.
*
* Since this calculation suffers from rounding issues in decimal values, this method
* won't work for passing in fractional (non integer) numbers. It will return a value,
* but that value won't necessarily be the mathematical correct least common multiple.
*/
export function leastCommonMultiple(a: number, b: number): number {
return Math.abs((a * b) / greatestCommonDivisor(a, b));
}

View file

@ -0,0 +1,142 @@
/*
* 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 { EditorConfigProviderRegistry } from './editor_config_providers';
import { EditorParamConfig, FixedParam, NumericIntervalParam } from './types';
describe('EditorConfigProvider', () => {
let registry: EditorConfigProviderRegistry;
beforeEach(() => {
registry = new EditorConfigProviderRegistry();
});
it('should call registered providers with given parameters', () => {
const provider = jest.fn(() => ({}));
registry.register(provider);
expect(provider).not.toHaveBeenCalled();
const aggType = {};
const indexPattern = {};
const aggConfig = {};
registry.getConfigForAgg(aggType, indexPattern, aggConfig);
expect(provider).toHaveBeenCalledWith(aggType, indexPattern, aggConfig);
});
it('should call all registered providers with given parameters', () => {
const provider = jest.fn(() => ({}));
const provider2 = jest.fn(() => ({}));
registry.register(provider);
registry.register(provider2);
expect(provider).not.toHaveBeenCalled();
expect(provider2).not.toHaveBeenCalled();
const aggType = {};
const indexPattern = {};
const aggConfig = {};
registry.getConfigForAgg(aggType, indexPattern, aggConfig);
expect(provider).toHaveBeenCalledWith(aggType, indexPattern, aggConfig);
expect(provider2).toHaveBeenCalledWith(aggType, indexPattern, aggConfig);
});
describe('merging configs', () => {
function singleConfig(paramConfig: EditorParamConfig) {
return () => ({ singleParam: paramConfig });
}
function getOutputConfig(reg: EditorConfigProviderRegistry) {
return reg.getConfigForAgg({}, {}, {}).singleParam;
}
it('should have hidden true if at least one config was hidden true', () => {
registry.register(singleConfig({ hidden: false }));
registry.register(singleConfig({ hidden: true }));
registry.register(singleConfig({ hidden: false }));
const config = getOutputConfig(registry);
expect(config.hidden).toBe(true);
});
it('should merge the same fixed values', () => {
registry.register(singleConfig({ fixedValue: 'foo' }));
registry.register(singleConfig({ fixedValue: 'foo' }));
const config = getOutputConfig(registry) as FixedParam;
expect(config).toHaveProperty('fixedValue');
expect(config.fixedValue).toBe('foo');
});
it('should throw having different fixed values', () => {
registry.register(singleConfig({ fixedValue: 'foo' }));
registry.register(singleConfig({ fixedValue: 'bar' }));
expect(() => {
getOutputConfig(registry);
}).toThrowError();
});
it('should allow same base values', () => {
registry.register(singleConfig({ base: 5 }));
registry.register(singleConfig({ base: 5 }));
const config = getOutputConfig(registry) as NumericIntervalParam;
expect(config).toHaveProperty('base');
expect(config.base).toBe(5);
});
it('should merge multiple base values, using least common multiple', () => {
registry.register(singleConfig({ base: 2 }));
registry.register(singleConfig({ base: 5 }));
registry.register(singleConfig({ base: 8 }));
const config = getOutputConfig(registry) as NumericIntervalParam;
expect(config).toHaveProperty('base');
expect(config.base).toBe(40);
});
it('should throw on combining fixedValue with base', () => {
registry.register(singleConfig({ fixedValue: 'foo' }));
registry.register(singleConfig({ base: 5 }));
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 }));
const config = getOutputConfig(registry) as FixedParam;
expect(config).toHaveProperty('fixedValue');
expect(config).toHaveProperty('hidden');
expect(config.fixedValue).toBe('foo');
expect(config.hidden).toBe(true);
});
it('should merge hidden together with base', () => {
registry.register(singleConfig({ base: 2, hidden: false }));
registry.register(singleConfig({ base: 13, hidden: false }));
const config = getOutputConfig(registry) as NumericIntervalParam;
expect(config).toHaveProperty('base');
expect(config).toHaveProperty('hidden');
expect(config.base).toBe(26);
expect(config.hidden).toBe(false);
});
it('should merge warnings together into one string', () => {
registry.register(singleConfig({ warning: 'Warning' }));
registry.register(singleConfig({ warning: 'Another warning' }));
const config = getOutputConfig(registry);
expect(config).toHaveProperty('warning');
expect(config.warning).toBe('Warning\n\nAnother warning');
});
});
});

View file

@ -0,0 +1,141 @@
/*
* 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 { AggConfig } from '../..';
import { AggType } from '../../../agg_types';
import { IndexPattern } from '../../../index_patterns';
import { leastCommonMultiple } from '../../../utils/math';
import { EditorConfig, EditorParamConfig, FixedParam, NumericIntervalParam } from './types';
type EditorConfigProvider = (
aggType: AggType,
indexPattern: IndexPattern,
aggConfig: AggConfig
) => EditorConfig;
class EditorConfigProviderRegistry {
private providers: Set<EditorConfigProvider> = new Set();
public register(configProvider: EditorConfigProvider): void {
this.providers.add(configProvider);
}
public getConfigForAgg(
aggType: AggType,
indexPattern: IndexPattern,
aggConfig: AggConfig
): EditorConfig {
const configs = Array.from(this.providers).map(provider =>
provider(aggType, indexPattern, aggConfig)
);
return this.mergeConfigs(configs);
}
private isBaseParam(config: EditorParamConfig): config is NumericIntervalParam {
return config.hasOwnProperty('base');
}
private isFixedParam(config: EditorParamConfig): config is FixedParam {
return config.hasOwnProperty('fixedValue');
}
private mergeHidden(current: EditorParamConfig, merged: EditorParamConfig): boolean {
return Boolean(current.hidden || merged.hidden);
}
private mergeWarning(current: EditorParamConfig, merged: EditorParamConfig): string | undefined {
if (!current.warning) {
return merged.warning;
}
return merged.warning ? `${merged.warning}\n\n${current.warning}` : current.warning;
}
private mergeFixedAndBase(
current: EditorParamConfig,
merged: EditorParamConfig,
paramName: string
): { fixedValue?: any; base?: number } {
if (
this.isFixedParam(current) &&
this.isFixedParam(merged) &&
current.fixedValue !== merged.fixedValue
) {
// In case multiple configurations provided a fixedValue, these must all be the same.
// If not we'll throw an error.
throw new Error(`Two EditorConfigProviders provided different fixed values for field ${paramName}:
${merged.fixedValue} !== ${current.fixedValue}`);
}
if (
(this.isFixedParam(current) && this.isBaseParam(merged)) ||
(this.isBaseParam(current) && this.isFixedParam(merged))
) {
// In case one config tries to set a fixed value and another setting a base value,
// we'll throw an error. This could be solved more elegantly, by allowing fixedValues
// that are the multiple of the specific base value, but since there is no use-case for that
// right now, this isn't implemented.
throw new Error(`Tried to provide a fixedValue and a base for param ${paramName}.`);
}
if (this.isBaseParam(current) && this.isBaseParam(merged)) {
// In case both had where interval values, just use the least common multiple between both interval
return {
base: leastCommonMultiple(current.base, merged.base),
};
}
// In this case we haven't had a fixed value of base for that param yet, we use the one specified
// in the current config
if (this.isFixedParam(current)) {
return {
fixedValue: current.fixedValue,
};
}
if (this.isBaseParam(current)) {
return {
base: current.base,
};
}
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),
};
}
});
return output;
}, {});
}
}
const editorConfigProviders = new EditorConfigProviderRegistry();
export { editorConfigProviders, EditorConfigProviderRegistry };

View file

@ -0,0 +1,48 @@
/*
* 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.
*/
/**
* A hidden parameter can be hidden from the UI completely.
*/
interface Param {
hidden?: boolean;
warning?: string;
}
/**
* A fixed parameter has a fixed value for a specific field.
* It can optionally also be hidden.
*/
export type FixedParam = Partial<Param> & {
fixedValue: any;
};
/**
* Numeric interval parameters must always be set in the editor to a multiple of
* the specified base. It can optionally also be hidden.
*/
export type NumericIntervalParam = Partial<Param> & {
base: number;
};
export type EditorParamConfig = NumericIntervalParam | FixedParam | Param;
export interface EditorConfig {
[paramName: string]: EditorParamConfig;
}

View file

@ -18,7 +18,7 @@
*/
import $ from 'jquery';
import _ from 'lodash';
import { has, get } from 'lodash';
import aggSelectHtml from './agg_select.html';
import advancedToggleHtml from './advanced_toggle.html';
import '../../../filters/match_any';
@ -28,6 +28,7 @@ import { uiModules } from '../../../modules';
import { documentationLinks } from '../../../documentation_links/documentation_links';
import aggParamsTemplate from './agg_params.html';
import { aggTypeFilters } from '../../../agg_types/filter';
import { editorConfigProviders } from '../config/editor_config_providers';
uiModules
.get('app/visualize')
@ -51,6 +52,27 @@ uiModules
// there is a possibility that the agg type can be automatically selected (if there is only one)
$scope.$watch('agg.type', updateAggParamEditor);
function updateEditorConfig() {
$scope.editorConfig = editorConfigProviders.getConfigForAgg(
aggTypes.byType[$scope.groupName],
$scope.indexPattern,
$scope.agg
);
Object.keys($scope.editorConfig).forEach(param => {
const config = $scope.editorConfig[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;
}
});
}
$scope.$watchCollection('agg.params', updateEditorConfig);
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
addSchemaEditor();
@ -77,9 +99,10 @@ uiModules
let $aggParamEditorsScope;
function updateAggParamEditor() {
updateEditorConfig();
$scope.aggHelpLink = null;
if (_.has($scope, 'agg.type.name')) {
$scope.aggHelpLink = _.get(documentationLinks, ['aggs', $scope.agg.type.name]);
if (has($scope, 'agg.type.name')) {
$scope.aggHelpLink = get(documentationLinks, ['aggs', $scope.agg.type.name]);
}
if ($aggParamEditors) {
@ -106,36 +129,39 @@ uiModules
};
// build collection of agg params html
$scope.agg.type.params.forEach(function (param, i) {
let aggParam;
let fields;
if ($scope.agg.schema.hideCustomLabel && param.name === 'customLabel') {
return;
}
// if field param exists, compute allowed fields
if (param.name === 'field') {
fields = $aggParamEditorsScope.indexedFields;
} else if (param.type === 'field') {
fields = $aggParamEditorsScope[`${param.name}Options`] = param.getFieldOptions($scope.agg);
}
if (fields) {
const hasIndexedFields = fields.length > 0;
const isExtraParam = i > 0;
if (!hasIndexedFields && isExtraParam) { // don't draw the rest of the options if there are no indexed fields.
$scope.agg.type.params
// Filter out, i.e. don't render, any parameter that is hidden via the editor config.
.filter(param => !get($scope, ['editorConfig', param.name, 'hidden'], false))
.forEach(function (param, i) {
let aggParam;
let fields;
if ($scope.agg.schema.hideCustomLabel && param.name === 'customLabel') {
return;
}
}
// if field param exists, compute allowed fields
if (param.name === 'field') {
fields = $aggParamEditorsScope.indexedFields;
} else if (param.type === 'field') {
fields = $aggParamEditorsScope[`${param.name}Options`] = param.getFieldOptions($scope.agg);
}
if (fields) {
const hasIndexedFields = fields.length > 0;
const isExtraParam = i > 0;
if (!hasIndexedFields && isExtraParam) { // don't draw the rest of the options if there are no indexed fields.
return;
}
}
let type = 'basic';
if (param.advanced) type = 'advanced';
let type = 'basic';
if (param.advanced) type = 'advanced';
if (aggParam = getAggParamHTML(param, i)) {
aggParamHTML[type].push(aggParam);
}
if (aggParam = getAggParamHTML(param, i)) {
aggParamHTML[type].push(aggParam);
}
});
});
// compile the paramEditors html elements
let paramEditors = aggParamHTML.basic;