3
0
mirror of https://github.com/grafana/grafana.git synced 2025-02-25 18:55:37 -06:00

FieldOverides: apply field overrides based on configuration ()

* test apply

* test apply

* Move standard field config editor registry to grafana-data

* merge master

* Apply field config defaults

* Make field and dataFrameIndex optional on on FieldOverrideContext

* Apply custom field config overrides

* Gauge - make sure thresholds are set

* Move series and field scoped vars calculation

* Enable template variables interpolation in title fields

* Expose standars field configs from grafana ui via function

* Add missing option to the config for the min value to be derived from field values

* Fix ts issue

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Ryan McKinley 2020-02-13 21:37:24 +01:00 committed by GitHub
parent 824f7362d4
commit 2c9b321c48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 553 additions and 339 deletions

View File

@ -1,29 +1,13 @@
module.exports = {
verbose: false,
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
"moduleDirectories": ["node_modules", "public"],
"roots": [
"<rootDir>/public/app",
"<rootDir>/public/test",
"<rootDir>/packages",
"<rootDir>/scripts",
],
"testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
],
"setupFiles": [
"./public/test/jest-shim.ts",
"./public/test/jest-setup.ts",
"jest-canvas-mock"
],
"snapshotSerializers": ["enzyme-to-json/serializer"],
"globals": { "ts-jest": { "isolatedModules": true } },
moduleDirectories: ['node_modules', 'public'],
roots: ['<rootDir>/public/app', '<rootDir>/public/test', '<rootDir>/packages', '<rootDir>/scripts'],
testRegex: '(\\.|/)(test)\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
setupFiles: ['jest-canvas-mock', './public/test/jest-shim.ts', './public/test/jest-setup.ts'],
snapshotSerializers: ['enzyme-to-json/serializer'],
globals: { 'ts-jest': { isolatedModules: true } },
};

View File

@ -5,7 +5,7 @@ import { ReducerID } from '../transformations/fieldReducer';
import { ThresholdsMode } from '../types/thresholds';
import { GrafanaTheme } from '../types/theme';
import { MappingType, FieldConfig } from '../types';
import { setFieldConfigDefaults } from './fieldOverrides';
import { validateFieldConfig } from './fieldOverrides';
describe('FieldDisplay', () => {
it('show first numeric values', () => {
@ -78,7 +78,7 @@ describe('FieldDisplay', () => {
],
},
};
setFieldConfigDefaults(config);
validateFieldConfig(config);
expect(config.thresholds!.steps.length).toEqual(2);
expect(config.thresholds!.steps[0].value).toBe(-Infinity);
});

View File

@ -100,7 +100,6 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
for (let s = 0; s < data.length && !hitLimit; s++) {
const series = data[s]; // Name is already set
scopedVars['__series'] = { text: 'Series', value: { name: series.name } };
const { timeField } = getTimeField(series);
const view = new DataFrameView(series);
@ -114,13 +113,6 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
}
const config = field.config; // already set by the prepare task
let name = field.name;
if (!name) {
name = `Field[${s}]`;
}
scopedVars['__field'] = { text: 'Field', value: { name } };
const display =
field.display ??
getDisplayProcessor({
@ -145,9 +137,12 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
};
}
}
const displayValue = display(field.values.get(j));
displayValue.title = replaceVariables(title, scopedVars);
displayValue.title = replaceVariables(title, {
...field.config.scopedVars, // series and field scoped vars
...scopedVars,
});
values.push({
name,
field: config,
@ -180,7 +175,10 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
for (const calc of calcs) {
scopedVars[VAR_CALC] = { value: calc, text: calc };
const displayValue = display(results[calc]);
displayValue.title = replaceVariables(title, scopedVars);
displayValue.title = replaceVariables(title, {
...field.config.scopedVars, // series and field scoped vars
...scopedVars,
});
values.push({
name: calc,
field: config,

View File

@ -1,96 +1,41 @@
import { setFieldConfigDefaults, findNumericFieldMinMax, applyFieldOverrides } from './fieldOverrides';
import { FieldOverrideEnv, findNumericFieldMinMax, setFieldConfigDefaults } from './fieldOverrides';
import { MutableDataFrame } from '../dataframe';
import { FieldConfig, FieldConfigSource, InterpolateFunction, GrafanaTheme } from '../types';
import { FieldMatcherID } from '../transformations';
import { FieldDisplayOptions } from './fieldDisplay';
import {
FieldConfig,
FieldConfigEditorRegistry,
FieldOverrideContext,
FieldPropertyEditorItem,
FieldType,
} from '../types';
import { Registry } from '../utils';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
describe('FieldOverrides', () => {
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 property1 = {
id: 'property1', // Match field properties
process: (value: any) => value,
shouldApply: () => true,
} as any;
// Hardcode the max value
f0.fields[1].config.max = 0;
f0.fields[1].config.decimals = 6;
const property2 = {
id: 'property2', // Match field properties
process: (value: any) => value,
shouldApply: () => true,
} as any;
const src: FieldConfigSource = {
defaults: {
unit: 'xyz',
decimals: 2,
},
overrides: [
{
matcher: { id: FieldMatcherID.numeric },
properties: [
{ prop: 'decimals', value: 1 }, // Numeric
{ prop: 'title', value: 'Kittens' }, // Text
],
},
],
};
const unit = {
id: 'unit', // Match field properties
process: (value: any) => value,
shouldApply: () => true,
} as any;
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');
});
export const customFieldRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>(() => {
return [property1, property2];
});
it('will apply field overrides', () => {
const data = applyFieldOverrides({
data: [f0], // the frame
fieldOptions: src as FieldDisplayOptions, // defaults + overrides
replaceVariables: (undefined as any) as InterpolateFunction,
theme: (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);
// Don't Automatically pick the min value
expect(config.min).toEqual(undefined);
// 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);
});
it('will apply set min/max when asked', () => {
const data = applyFieldOverrides({
data: [f0], // the frame
fieldOptions: src as FieldDisplayOptions, // defaults + overrides
replaceVariables: (undefined as any) as InterpolateFunction,
theme: (undefined as any) as GrafanaTheme,
autoMinMax: true,
})[0];
const valueColumn = data.fields[1];
const config = valueColumn.config;
// Keep max from the original setting
expect(config.max).toEqual(0);
// Don't Automatically pick the min value
expect(config.min).toEqual(-20);
});
// For the need of this test we need to mock the standard registry
// as we cannot imporrt from grafana/ui
standardFieldConfigEditorRegistry.setInit(() => {
return [unit];
});
describe('Global MinMax', () => {
@ -106,3 +51,73 @@ describe('Global MinMax', () => {
expect(minmax.max).toEqual(1234);
});
});
describe('setFieldConfigDefaults', () => {
it('applies field config defaults', () => {
const dsFieldConfig: FieldConfig = {
decimals: 2,
min: 0,
max: 100,
};
const panelFieldConfig: FieldConfig = {
decimals: 1,
min: 10,
max: 50,
unit: 'km',
};
const context: FieldOverrideContext = {
data: [] as any,
field: { type: FieldType.number } as any,
dataFrameIndex: 0,
};
console.log(standardFieldConfigEditorRegistry);
// we mutate dsFieldConfig
setFieldConfigDefaults(dsFieldConfig, panelFieldConfig, context);
expect(dsFieldConfig).toMatchInlineSnapshot(`
Object {
"decimals": 2,
"max": 100,
"min": 0,
"unit": "km",
}
`);
});
it('applies field config defaults for custom properties', () => {
const dsFieldConfig: FieldConfig = {
custom: {
property1: 10,
},
};
const panelFieldConfig: FieldConfig = {
custom: {
property1: 20,
property2: 10,
},
};
const context: FieldOverrideEnv = {
data: [] as any,
field: { type: FieldType.number } as any,
dataFrameIndex: 0,
custom: customFieldRegistry,
};
// we mutate dsFieldConfig
setFieldConfigDefaults(dsFieldConfig, panelFieldConfig, context);
expect(dsFieldConfig).toMatchInlineSnapshot(`
Object {
"custom": Object {
"property1": 10,
"property2": 10,
},
}
`);
});
});

View File

@ -1,4 +1,3 @@
import set from 'lodash/set';
import {
GrafanaTheme,
DynamicConfigValue,
@ -12,13 +11,16 @@ import {
FieldColorMode,
ColorScheme,
TimeZone,
FieldConfigEditorRegistry,
FieldOverrideContext,
ScopedVars,
} 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';
import { guessFieldTypeForField } from '../dataframe';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
interface OverrideProps {
match: FieldMatcher;
@ -37,6 +39,8 @@ export interface ApplyFieldOverrideOptions {
theme: GrafanaTheme;
timeZone?: TimeZone;
autoMinMax?: boolean;
standard?: FieldConfigEditorRegistry;
custom?: FieldConfigEditorRegistry;
}
export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax {
@ -65,6 +69,7 @@ export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax {
* Return a copy of the DataFrame with all rules applied
*/
export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFrame[] {
const scopedVars: ScopedVars = {};
if (!options.data) {
return [];
}
@ -95,29 +100,42 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
if (!name) {
name = `Series[${index}]`;
}
scopedVars['__series'] = { text: 'Series', value: { name } };
const fields: Field[] = frame.fields.map(field => {
const fields: Field[] = frame.fields.map((field, fieldIndex) => {
// Config is mutable within this scope
const config: FieldConfig = { ...field.config } || {};
if (field.type === FieldType.number) {
setFieldConfigDefaults(config, source.defaults);
let fieldName = field.name;
if (!fieldName) {
fieldName = `Field[${fieldIndex}]`;
}
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);
// 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: options.replaceVariables,
});
// config.scopedVars is set already here
setDynamicConfigValue(config, prop, context);
}
}
}
// console.log(config)
// Try harder to set a real value that is not 'other'
let type = field.type;
if (!type || type === FieldType.other) {
@ -182,65 +200,87 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
});
}
interface DynamicConfigValueOptions {
value: DynamicConfigValue;
config: FieldConfig;
field: Field;
data: DataFrame;
replaceVariables: InterpolateFunction;
export interface FieldOverrideEnv extends FieldOverrideContext {
custom?: FieldConfigEditorRegistry;
}
const numericFieldProps: any = {
decimals: true,
min: true,
max: true,
};
function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigValue, context: FieldOverrideEnv) {
const reg = value.custom ? context.custom : standardFieldConfigEditorRegistry;
function prepareConfigValue(key: string, input: any, options?: DynamicConfigValueOptions): any {
if (options) {
// TODO template variables etc
const item = reg?.getIfExists(value.prop);
if (!item || !item.shouldApply(context.field!)) {
return;
}
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;
}
const val = item.process(value.value, context, item.settings);
export function setDynamicConfigValue(config: FieldConfig, options: DynamicConfigValueOptions) {
const { value } = options;
const v = prepareConfigValue(value.prop, value.value, options);
set(config, value.prop, v);
}
const remove = val === undefined || val === null;
/**
* 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;
if (remove) {
if (value.custom) {
delete (config?.custom as any)[value.prop];
} else {
delete (config as any)[value.prop];
}
} else {
if (value.custom) {
if (!config.custom) {
config.custom = {};
}
set(config, key, val);
config.custom[value.prop] = val;
} else {
(config as any)[value.prop] = val;
}
}
}
// config -> from DS
// defaults -> from Panel config
export function setFieldConfigDefaults(config: FieldConfig, defaults: FieldConfig, context: FieldOverrideEnv) {
if (defaults) {
const keys = Object.keys(defaults);
for (const key of keys) {
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);
}
}
}
validateFieldConfig(config);
}
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);
if (item && item.shouldApply(context.field!)) {
const val = item.process(source[key], context, item.settings);
if (val !== undefined && val !== null) {
destination[key] = val;
}
}
}
};
/**
* 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)

View File

@ -1,5 +1,6 @@
export * from './fieldDisplay';
export * from './displayProcessor';
export * from './scale';
export * from './standardFieldConfigEditorRegistry';
export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides';

View File

@ -0,0 +1,4 @@
import { FieldConfigEditorRegistry, FieldPropertyEditorItem } from '../types/fieldOverrides';
import { Registry } from '../utils/Registry';
export const standardFieldConfigEditorRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>();

View File

@ -6,6 +6,7 @@ import { DataLink } from './dataLink';
import { Vector } from './vector';
import { FieldCalcs } from '../transformations/fieldReducer';
import { FieldColor } from './fieldColor';
import { ScopedVars } from './ScopedVars';
export enum FieldType {
time = 'time', // or date
@ -50,6 +51,8 @@ export interface FieldConfig {
// Panel Specific Values
custom?: Record<string, any>;
scopedVars?: ScopedVars;
}
export interface Field<T = any, V = Vector<T>> {

View File

@ -1,5 +1,5 @@
import { ComponentType } from 'react';
import { MatcherConfig, FieldConfig, Field, DataFrame, VariableSuggestion, VariableSuggestionsScope } from '../types';
import { MatcherConfig, FieldConfig, Field, DataFrame, VariableSuggestionsScope, VariableSuggestion } from '../types';
import { Registry, RegistryItem } from '../utils';
import { InterpolateFunction } from './panel';
@ -30,8 +30,9 @@ export interface FieldConfigEditorProps<TValue, TSettings> {
}
export interface FieldOverrideContext {
data: DataFrame[];
field?: Field;
dataFrameIndex?: number; // The index for the selected field frame
data: DataFrame[]; // All results
replaceVariables?: InterpolateFunction;
getSuggestions?: (scope?: VariableSuggestionsScope) => VariableSuggestion[];
}
@ -55,6 +56,9 @@ export interface FieldPropertyEditorItem<TValue = any, TSettings = any> extends
// Configuration options for the particular property
settings: TSettings;
// Checks if field should be processed
shouldApply: (field: Field) => boolean;
}
export type FieldConfigEditorRegistry = Registry<FieldPropertyEditorItem>;

View File

@ -33,19 +33,26 @@ interface RegistrySelectInfo {
export class Registry<T extends RegistryItem> {
private ordered: T[] = [];
private byId = new Map<string, T>();
private initalized = false;
private initialized = false;
constructor(private init?: () => T[]) {}
setInit = (init: () => T[]) => {
if (this.initialized) {
throw new Error('Registry already initialized');
}
this.init = init;
};
getIfExists(id: string | undefined): T | undefined {
if (!this.initalized) {
if (!this.initialized) {
if (this.init) {
for (const ext of this.init()) {
this.register(ext);
}
}
this.sort();
this.initalized = true;
this.initialized = true;
}
if (id) {
return this.byId.get(id);
@ -62,7 +69,7 @@ export class Registry<T extends RegistryItem> {
}
selectOptions(current?: string[], filter?: (ext: T) => boolean): RegistrySelectInfo {
if (!this.initalized) {
if (!this.initialized) {
this.getIfExists('xxx'); // will trigger init
}
@ -114,7 +121,7 @@ export class Registry<T extends RegistryItem> {
}
return found;
}
if (!this.initalized) {
if (!this.initialized) {
this.getIfExists('xxx'); // will trigger init
}
return [...this.ordered]; // copy of everythign just in case
@ -135,7 +142,7 @@ export class Registry<T extends RegistryItem> {
}
}
if (this.initalized) {
if (this.initialized) {
this.sort();
}
}

View File

@ -0,0 +1,125 @@
import {
applyFieldOverrides,
FieldConfig,
FieldConfigSource,
InterpolateFunction,
GrafanaTheme,
FieldMatcherID,
FieldDisplayOptions,
MutableDataFrame,
DataFrame,
toDataFrame,
standardFieldConfigEditorRegistry,
FieldType,
} from '@grafana/data';
import { getTheme } from '../../themes';
import { getStandardFieldConfigs } from './standardFieldConfigEditors';
describe('FieldOverrides', () => {
beforeAll(() => {
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
});
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: [
{ prop: 'decimals', value: 1 }, // Numeric
{ prop: 'title', value: 'Kittens' }, // Text
],
},
],
};
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!
};
const f: DataFrame = toDataFrame({
fields: [{ type: FieldType.number, name: 'x', config: field, values: [] }],
});
const processed = applyFieldOverrides({
data: [f],
standard: standardFieldConfigEditorRegistry,
fieldOptions: {
defaults: f1 as FieldConfig,
overrides: [],
},
replaceVariables: v => v,
theme: getTheme(),
})[0];
const out = processed.fields[0].config;
expect(out.min).toEqual(0);
expect(out.max).toEqual(100);
expect(out.unit).toEqual('ms');
});
it('will apply field overrides', () => {
const data = applyFieldOverrides({
data: [f0], // the frame
fieldOptions: src as FieldDisplayOptions, // defaults + overrides
replaceVariables: (undefined as any) as InterpolateFunction,
theme: (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);
// Don't Automatically pick the min value
expect(config.min).toEqual(undefined);
// 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);
});
it('will apply set min/max when asked', () => {
const data = applyFieldOverrides({
data: [f0], // the frame
fieldOptions: src as FieldDisplayOptions, // defaults + overrides
replaceVariables: (undefined as any) as InterpolateFunction,
theme: (undefined as any) as GrafanaTheme,
autoMinMax: true,
})[0];
const valueColumn = data.fields[1];
const config = valueColumn.config;
// Keep max from the original setting
expect(config.max).toEqual(0);
// Don't Automatically pick the min value
expect(config.min).toEqual(-20);
});
});

View File

@ -1,136 +0,0 @@
import {
FieldConfigEditorRegistry,
Registry,
FieldPropertyEditorItem,
ThresholdsConfig,
DataLink,
} from '@grafana/data';
import { StringValueEditor, StringOverrideEditor, stringOverrideProcessor, StringFieldConfigSettings } from './string';
import { NumberValueEditor, NumberOverrideEditor, numberOverrideProcessor, NumberFieldConfigSettings } from './number';
import { UnitValueEditor, UnitOverrideEditor } from './units';
import {
ThresholdsValueEditor,
ThresholdsOverrideEditor,
thresholdsOverrideProcessor,
ThresholdsFieldConfigSettings,
} from './thresholds';
import { DataLinksValueEditor, DataLinksOverrideEditor, dataLinksOverrideProcessor } from './links';
const title: FieldPropertyEditorItem<string, StringFieldConfigSettings> = {
id: 'title', // Match field properties
name: 'Title',
description: 'The field title',
editor: StringValueEditor,
override: StringOverrideEditor,
process: stringOverrideProcessor,
settings: {
placeholder: 'auto',
},
};
const unit: FieldPropertyEditorItem<string, StringFieldConfigSettings> = {
id: 'unit', // Match field properties
name: 'Unit',
description: 'value units',
editor: UnitValueEditor,
override: UnitOverrideEditor,
process: stringOverrideProcessor,
settings: {
placeholder: 'none',
},
};
const min: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = {
id: 'min', // Match field properties
name: 'Min',
description: 'Minimum expected value',
editor: NumberValueEditor,
override: NumberOverrideEditor,
process: numberOverrideProcessor,
settings: {
placeholder: 'auto',
},
};
const max: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = {
id: 'max', // Match field properties
name: 'Max',
description: 'Maximum expected value',
editor: NumberValueEditor,
override: NumberOverrideEditor,
process: numberOverrideProcessor,
settings: {
placeholder: 'auto',
},
};
const decimals: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = {
id: 'decimals', // Match field properties
name: 'Decimals',
description: 'How many decimal places should be shown on a number',
editor: NumberValueEditor,
override: NumberOverrideEditor,
process: numberOverrideProcessor,
settings: {
placeholder: 'auto',
min: 0,
max: 15,
integer: true,
},
};
const thresholds: FieldPropertyEditorItem<ThresholdsConfig, ThresholdsFieldConfigSettings> = {
id: 'thresholds', // Match field properties
name: 'Thresholds',
description: 'Manage Thresholds',
editor: ThresholdsValueEditor,
override: ThresholdsOverrideEditor,
process: thresholdsOverrideProcessor,
settings: {
// ??
},
};
const noValue: FieldPropertyEditorItem<string, StringFieldConfigSettings> = {
id: 'noValue', // Match field properties
name: 'No Value',
description: 'What to show when there is no value',
editor: StringValueEditor,
override: StringOverrideEditor,
process: stringOverrideProcessor,
settings: {
placeholder: '-',
},
};
const links: FieldPropertyEditorItem<DataLink[], StringFieldConfigSettings> = {
id: 'links', // Match field properties
name: 'DataLinks',
description: 'Manage date links',
editor: DataLinksValueEditor,
override: DataLinksOverrideEditor,
process: dataLinksOverrideProcessor,
settings: {
placeholder: '-',
},
};
export const standardFieldConfigEditorRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>(
() => {
return [title, unit, min, max, decimals, thresholds, noValue, links];
}
);

View File

@ -0,0 +1,152 @@
import { DataLink, FieldPropertyEditorItem, FieldType, ThresholdsConfig } from '@grafana/data';
import { StringFieldConfigSettings, StringOverrideEditor, stringOverrideProcessor, StringValueEditor } from './string';
import { NumberFieldConfigSettings, NumberOverrideEditor, numberOverrideProcessor, NumberValueEditor } from './number';
import { UnitOverrideEditor, UnitValueEditor } from './units';
import {
ThresholdsFieldConfigSettings,
ThresholdsOverrideEditor,
thresholdsOverrideProcessor,
ThresholdsValueEditor,
} from './thresholds';
import { DataLinksOverrideEditor, dataLinksOverrideProcessor, DataLinksValueEditor } from './links';
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace StandardFieldConfigEditors {
export const title: FieldPropertyEditorItem<string, StringFieldConfigSettings> = {
id: 'title', // Match field properties
name: 'Title',
description: 'The field title',
editor: StringValueEditor,
override: StringOverrideEditor,
process: stringOverrideProcessor,
settings: {
placeholder: 'auto',
expandTemplateVars: true,
},
shouldApply: field => field.type !== FieldType.time,
};
export const unit: FieldPropertyEditorItem<string, StringFieldConfigSettings> = {
id: 'unit', // Match field properties
name: 'Unit',
description: 'value units',
editor: UnitValueEditor,
override: UnitOverrideEditor,
process: stringOverrideProcessor,
settings: {
placeholder: 'none',
},
shouldApply: field => field.type === FieldType.number,
};
export const min: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = {
id: 'min', // Match field properties
name: 'Min',
description: 'Minimum expected value',
editor: NumberValueEditor,
override: NumberOverrideEditor,
process: numberOverrideProcessor,
settings: {
placeholder: 'auto',
},
shouldApply: field => field.type === FieldType.number,
};
export const max: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = {
id: 'max', // Match field properties
name: 'Max',
description: 'Maximum expected value',
editor: NumberValueEditor,
override: NumberOverrideEditor,
process: numberOverrideProcessor,
settings: {
placeholder: 'auto',
},
shouldApply: field => field.type === FieldType.number,
};
export const decimals: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = {
id: 'decimals', // Match field properties
name: 'Decimals',
description: 'How many decimal places should be shown on a number',
editor: NumberValueEditor,
override: NumberOverrideEditor,
process: numberOverrideProcessor,
settings: {
placeholder: 'auto',
min: 0,
max: 15,
integer: true,
},
shouldApply: field => field.type === FieldType.number,
};
export const thresholds: FieldPropertyEditorItem<ThresholdsConfig, ThresholdsFieldConfigSettings> = {
id: 'thresholds', // Match field properties
name: 'Thresholds',
description: 'Manage Thresholds',
editor: ThresholdsValueEditor,
override: ThresholdsOverrideEditor,
process: thresholdsOverrideProcessor,
settings: {
// ??
},
shouldApply: field => field.type === FieldType.number,
};
export const noValue: FieldPropertyEditorItem<string, StringFieldConfigSettings> = {
id: 'noValue', // Match field properties
name: 'No Value',
description: 'What to show when there is no value',
editor: StringValueEditor,
override: StringOverrideEditor,
process: stringOverrideProcessor,
settings: {
placeholder: '-',
},
// ??? any field with no value
shouldApply: () => true,
};
export const links: FieldPropertyEditorItem<DataLink[], StringFieldConfigSettings> = {
id: 'links', // Match field properties
name: 'DataLinks',
description: 'Manage date links',
editor: DataLinksValueEditor,
override: DataLinksOverrideEditor,
process: dataLinksOverrideProcessor,
settings: {
placeholder: '-',
},
shouldApply: () => true,
};
}
export const getStandardFieldConfigs = () => {
return [
StandardFieldConfigEditors.decimals,
StandardFieldConfigEditors.max,
StandardFieldConfigEditors.min,
StandardFieldConfigEditors.noValue,
StandardFieldConfigEditors.thresholds,
StandardFieldConfigEditors.title,
StandardFieldConfigEditors.unit,
];
};

View File

@ -6,6 +6,7 @@ import Forms from '../Forms';
export interface StringFieldConfigSettings {
placeholder?: string;
maxLength?: number;
expandTemplateVars?: boolean;
}
export const stringOverrideProcessor = (
@ -13,6 +14,9 @@ export const stringOverrideProcessor = (
context: FieldOverrideContext,
settings: StringFieldConfigSettings
) => {
if (settings.expandTemplateVars && context.replaceVariables) {
return context.replaceVariables(value, context.field!.config.scopedVars);
}
return `${value}`;
};

View File

@ -54,8 +54,9 @@ export class Gauge extends PureComponent<Props> {
getFormattedThresholds(): Threshold[] {
const { field, theme } = this.props;
const isPercent = field.thresholds?.mode === ThresholdsMode.Percentage;
const steps = field.thresholds!.steps;
const thresholds = field.thresholds ?? Gauge.defaultProps.field?.thresholds!;
const isPercent = thresholds.mode === ThresholdsMode.Percentage;
const steps = thresholds.steps;
let min = field.min!;
let max = field.max!;
if (isPercent) {

View File

@ -134,4 +134,4 @@ export {
export { default as Forms } from './Forms';
export { ValuePicker } from './ValuePicker/ValuePicker';
export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI';
export { standardFieldConfigEditorRegistry } from './FieldConfigs/standardFieldConfigEditorRegistry';
export { getStandardFieldConfigs } from './FieldConfigs/standardFieldConfigEditors';

View File

@ -25,7 +25,7 @@ import angular from 'angular';
import config from 'app/core/config';
// @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move
import _ from 'lodash';
import { AppEvents, setLocale, setMarkdownOptions } from '@grafana/data';
import { AppEvents, setLocale, setMarkdownOptions, standardFieldConfigEditorRegistry } from '@grafana/data';
import appEvents from 'app/core/app_events';
import { addClassIfNoOverlayScrollbar } from 'app/core/utils/scrollbar';
import { checkBrowserCompatibility } from 'app/core/utils/browser';
@ -40,6 +40,7 @@ import { PerformanceBackend } from './core/services/echo/backends/PerformanceBac
import 'app/routes/GrafanaCtrl';
import 'app/features/all';
import { getStandardFieldConfigs } from '@grafana/ui';
// add move to lodash for backward compatabiltiy
// @ts-ignore
@ -82,6 +83,7 @@ export class GrafanaApp {
setLocale(config.bootData.user.locale);
setMarkdownOptions({ sanitize: !config.disableSanitizeHtml });
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
app.config(
(

View File

@ -7,8 +7,9 @@ import {
FieldPropertyEditorItem,
DynamicConfigValue,
VariableSuggestionsScope,
standardFieldConfigEditorRegistry,
} from '@grafana/data';
import { standardFieldConfigEditorRegistry, Forms, fieldMatchersUI, ValuePicker } from '@grafana/ui';
import { Forms, fieldMatchersUI, ValuePicker } from '@grafana/ui';
import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
import { OptionsGroup } from './OptionsGroup';
@ -151,6 +152,7 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
<div>
{config.overrides.map((o, i) => {
const matcherUi = fieldMatchersUI.get(o.matcher.id);
// TODO: apply matcher to retrieve fields
return (
<div key={`${o.matcher.id}/${i}`} style={{ border: `2px solid red`, marginBottom: '10px' }}>
<Forms.Field label={matcherUi.name} description={matcherUi.description}>

View File

@ -1,5 +1,4 @@
import { FieldConfig } from '@grafana/data';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
import { FieldConfig, standardFieldConfigEditorRegistry } from '@grafana/data';
describe('standardFieldConfigEditorRegistry', () => {
const dummyConfig: FieldConfig = {

View File

@ -20,6 +20,7 @@ const columWidth: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = {
min: 20,
max: 300,
},
shouldApply: () => true,
};
export const tableFieldRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>(() => {

View File

@ -44,8 +44,6 @@ const localStorageMock = (() => {
global.localStorage = localStorageMock;
HTMLCanvasElement.prototype.getContext = jest.fn() as any;
const throwUnhandledRejections = () => {
process.on('unhandledRejection', err => {
throw err;

View File

@ -63,7 +63,12 @@ module.exports = (env = {}) =>
modules: false,
},
],
'@babel/preset-typescript',
[
'@babel/preset-typescript',
{
allowNamespaces: true,
},
],
'@babel/preset-react',
],
},

View File

@ -56,7 +56,12 @@ module.exports = merge(common, {
modules: false,
},
],
'@babel/preset-typescript',
[
'@babel/preset-typescript',
{
allowNamespaces: true,
},
],
'@babel/preset-react',
],
},
@ -66,7 +71,7 @@ module.exports = merge(common, {
options: {
emitError: true,
emitWarning: true,
}
},
},
],
},