mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
FieldConfig: support overrides model (#20986)
This commit is contained in:
parent
1f73e2aadf
commit
1aa39ee458
@ -1,39 +1,13 @@
|
|||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import { getFieldProperties, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
|
import { getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
|
||||||
import { toDataFrame } from '../dataframe/processDataFrame';
|
import { toDataFrame } from '../dataframe/processDataFrame';
|
||||||
import { ReducerID } from '../transformations/fieldReducer';
|
import { ReducerID } from '../transformations/fieldReducer';
|
||||||
import { Threshold } from '../types/threshold';
|
import { Threshold } from '../types/threshold';
|
||||||
import { GrafanaTheme } from '../types/theme';
|
import { GrafanaTheme } from '../types/theme';
|
||||||
import { MappingType } from '../types';
|
import { MappingType } from '../types';
|
||||||
|
import { setFieldConfigDefaults } from './fieldOverrides';
|
||||||
|
|
||||||
describe('FieldDisplay', () => {
|
describe('FieldDisplay', () => {
|
||||||
it('Construct simple field properties', () => {
|
|
||||||
const f0 = {
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
};
|
|
||||||
const f1 = {
|
|
||||||
unit: 'ms',
|
|
||||||
dateFormat: '', // should be ignored
|
|
||||||
max: parseFloat('NOPE'), // should be ignored
|
|
||||||
min: null,
|
|
||||||
};
|
|
||||||
let field = getFieldProperties(f0, f1);
|
|
||||||
expect(field.min).toEqual(0);
|
|
||||||
expect(field.max).toEqual(100);
|
|
||||||
expect(field.unit).toEqual('ms');
|
|
||||||
|
|
||||||
// last one overrieds
|
|
||||||
const f2 = {
|
|
||||||
unit: 'none', // ignore 'none'
|
|
||||||
max: -100, // lower than min! should flip min/max
|
|
||||||
};
|
|
||||||
field = getFieldProperties(f0, f1, f2);
|
|
||||||
expect(field.max).toEqual(0);
|
|
||||||
expect(field.min).toEqual(-100);
|
|
||||||
expect(field.unit).toEqual('ms');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('show first numeric values', () => {
|
it('show first numeric values', () => {
|
||||||
const options = createDisplayOptions({
|
const options = createDisplayOptions({
|
||||||
fieldOptions: {
|
fieldOptions: {
|
||||||
@ -89,7 +63,7 @@ describe('FieldDisplay', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should restore -Infinity value for base threshold', () => {
|
it('should restore -Infinity value for base threshold', () => {
|
||||||
const field = getFieldProperties({
|
const field = {
|
||||||
thresholds: [
|
thresholds: [
|
||||||
({
|
({
|
||||||
color: '#73BF69',
|
color: '#73BF69',
|
||||||
@ -100,7 +74,8 @@ describe('FieldDisplay', () => {
|
|||||||
value: 50,
|
value: 50,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
};
|
||||||
|
setFieldConfigDefaults(field);
|
||||||
expect(field.thresholds!.length).toEqual(2);
|
expect(field.thresholds!.length).toEqual(2);
|
||||||
expect(field.thresholds![0].value).toBe(-Infinity);
|
expect(field.thresholds![0].value).toBe(-Infinity);
|
||||||
});
|
});
|
||||||
@ -130,7 +105,7 @@ describe('FieldDisplay', () => {
|
|||||||
const mapEmptyToText = '0';
|
const mapEmptyToText = '0';
|
||||||
const options = createEmptyDisplayOptions({
|
const options = createEmptyDisplayOptions({
|
||||||
fieldOptions: {
|
fieldOptions: {
|
||||||
override: {
|
defaults: {
|
||||||
mappings: [
|
mappings: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -203,8 +178,8 @@ function createDisplayOptions(extend = {}): GetFieldDisplayValuesOptions {
|
|||||||
},
|
},
|
||||||
fieldOptions: {
|
fieldOptions: {
|
||||||
calcs: [],
|
calcs: [],
|
||||||
override: {},
|
|
||||||
defaults: {},
|
defaults: {},
|
||||||
|
overrides: [],
|
||||||
},
|
},
|
||||||
theme: {} as GrafanaTheme,
|
theme: {} as GrafanaTheme,
|
||||||
};
|
};
|
||||||
|
@ -1,26 +1,29 @@
|
|||||||
import toNumber from 'lodash/toNumber';
|
|
||||||
import toString from 'lodash/toString';
|
import toString from 'lodash/toString';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
|
||||||
import { getDisplayProcessor } from './displayProcessor';
|
import { getDisplayProcessor } from './displayProcessor';
|
||||||
import { getFlotPairs } from '../utils/flotPairs';
|
import { getFlotPairs } from '../utils/flotPairs';
|
||||||
import { FieldConfig, DataFrame, FieldType } from '../types/dataFrame';
|
import {
|
||||||
import { InterpolateFunction } from '../types/panel';
|
FieldConfig,
|
||||||
|
DataFrame,
|
||||||
|
FieldType,
|
||||||
|
DisplayValue,
|
||||||
|
DisplayValueAlignmentFactors,
|
||||||
|
FieldConfigSource,
|
||||||
|
InterpolateFunction,
|
||||||
|
} from '../types';
|
||||||
import { DataFrameView } from '../dataframe/DataFrameView';
|
import { DataFrameView } from '../dataframe/DataFrameView';
|
||||||
import { GraphSeriesValue } from '../types/graph';
|
import { GraphSeriesValue } from '../types/graph';
|
||||||
import { DisplayValue, DisplayValueAlignmentFactors } from '../types/displayValue';
|
|
||||||
import { GrafanaTheme } from '../types/theme';
|
import { GrafanaTheme } from '../types/theme';
|
||||||
import { ReducerID, reduceField } from '../transformations/fieldReducer';
|
import { ReducerID, reduceField } from '../transformations/fieldReducer';
|
||||||
import { ScopedVars } from '../types/ScopedVars';
|
import { ScopedVars } from '../types/ScopedVars';
|
||||||
import { getTimeField } from '../dataframe/processDataFrame';
|
import { getTimeField } from '../dataframe/processDataFrame';
|
||||||
|
import { applyFieldOverrides } from './fieldOverrides';
|
||||||
|
|
||||||
export interface FieldDisplayOptions {
|
export interface FieldDisplayOptions extends FieldConfigSource {
|
||||||
values?: boolean; // If true show each row value
|
values?: boolean; // If true show each row value
|
||||||
limit?: number; // if showing all values limit
|
limit?: number; // if showing all values limit
|
||||||
calcs: string[]; // when !values, pick one value for the whole field
|
calcs: string[]; // when !values, pick one value for the whole field
|
||||||
|
|
||||||
defaults: FieldConfig; // Use these values unless otherwise stated
|
|
||||||
override: FieldConfig; // Set these values regardless of the source
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use built in variables, same as for data links?
|
// TODO: use built in variables, same as for data links?
|
||||||
@ -81,27 +84,21 @@ export interface GetFieldDisplayValuesOptions {
|
|||||||
export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25;
|
export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25;
|
||||||
|
|
||||||
export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => {
|
export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => {
|
||||||
const { data, replaceVariables, fieldOptions } = options;
|
const { replaceVariables, fieldOptions } = options;
|
||||||
const { defaults, override } = fieldOptions;
|
|
||||||
const calcs = fieldOptions.calcs.length ? fieldOptions.calcs : [ReducerID.last];
|
const calcs = fieldOptions.calcs.length ? fieldOptions.calcs : [ReducerID.last];
|
||||||
|
|
||||||
const values: FieldDisplay[] = [];
|
const values: FieldDisplay[] = [];
|
||||||
|
|
||||||
if (data) {
|
if (options.data) {
|
||||||
|
const data = applyFieldOverrides(options.data, fieldOptions, replaceVariables, options.theme);
|
||||||
|
|
||||||
let hitLimit = false;
|
let hitLimit = false;
|
||||||
const limit = fieldOptions.limit ? fieldOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
|
const limit = fieldOptions.limit ? fieldOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
|
||||||
const defaultTitle = getTitleTemplate(fieldOptions.defaults.title, calcs, data);
|
const defaultTitle = getTitleTemplate(fieldOptions.defaults.title, calcs, data);
|
||||||
const scopedVars: ScopedVars = {};
|
const scopedVars: ScopedVars = {};
|
||||||
|
|
||||||
for (let s = 0; s < data.length && !hitLimit; s++) {
|
for (let s = 0; s < data.length && !hitLimit; s++) {
|
||||||
let series = data[s];
|
const series = data[s]; // Name is already set
|
||||||
if (!series.name) {
|
|
||||||
series = {
|
|
||||||
...series,
|
|
||||||
name: series.refId ? series.refId : `Series[${s}]`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
scopedVars['__series'] = { text: 'Series', value: { name: series.name } };
|
scopedVars['__series'] = { text: 'Series', value: { name: series.name } };
|
||||||
|
|
||||||
const { timeField } = getTimeField(series);
|
const { timeField } = getTimeField(series);
|
||||||
@ -114,7 +111,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
if (field.type !== FieldType.number) {
|
if (field.type !== FieldType.number) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const config = getFieldProperties(defaults, field.config || {}, override);
|
const config = field.config; // already set by the prepare task
|
||||||
|
|
||||||
let name = field.name;
|
let name = field.name;
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@ -123,11 +120,13 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
|
|
||||||
scopedVars['__field'] = { text: 'Field', value: { name } };
|
scopedVars['__field'] = { text: 'Field', value: { name } };
|
||||||
|
|
||||||
const display = getDisplayProcessor({
|
const display =
|
||||||
config,
|
field.display ??
|
||||||
theme: options.theme,
|
getDisplayProcessor({
|
||||||
type: field.type,
|
config,
|
||||||
});
|
theme: options.theme,
|
||||||
|
type: field.type,
|
||||||
|
});
|
||||||
|
|
||||||
const title = config.title ? config.title : defaultTitle;
|
const title = config.title ? config.title : defaultTitle;
|
||||||
// Show all rows
|
// Show all rows
|
||||||
@ -206,72 +205,6 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
return values;
|
return values;
|
||||||
};
|
};
|
||||||
|
|
||||||
const numericFieldProps: any = {
|
|
||||||
decimals: true,
|
|
||||||
min: true,
|
|
||||||
max: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a version of the field with the overries applied. Any property with
|
|
||||||
* value: null | undefined | empty string are skipped.
|
|
||||||
*
|
|
||||||
* For numeric values, only valid numbers will be applied
|
|
||||||
* for units, 'none' will be skipped
|
|
||||||
*/
|
|
||||||
export function applyFieldProperties(field: FieldConfig, props?: FieldConfig): FieldConfig {
|
|
||||||
if (!props) {
|
|
||||||
return field;
|
|
||||||
}
|
|
||||||
const keys = Object.keys(props);
|
|
||||||
if (!keys.length) {
|
|
||||||
return field;
|
|
||||||
}
|
|
||||||
const copy = { ...field } as any; // make a copy that we will manipulate directly
|
|
||||||
for (const key of keys) {
|
|
||||||
const val = (props as any)[key];
|
|
||||||
if (val === null || val === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numericFieldProps[key]) {
|
|
||||||
const num = toNumber(val);
|
|
||||||
if (!isNaN(num)) {
|
|
||||||
copy[key] = num;
|
|
||||||
}
|
|
||||||
} else if (val) {
|
|
||||||
// skips empty string
|
|
||||||
if (key === 'unit' && val === 'none') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
copy[key] = val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return copy as FieldConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFieldProperties(...props: FieldConfig[]): FieldConfig {
|
|
||||||
let field = props[0] as FieldConfig;
|
|
||||||
for (let i = 1; i < props.length; i++) {
|
|
||||||
field = applyFieldProperties(field, props[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// First value is always -Infinity
|
|
||||||
if (field.thresholds && field.thresholds.length) {
|
|
||||||
field.thresholds[0].value = -Infinity;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that max > min
|
|
||||||
if (field.hasOwnProperty('min') && field.hasOwnProperty('max') && field.min! > field.max!) {
|
|
||||||
return {
|
|
||||||
...field,
|
|
||||||
min: field.max,
|
|
||||||
max: field.min,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return field;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): DisplayValueAlignmentFactors {
|
export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): DisplayValueAlignmentFactors {
|
||||||
const info: DisplayValueAlignmentFactors = {
|
const info: DisplayValueAlignmentFactors = {
|
||||||
title: '',
|
title: '',
|
||||||
@ -308,11 +241,10 @@ export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): Display
|
|||||||
function createNoValuesFieldDisplay(options: GetFieldDisplayValuesOptions): FieldDisplay {
|
function createNoValuesFieldDisplay(options: GetFieldDisplayValuesOptions): FieldDisplay {
|
||||||
const displayName = 'No data';
|
const displayName = 'No data';
|
||||||
const { fieldOptions } = options;
|
const { fieldOptions } = options;
|
||||||
const { defaults, override } = fieldOptions;
|
const { defaults } = fieldOptions;
|
||||||
|
|
||||||
const config = getFieldProperties(defaults, {}, override);
|
|
||||||
const displayProcessor = getDisplayProcessor({
|
const displayProcessor = getDisplayProcessor({
|
||||||
config,
|
config: defaults,
|
||||||
theme: options.theme,
|
theme: options.theme,
|
||||||
type: FieldType.other,
|
type: FieldType.other,
|
||||||
});
|
});
|
||||||
|
89
packages/grafana-data/src/field/fieldOverrides.test.ts
Normal file
89
packages/grafana-data/src/field/fieldOverrides.test.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { setFieldConfigDefaults, findNumericFieldMinMax, applyFieldOverrides } from './fieldOverrides';
|
||||||
|
import { MutableDataFrame } from '../dataframe';
|
||||||
|
import { FieldConfig, FieldConfigSource, InterpolateFunction, GrafanaTheme } from '../types';
|
||||||
|
import { FieldMatcherID } from '../transformations';
|
||||||
|
|
||||||
|
describe('FieldOverrides', () => {
|
||||||
|
it('will merge FieldConfig with default values', () => {
|
||||||
|
const field: FieldConfig = {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
};
|
||||||
|
const f1 = {
|
||||||
|
unit: 'ms',
|
||||||
|
dateFormat: '', // should be ignored
|
||||||
|
max: parseFloat('NOPE'), // should be ignored
|
||||||
|
min: null, // should alo be ignored!
|
||||||
|
};
|
||||||
|
setFieldConfigDefaults(field, f1 as FieldConfig);
|
||||||
|
expect(field.min).toEqual(0);
|
||||||
|
expect(field.max).toEqual(100);
|
||||||
|
expect(field.unit).toEqual('ms');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will apply field overrides', () => {
|
||||||
|
const f0 = new MutableDataFrame();
|
||||||
|
f0.add({ title: 'AAA', value: 100, value2: 1234 }, true);
|
||||||
|
f0.add({ title: 'BBB', value: -20 }, true);
|
||||||
|
f0.add({ title: 'CCC', value: 200, value2: 1000 }, true);
|
||||||
|
expect(f0.length).toEqual(3);
|
||||||
|
|
||||||
|
// Hardcode the max value
|
||||||
|
f0.fields[1].config.max = 0;
|
||||||
|
f0.fields[1].config.decimals = 6;
|
||||||
|
|
||||||
|
const src: FieldConfigSource = {
|
||||||
|
defaults: {
|
||||||
|
unit: 'xyz',
|
||||||
|
decimals: 2,
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
matcher: { id: FieldMatcherID.numeric },
|
||||||
|
properties: [
|
||||||
|
{ path: 'decimals', value: 1 }, // Numeric
|
||||||
|
{ path: 'title', value: 'Kittens' }, // Text
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = applyFieldOverrides(
|
||||||
|
[f0], // the frame
|
||||||
|
src, // defaults + overrides
|
||||||
|
(undefined as any) as InterpolateFunction,
|
||||||
|
(undefined as any) as GrafanaTheme
|
||||||
|
)[0];
|
||||||
|
const valueColumn = data.fields[1];
|
||||||
|
const config = valueColumn.config;
|
||||||
|
|
||||||
|
// Keep max from the original setting
|
||||||
|
expect(config.max).toEqual(0);
|
||||||
|
|
||||||
|
// Automatically pick the min value
|
||||||
|
expect(config.min).toEqual(-20);
|
||||||
|
|
||||||
|
// The default value applied
|
||||||
|
expect(config.unit).toEqual('xyz');
|
||||||
|
|
||||||
|
// The default value applied
|
||||||
|
expect(config.title).toEqual('Kittens');
|
||||||
|
|
||||||
|
// The override applied
|
||||||
|
expect(config.decimals).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Global MinMax', () => {
|
||||||
|
it('find global min max', () => {
|
||||||
|
const f0 = new MutableDataFrame();
|
||||||
|
f0.add({ title: 'AAA', value: 100, value2: 1234 }, true);
|
||||||
|
f0.add({ title: 'BBB', value: -20 }, true);
|
||||||
|
f0.add({ title: 'CCC', value: 200, value2: 1000 }, true);
|
||||||
|
expect(f0.length).toEqual(3);
|
||||||
|
|
||||||
|
const minmax = findNumericFieldMinMax([f0]);
|
||||||
|
expect(minmax.min).toEqual(-20);
|
||||||
|
expect(minmax.max).toEqual(1234);
|
||||||
|
});
|
||||||
|
});
|
213
packages/grafana-data/src/field/fieldOverrides.ts
Normal file
213
packages/grafana-data/src/field/fieldOverrides.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import set from 'lodash/set';
|
||||||
|
import {
|
||||||
|
DynamicConfigValue,
|
||||||
|
FieldConfigSource,
|
||||||
|
FieldConfig,
|
||||||
|
InterpolateFunction,
|
||||||
|
GrafanaTheme,
|
||||||
|
DataFrame,
|
||||||
|
Field,
|
||||||
|
FieldType,
|
||||||
|
} from '../types';
|
||||||
|
import { fieldMatchers, ReducerID, reduceField } from '../transformations';
|
||||||
|
import { FieldMatcher } from '../types/transformations';
|
||||||
|
import isNumber from 'lodash/isNumber';
|
||||||
|
import toNumber from 'lodash/toNumber';
|
||||||
|
import { getDisplayProcessor } from './displayProcessor';
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
export function applyFieldOverrides(
|
||||||
|
data: DataFrame[],
|
||||||
|
source: FieldConfigSource,
|
||||||
|
replaceVariables: InterpolateFunction,
|
||||||
|
theme: GrafanaTheme,
|
||||||
|
isUtc?: boolean
|
||||||
|
): DataFrame[] {
|
||||||
|
if (!source) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
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({
|
||||||
|
match: info.get(rule.matcher),
|
||||||
|
properties: rule.properties,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.map((frame, index) => {
|
||||||
|
let name = frame.name;
|
||||||
|
if (!name) {
|
||||||
|
name = `Series[${index}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = frame.fields.map(field => {
|
||||||
|
// Config is mutable within this scope
|
||||||
|
const config: FieldConfig = { ...field.config } || {};
|
||||||
|
if (field.type === FieldType.number) {
|
||||||
|
setFieldConfigDefaults(config, source.defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find any matching rules and then override
|
||||||
|
for (const rule of override) {
|
||||||
|
if (rule.match(field)) {
|
||||||
|
for (const prop of rule.properties) {
|
||||||
|
setDynamicConfigValue(config, {
|
||||||
|
value: prop,
|
||||||
|
config,
|
||||||
|
field,
|
||||||
|
data: frame,
|
||||||
|
replaceVariables,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the Min/Max value automatically
|
||||||
|
if (field.type === FieldType.number) {
|
||||||
|
if (!isNumber(config.min) || !isNumber(config.max)) {
|
||||||
|
if (!range) {
|
||||||
|
range = findNumericFieldMinMax(data);
|
||||||
|
}
|
||||||
|
if (!isNumber(config.min)) {
|
||||||
|
config.min = range.min;
|
||||||
|
}
|
||||||
|
if (!isNumber(config.max)) {
|
||||||
|
config.max = range.max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
|
||||||
|
// Overwrite the configs
|
||||||
|
config,
|
||||||
|
|
||||||
|
// Set the display processor
|
||||||
|
processor: getDisplayProcessor({
|
||||||
|
type: field.type,
|
||||||
|
config: config,
|
||||||
|
theme,
|
||||||
|
isUtc,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...frame,
|
||||||
|
fields,
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DynamicConfigValueOptions {
|
||||||
|
value: DynamicConfigValue;
|
||||||
|
config: FieldConfig;
|
||||||
|
field: Field;
|
||||||
|
data: DataFrame;
|
||||||
|
replaceVariables: InterpolateFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericFieldProps: any = {
|
||||||
|
decimals: true,
|
||||||
|
min: true,
|
||||||
|
max: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
function prepareConfigValue(key: string, input: any, options?: DynamicConfigValueOptions): any {
|
||||||
|
if (options) {
|
||||||
|
// TODO template variables etc
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numericFieldProps[key]) {
|
||||||
|
const num = toNumber(input);
|
||||||
|
if (isNaN(num)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
} else if (input) {
|
||||||
|
// skips empty string
|
||||||
|
if (key === 'unit' && input === 'none') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDynamicConfigValue(config: FieldConfig, options: DynamicConfigValueOptions) {
|
||||||
|
const { value } = options;
|
||||||
|
const v = prepareConfigValue(value.path, value.value, options);
|
||||||
|
set(config, value.path, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For numeric values, only valid numbers will be applied
|
||||||
|
* for units, 'none' will be skipped
|
||||||
|
*/
|
||||||
|
export function setFieldConfigDefaults(config: FieldConfig, props?: FieldConfig) {
|
||||||
|
if (props) {
|
||||||
|
const keys = Object.keys(props);
|
||||||
|
for (const key of keys) {
|
||||||
|
const val = prepareConfigValue(key, (props as any)[key]);
|
||||||
|
if (val === null || val === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
set(config, key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First value is always -Infinity
|
||||||
|
if (config.thresholds && config.thresholds.length) {
|
||||||
|
config.thresholds[0].value = -Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,4 @@
|
|||||||
export * from './fieldDisplay';
|
export * from './fieldDisplay';
|
||||||
export * from './displayProcessor';
|
export * from './displayProcessor';
|
||||||
|
|
||||||
|
export { applyFieldOverrides } from './fieldOverrides';
|
||||||
|
19
packages/grafana-data/src/types/fieldOverrides.ts
Normal file
19
packages/grafana-data/src/types/fieldOverrides.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { MatcherConfig, FieldConfig } from '../types';
|
||||||
|
|
||||||
|
export interface DynamicConfigValue {
|
||||||
|
path: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigOverrideRule {
|
||||||
|
matcher: MatcherConfig;
|
||||||
|
properties: DynamicConfigValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldConfigSource {
|
||||||
|
// Defatuls applied to all numeric fields
|
||||||
|
defaults: FieldConfig;
|
||||||
|
|
||||||
|
// Rules to override individual values
|
||||||
|
overrides: ConfigOverrideRule[];
|
||||||
|
}
|
@ -12,6 +12,7 @@ export * from './displayValue';
|
|||||||
export * from './graph';
|
export * from './graph';
|
||||||
export * from './ScopedVars';
|
export * from './ScopedVars';
|
||||||
export * from './transformations';
|
export * from './transformations';
|
||||||
|
export * from './fieldOverrides';
|
||||||
export * from './vector';
|
export * from './vector';
|
||||||
export * from './app';
|
export * from './app';
|
||||||
export * from './datasource';
|
export * from './datasource';
|
||||||
|
@ -36,4 +36,30 @@ describe('sharedSingleStatMigrationHandler', () => {
|
|||||||
|
|
||||||
expect(sharedSingleStatMigrationHandler(panel as any)).toMatchSnapshot();
|
expect(sharedSingleStatMigrationHandler(panel as any)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Remove unused `overrides` option', () => {
|
||||||
|
const panel = {
|
||||||
|
options: {
|
||||||
|
fieldOptions: {
|
||||||
|
unit: 'watt',
|
||||||
|
stat: 'last',
|
||||||
|
decimals: 5,
|
||||||
|
defaults: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
mappings: [],
|
||||||
|
},
|
||||||
|
override: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
mappings: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: 'Usage',
|
||||||
|
type: 'bargauge',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(sharedSingleStatMigrationHandler(panel as any)).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
VizOrientation,
|
VizOrientation,
|
||||||
PanelModel,
|
PanelModel,
|
||||||
FieldDisplayOptions,
|
FieldDisplayOptions,
|
||||||
|
ConfigOverrideRule,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
export interface SingleStatBaseOptions {
|
export interface SingleStatBaseOptions {
|
||||||
@ -33,7 +34,7 @@ export function sharedSingleStatPanelChangedHandler(
|
|||||||
const options = {
|
const options = {
|
||||||
fieldOptions: {
|
fieldOptions: {
|
||||||
defaults: {} as FieldConfig,
|
defaults: {} as FieldConfig,
|
||||||
override: {} as FieldConfig,
|
overrides: [] as ConfigOverrideRule[],
|
||||||
calcs: [reducer ? reducer.id : ReducerID.mean],
|
calcs: [reducer ? reducer.id : ReducerID.mean],
|
||||||
},
|
},
|
||||||
orientation: VizOrientation.Horizontal,
|
orientation: VizOrientation.Horizontal,
|
||||||
@ -110,6 +111,20 @@ export function sharedSingleStatMigrationHandler(panel: PanelModel<SingleStatBas
|
|||||||
options = moveThresholdsAndMappingsToField(options);
|
options = moveThresholdsAndMappingsToField(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (previousVersion < 6.6) {
|
||||||
|
// discard the old `override` options and enter an empty array
|
||||||
|
if (options.fieldOptions && options.fieldOptions.override) {
|
||||||
|
const { override, ...rest } = options.fieldOptions;
|
||||||
|
options = {
|
||||||
|
...options,
|
||||||
|
fieldOptions: {
|
||||||
|
...rest,
|
||||||
|
overrides: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return options as SingleStatBaseOptions;
|
return options as SingleStatBaseOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,22 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`sharedSingleStatMigrationHandler Remove unused \`overrides\` option 1`] = `
|
||||||
|
Object {
|
||||||
|
"fieldOptions": Object {
|
||||||
|
"decimals": 5,
|
||||||
|
"defaults": Object {
|
||||||
|
"mappings": undefined,
|
||||||
|
"max": 100,
|
||||||
|
"min": 0,
|
||||||
|
"thresholds": undefined,
|
||||||
|
},
|
||||||
|
"overrides": Array [],
|
||||||
|
"stat": "last",
|
||||||
|
"unit": "watt",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`sharedSingleStatMigrationHandler from old valueOptions model without pluginVersion 1`] = `
|
exports[`sharedSingleStatMigrationHandler from old valueOptions model without pluginVersion 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"fieldOptions": Object {
|
"fieldOptions": Object {
|
||||||
|
@ -28,7 +28,7 @@ Object {
|
|||||||
],
|
],
|
||||||
"unit": "watt",
|
"unit": "watt",
|
||||||
},
|
},
|
||||||
"override": Object {},
|
"overrides": Array [],
|
||||||
"values": false,
|
"values": false,
|
||||||
},
|
},
|
||||||
"orientation": "vertical",
|
"orientation": "vertical",
|
||||||
|
@ -35,7 +35,7 @@ export const standardFieldDisplayOptions: FieldDisplayOptions = {
|
|||||||
],
|
],
|
||||||
mappings: [],
|
mappings: [],
|
||||||
},
|
},
|
||||||
override: {},
|
overrides: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaults: StatPanelOptions = {
|
export const defaults: StatPanelOptions = {
|
||||||
|
Loading…
Reference in New Issue
Block a user