FieldConfig: Unify the custom and standard registry (#23307)

* FieldConfig: Unifying standard and custom registry

* Adding path to option items to make id be prefixed for custom options

* Code updates progress

* Add docs back

* Fix TS

* ld overrides tests from ui to data

* Refactor - rename

* Gauge and table cleanup

* F-I-X e2e

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Torkel Ödegaard
2020-04-06 16:24:41 +02:00
committed by GitHub
parent 6347a1f1eb
commit b10392733d
33 changed files with 706 additions and 553 deletions

View File

@@ -0,0 +1,4 @@
import { Registry } from '../utils';
import { FieldPropertyEditorItem } from '../types';
export class FieldConfigOptionsRegistry extends Registry<FieldPropertyEditorItem> {}

View File

@@ -4,44 +4,37 @@ import {
setFieldConfigDefaults,
applyFieldOverrides,
} from './fieldOverrides';
import { MutableDataFrame } from '../dataframe';
import { MutableDataFrame, toDataFrame } from '../dataframe';
import {
FieldConfig,
FieldConfigEditorRegistry,
FieldOverrideContext,
FieldPropertyEditorItem,
GrafanaTheme,
FieldType,
DataFrame,
FieldConfigSource,
InterpolateFunction,
} from '../types';
import { Registry } from '../utils';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
import { mockStandardProperties } from '../utils/tests/mockStandardProperties';
import { FieldMatcherID } from '../transformations';
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
const property1 = {
id: 'property1', // Match field properties
id: 'custom.property1', // Match field properties
path: 'property1', // Match field properties
process: (value: any) => value,
shouldApply: () => true,
} as any;
const property2 = {
id: 'property2', // Match field properties
id: 'custom.property2', // Match field properties
path: 'property2', // Match field properties
process: (value: any) => value,
shouldApply: () => true,
} as any;
const unit = {
id: 'unit', // Match field properties
process: (value: any) => value,
shouldApply: () => true,
} as any;
export const customFieldRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>(() => {
return [property1, property2];
});
// For the need of this test we need to mock the standard registry
// as we cannot imporrt from grafana/ui
standardFieldConfigEditorRegistry.setInit(() => {
return [unit];
export const customFieldRegistry: FieldConfigOptionsRegistry = new Registry<FieldPropertyEditorItem>(() => {
return [property1, property2, ...mockStandardProperties()];
});
describe('Global MinMax', () => {
@@ -59,6 +52,32 @@ describe('Global MinMax', () => {
});
describe('applyFieldOverrides', () => {
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: [
{ id: 'decimals', value: 1 }, // Numeric
{ id: 'title', value: 'Kittens' }, // Text
],
},
],
};
describe('given multiple data frames', () => {
const f0 = new MutableDataFrame({
name: 'A',
@@ -72,12 +91,13 @@ describe('applyFieldOverrides', () => {
it('should add scopedVars to fields', () => {
const withOverrides = applyFieldOverrides({
data: [f0, f1],
fieldOptions: {
fieldConfig: {
defaults: {},
overrides: [],
},
replaceVariables: (value: any) => value,
theme: {} as GrafanaTheme,
fieldConfigRegistry: new FieldConfigOptionsRegistry(),
});
expect(withOverrides[0].fields[0].config.scopedVars).toMatchInlineSnapshot(`
@@ -115,6 +135,83 @@ describe('applyFieldOverrides', () => {
`);
});
});
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],
fieldConfig: {
defaults: f1 as FieldConfig,
overrides: [],
},
fieldConfigRegistry: customFieldRegistry,
replaceVariables: v => v,
theme: {} as GrafanaTheme,
})[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
fieldConfig: src as FieldConfigSource, // defaults + overrides
replaceVariables: (undefined as any) as InterpolateFunction,
theme: (undefined as any) as GrafanaTheme,
fieldConfigRegistry: customFieldRegistry,
})[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
fieldConfig: src as FieldConfigSource, // 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);
});
});
describe('setFieldConfigDefaults', () => {
@@ -132,10 +229,11 @@ describe('setFieldConfigDefaults', () => {
unit: 'km',
};
const context: FieldOverrideContext = {
const context: FieldOverrideEnv = {
data: [] as any,
field: { type: FieldType.number } as any,
dataFrameIndex: 0,
fieldConfigRegistry: customFieldRegistry,
};
// we mutate dsFieldConfig
@@ -169,7 +267,7 @@ describe('setFieldConfigDefaults', () => {
data: [] as any,
field: { type: FieldType.number } as any,
dataFrameIndex: 0,
custom: customFieldRegistry,
fieldConfigRegistry: customFieldRegistry,
};
// we mutate dsFieldConfig
@@ -178,7 +276,7 @@ describe('setFieldConfigDefaults', () => {
expect(dsFieldConfig).toMatchInlineSnapshot(`
Object {
"custom": Object {
"property1": 10,
"property1": 20,
"property2": 10,
},
}

View File

@@ -7,7 +7,6 @@ import {
ThresholdsMode,
FieldColorMode,
ColorScheme,
FieldConfigEditorRegistry,
FieldOverrideContext,
ScopedVars,
ApplyFieldOverrideOptions,
@@ -18,6 +17,7 @@ import isNumber from 'lodash/isNumber';
import { getDisplayProcessor } from './displayProcessor';
import { guessFieldTypeForField } from '../dataframe';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
interface OverrideProps {
match: FieldMatcher;
@@ -59,11 +59,13 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
return [];
}
const source = options.fieldOptions;
const source = options.fieldConfig;
if (!source) {
return options.data;
}
const fieldConfigRegistry = options.fieldConfigRegistry ?? standardFieldConfigEditorRegistry;
let range: GlobalMinMax | undefined = undefined;
// Prepare the Matchers
@@ -105,7 +107,7 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
data: options.data!,
dataFrameIndex: index,
replaceVariables: options.replaceVariables,
custom: options.custom,
fieldConfigRegistry: fieldConfigRegistry,
};
// Anything in the field config that's not set by the datasource
@@ -188,13 +190,13 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
}
export interface FieldOverrideEnv extends FieldOverrideContext {
custom?: FieldConfigEditorRegistry;
fieldConfigRegistry: FieldConfigOptionsRegistry;
}
function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigValue, context: FieldOverrideEnv) {
const reg = value.custom ? context.custom : standardFieldConfigEditorRegistry;
const reg = context.fieldConfigRegistry;
const item = reg?.getIfExists(value.prop);
const item = reg.getIfExists(value.id);
if (!item || !item.shouldApply(context.field!)) {
return;
}
@@ -204,19 +206,19 @@ function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigValue, c
const remove = val === undefined || val === null;
if (remove) {
if (value.custom && config.custom) {
delete config.custom[value.prop];
if (value.isCustom && config.custom) {
delete config.custom[item.path];
} else {
delete (config as any)[value.prop];
delete (config as any)[item.path];
}
} else {
if (value.custom) {
if (value.isCustom) {
if (!config.custom) {
config.custom = {};
}
config.custom[value.prop] = val;
config.custom[item.path] = val;
} else {
(config as any)[value.prop] = val;
(config as any)[item.path] = val;
}
}
}
@@ -228,23 +230,24 @@ export function setFieldConfigDefaults(config: FieldConfig, defaults: FieldConfi
const keys = Object.keys(defaults);
for (const key of keys) {
if (key === 'custom') {
if (!context.custom) {
if (!context.fieldConfigRegistry) {
continue;
}
if (!config.custom) {
config.custom = {};
}
const customKeys = Object.keys(defaults.custom!);
const customKeys = Object.keys(defaults.custom!);
for (const customKey of customKeys) {
processFieldConfigValue(config.custom!, defaults.custom!, customKey, context.custom, context);
processFieldConfigValue(config.custom!, defaults.custom!, `custom.${customKey}`, context);
}
} else {
// when config from ds exists for a given field -> use it
processFieldConfigValue(config, defaults, key, standardFieldConfigEditorRegistry, context);
processFieldConfigValue(config, defaults, key, context);
}
}
}
validateFieldConfig(config);
}
@@ -252,20 +255,19 @@ const processFieldConfigValue = (
destination: Record<string, any>, // it's mutable
source: Record<string, any>,
key: string,
registry: FieldConfigEditorRegistry,
context: FieldOverrideContext
context: FieldOverrideEnv
) => {
const currentConfig = destination[key];
if (currentConfig === null || currentConfig === undefined) {
const item = registry.getIfExists(key);
const item = context.fieldConfigRegistry.getIfExists(key);
if (!item) {
return;
}
if (item && item.shouldApply(context.field!)) {
const val = item.process(source[key], context, item.settings);
const val = item.process(source[item.path], context, item.settings);
if (val !== undefined && val !== null) {
destination[key] = val;
destination[item.path] = val;
}
}
}

View File

@@ -3,5 +3,6 @@ export * from './displayProcessor';
export * from './scale';
export * from './standardFieldConfigEditorRegistry';
export * from './overrides/processors';
export { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides';

View File

@@ -1,6 +1,6 @@
import { FieldConfigEditorRegistry, FieldPropertyEditorItem } from '../types/fieldOverrides';
import { Registry, RegistryItem } from '../utils/Registry';
import { ComponentType } from 'react';
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
export interface StandardEditorProps<TValue = any, TSettings = any> {
value: TValue;
@@ -11,6 +11,6 @@ export interface StandardEditorsRegistryItem<TValue = any, TSettings = any> exte
editor: ComponentType<StandardEditorProps<TValue, TSettings>>;
settings?: TSettings;
}
export const standardFieldConfigEditorRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>();
export const standardFieldConfigEditorRegistry = new FieldConfigOptionsRegistry();
export const standardEditorsRegistry = new Registry<StandardEditorsRegistryItem<any>>();

View File

@@ -1,11 +1,23 @@
import React from 'react';
import { identityOverrideProcessor, standardEditorsRegistry } from '../field';
import { PanelPlugin, standardFieldConfigProperties } from './PanelPlugin';
import { identityOverrideProcessor, standardEditorsRegistry, standardFieldConfigEditorRegistry } from '../field';
import { PanelPlugin } from './PanelPlugin';
import { FieldConfigProperty } from '../types';
describe('PanelPlugin', () => {
describe('declarative options', () => {
beforeAll(() => {
standardFieldConfigEditorRegistry.setInit(() => {
return [
{
id: 'min',
path: 'min',
},
{
id: 'max',
path: 'max',
},
] as any;
});
standardEditorsRegistry.setInit(() => {
return [
{
@@ -14,26 +26,29 @@ describe('PanelPlugin', () => {
] as any;
});
});
test('field config UI API', () => {
const panel = new PanelPlugin(() => {
return <div>Panel</div>;
});
panel.setCustomFieldOptions(builder => {
builder.addCustomEditor({
id: 'custom',
name: 'Custom',
description: 'Custom field config property description',
editor: () => <div>Editor</div>,
override: () => <div>Editor</div>,
process: identityOverrideProcessor,
settings: {},
shouldApply: () => true,
});
panel.useFieldConfig({
useCustomConfig: builder => {
builder.addCustomEditor({
id: 'custom',
path: 'custom',
name: 'Custom',
description: 'Custom field config property description',
editor: () => <div>Editor</div>,
override: () => <div>Editor</div>,
process: identityOverrideProcessor,
settings: {},
shouldApply: () => true,
});
},
});
expect(panel.customFieldConfigs).toBeDefined();
expect(panel.customFieldConfigs!.list()).toHaveLength(1);
expect(panel.fieldConfigRegistry.list()).toHaveLength(3);
});
test('options UI API', () => {
@@ -44,6 +59,7 @@ describe('PanelPlugin', () => {
panel.setPanelOptions(builder => {
builder.addCustomEditor({
id: 'option',
path: 'option',
name: 'Option editor',
description: 'Option editor description',
editor: () => <div>Editor</div>,
@@ -66,18 +82,19 @@ describe('PanelPlugin', () => {
panel.setPanelOptions(builder => {
builder
.addNumberInput({
id: 'numericOption',
path: 'numericOption',
name: 'Option editor',
description: 'Option editor description',
defaultValue: 10,
})
.addNumberInput({
id: 'numericOptionNoDefault',
path: 'numericOptionNoDefault',
name: 'Option editor',
description: 'Option editor description',
})
.addCustomEditor({
id: 'customOption',
path: 'customOption',
name: 'Option editor',
description: 'Option editor description',
editor: () => <div>Editor</div>,
@@ -101,7 +118,7 @@ describe('PanelPlugin', () => {
panel.setPanelOptions(builder => {
builder.addNumberInput({
id: 'numericOption.nested',
path: 'numericOption.nested',
name: 'Option editor',
description: 'Option editor description',
defaultValue: 10,
@@ -122,30 +139,33 @@ describe('PanelPlugin', () => {
return <div>Panel</div>;
});
panel.setCustomFieldOptions(builder => {
builder
.addNumberInput({
id: 'numericOption',
name: 'Option editor',
description: 'Option editor description',
defaultValue: 10,
})
.addNumberInput({
id: 'numericOptionNoDefault',
name: 'Option editor',
description: 'Option editor description',
})
.addCustomEditor({
id: 'customOption',
name: 'Option editor',
description: 'Option editor description',
editor: () => <div>Editor</div>,
override: () => <div>Override editor</div>,
process: identityOverrideProcessor,
shouldApply: () => true,
settings: {},
defaultValue: { value: 'Custom default value' },
});
panel.useFieldConfig({
useCustomConfig: builder => {
builder
.addNumberInput({
path: 'numericOption',
name: 'Option editor',
description: 'Option editor description',
defaultValue: 10,
})
.addNumberInput({
path: 'numericOptionNoDefault',
name: 'Option editor',
description: 'Option editor description',
})
.addCustomEditor({
id: 'customOption',
path: 'customOption',
name: 'Option editor',
description: 'Option editor description',
editor: () => <div>Editor</div>,
override: () => <div>Override editor</div>,
process: identityOverrideProcessor,
shouldApply: () => true,
settings: {},
defaultValue: { value: 'Custom default value' },
});
},
});
const expectedDefaults = {
@@ -161,13 +181,15 @@ describe('PanelPlugin', () => {
return <div>Panel</div>;
});
panel.setCustomFieldOptions(builder => {
builder.addNumberInput({
id: 'numericOption.nested',
name: 'Option editor',
description: 'Option editor description',
defaultValue: 10,
});
panel.useFieldConfig({
useCustomConfig: builder => {
builder.addNumberInput({
path: 'numericOption.nested',
name: 'Option editor',
description: 'Option editor description',
defaultValue: 10,
});
},
});
const expectedDefaults = {
@@ -184,8 +206,8 @@ describe('PanelPlugin', () => {
return <div>Panel</div>;
});
panel.useStandardFieldConfig();
expect(panel.standardFieldConfigProperties).toEqual(Array.from(standardFieldConfigProperties.keys()));
panel.useFieldConfig();
expect(panel.fieldConfigRegistry.list()).toHaveLength(2);
});
test('selected standard config', () => {
@@ -193,8 +215,10 @@ describe('PanelPlugin', () => {
return <div>Panel</div>;
});
panel.useStandardFieldConfig([FieldConfigProperty.Min, FieldConfigProperty.Thresholds]);
expect(panel.standardFieldConfigProperties).toEqual(['min', 'thresholds']);
panel.useFieldConfig({
standardOptions: [FieldConfigProperty.Min, FieldConfigProperty.Max],
});
expect(panel.fieldConfigRegistry.list()).toHaveLength(2);
});
describe('default values', () => {
@@ -203,17 +227,21 @@ describe('PanelPlugin', () => {
return <div>Panel</div>;
});
panel.useStandardFieldConfig([FieldConfigProperty.Color, FieldConfigProperty.Min], {
[FieldConfigProperty.Color]: '#ff00ff',
[FieldConfigProperty.Min]: 10,
panel.useFieldConfig({
standardOptions: [FieldConfigProperty.Max, FieldConfigProperty.Min],
standardOptionsDefaults: {
[FieldConfigProperty.Max]: 20,
[FieldConfigProperty.Min]: 10,
},
});
expect(panel.standardFieldConfigProperties).toEqual(['color', 'min']);
expect(panel.fieldConfigRegistry.list()).toHaveLength(2);
expect(panel.fieldConfigDefaults).toEqual({
defaults: {
min: 10,
color: '#ff00ff',
max: 20,
custom: {},
},
overrides: [],
});
@@ -224,16 +252,20 @@ describe('PanelPlugin', () => {
return <div>Panel</div>;
});
panel.useStandardFieldConfig([FieldConfigProperty.Color], {
[FieldConfigProperty.Color]: '#ff00ff',
[FieldConfigProperty.Min]: 10,
panel.useFieldConfig({
standardOptions: [FieldConfigProperty.Max],
standardOptionsDefaults: {
[FieldConfigProperty.Max]: 20,
[FieldConfigProperty.Min]: 10,
},
});
expect(panel.standardFieldConfigProperties).toEqual(['color']);
expect(panel.fieldConfigRegistry.list()).toHaveLength(1);
expect(panel.fieldConfigDefaults).toEqual({
defaults: {
color: '#ff00ff',
max: 20,
custom: {},
},
overrides: [],
});

View File

@@ -1,5 +1,4 @@
import {
FieldConfigEditorRegistry,
FieldConfigSource,
GrafanaPlugin,
PanelEditorProps,
@@ -9,55 +8,34 @@ import {
PanelProps,
PanelTypeChangedHandler,
FieldConfigProperty,
ThresholdsMode,
} from '../types';
import { FieldConfigEditorBuilder, PanelOptionsEditorBuilder } from '../utils/OptionsUIBuilders';
import { ComponentClass, ComponentType } from 'react';
import set from 'lodash/set';
import { deprecationWarning } from '../utils';
import { FieldConfigOptionsRegistry, standardFieldConfigEditorRegistry } from '../field';
export const allStandardFieldConfigProperties: FieldConfigProperty[] = [
FieldConfigProperty.Min,
FieldConfigProperty.Max,
FieldConfigProperty.Title,
FieldConfigProperty.Unit,
FieldConfigProperty.Decimals,
FieldConfigProperty.NoValue,
FieldConfigProperty.Color,
FieldConfigProperty.Thresholds,
FieldConfigProperty.Mappings,
FieldConfigProperty.Links,
];
export const standardFieldConfigDefaults: Partial<Record<FieldConfigProperty, any>> = {
[FieldConfigProperty.Thresholds]: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: 80, color: 'red' },
],
},
[FieldConfigProperty.Mappings]: [],
};
export const standardFieldConfigProperties = new Map(allStandardFieldConfigProperties.map(p => [p, undefined]));
export interface SetFieldConfigOptionsArgs<TFieldConfigOptions = any> {
standardOptions?: FieldConfigProperty[];
standardOptionsDefaults?: Partial<Record<FieldConfigProperty, any>>;
useCustomConfig?: (builder: FieldConfigEditorBuilder<TFieldConfigOptions>) => void;
}
export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = any> extends GrafanaPlugin<
PanelPluginMeta
> {
private _defaults?: TOptions;
private _standardFieldConfigProperties?: Map<FieldConfigProperty, any>;
private _fieldConfigDefaults: FieldConfigSource<TFieldConfigOptions> = {
defaults: {},
overrides: [],
};
private _customFieldConfigs?: FieldConfigEditorRegistry;
private customFieldConfigsUIBuilder = new FieldConfigEditorBuilder<TFieldConfigOptions>();
private registerCustomFieldConfigs?: (builder: FieldConfigEditorBuilder<TFieldConfigOptions>) => void;
private _fieldConfigRegistry?: FieldConfigOptionsRegistry;
private _initConfigRegistry = () => {
return new FieldConfigOptionsRegistry();
};
private _optionEditors?: PanelOptionEditorsRegistry;
private optionsUIBuilder = new PanelOptionsEditorBuilder<TOptions>();
private registerOptionEditors?: (builder: PanelOptionsEditorBuilder<TOptions>) => void;
panel: ComponentType<PanelProps<TOptions>>;
@@ -94,39 +72,21 @@ export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = an
}
get fieldConfigDefaults(): FieldConfigSource<TFieldConfigOptions> {
let customPropertiesDefaults = this._fieldConfigDefaults.defaults.custom;
const configDefaults = this._fieldConfigDefaults.defaults;
configDefaults.custom = {} as TFieldConfigOptions;
if (!customPropertiesDefaults) {
customPropertiesDefaults = {} as TFieldConfigOptions;
}
const editors = this.customFieldConfigs;
if (editors && editors.list().length !== 0) {
for (const editor of editors.list()) {
set(customPropertiesDefaults, editor.id, editor.defaultValue);
}
for (const option of this.fieldConfigRegistry.list()) {
set(configDefaults, option.id, option.defaultValue);
}
return {
defaults: {
...(this._standardFieldConfigProperties ? Object.fromEntries(this._standardFieldConfigProperties) : {}),
custom:
Object.keys(customPropertiesDefaults).length > 0
? {
...customPropertiesDefaults,
}
: undefined,
...this._fieldConfigDefaults.defaults,
...configDefaults,
},
// TODO: not sure yet what about overrides, if anything
overrides: this._fieldConfigDefaults.overrides,
};
}
get standardFieldConfigProperties() {
return this._standardFieldConfigProperties ? Array.from(this._standardFieldConfigProperties.keys()) : [];
}
/**
* @deprecated setDefaults is deprecated in favor of setPanelOptions
*/
@@ -136,19 +96,19 @@ export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = an
return this;
}
get customFieldConfigs() {
if (!this._customFieldConfigs && this.registerCustomFieldConfigs) {
this.registerCustomFieldConfigs(this.customFieldConfigsUIBuilder);
this._customFieldConfigs = this.customFieldConfigsUIBuilder.getRegistry();
get fieldConfigRegistry() {
if (!this._fieldConfigRegistry) {
this._fieldConfigRegistry = this._initConfigRegistry();
}
return this._customFieldConfigs;
return this._fieldConfigRegistry;
}
get optionEditors() {
if (!this._optionEditors && this.registerOptionEditors) {
this.registerOptionEditors(this.optionsUIBuilder);
this._optionEditors = this.optionsUIBuilder.getRegistry();
const builder = new PanelOptionsEditorBuilder<TOptions>();
this.registerOptionEditors(builder);
this._optionEditors = builder.getRegistry();
}
return this._optionEditors;
@@ -188,47 +148,6 @@ export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = an
return this;
}
/**
* Enables custom field properties editor creation
*
* @example
* ```typescript
*
* import { ShapePanel } from './ShapePanel';
*
* interface ShapePanelOptions {}
*
* export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel)
* .setCustomFieldOptions(builder => {
* builder
* .addNumberInput({
* id: 'shapeBorderWidth',
* name: 'Border width',
* description: 'Border width of the shape',
* settings: {
* min: 1,
* max: 5,
* },
* })
* .addSelect({
* id: 'displayMode',
* name: 'Display mode',
* description: 'How the shape shout be rendered'
* settings: {
* options: [{value: 'fill', label: 'Fill' }, {value: 'transparent', label: 'Transparent }]
* },
* })
* })
* ```
*
* @public
**/
setCustomFieldOptions(builder: (builder: FieldConfigEditorBuilder<TFieldConfigOptions>) => void) {
// builder is applied lazily when custom field configs are accessed
this.registerCustomFieldConfigs = builder;
return this;
}
/**
* Enables panel options editor creation
*
@@ -277,44 +196,94 @@ export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = an
*
* // when plugin should use all standard options
* export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel)
* .useStandardFieldConfig();
* .useFieldConfig();
*
* // when plugin should only display specific standard options
* // note, that options will be displayed in the order they are provided
* export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel)
* .useStandardFieldConfig([FieldConfigProperty.Min, FieldConfigProperty.Max, FieldConfigProperty.Links]);
* .useFieldConfig({
* standardOptions: [FieldConfigProperty.Min, FieldConfigProperty.Max]
* });
*
* // when standard option's default value needs to be provided
* export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel)
* .useStandardFieldConfig([FieldConfigProperty.Min, FieldConfigProperty.Max], {
* [FieldConfigProperty.Min]: 20,
* [FieldConfigProperty.Max]: 100
* .useFieldConfig({
* standardOptions: [FieldConfigProperty.Min, FieldConfigProperty.Max],
* standardOptionsDefaults: {
* [FieldConfigProperty.Min]: 20,
* [FieldConfigProperty.Max]: 100
* }
* });
*
* // when custom field config options needs to be provided
* export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel)
* .useFieldConfig({
* useCustomConfig: builder => {
builder
* .addNumberInput({
* id: 'shapeBorderWidth',
* name: 'Border width',
* description: 'Border width of the shape',
* settings: {
* min: 1,
* max: 5,
* },
* })
* .addSelect({
* id: 'displayMode',
* name: 'Display mode',
* description: 'How the shape shout be rendered'
* settings: {
* options: [{value: 'fill', label: 'Fill' }, {value: 'transparent', label: 'Transparent }]
* },
* })
* },
* });
*
* ```
*
* @public
*/
useStandardFieldConfig(
properties?: FieldConfigProperty[] | null,
customDefaults?: Partial<Record<FieldConfigProperty, any>>
) {
if (!properties) {
this._standardFieldConfigProperties = standardFieldConfigProperties;
return this;
} else {
this._standardFieldConfigProperties = new Map(properties.map(p => [p, standardFieldConfigProperties.get(p)]));
}
useFieldConfig(config?: SetFieldConfigOptionsArgs<TFieldConfigOptions>) {
// builder is applied lazily when custom field configs are accessed
this._initConfigRegistry = () => {
const registry = new FieldConfigOptionsRegistry();
const defaults = customDefaults ?? standardFieldConfigDefaults;
// Add custom options
if (config && config.useCustomConfig) {
const builder = new FieldConfigEditorBuilder<TFieldConfigOptions>();
config.useCustomConfig(builder);
if (defaults) {
Object.keys(defaults).map(k => {
if (properties.indexOf(k as FieldConfigProperty) > -1) {
this._standardFieldConfigProperties!.set(k as FieldConfigProperty, defaults[k as FieldConfigProperty]);
for (const customProp of builder.getRegistry().list()) {
customProp.isCustom = true;
// need to do something to make the custom items not conflict with standard ones
// problem is id (registry index) is used as property path
// so sort of need a property path on the FieldPropertyEditorItem
customProp.id = 'custom.' + customProp.id;
registry.register(customProp);
}
});
}
}
if (config && config.standardOptions) {
for (const standardOption of config.standardOptions) {
const standardEditor = standardFieldConfigEditorRegistry.get(standardOption);
registry.register({
...standardEditor,
defaultValue:
(config.standardOptionsDefaults && config.standardOptionsDefaults[standardOption]) ||
standardEditor.defaultValue,
});
}
} else {
for (const fieldConfigProp of standardFieldConfigEditorRegistry.list()) {
console.log(fieldConfigProp);
registry.register(fieldConfigProp);
}
}
return registry;
};
return this;
}
}

View File

@@ -6,7 +6,7 @@ import { NumberFieldConfigSettings, SelectFieldConfigSettings, StringFieldConfig
* Option editor registry item
*/
export interface OptionsEditorItem<TOptions, TSettings, TEditorProps, TValue> extends RegistryItem {
id: (keyof TOptions & string) | string;
path: (keyof TOptions & string) | string;
editor: ComponentType<TEditorProps>;
settings?: TSettings;
defaultValue?: TValue;

View File

@@ -15,9 +15,9 @@ import { StandardEditorProps } from '../field';
import { OptionsEditorItem } from './OptionsUIRegistryBuilder';
export interface DynamicConfigValue {
prop: string;
id: string;
value?: any;
custom?: boolean;
isCustom?: boolean;
}
export interface ConfigOverrideRule {
@@ -55,7 +55,7 @@ export interface FieldOverrideEditorProps<TValue, TSettings> extends Omit<Standa
}
export interface FieldConfigEditorConfig<TOptions, TSettings = any, TValue = any> {
id: (keyof TOptions & string) | string;
path: (keyof TOptions & string) | string;
name: string;
description: string;
settings?: TSettings;
@@ -68,6 +68,9 @@ export interface FieldPropertyEditorItem<TOptions = any, TValue = any, TSettings
// An editor that can be filled in with context info (template variables etc)
override: ComponentType<FieldOverrideEditorProps<TValue, TSettings>>;
/** true for plugin field config properties */
isCustom?: boolean;
// Convert the override value to a well typed value
process: (value: any, context: FieldOverrideContext, settings?: TSettings) => TValue | undefined | null;
@@ -75,17 +78,14 @@ export interface FieldPropertyEditorItem<TOptions = any, TValue = any, TSettings
shouldApply: (field: Field) => boolean;
}
export type FieldConfigEditorRegistry = Registry<FieldPropertyEditorItem>;
export interface ApplyFieldOverrideOptions {
data?: DataFrame[];
fieldOptions: FieldConfigSource;
fieldConfig: FieldConfigSource;
replaceVariables: InterpolateFunction;
theme: GrafanaTheme;
timeZone?: TimeZone;
autoMinMax?: boolean;
standard?: FieldConfigEditorRegistry;
custom?: FieldConfigEditorRegistry;
fieldConfigRegistry?: Registry<FieldPropertyEditorItem>;
}
export enum FieldConfigProperty {

View File

@@ -119,7 +119,7 @@ export interface PanelOptionsEditorItem<TOptions = any, TValue = any, TSettings
extends OptionsEditorItem<TOptions, TSettings, PanelOptionsEditorProps<TValue>, TValue> {}
export interface PanelOptionsEditorConfig<TOptions, TSettings = any, TValue = any> {
id: (keyof TOptions & string) | string;
path: (keyof TOptions & string) | string;
name: string;
description: string;
settings?: TSettings;

View File

@@ -34,6 +34,7 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
addNumberInput<TSettings>(config: FieldConfigEditorConfig<TOptions, TSettings & NumberFieldConfigSettings, number>) {
return this.addCustomEditor({
...config,
id: config.path,
override: standardEditorsRegistry.get('number').editor as any,
editor: standardEditorsRegistry.get('number').editor as any,
process: numberOverrideProcessor,
@@ -45,6 +46,7 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
addTextInput<TSettings>(config: FieldConfigEditorConfig<TOptions, TSettings & StringFieldConfigSettings, string>) {
return this.addCustomEditor({
...config,
id: config.path,
override: standardEditorsRegistry.get('text').editor as any,
editor: standardEditorsRegistry.get('text').editor as any,
process: stringOverrideProcessor,
@@ -58,6 +60,7 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
) {
return this.addCustomEditor({
...config,
id: config.path,
override: standardEditorsRegistry.get('select').editor as any,
editor: standardEditorsRegistry.get('select').editor as any,
process: selectOverrideProcessor,
@@ -70,6 +73,7 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
addRadio<TOption, TSettings = any>(config: FieldConfigEditorConfig<TOptions, TSettings, TOption>) {
return this.addCustomEditor({
...config,
id: config.path,
override: standardEditorsRegistry.get('radio').editor as any,
editor: standardEditorsRegistry.get('radio').editor as any,
process: selectOverrideProcessor,
@@ -82,6 +86,7 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
addBooleanSwitch<TSettings = any>(config: FieldConfigEditorConfig<TOptions, TSettings, boolean>) {
return this.addCustomEditor({
...config,
id: config.path,
editor: standardEditorsRegistry.get('boolean').editor as any,
override: standardEditorsRegistry.get('boolean').editor as any,
process: booleanOverrideProcessor,
@@ -95,6 +100,7 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
) {
return this.addCustomEditor({
...config,
id: config.path,
editor: standardEditorsRegistry.get('color').editor as any,
override: standardEditorsRegistry.get('color').editor as any,
process: identityOverrideProcessor,
@@ -108,6 +114,7 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
) {
return this.addCustomEditor({
...config,
id: config.path,
editor: standardEditorsRegistry.get('unit').editor as any,
override: standardEditorsRegistry.get('unit').editor as any,
process: unitOverrideProcessor,
@@ -128,6 +135,7 @@ export class PanelOptionsEditorBuilder<TOptions> extends OptionsUIRegistryBuilde
addNumberInput<TSettings>(config: PanelOptionsEditorConfig<TOptions, TSettings & NumberFieldConfigSettings, number>) {
return this.addCustomEditor({
...config,
id: config.path,
editor: standardEditorsRegistry.get('number').editor as any,
});
}
@@ -135,6 +143,7 @@ export class PanelOptionsEditorBuilder<TOptions> extends OptionsUIRegistryBuilde
addTextInput<TSettings>(config: PanelOptionsEditorConfig<TOptions, TSettings & StringFieldConfigSettings, string>) {
return this.addCustomEditor({
...config,
id: config.path,
editor: standardEditorsRegistry.get('text').editor as any,
});
}
@@ -144,6 +153,7 @@ export class PanelOptionsEditorBuilder<TOptions> extends OptionsUIRegistryBuilde
) {
return this.addCustomEditor({
...config,
id: config.path,
editor: standardEditorsRegistry.get('select').editor as any,
});
}
@@ -153,6 +163,7 @@ export class PanelOptionsEditorBuilder<TOptions> extends OptionsUIRegistryBuilde
) {
return this.addCustomEditor({
...config,
id: config.path,
editor: standardEditorsRegistry.get('radio').editor as any,
});
}
@@ -160,6 +171,7 @@ export class PanelOptionsEditorBuilder<TOptions> extends OptionsUIRegistryBuilde
addBooleanSwitch<TSettings = any>(config: PanelOptionsEditorConfig<TOptions, TSettings, boolean>) {
return this.addCustomEditor({
...config,
id: config.path,
editor: standardEditorsRegistry.get('boolean').editor as any,
});
}
@@ -169,6 +181,7 @@ export class PanelOptionsEditorBuilder<TOptions> extends OptionsUIRegistryBuilde
): this {
return this.addCustomEditor({
...config,
id: config.path,
editor: standardEditorsRegistry.get('color').editor as any,
settings: config.settings || {},
});
@@ -179,6 +192,7 @@ export class PanelOptionsEditorBuilder<TOptions> extends OptionsUIRegistryBuilde
): this {
return this.addCustomEditor({
...config,
id: config.path,
editor: standardEditorsRegistry.get('unit').editor as any,
});
}

View File

@@ -124,7 +124,7 @@ export class Registry<T extends RegistryItem> {
if (!this.initialized) {
this.getIfExists('xxx'); // will trigger init
}
return [...this.ordered]; // copy of everythign just in case
return this.ordered; // copy of everythign just in case
}
register(ext: T) {

View File

@@ -0,0 +1,170 @@
import { identityOverrideProcessor } from '../../field';
import { ThresholdsMode } from '../../types';
export const mockStandardProperties = () => {
const title = {
id: 'title',
path: 'title',
name: 'Title',
description: "Field's title",
editor: () => null,
override: () => null,
process: identityOverrideProcessor,
settings: {
placeholder: 'none',
expandTemplateVars: true,
},
shouldApply: () => true,
};
const unit = {
id: 'unit',
path: 'unit',
name: 'Unit',
description: 'Value units',
editor: () => null,
override: () => null,
process: identityOverrideProcessor,
settings: {
placeholder: 'none',
},
shouldApply: () => true,
};
const min = {
id: 'min',
path: 'min',
name: 'Min',
description: 'Minimum expected value',
editor: () => null,
override: () => null,
process: identityOverrideProcessor,
settings: {
placeholder: 'auto',
},
shouldApply: () => true,
};
const max = {
id: 'max',
path: 'max',
name: 'Max',
description: 'Maximum expected value',
editor: () => null,
override: () => null,
process: identityOverrideProcessor,
settings: {
placeholder: 'auto',
},
shouldApply: () => true,
};
const decimals = {
id: 'decimals',
path: 'decimals',
name: 'Decimals',
description: 'Number of decimal to be shown for a value',
editor: () => null,
override: () => null,
process: identityOverrideProcessor,
settings: {
placeholder: 'auto',
min: 0,
max: 15,
integer: true,
},
shouldApply: () => true,
};
const thresholds = {
id: 'thresholds',
path: 'thresholds',
name: 'Thresholds',
description: 'Manage thresholds',
editor: () => null,
override: () => null,
process: identityOverrideProcessor,
settings: {},
defaultValue: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: 80, color: 'red' },
],
},
shouldApply: () => true,
};
const mappings = {
id: 'mappings',
path: 'mappings',
name: 'Value mappings',
description: 'Manage value mappings',
editor: () => null,
override: () => null,
process: identityOverrideProcessor,
settings: {},
defaultValue: [],
shouldApply: () => true,
};
const noValue = {
id: 'noValue',
path: 'noValue',
name: 'No Value',
description: 'What to show when there is no value',
editor: () => null,
override: () => null,
process: identityOverrideProcessor,
settings: {
placeholder: '-',
},
// ??? any optionsUi with no value
shouldApply: () => true,
};
const links = {
id: 'links',
path: 'links',
name: 'DataLinks',
description: 'Manage date links',
editor: () => null,
override: () => null,
process: identityOverrideProcessor,
settings: {
placeholder: '-',
},
shouldApply: () => true,
};
const color = {
id: 'color',
path: 'color',
name: 'Color',
description: 'Customise color',
editor: () => null,
override: () => null,
process: identityOverrideProcessor,
settings: {
placeholder: '-',
},
shouldApply: () => true,
};
return [unit, min, max, decimals, title, noValue, thresholds, mappings, links, color];
};

View File

@@ -1,126 +0,0 @@
import {
FieldConfig,
FieldConfigSource,
InterpolateFunction,
GrafanaTheme,
FieldMatcherID,
MutableDataFrame,
DataFrame,
FieldType,
applyFieldOverrides,
toDataFrame,
standardFieldConfigEditorRegistry,
standardEditorsRegistry,
} from '@grafana/data';
import { getTheme } from '../../themes';
import { getStandardFieldConfigs, getStandardOptionEditors } from '../../utils';
describe('FieldOverrides', () => {
beforeAll(() => {
standardEditorsRegistry.setInit(getStandardOptionEditors);
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 FieldConfigSource, // 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 FieldConfigSource, // 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

@@ -77,7 +77,7 @@ function buildData(theme: GrafanaTheme, overrides: ConfigOverrideRule[]): DataFr
return applyFieldOverrides({
data: [data],
fieldOptions: {
fieldConfig: {
overrides,
defaults: {},
},
@@ -105,10 +105,10 @@ export const BarGaugeCell = () => {
{
matcher: { id: FieldMatcherID.byName, options: 'Progress' },
properties: [
{ prop: 'width', value: '200', custom: true },
{ prop: 'displayMode', value: 'gradient-gauge', custom: true },
{ prop: 'min', value: '0' },
{ prop: 'max', value: '100' },
{ id: 'width', value: '200', isCustom: true },
{ id: 'displayMode', value: 'gradient-gauge', isCustom: true },
{ id: 'min', value: '0' },
{ id: 'max', value: '100' },
],
},
]);
@@ -141,11 +141,11 @@ export const ColoredCells = () => {
{
matcher: { id: FieldMatcherID.byName, options: 'Progress' },
properties: [
{ prop: 'width', value: '80', custom: true },
{ prop: 'displayMode', value: 'color-background', custom: true },
{ prop: 'min', value: '0' },
{ prop: 'max', value: '100' },
{ prop: 'thresholds', value: defaultThresholds },
{ id: 'width', value: '80', isCustom: true },
{ id: 'displayMode', value: 'color-background', isCustom: true },
{ id: 'min', value: '0' },
{ id: 'max', value: '100' },
{ id: 'thresholds', value: defaultThresholds },
],
},
]);

View File

@@ -17,6 +17,7 @@ import {
ValueMapping,
ValueMappingFieldConfigSettings,
valueMappingsOverrideProcessor,
ThresholdsMode,
} from '@grafana/data';
import { NumberValueEditor, Forms, StringValueEditor, Select } from '../components';
import { ValueMappingsValueEditor } from '../components/OptionsUI/mappings';
@@ -32,6 +33,7 @@ import { StatsPickerEditor } from '../components/OptionsUI/stats';
export const getStandardFieldConfigs = () => {
const title: FieldPropertyEditorItem<any, string, StringFieldConfigSettings> = {
id: 'title',
path: 'title',
name: 'Title',
description: "Field's title",
editor: standardEditorsRegistry.get('text').editor as any,
@@ -46,6 +48,7 @@ export const getStandardFieldConfigs = () => {
const unit: FieldPropertyEditorItem<any, string, StringFieldConfigSettings> = {
id: 'unit',
path: 'unit',
name: 'Unit',
description: 'Value units',
@@ -62,6 +65,7 @@ export const getStandardFieldConfigs = () => {
const min: FieldPropertyEditorItem<any, number, NumberFieldConfigSettings> = {
id: 'min',
path: 'min',
name: 'Min',
description: 'Minimum expected value',
@@ -77,6 +81,7 @@ export const getStandardFieldConfigs = () => {
const max: FieldPropertyEditorItem<any, number, NumberFieldConfigSettings> = {
id: 'max',
path: 'max',
name: 'Max',
description: 'Maximum expected value',
@@ -93,6 +98,7 @@ export const getStandardFieldConfigs = () => {
const decimals: FieldPropertyEditorItem<any, number, NumberFieldConfigSettings> = {
id: 'decimals',
path: 'decimals',
name: 'Decimals',
description: 'Number of decimal to be shown for a value',
@@ -112,37 +118,41 @@ export const getStandardFieldConfigs = () => {
const thresholds: FieldPropertyEditorItem<any, ThresholdsConfig, ThresholdsFieldConfigSettings> = {
id: 'thresholds',
path: 'thresholds',
name: 'Thresholds',
description: 'Manage thresholds',
editor: standardEditorsRegistry.get('thresholds').editor as any,
override: standardEditorsRegistry.get('thresholds').editor as any,
process: thresholdsOverrideProcessor,
settings: {
// ??
settings: {},
defaultValue: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: 80, color: 'red' },
],
},
shouldApply: field => field.type === FieldType.number,
};
const mappings: FieldPropertyEditorItem<any, ValueMapping[], ValueMappingFieldConfigSettings> = {
id: 'mappings',
path: 'mappings',
name: 'Value mappings',
description: 'Manage value mappings',
editor: standardEditorsRegistry.get('mappings').editor as any,
override: standardEditorsRegistry.get('mappings').editor as any,
process: valueMappingsOverrideProcessor,
settings: {
// ??
},
settings: {},
defaultValue: [],
shouldApply: field => field.type === FieldType.number,
};
const noValue: FieldPropertyEditorItem<any, string, StringFieldConfigSettings> = {
id: 'noValue',
path: 'noValue',
name: 'No Value',
description: 'What to show when there is no value',
@@ -159,6 +169,7 @@ export const getStandardFieldConfigs = () => {
const links: FieldPropertyEditorItem<any, DataLink[], StringFieldConfigSettings> = {
id: 'links',
path: 'links',
name: 'DataLinks',
description: 'Manage date links',
editor: standardEditorsRegistry.get('links').editor as any,
@@ -172,6 +183,7 @@ export const getStandardFieldConfigs = () => {
const color: FieldPropertyEditorItem<any, string, StringFieldConfigSettings> = {
id: 'color',
path: 'color',
name: 'Color',
description: 'Customise color',
editor: standardEditorsRegistry.get('color').editor as any,

View File

@@ -181,7 +181,7 @@ export class PanelInspector extends PureComponent<Props, State> {
const processed = applyFieldOverrides({
data,
theme: config.theme,
fieldOptions: { defaults: {}, overrides: [] },
fieldConfig: { defaults: {}, overrides: [] },
replaceVariables: (value: string) => {
return value;
},

View File

@@ -1,11 +1,11 @@
import React from 'react';
import { DynamicConfigValue, FieldConfigEditorRegistry, FieldOverrideContext, GrafanaTheme } from '@grafana/data';
import { DynamicConfigValue, FieldConfigOptionsRegistry, FieldOverrideContext, GrafanaTheme } from '@grafana/data';
import { FieldConfigItemHeaderTitle, selectThemeVariant, stylesFactory, useTheme } from '@grafana/ui';
import { css } from 'emotion';
interface DynamicConfigValueEditorProps {
property: DynamicConfigValue;
editorsRegistry: FieldConfigEditorRegistry;
registry: FieldConfigOptionsRegistry;
onChange: (value: DynamicConfigValue) => void;
context: FieldOverrideContext;
onRemove: () => void;
@@ -14,13 +14,13 @@ interface DynamicConfigValueEditorProps {
export const DynamicConfigValueEditor: React.FC<DynamicConfigValueEditorProps> = ({
property,
context,
editorsRegistry,
registry,
onChange,
onRemove,
}) => {
const theme = useTheme();
const styles = getStyles(theme);
const item = editorsRegistry?.getIfExists(property.prop);
const item = registry?.getIfExists(property.id);
if (!item) {
return null;

View File

@@ -5,10 +5,8 @@ import {
DataFrame,
FieldPropertyEditorItem,
VariableSuggestionsScope,
standardFieldConfigEditorRegistry,
PanelPlugin,
SelectableValue,
FieldConfigProperty,
} from '@grafana/data';
import { Forms, fieldMatchersUI, ValuePicker, useTheme } from '@grafana/ui';
import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
@@ -18,7 +16,6 @@ import { css } from 'emotion';
interface Props {
plugin: PanelPlugin;
config: FieldConfigSource;
include?: FieldConfigProperty[]; // Ordered list of which fields should be shown/included
onChange: (config: FieldConfigSource) => void;
/* Helpful for IntelliSense */
data: DataFrame[];
@@ -62,33 +59,12 @@ export const OverrideFieldConfigEditor: React.FC<Props> = props => {
const renderOverrides = () => {
const { config, data, plugin } = props;
const { customFieldConfigs } = plugin;
const { fieldConfigRegistry } = plugin;
if (config.overrides.length === 0) {
return null;
}
let configPropertiesOptions = plugin.standardFieldConfigProperties.map(i => {
const editor = standardFieldConfigEditorRegistry.get(i);
return {
label: editor.name,
value: editor.id,
description: editor.description,
custom: false,
};
});
if (customFieldConfigs) {
configPropertiesOptions = configPropertiesOptions.concat(
customFieldConfigs.list().map(i => ({
label: i.name,
value: i.id,
description: i.description,
custom: true,
}))
);
}
return (
<div>
{config.overrides.map((o, i) => {
@@ -100,8 +76,7 @@ export const OverrideFieldConfigEditor: React.FC<Props> = props => {
override={o}
onChange={value => onOverrideChange(i, value)}
onRemove={() => onOverrideRemove(i)}
configPropertiesOptions={configPropertiesOptions}
customPropertiesRegistry={customFieldConfigs}
registry={fieldConfigRegistry}
/>
);
})}
@@ -135,7 +110,7 @@ export const OverrideFieldConfigEditor: React.FC<Props> = props => {
);
};
export const DefaultFieldConfigEditor: React.FC<Props> = ({ include, data, onChange, config, plugin }) => {
export const DefaultFieldConfigEditor: React.FC<Props> = ({ data, onChange, config, plugin }) => {
const setDefaultValue = useCallback(
(name: string, value: any, custom: boolean) => {
const defaults = { ...config.defaults };
@@ -167,16 +142,20 @@ export const DefaultFieldConfigEditor: React.FC<Props> = ({ include, data, onCha
);
const renderEditor = useCallback(
(item: FieldPropertyEditorItem, custom: boolean) => {
(item: FieldPropertyEditorItem) => {
const defaults = config.defaults;
const value = custom ? (defaults.custom ? defaults.custom[item.id] : undefined) : (defaults as any)[item.id];
const value = item.isCustom
? defaults.custom
? defaults.custom[item.path]
: undefined
: (defaults as any)[item.path];
return (
<Forms.Field label={item.name} description={item.description} key={`${item.id}/${custom}`}>
<Forms.Field label={item.name} description={item.description} key={`${item.id}`}>
<item.editor
item={item}
value={value}
onChange={v => setDefaultValue(item.id, v, custom)}
onChange={v => setDefaultValue(item.path, v, item.isCustom)}
context={{
data,
getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope),
@@ -188,28 +167,6 @@ export const DefaultFieldConfigEditor: React.FC<Props> = ({ include, data, onCha
[config]
);
const renderStandardConfigs = useCallback(() => {
if (include && include.length === 0) {
return null;
}
if (include) {
return <>{include.map(f => renderEditor(standardFieldConfigEditorRegistry.get(f), false))}</>;
}
return <>{standardFieldConfigEditorRegistry.list().map(f => renderEditor(f, false))}</>;
}, [plugin, config]);
const renderCustomConfigs = useCallback(() => {
if (!plugin.customFieldConfigs) {
return null;
}
return plugin.customFieldConfigs.list().map(f => renderEditor(f, true));
}, [plugin, config]);
return (
<>
{plugin.customFieldConfigs && renderCustomConfigs()}
{renderStandardConfigs()}
</>
);
// render all field configs
return <>{plugin.fieldConfigRegistry.list().map(renderEditor)}</>;
};

View File

@@ -60,7 +60,6 @@ export const OptionsPaneContent: React.FC<{
plugin={plugin}
onChange={onFieldConfigsChange}
data={data.series}
include={plugin.standardFieldConfigProperties}
/>
</Container>
);

View File

@@ -3,10 +3,8 @@ import {
ConfigOverrideRule,
DataFrame,
DynamicConfigValue,
FieldConfigEditorRegistry,
standardFieldConfigEditorRegistry,
FieldConfigOptionsRegistry,
VariableSuggestionsScope,
SelectableValue,
GrafanaTheme,
} from '@grafana/data';
import { fieldMatchersUI, stylesFactory, useTheme, ValuePicker, selectThemeVariant } from '@grafana/ui';
@@ -21,18 +19,10 @@ interface OverrideEditorProps {
override: ConfigOverrideRule;
onChange: (config: ConfigOverrideRule) => void;
onRemove: () => void;
customPropertiesRegistry?: FieldConfigEditorRegistry;
configPropertiesOptions: Array<SelectableValue<string>>;
registry: FieldConfigOptionsRegistry;
}
export const OverrideEditor: React.FC<OverrideEditorProps> = ({
data,
override,
onChange,
onRemove,
customPropertiesRegistry,
configPropertiesOptions,
}) => {
export const OverrideEditor: React.FC<OverrideEditorProps> = ({ data, override, onChange, onRemove, registry }) => {
const theme = useTheme();
const onMatcherConfigChange = useCallback(
(matcherConfig: any) => {
@@ -59,10 +49,10 @@ export const OverrideEditor: React.FC<OverrideEditorProps> = ({
);
const onDynamicConfigValueAdd = useCallback(
(prop: string, custom?: boolean) => {
(id: string, custom?: boolean) => {
const propertyConfig: DynamicConfigValue = {
prop,
custom,
id,
isCustom: custom,
};
if (override.properties) {
override.properties.push(propertyConfig);
@@ -74,6 +64,15 @@ export const OverrideEditor: React.FC<OverrideEditorProps> = ({
[override, onChange]
);
let configPropertiesOptions = registry.list().map(item => {
return {
label: item.name,
value: item.id,
description: item.description,
custom: item.isCustom,
};
});
const matcherUi = fieldMatchersUI.get(override.matcher.id);
const styles = getStyles(theme);
return (
@@ -90,20 +89,19 @@ export const OverrideEditor: React.FC<OverrideEditorProps> = ({
</FieldConfigItemHeaderTitle>
<div>
{override.properties.map((p, j) => {
const reg = p.custom ? customPropertiesRegistry : standardFieldConfigEditorRegistry;
const item = reg?.getIfExists(p.prop);
const item = registry.getIfExists(p.id);
if (!item) {
return <div>Unknown property: {p.prop}</div>;
return <div>Unknown property: {p.id}</div>;
}
return (
<div key={`${p.prop}/${j}`}>
<div key={`${p.id}/${j}`}>
<DynamicConfigValueEditor
onChange={value => onDynamicConfigValueChange(j, value)}
onRemove={() => onDynamicConfigValueRemove(j)}
property={p}
editorsRegistry={reg}
registry={registry}
context={{
data,
getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope),

View File

@@ -22,7 +22,7 @@ export const PanelOptionsEditor: React.FC<PanelOptionsEditorProps<any>> = ({ plu
{optionEditors.list().map(e => {
return (
<Forms.Field label={e.name} description={e.description} key={e.id}>
<e.editor value={lodashGet(options, e.id)} onChange={value => onOptionChange(e.id, value)} item={e} />
<e.editor value={lodashGet(options, e.path)} onChange={value => onOptionChange(e.path, value)} item={e} />
</Forms.Field>
);
})}

View File

@@ -1,10 +1,46 @@
import { PanelModel } from './PanelModel';
import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
import { PanelProps, FieldConfigProperty } from '@grafana/data';
import {
FieldConfigProperty,
identityOverrideProcessor,
PanelProps,
standardFieldConfigEditorRegistry,
} from '@grafana/data';
import { ComponentClass } from 'react';
class TablePanelCtrl {}
export const mockStandardProperties = () => {
const unit = {
id: 'unit',
path: 'unit',
name: 'Unit',
description: 'Value units',
// @ts-ignore
editor: () => null,
// @ts-ignore
override: () => null,
process: identityOverrideProcessor,
shouldApply: () => true,
};
const decimals = {
id: 'decimals',
path: 'decimals',
name: 'Decimals',
description: 'Number of decimal to be shown for a value',
// @ts-ignore
editor: () => null,
// @ts-ignore
override: () => null,
process: identityOverrideProcessor,
shouldApply: () => true,
};
return [unit, decimals];
};
standardFieldConfigEditorRegistry.setInit(() => mockStandardProperties());
describe('PanelModel', () => {
describe('when creating new panel model', () => {
let model: any;
@@ -79,9 +115,16 @@ describe('PanelModel', () => {
TablePanelCtrl // angular
);
panelPlugin.setDefaults(defaultOptionsMock);
panelPlugin.useStandardFieldConfig([FieldConfigProperty.Unit, FieldConfigProperty.Decimals], {
[FieldConfigProperty.Unit]: 'flop',
[FieldConfigProperty.Decimals]: 2,
/* panelPlugin.useStandardFieldConfig([FieldConfigOptionId.Unit, FieldConfigOptionId.Decimals], {
[FieldConfigOptionId.Unit]: 'flop',
[FieldConfigOptionId.Decimals]: 2,
}); */
panelPlugin.useFieldConfig({
standardOptions: [FieldConfigProperty.Unit, FieldConfigProperty.Decimals],
standardOptionsDefaults: {
[FieldConfigProperty.Unit]: 'flop',
[FieldConfigProperty.Decimals]: 2,
},
});
model.pluginLoaded(panelPlugin);
});
@@ -100,9 +143,9 @@ describe('PanelModel', () => {
it('should apply field config defaults', () => {
// default unit is overriden by model
expect(model.getFieldOverrideOptions().fieldOptions.defaults.unit).toBe('mpg');
expect(model.getFieldOverrideOptions().fieldConfig.defaults.unit).toBe('mpg');
// default decimals are aplied
expect(model.getFieldOverrideOptions().fieldOptions.defaults.decimals).toBe(2);
expect(model.getFieldOverrideOptions().fieldConfig.defaults.decimals).toBe(2);
});
it('should set model props on instance', () => {

View File

@@ -415,9 +415,9 @@ export class PanelModel implements DataConfigSource {
}
return {
fieldOptions: this.fieldConfig,
fieldConfig: this.fieldConfig,
replaceVariables: this.replaceVariables,
custom: this.plugin.customFieldConfigs,
fieldConfigRegistry: this.plugin.fieldConfigRegistry,
theme: config.theme,
};
}

View File

@@ -210,7 +210,7 @@ describe('PanelQueryRunner', () => {
},
{
getFieldOverrideOptions: () => ({
fieldOptions: {
fieldConfig: {
defaults: {
unit: 'm/s',
},

View File

@@ -21,7 +21,7 @@ describe('getFieldDisplayValuesProxy', () => {
],
}),
],
fieldOptions: {
fieldConfig: {
defaults: {},
overrides: [],
},

View File

@@ -129,7 +129,7 @@ describe('getLinksFromLogsField', () => {
],
}),
],
fieldOptions: {
fieldConfig: {
defaults: {},
overrides: [],
},

View File

@@ -9,12 +9,13 @@ import { barGaugePanelMigrationHandler } from './BarGaugeMigrations';
export const plugin = new PanelPlugin<BarGaugeOptions>(BarGaugePanel)
.setDefaults(defaults)
.setEditor(BarGaugePanelEditor)
.useFieldConfig()
.setPanelOptions(builder => {
addStandardDataReduceOptions(builder);
builder
.addRadio({
id: 'displayMode',
path: 'displayMode',
name: 'Display mode',
description: 'Controls the bar style',
settings: {
@@ -26,11 +27,10 @@ export const plugin = new PanelPlugin<BarGaugeOptions>(BarGaugePanel)
},
})
.addBooleanSwitch({
id: 'showUnfilled',
path: 'showUnfilled',
name: 'Show unfilled area',
description: 'When enabled renders the unfilled region as gray',
});
})
.setPanelChangeHandler(sharedSingleStatPanelChangedHandler)
.setMigrationHandler(barGaugePanelMigrationHandler)
.useStandardFieldConfig();
.setMigrationHandler(barGaugePanelMigrationHandler);

View File

@@ -8,21 +8,20 @@ import { gaugePanelMigrationHandler, gaugePanelChangedHandler } from './GaugeMig
export const plugin = new PanelPlugin<GaugeOptions>(GaugePanel)
.setDefaults(defaults)
.setEditor(GaugePanelEditor)
.useFieldConfig()
.setPanelOptions(builder => {
addStandardDataReduceOptions(builder);
builder
.addBooleanSwitch({
id: 'showThresholdLabels',
path: 'showThresholdLabels',
name: 'Show threshold Labels',
description: 'Render the threshold values around the gauge bar',
})
.addBooleanSwitch({
id: 'showThresholdMarkers',
path: 'showThresholdMarkers',
name: 'Show threshold markers',
description: 'Renders the thresholds as an outer bar',
});
})
.setPanelChangeHandler(gaugePanelChangedHandler)
.setMigrationHandler(gaugePanelMigrationHandler)
.useStandardFieldConfig();
.setMigrationHandler(gaugePanelMigrationHandler);

View File

@@ -1,11 +1,8 @@
import { PanelPlugin, FieldConfigProperty } from '@grafana/data';
import { PanelPlugin } from '@grafana/data';
import { PieChartPanelEditor } from './PieChartPanelEditor';
import { PieChartPanel } from './PieChartPanel';
import { PieChartOptions, defaults } from './types';
export const plugin = new PanelPlugin<PieChartOptions>(PieChartPanel)
.setDefaults(defaults)
.useStandardFieldConfig(null, {
[FieldConfigProperty.Unit]: 'short',
})
.setEditor(PieChartPanelEditor);

View File

@@ -7,12 +7,13 @@ import { StatPanelEditor } from './StatPanelEditor';
export const plugin = new PanelPlugin<StatPanelOptions>(StatPanel)
.setDefaults(defaults)
.setEditor(StatPanelEditor)
.useFieldConfig()
.setPanelOptions(builder => {
addStandardDataReduceOptions(builder);
builder
.addRadio({
id: 'colorMode',
path: 'colorMode',
name: 'Color mode',
description: 'Color either the value or the background',
settings: {
@@ -23,7 +24,7 @@ export const plugin = new PanelPlugin<StatPanelOptions>(StatPanel)
},
})
.addRadio({
id: 'graphMode',
path: 'graphMode',
name: 'Graph mode',
description: 'Stat panel graph / sparkline mode',
settings: {
@@ -34,7 +35,7 @@ export const plugin = new PanelPlugin<StatPanelOptions>(StatPanel)
},
})
.addRadio({
id: 'justifyMode',
path: 'justifyMode',
name: 'Justify mode',
description: 'Value & title posititioning',
settings: {
@@ -47,5 +48,4 @@ export const plugin = new PanelPlugin<StatPanelOptions>(StatPanel)
})
.setNoPadding()
.setPanelChangeHandler(sharedSingleStatPanelChangedHandler)
.setMigrationHandler(sharedSingleStatMigrationHandler)
.useStandardFieldConfig();
.setMigrationHandler(sharedSingleStatMigrationHandler);

View File

@@ -1,13 +1,5 @@
import { SingleStatBaseOptions, BigValueColorMode, BigValueGraphMode, BigValueJustifyMode } from '@grafana/ui';
import {
VizOrientation,
ReducerID,
ReduceDataOptions,
SelectableValue,
ThresholdsMode,
standardEditorsRegistry,
FieldConfigProperty,
} from '@grafana/data';
import { VizOrientation, ReducerID, ReduceDataOptions, SelectableValue, standardEditorsRegistry } from '@grafana/data';
import { PanelOptionsEditorBuilder } from '@grafana/data/src/utils/OptionsUIBuilders';
// Structure copied from angular
@@ -37,20 +29,9 @@ export const commonValueOptionDefaults: ReduceDataOptions = {
calcs: [ReducerID.mean],
};
export const standardFieldConfigDefaults: Partial<Record<FieldConfigProperty, any>> = {
[FieldConfigProperty.Thresholds]: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: 80, color: 'red' },
],
},
[FieldConfigProperty.Mappings]: [],
};
export function addStandardDataReduceOptions(builder: PanelOptionsEditorBuilder<StatPanelOptions>) {
builder.addRadio({
id: 'reduceOptions.values',
path: 'reduceOptions.values',
name: 'Show',
description: 'Calculate a single value per colum or series or show each row',
settings: {
@@ -62,7 +43,7 @@ export function addStandardDataReduceOptions(builder: PanelOptionsEditorBuilder<
});
builder.addNumberInput({
id: 'reduceOptions.limit',
path: 'reduceOptions.limit',
name: 'Limit',
description: 'Max number of rows to display',
settings: {
@@ -75,13 +56,14 @@ export function addStandardDataReduceOptions(builder: PanelOptionsEditorBuilder<
builder.addCustomEditor({
id: 'reduceOptions.calcs',
path: 'reduceOptions.calcs',
name: 'Value',
description: 'Choose a reducer function / calculation',
editor: standardEditorsRegistry.get('stats-picker').editor as any,
});
builder.addRadio({
id: 'orientation',
path: 'orientation',
name: 'Orientation',
description: 'Stacking direction in case of multiple series or fields',
settings: {

View File

@@ -4,35 +4,37 @@ import { CustomFieldConfig, defaults, Options } from './types';
export const plugin = new PanelPlugin<Options, CustomFieldConfig>(TablePanel)
.setDefaults(defaults)
.setCustomFieldOptions(builder => {
builder
.addNumberInput({
id: 'width',
name: 'Column width',
description: 'column width (for table)',
settings: {
placeholder: 'auto',
min: 20,
max: 300,
},
})
.addSelect({
id: 'displayMode',
name: 'Cell display mode',
description: 'Color value, background, show as gauge, etc',
settings: {
options: [
{ value: 'auto', label: 'Auto' },
{ value: 'color-background', label: 'Color background' },
{ value: 'gradient-gauge', label: 'Gradient gauge' },
{ value: 'lcd-gauge', label: 'LCD gauge' },
],
},
});
.useFieldConfig({
useCustomConfig: builder => {
builder
.addNumberInput({
path: 'width',
name: 'Column width',
description: 'column width (for table)',
settings: {
placeholder: 'auto',
min: 20,
max: 300,
},
})
.addSelect({
path: 'displayMode',
name: 'Cell display mode',
description: 'Color value, background, show as gauge, etc',
settings: {
options: [
{ value: 'auto', label: 'Auto' },
{ value: 'color-background', label: 'Color background' },
{ value: 'gradient-gauge', label: 'Gradient gauge' },
{ value: 'lcd-gauge', label: 'LCD gauge' },
],
},
});
},
})
.setPanelOptions(builder => {
builder.addBooleanSwitch({
id: 'showHeader',
path: 'showHeader',
name: 'Show header',
description: "To display table's header or not to display",
});