mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -0,0 +1,4 @@
|
||||
import { Registry } from '../utils';
|
||||
import { FieldPropertyEditorItem } from '../types';
|
||||
|
||||
export class FieldConfigOptionsRegistry extends Registry<FieldPropertyEditorItem> {}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>>();
|
||||
|
||||
@@ -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: [],
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
170
packages/grafana-data/src/utils/tests/mockStandardProperties.ts
Normal file
170
packages/grafana-data/src/utils/tests/mockStandardProperties.ts
Normal 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];
|
||||
};
|
||||
@@ -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({
|
||||
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 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}</>;
|
||||
};
|
||||
|
||||
@@ -60,7 +60,6 @@ export const OptionsPaneContent: React.FC<{
|
||||
plugin={plugin}
|
||||
onChange={onFieldConfigsChange}
|
||||
data={data.series}
|
||||
include={plugin.standardFieldConfigProperties}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ describe('PanelQueryRunner', () => {
|
||||
},
|
||||
{
|
||||
getFieldOverrideOptions: () => ({
|
||||
fieldOptions: {
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
unit: 'm/s',
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('getFieldDisplayValuesProxy', () => {
|
||||
],
|
||||
}),
|
||||
],
|
||||
fieldOptions: {
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
},
|
||||
|
||||
@@ -129,7 +129,7 @@ describe('getLinksFromLogsField', () => {
|
||||
],
|
||||
}),
|
||||
],
|
||||
fieldOptions: {
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user