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>
pull/23374/head
Torkel Ödegaard 6 years ago committed by GitHub
parent 6347a1f1eb
commit b10392733d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      packages/grafana-data/src/field/FieldConfigOptionsRegistry.tsx
  2. 146
      packages/grafana-data/src/field/fieldOverrides.test.ts
  3. 44
      packages/grafana-data/src/field/fieldOverrides.ts
  4. 1
      packages/grafana-data/src/field/index.ts
  5. 4
      packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts
  6. 158
      packages/grafana-data/src/panel/PanelPlugin.test.tsx
  7. 223
      packages/grafana-data/src/panel/PanelPlugin.ts
  8. 2
      packages/grafana-data/src/types/OptionsUIRegistryBuilder.ts
  9. 16
      packages/grafana-data/src/types/fieldOverrides.ts
  10. 2
      packages/grafana-data/src/types/panel.ts
  11. 14
      packages/grafana-data/src/utils/OptionsUIBuilders.ts
  12. 2
      packages/grafana-data/src/utils/Registry.ts
  13. 170
      packages/grafana-data/src/utils/tests/mockStandardProperties.ts
  14. 126
      packages/grafana-ui/src/components/FieldConfigs/fieldOverrides.test.ts
  15. 20
      packages/grafana-ui/src/components/Table/Table.story.tsx
  16. 28
      packages/grafana-ui/src/utils/standardEditors.tsx
  17. 2
      public/app/features/dashboard/components/Inspector/PanelInspector.tsx
  18. 8
      public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx
  19. 69
      public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx
  20. 1
      public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx
  21. 40
      public/app/features/dashboard/components/PanelEditor/OverrideEditor.tsx
  22. 2
      public/app/features/dashboard/components/PanelEditor/PanelOptionsEditor.tsx
  23. 55
      public/app/features/dashboard/state/PanelModel.test.ts
  24. 4
      public/app/features/dashboard/state/PanelModel.ts
  25. 2
      public/app/features/dashboard/state/PanelQueryRunner.test.ts
  26. 2
      public/app/features/panel/panellinks/fieldDisplayValuesProxy.test.ts
  27. 2
      public/app/features/panel/panellinks/linkSuppliers.test.ts
  28. 8
      public/app/plugins/panel/bargauge/module.tsx
  29. 9
      public/app/plugins/panel/gauge/module.tsx
  30. 5
      public/app/plugins/panel/piechart/module.tsx
  31. 10
      public/app/plugins/panel/stat/module.tsx
  32. 28
      public/app/plugins/panel/stat/types.ts
  33. 54
      public/app/plugins/panel/table2/module.tsx

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

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

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

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

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

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

@ -1,5 +1,4 @@
import { import {
FieldConfigEditorRegistry,
FieldConfigSource, FieldConfigSource,
GrafanaPlugin, GrafanaPlugin,
PanelEditorProps, PanelEditorProps,
@ -9,55 +8,34 @@ import {
PanelProps, PanelProps,
PanelTypeChangedHandler, PanelTypeChangedHandler,
FieldConfigProperty, FieldConfigProperty,
ThresholdsMode,
} from '../types'; } from '../types';
import { FieldConfigEditorBuilder, PanelOptionsEditorBuilder } from '../utils/OptionsUIBuilders'; import { FieldConfigEditorBuilder, PanelOptionsEditorBuilder } from '../utils/OptionsUIBuilders';
import { ComponentClass, ComponentType } from 'react'; import { ComponentClass, ComponentType } from 'react';
import set from 'lodash/set'; import set from 'lodash/set';
import { deprecationWarning } from '../utils'; import { deprecationWarning } from '../utils';
import { FieldConfigOptionsRegistry, standardFieldConfigEditorRegistry } from '../field';
export const allStandardFieldConfigProperties: FieldConfigProperty[] = [ export interface SetFieldConfigOptionsArgs<TFieldConfigOptions = any> {
FieldConfigProperty.Min, standardOptions?: FieldConfigProperty[];
FieldConfigProperty.Max, standardOptionsDefaults?: Partial<Record<FieldConfigProperty, any>>;
FieldConfigProperty.Title, useCustomConfig?: (builder: FieldConfigEditorBuilder<TFieldConfigOptions>) => void;
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 class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = any> extends GrafanaPlugin< export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = any> extends GrafanaPlugin<
PanelPluginMeta PanelPluginMeta
> { > {
private _defaults?: TOptions; private _defaults?: TOptions;
private _standardFieldConfigProperties?: Map<FieldConfigProperty, any>;
private _fieldConfigDefaults: FieldConfigSource<TFieldConfigOptions> = { private _fieldConfigDefaults: FieldConfigSource<TFieldConfigOptions> = {
defaults: {}, defaults: {},
overrides: [], overrides: [],
}; };
private _customFieldConfigs?: FieldConfigEditorRegistry;
private customFieldConfigsUIBuilder = new FieldConfigEditorBuilder<TFieldConfigOptions>(); private _fieldConfigRegistry?: FieldConfigOptionsRegistry;
private registerCustomFieldConfigs?: (builder: FieldConfigEditorBuilder<TFieldConfigOptions>) => void; private _initConfigRegistry = () => {
return new FieldConfigOptionsRegistry();
};
private _optionEditors?: PanelOptionEditorsRegistry; private _optionEditors?: PanelOptionEditorsRegistry;
private optionsUIBuilder = new PanelOptionsEditorBuilder<TOptions>();
private registerOptionEditors?: (builder: PanelOptionsEditorBuilder<TOptions>) => void; private registerOptionEditors?: (builder: PanelOptionsEditorBuilder<TOptions>) => void;
panel: ComponentType<PanelProps<TOptions>>; panel: ComponentType<PanelProps<TOptions>>;
@ -94,39 +72,21 @@ export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = an
} }
get fieldConfigDefaults(): FieldConfigSource<TFieldConfigOptions> { 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 option of this.fieldConfigRegistry.list()) {
for (const editor of editors.list()) { set(configDefaults, option.id, option.defaultValue);
set(customPropertiesDefaults, editor.id, editor.defaultValue);
}
} }
return { return {
defaults: { defaults: {
...(this._standardFieldConfigProperties ? Object.fromEntries(this._standardFieldConfigProperties) : {}), ...configDefaults,
custom:
Object.keys(customPropertiesDefaults).length > 0
? {
...customPropertiesDefaults,
}
: undefined,
...this._fieldConfigDefaults.defaults,
}, },
// TODO: not sure yet what about overrides, if anything
overrides: this._fieldConfigDefaults.overrides, overrides: this._fieldConfigDefaults.overrides,
}; };
} }
get standardFieldConfigProperties() {
return this._standardFieldConfigProperties ? Array.from(this._standardFieldConfigProperties.keys()) : [];
}
/** /**
* @deprecated setDefaults is deprecated in favor of setPanelOptions * @deprecated setDefaults is deprecated in favor of setPanelOptions
*/ */
@ -136,19 +96,19 @@ export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = an
return this; return this;
} }
get customFieldConfigs() { get fieldConfigRegistry() {
if (!this._customFieldConfigs && this.registerCustomFieldConfigs) { if (!this._fieldConfigRegistry) {
this.registerCustomFieldConfigs(this.customFieldConfigsUIBuilder); this._fieldConfigRegistry = this._initConfigRegistry();
this._customFieldConfigs = this.customFieldConfigsUIBuilder.getRegistry();
} }
return this._customFieldConfigs; return this._fieldConfigRegistry;
} }
get optionEditors() { get optionEditors() {
if (!this._optionEditors && this.registerOptionEditors) { if (!this._optionEditors && this.registerOptionEditors) {
this.registerOptionEditors(this.optionsUIBuilder); const builder = new PanelOptionsEditorBuilder<TOptions>();
this._optionEditors = this.optionsUIBuilder.getRegistry(); this.registerOptionEditors(builder);
this._optionEditors = builder.getRegistry();
} }
return this._optionEditors; return this._optionEditors;
@ -188,47 +148,6 @@ export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = an
return this; 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 * 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 * // when plugin should use all standard options
* export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel) * export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel)
* .useStandardFieldConfig(); * .useFieldConfig();
* *
* // when plugin should only display specific standard options * // when plugin should only display specific standard options
* // note, that options will be displayed in the order they are provided * // note, that options will be displayed in the order they are provided
* export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel) * 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 * // when standard option's default value needs to be provided
* export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel) * export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel)
* .useStandardFieldConfig([FieldConfigProperty.Min, FieldConfigProperty.Max], { * .useFieldConfig({
* [FieldConfigProperty.Min]: 20, * standardOptions: [FieldConfigProperty.Min, FieldConfigProperty.Max],
* [FieldConfigProperty.Max]: 100 * 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 * @public
*/ */
useStandardFieldConfig( useFieldConfig(config?: SetFieldConfigOptionsArgs<TFieldConfigOptions>) {
properties?: FieldConfigProperty[] | null, // builder is applied lazily when custom field configs are accessed
customDefaults?: Partial<Record<FieldConfigProperty, any>> this._initConfigRegistry = () => {
) { const registry = new FieldConfigOptionsRegistry();
if (!properties) {
this._standardFieldConfigProperties = standardFieldConfigProperties; // Add custom options
return this; if (config && config.useCustomConfig) {
} else { const builder = new FieldConfigEditorBuilder<TFieldConfigOptions>();
this._standardFieldConfigProperties = new Map(properties.map(p => [p, standardFieldConfigProperties.get(p)])); config.useCustomConfig(builder);
}
for (const customProp of builder.getRegistry().list()) {
const defaults = customDefaults ?? standardFieldConfigDefaults; 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 (defaults) { if (config && config.standardOptions) {
Object.keys(defaults).map(k => { for (const standardOption of config.standardOptions) {
if (properties.indexOf(k as FieldConfigProperty) > -1) { const standardEditor = standardFieldConfigEditorRegistry.get(standardOption);
this._standardFieldConfigProperties!.set(k as FieldConfigProperty, defaults[k as FieldConfigProperty]); 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; return this;
} }
} }

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

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

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

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

@ -124,7 +124,7 @@ export class Registry<T extends RegistryItem> {
if (!this.initialized) { if (!this.initialized) {
this.getIfExists('xxx'); // will trigger init 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) { register(ext: T) {

@ -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];
};

@ -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);
});
});

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

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

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

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

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

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

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

@ -22,7 +22,7 @@ export const PanelOptionsEditor: React.FC<PanelOptionsEditorProps<any>> = ({ plu
{optionEditors.list().map(e => { {optionEditors.list().map(e => {
return ( return (
<Forms.Field label={e.name} description={e.description} key={e.id}> <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> </Forms.Field>
); );
})} })}

@ -1,10 +1,46 @@
import { PanelModel } from './PanelModel'; import { PanelModel } from './PanelModel';
import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks'; import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
import { PanelProps, FieldConfigProperty } from '@grafana/data'; import {
FieldConfigProperty,
identityOverrideProcessor,
PanelProps,
standardFieldConfigEditorRegistry,
} from '@grafana/data';
import { ComponentClass } from 'react'; import { ComponentClass } from 'react';
class TablePanelCtrl {} 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('PanelModel', () => {
describe('when creating new panel model', () => { describe('when creating new panel model', () => {
let model: any; let model: any;
@ -79,9 +115,16 @@ describe('PanelModel', () => {
TablePanelCtrl // angular TablePanelCtrl // angular
); );
panelPlugin.setDefaults(defaultOptionsMock); panelPlugin.setDefaults(defaultOptionsMock);
panelPlugin.useStandardFieldConfig([FieldConfigProperty.Unit, FieldConfigProperty.Decimals], { /* panelPlugin.useStandardFieldConfig([FieldConfigOptionId.Unit, FieldConfigOptionId.Decimals], {
[FieldConfigProperty.Unit]: 'flop', [FieldConfigOptionId.Unit]: 'flop',
[FieldConfigProperty.Decimals]: 2, [FieldConfigOptionId.Decimals]: 2,
}); */
panelPlugin.useFieldConfig({
standardOptions: [FieldConfigProperty.Unit, FieldConfigProperty.Decimals],
standardOptionsDefaults: {
[FieldConfigProperty.Unit]: 'flop',
[FieldConfigProperty.Decimals]: 2,
},
}); });
model.pluginLoaded(panelPlugin); model.pluginLoaded(panelPlugin);
}); });
@ -100,9 +143,9 @@ describe('PanelModel', () => {
it('should apply field config defaults', () => { it('should apply field config defaults', () => {
// default unit is overriden by model // 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 // 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', () => { it('should set model props on instance', () => {

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save