2019-12-23 06:22:54 +01:00
|
|
|
import {
|
|
|
|
|
DynamicConfigValue,
|
|
|
|
|
FieldConfig,
|
|
|
|
|
DataFrame,
|
|
|
|
|
Field,
|
|
|
|
|
FieldType,
|
2019-12-28 17:32:58 -08:00
|
|
|
ThresholdsMode,
|
|
|
|
|
FieldColorMode,
|
|
|
|
|
ColorScheme,
|
2020-02-13 21:37:24 +01:00
|
|
|
FieldConfigEditorRegistry,
|
|
|
|
|
FieldOverrideContext,
|
|
|
|
|
ScopedVars,
|
2020-03-16 14:26:03 +01:00
|
|
|
ApplyFieldOverrideOptions,
|
2019-12-23 06:22:54 +01:00
|
|
|
} from '../types';
|
2019-12-12 14:55:30 -08:00
|
|
|
import { fieldMatchers, ReducerID, reduceField } from '../transformations';
|
|
|
|
|
import { FieldMatcher } from '../types/transformations';
|
|
|
|
|
import isNumber from 'lodash/isNumber';
|
|
|
|
|
import { getDisplayProcessor } from './displayProcessor';
|
2019-12-28 17:32:58 -08:00
|
|
|
import { guessFieldTypeForField } from '../dataframe';
|
2020-02-13 21:37:24 +01:00
|
|
|
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
|
2019-12-12 14:55:30 -08:00
|
|
|
|
|
|
|
|
interface OverrideProps {
|
|
|
|
|
match: FieldMatcher;
|
|
|
|
|
properties: DynamicConfigValue[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface GlobalMinMax {
|
|
|
|
|
min: number;
|
|
|
|
|
max: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax {
|
|
|
|
|
let min = Number.MAX_VALUE;
|
|
|
|
|
let max = Number.MIN_VALUE;
|
|
|
|
|
|
|
|
|
|
const reducers = [ReducerID.min, ReducerID.max];
|
|
|
|
|
for (const frame of data) {
|
|
|
|
|
for (const field of frame.fields) {
|
|
|
|
|
if (field.type === FieldType.number) {
|
|
|
|
|
const stats = reduceField({ field, reducers });
|
|
|
|
|
if (stats[ReducerID.min] < min) {
|
|
|
|
|
min = stats[ReducerID.min];
|
|
|
|
|
}
|
|
|
|
|
if (stats[ReducerID.max] > max) {
|
|
|
|
|
max = stats[ReducerID.max];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { min, max };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return a copy of the DataFrame with all rules applied
|
|
|
|
|
*/
|
2019-12-23 06:22:54 +01:00
|
|
|
export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFrame[] {
|
2019-12-13 08:36:49 -08:00
|
|
|
if (!options.data) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
2019-12-23 06:22:54 +01:00
|
|
|
|
2019-12-13 08:36:49 -08:00
|
|
|
const source = options.fieldOptions;
|
2019-12-12 14:55:30 -08:00
|
|
|
if (!source) {
|
2019-12-13 08:36:49 -08:00
|
|
|
return options.data;
|
2019-12-12 14:55:30 -08:00
|
|
|
}
|
2019-12-23 06:22:54 +01:00
|
|
|
|
2019-12-12 14:55:30 -08:00
|
|
|
let range: GlobalMinMax | undefined = undefined;
|
|
|
|
|
|
|
|
|
|
// Prepare the Matchers
|
|
|
|
|
const override: OverrideProps[] = [];
|
|
|
|
|
if (source.overrides) {
|
|
|
|
|
for (const rule of source.overrides) {
|
|
|
|
|
const info = fieldMatchers.get(rule.matcher.id);
|
|
|
|
|
if (info) {
|
|
|
|
|
override.push({
|
2019-12-23 06:22:54 +01:00
|
|
|
match: info.get(rule.matcher.options),
|
2019-12-12 14:55:30 -08:00
|
|
|
properties: rule.properties,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-13 08:36:49 -08:00
|
|
|
return options.data.map((frame, index) => {
|
2019-12-12 14:55:30 -08:00
|
|
|
let name = frame.name;
|
|
|
|
|
if (!name) {
|
|
|
|
|
name = `Series[${index}]`;
|
|
|
|
|
}
|
2020-02-16 14:56:24 +01:00
|
|
|
|
|
|
|
|
const scopedVars: ScopedVars = {
|
|
|
|
|
__series: { text: 'Series', value: { name } },
|
|
|
|
|
};
|
2019-12-12 14:55:30 -08:00
|
|
|
|
2020-02-13 21:37:24 +01:00
|
|
|
const fields: Field[] = frame.fields.map((field, fieldIndex) => {
|
2019-12-12 14:55:30 -08:00
|
|
|
// Config is mutable within this scope
|
2020-02-13 21:37:24 +01:00
|
|
|
let fieldName = field.name;
|
|
|
|
|
if (!fieldName) {
|
|
|
|
|
fieldName = `Field[${fieldIndex}]`;
|
2019-12-12 14:55:30 -08:00
|
|
|
}
|
|
|
|
|
|
2020-02-13 21:37:24 +01:00
|
|
|
scopedVars['__field'] = { text: 'Field', value: { name: fieldName } };
|
|
|
|
|
|
|
|
|
|
const config: FieldConfig = { ...field.config, scopedVars } || {};
|
|
|
|
|
const context = {
|
|
|
|
|
field,
|
|
|
|
|
data: options.data!,
|
|
|
|
|
dataFrameIndex: index,
|
|
|
|
|
replaceVariables: options.replaceVariables,
|
|
|
|
|
custom: options.custom,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Anything in the field config that's not set by the datasource
|
|
|
|
|
// will be filled in by panel's field configuration
|
|
|
|
|
setFieldConfigDefaults(config, source.defaults, context);
|
|
|
|
|
|
2019-12-12 14:55:30 -08:00
|
|
|
// Find any matching rules and then override
|
|
|
|
|
for (const rule of override) {
|
|
|
|
|
if (rule.match(field)) {
|
|
|
|
|
for (const prop of rule.properties) {
|
2020-02-13 21:37:24 +01:00
|
|
|
// config.scopedVars is set already here
|
|
|
|
|
setDynamicConfigValue(config, prop, context);
|
2019-12-12 14:55:30 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-28 17:32:58 -08:00
|
|
|
// Try harder to set a real value that is not 'other'
|
|
|
|
|
let type = field.type;
|
|
|
|
|
if (!type || type === FieldType.other) {
|
|
|
|
|
const t = guessFieldTypeForField(field);
|
|
|
|
|
if (t) {
|
|
|
|
|
type = t;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Some units have an implied range
|
|
|
|
|
if (config.unit === 'percent') {
|
|
|
|
|
if (!isNumber(config.min)) {
|
|
|
|
|
config.min = 0;
|
|
|
|
|
}
|
|
|
|
|
if (!isNumber(config.max)) {
|
|
|
|
|
config.max = 100;
|
|
|
|
|
}
|
|
|
|
|
} else if (config.unit === 'percentunit') {
|
|
|
|
|
if (!isNumber(config.min)) {
|
|
|
|
|
config.min = 0;
|
|
|
|
|
}
|
|
|
|
|
if (!isNumber(config.max)) {
|
|
|
|
|
config.max = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-12 14:55:30 -08:00
|
|
|
// Set the Min/Max value automatically
|
2019-12-13 08:36:49 -08:00
|
|
|
if (options.autoMinMax && field.type === FieldType.number) {
|
2019-12-12 14:55:30 -08:00
|
|
|
if (!isNumber(config.min) || !isNumber(config.max)) {
|
|
|
|
|
if (!range) {
|
2019-12-13 08:36:49 -08:00
|
|
|
range = findNumericFieldMinMax(options.data!); // Global value
|
2019-12-12 14:55:30 -08:00
|
|
|
}
|
|
|
|
|
if (!isNumber(config.min)) {
|
|
|
|
|
config.min = range.min;
|
|
|
|
|
}
|
|
|
|
|
if (!isNumber(config.max)) {
|
|
|
|
|
config.max = range.max;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-28 17:32:58 -08:00
|
|
|
// Overwrite the configs
|
|
|
|
|
const f: Field = {
|
2019-12-12 14:55:30 -08:00
|
|
|
...field,
|
|
|
|
|
config,
|
2019-12-28 17:32:58 -08:00
|
|
|
type,
|
2019-12-12 14:55:30 -08:00
|
|
|
};
|
2020-02-16 14:56:24 +01:00
|
|
|
|
2019-12-28 17:32:58 -08:00
|
|
|
// and set the display processor using it
|
2020-01-15 12:02:52 -08:00
|
|
|
f.display = getDisplayProcessor({
|
|
|
|
|
field: f,
|
|
|
|
|
theme: options.theme,
|
|
|
|
|
timeZone: options.timeZone,
|
|
|
|
|
});
|
2019-12-28 17:32:58 -08:00
|
|
|
return f;
|
2019-12-12 14:55:30 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...frame,
|
|
|
|
|
fields,
|
|
|
|
|
name,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-13 21:37:24 +01:00
|
|
|
export interface FieldOverrideEnv extends FieldOverrideContext {
|
|
|
|
|
custom?: FieldConfigEditorRegistry;
|
2019-12-12 14:55:30 -08:00
|
|
|
}
|
|
|
|
|
|
2020-02-13 21:37:24 +01:00
|
|
|
function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigValue, context: FieldOverrideEnv) {
|
|
|
|
|
const reg = value.custom ? context.custom : standardFieldConfigEditorRegistry;
|
2019-12-12 14:55:30 -08:00
|
|
|
|
2020-02-13 21:37:24 +01:00
|
|
|
const item = reg?.getIfExists(value.prop);
|
|
|
|
|
if (!item || !item.shouldApply(context.field!)) {
|
|
|
|
|
return;
|
2019-12-12 14:55:30 -08:00
|
|
|
}
|
|
|
|
|
|
2020-02-13 21:37:24 +01:00
|
|
|
const val = item.process(value.value, context, item.settings);
|
|
|
|
|
|
|
|
|
|
const remove = val === undefined || val === null;
|
|
|
|
|
|
|
|
|
|
if (remove) {
|
2020-03-25 14:04:41 +01:00
|
|
|
if (value.custom && config.custom) {
|
|
|
|
|
delete config.custom[value.prop];
|
2020-02-13 21:37:24 +01:00
|
|
|
} else {
|
|
|
|
|
delete (config as any)[value.prop];
|
2019-12-12 14:55:30 -08:00
|
|
|
}
|
2020-02-13 21:37:24 +01:00
|
|
|
} else {
|
|
|
|
|
if (value.custom) {
|
|
|
|
|
if (!config.custom) {
|
|
|
|
|
config.custom = {};
|
|
|
|
|
}
|
|
|
|
|
config.custom[value.prop] = val;
|
|
|
|
|
} else {
|
|
|
|
|
(config as any)[value.prop] = val;
|
2019-12-12 14:55:30 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-13 21:37:24 +01:00
|
|
|
// config -> from DS
|
|
|
|
|
// defaults -> from Panel config
|
|
|
|
|
export function setFieldConfigDefaults(config: FieldConfig, defaults: FieldConfig, context: FieldOverrideEnv) {
|
|
|
|
|
if (defaults) {
|
|
|
|
|
const keys = Object.keys(defaults);
|
2019-12-12 14:55:30 -08:00
|
|
|
for (const key of keys) {
|
2020-02-13 21:37:24 +01:00
|
|
|
if (key === 'custom') {
|
|
|
|
|
if (!context.custom) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!config.custom) {
|
|
|
|
|
config.custom = {};
|
|
|
|
|
}
|
|
|
|
|
const customKeys = Object.keys(defaults.custom!);
|
|
|
|
|
|
|
|
|
|
for (const customKey of customKeys) {
|
|
|
|
|
processFieldConfigValue(config.custom!, defaults.custom!, customKey, context.custom, context);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// when config from ds exists for a given field -> use it
|
|
|
|
|
processFieldConfigValue(config, defaults, key, standardFieldConfigEditorRegistry, context);
|
2019-12-12 14:55:30 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-12-28 17:32:58 -08:00
|
|
|
validateFieldConfig(config);
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-13 21:37:24 +01:00
|
|
|
const processFieldConfigValue = (
|
|
|
|
|
destination: Record<string, any>, // it's mutable
|
|
|
|
|
source: Record<string, any>,
|
|
|
|
|
key: string,
|
|
|
|
|
registry: FieldConfigEditorRegistry,
|
|
|
|
|
context: FieldOverrideContext
|
|
|
|
|
) => {
|
|
|
|
|
const currentConfig = destination[key];
|
|
|
|
|
if (currentConfig === null || currentConfig === undefined) {
|
|
|
|
|
const item = registry.getIfExists(key);
|
2020-03-09 15:09:32 +01:00
|
|
|
if (!item) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-13 21:37:24 +01:00
|
|
|
if (item && item.shouldApply(context.field!)) {
|
|
|
|
|
const val = item.process(source[key], context, item.settings);
|
|
|
|
|
if (val !== undefined && val !== null) {
|
|
|
|
|
destination[key] = val;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2019-12-28 17:32:58 -08:00
|
|
|
/**
|
|
|
|
|
* This checks that all options on FieldConfig make sense. It mutates any value that needs
|
|
|
|
|
* fixed. In particular this makes sure that the first threshold value is -Infinity (not valid in JSON)
|
|
|
|
|
*/
|
|
|
|
|
export function validateFieldConfig(config: FieldConfig) {
|
|
|
|
|
const { thresholds } = config;
|
|
|
|
|
if (thresholds) {
|
|
|
|
|
if (!thresholds.mode) {
|
|
|
|
|
thresholds.mode = ThresholdsMode.Absolute;
|
|
|
|
|
}
|
|
|
|
|
if (!thresholds.steps) {
|
|
|
|
|
thresholds.steps = [];
|
|
|
|
|
} else if (thresholds.steps.length) {
|
|
|
|
|
// First value is always -Infinity
|
|
|
|
|
// JSON saves it as null
|
|
|
|
|
thresholds.steps[0].value = -Infinity;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!config.color) {
|
|
|
|
|
if (thresholds) {
|
|
|
|
|
config.color = {
|
|
|
|
|
mode: FieldColorMode.Thresholds,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
// No Color settings
|
|
|
|
|
} else if (!config.color.mode) {
|
|
|
|
|
// Without a mode, skip color altogether
|
|
|
|
|
delete config.color;
|
|
|
|
|
} else {
|
|
|
|
|
const { color } = config;
|
|
|
|
|
if (color.mode === FieldColorMode.Scheme) {
|
|
|
|
|
if (!color.schemeName) {
|
|
|
|
|
color.schemeName = ColorScheme.BrBG;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
delete color.schemeName;
|
|
|
|
|
}
|
2019-12-12 14:55:30 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify that max > min (swap if necessary)
|
|
|
|
|
if (config.hasOwnProperty('min') && config.hasOwnProperty('max') && config.min! > config.max!) {
|
|
|
|
|
const tmp = config.max;
|
|
|
|
|
config.max = config.min;
|
|
|
|
|
config.min = tmp;
|
|
|
|
|
}
|
|
|
|
|
}
|