diff --git a/packages/grafana-data/src/field/FieldConfigOptionsRegistry.tsx b/packages/grafana-data/src/field/FieldConfigOptionsRegistry.tsx index 30af765b20b..eb9ac4e058a 100644 --- a/packages/grafana-data/src/field/FieldConfigOptionsRegistry.tsx +++ b/packages/grafana-data/src/field/FieldConfigOptionsRegistry.tsx @@ -1,4 +1,4 @@ import { Registry } from '../utils'; -import { FieldPropertyEditorItem } from '../types'; +import { FieldConfigPropertyItem } from '../types'; -export class FieldConfigOptionsRegistry extends Registry {} +export class FieldConfigOptionsRegistry extends Registry {} diff --git a/packages/grafana-data/src/field/fieldOverrides.test.ts b/packages/grafana-data/src/field/fieldOverrides.test.ts index 2bb2818b58c..5a44485e87d 100644 --- a/packages/grafana-data/src/field/fieldOverrides.test.ts +++ b/packages/grafana-data/src/field/fieldOverrides.test.ts @@ -2,12 +2,13 @@ import { FieldOverrideEnv, findNumericFieldMinMax, setFieldConfigDefaults, + setDynamicConfigValue, applyFieldOverrides, } from './fieldOverrides'; import { MutableDataFrame, toDataFrame } from '../dataframe'; import { FieldConfig, - FieldPropertyEditorItem, + FieldConfigPropertyItem, GrafanaTheme, FieldType, DataFrame, @@ -22,6 +23,7 @@ import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry'; const property1 = { id: 'custom.property1', // Match field properties path: 'property1', // Match field properties + isCustom: true, process: (value: any) => value, shouldApply: () => true, } as any; @@ -29,12 +31,21 @@ const property1 = { const property2 = { id: 'custom.property2', // Match field properties path: 'property2', // Match field properties + isCustom: true, process: (value: any) => value, shouldApply: () => true, } as any; -export const customFieldRegistry: FieldConfigOptionsRegistry = new Registry(() => { - return [property1, property2, ...mockStandardProperties()]; +const property3 = { + id: 'custom.property3.nested', // Match field properties + path: 'property3.nested', // Match field properties + isCustom: true, + process: (value: any) => value, + shouldApply: () => true, +} as any; + +export const customFieldRegistry: FieldConfigOptionsRegistry = new Registry(() => { + return [property1, property2, property3, ...mockStandardProperties()]; }); describe('Global MinMax', () => { @@ -241,6 +252,7 @@ describe('setFieldConfigDefaults', () => { expect(dsFieldConfig).toMatchInlineSnapshot(` Object { + "custom": Object {}, "decimals": 2, "max": 100, "min": 0, @@ -276,10 +288,125 @@ describe('setFieldConfigDefaults', () => { expect(dsFieldConfig).toMatchInlineSnapshot(` Object { "custom": Object { - "property1": 20, + "property1": 10, "property2": 10, }, } `); }); }); + +describe('setDynamicConfigValue', () => { + it('applies dynamic config values', () => { + const config = { + title: 'test', + // custom: { + // property1: 1, + // }, + }; + setDynamicConfigValue( + config, + { + id: 'title', + value: 'applied', + }, + { + fieldConfigRegistry: customFieldRegistry, + data: [] as any, + field: { type: FieldType.number } as any, + dataFrameIndex: 0, + } + ); + + expect(config.title).toEqual('applied'); + }); + + it('applies custom dynamic config values', () => { + const config = { + custom: { + property1: 1, + }, + }; + setDynamicConfigValue( + config, + { + id: 'custom.property1', + value: 'applied', + }, + { + fieldConfigRegistry: customFieldRegistry, + data: [] as any, + field: { type: FieldType.number } as any, + dataFrameIndex: 0, + } + ); + + expect(config.custom.property1).toEqual('applied'); + }); + + it('applies nested custom dynamic config values', () => { + const config = { + custom: { + property3: { + nested: 1, + }, + }, + }; + setDynamicConfigValue( + config, + { + id: 'custom.property3.nested', + value: 'applied', + }, + { + fieldConfigRegistry: customFieldRegistry, + data: [] as any, + field: { type: FieldType.number } as any, + dataFrameIndex: 0, + } + ); + + expect(config.custom.property3.nested).toEqual('applied'); + }); + + it('removes properties', () => { + const config = { + title: 'title', + custom: { + property3: { + nested: 1, + }, + }, + }; + setDynamicConfigValue( + config, + { + id: 'custom.property3.nested', + value: undefined, + }, + { + fieldConfigRegistry: customFieldRegistry, + data: [] as any, + field: { type: FieldType.number } as any, + dataFrameIndex: 0, + } + ); + + setDynamicConfigValue( + config, + { + id: 'title', + value: undefined, + }, + { + fieldConfigRegistry: customFieldRegistry, + data: [] as any, + field: { type: FieldType.number } as any, + dataFrameIndex: 0, + } + ); + + expect(config.custom.property3).toEqual({}); + expect(config.title).toBeUndefined(); + }); +}); diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 126693a2d3d..51bafa9fa80 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -10,10 +10,14 @@ import { FieldOverrideContext, ScopedVars, ApplyFieldOverrideOptions, + FieldConfigPropertyItem, } from '../types'; import { fieldMatchers, ReducerID, reduceField } from '../transformations'; import { FieldMatcher } from '../types/transformations'; import isNumber from 'lodash/isNumber'; +import set from 'lodash/set'; +import unset from 'lodash/unset'; +import get from 'lodash/get'; import { getDisplayProcessor } from './displayProcessor'; import { guessFieldTypeForField } from '../dataframe'; import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry'; @@ -193,9 +197,8 @@ export interface FieldOverrideEnv extends FieldOverrideContext { fieldConfigRegistry: FieldConfigOptionsRegistry; } -function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigValue, context: FieldOverrideEnv) { +export function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigValue, context: FieldOverrideEnv) { const reg = context.fieldConfigRegistry; - const item = reg.getIfExists(value.id); if (!item || !item.shouldApply(context.field!)) { return; @@ -206,19 +209,19 @@ function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigValue, c const remove = val === undefined || val === null; if (remove) { - if (value.isCustom && config.custom) { - delete config.custom[item.path]; + if (item.isCustom && config.custom) { + unset(config.custom, item.path); } else { - delete (config as any)[item.path]; + unset(config, item.path); } } else { - if (value.isCustom) { + if (item.isCustom) { if (!config.custom) { config.custom = {}; } - config.custom[item.path] = val; + set(config.custom, item.path, val); } else { - (config as any)[item.path] = val; + set(config, item.path, val); } } } @@ -226,26 +229,16 @@ function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigValue, c // config -> from DS // defaults -> from Panel config export function setFieldConfigDefaults(config: FieldConfig, defaults: FieldConfig, context: FieldOverrideEnv) { - if (defaults) { - const keys = Object.keys(defaults); - for (const key of keys) { - if (key === 'custom') { - if (!context.fieldConfigRegistry) { - continue; - } - if (!config.custom) { - config.custom = {}; - } - - const customKeys = Object.keys(defaults.custom!); - for (const customKey of customKeys) { - processFieldConfigValue(config.custom!, defaults.custom!, `custom.${customKey}`, context); - } - } else { - // when config from ds exists for a given field -> use it - processFieldConfigValue(config, defaults, key, context); - } + for (const fieldConfigProperty of context.fieldConfigRegistry.list()) { + if (fieldConfigProperty.isCustom && !config.custom) { + config.custom = {}; } + processFieldConfigValue( + fieldConfigProperty.isCustom ? config.custom : config, + fieldConfigProperty.isCustom ? defaults.custom : defaults, + fieldConfigProperty, + context + ); } validateFieldConfig(config); @@ -254,20 +247,21 @@ export function setFieldConfigDefaults(config: FieldConfig, defaults: FieldConfi const processFieldConfigValue = ( destination: Record, // it's mutable source: Record, - key: string, + fieldConfigProperty: FieldConfigPropertyItem, context: FieldOverrideEnv ) => { - const currentConfig = destination[key]; + const currentConfig = get(destination, fieldConfigProperty.path); + if (currentConfig === null || currentConfig === undefined) { - const item = context.fieldConfigRegistry.getIfExists(key); + const item = context.fieldConfigRegistry.getIfExists(fieldConfigProperty.id); if (!item) { return; } if (item && item.shouldApply(context.field!)) { - const val = item.process(source[item.path], context, item.settings); + const val = item.process(get(source, item.path), context, item.settings); if (val !== undefined && val !== null) { - destination[item.path] = val; + set(destination, item.path, val); } } } diff --git a/packages/grafana-data/src/panel/PanelPlugin.ts b/packages/grafana-data/src/panel/PanelPlugin.ts index 7c517fc4c12..b30d547e0d7 100644 --- a/packages/grafana-data/src/panel/PanelPlugin.ts +++ b/packages/grafana-data/src/panel/PanelPlugin.ts @@ -276,7 +276,6 @@ export class PanelPlugin extends Omit, 'item'> { - item: FieldPropertyEditorItem; // The property info + item: FieldConfigPropertyItem; // The property info value: TValue; context: FieldOverrideContext; onChange: (value?: TValue) => void; } export interface FieldOverrideEditorProps extends Omit, 'item'> { - item: FieldPropertyEditorItem; + item: FieldConfigPropertyItem; context: FieldOverrideContext; } @@ -63,7 +62,7 @@ export interface FieldConfigEditorConfig +export interface FieldConfigPropertyItem extends OptionsEditorItem, TValue> { // An editor that can be filled in with context info (template variables etc) override: ComponentType>; @@ -85,7 +84,7 @@ export interface ApplyFieldOverrideOptions { theme: GrafanaTheme; timeZone?: TimeZone; autoMinMax?: boolean; - fieldConfigRegistry?: Registry; + fieldConfigRegistry?: Registry; } export enum FieldConfigProperty { diff --git a/packages/grafana-data/src/utils/OptionsUIBuilders.ts b/packages/grafana-data/src/utils/OptionsUIBuilders.ts index ed4e25e8ff2..7740ba7c277 100644 --- a/packages/grafana-data/src/utils/OptionsUIBuilders.ts +++ b/packages/grafana-data/src/utils/OptionsUIBuilders.ts @@ -1,7 +1,7 @@ import { FieldType, FieldConfigEditorProps, - FieldPropertyEditorItem, + FieldConfigPropertyItem, PanelOptionsEditorConfig, PanelOptionsEditorItem, FieldConfigEditorConfig, @@ -29,7 +29,7 @@ import { export class FieldConfigEditorBuilder extends OptionsUIRegistryBuilder< TOptions, FieldConfigEditorProps, - FieldPropertyEditorItem + FieldConfigPropertyItem > { addNumberInput(config: FieldConfigEditorConfig) { return this.addCustomEditor({ diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index 8c7a0ff1d50..99b382a207e 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -105,8 +105,8 @@ export const BarGaugeCell = () => { { matcher: { id: FieldMatcherID.byName, options: 'Progress' }, properties: [ - { id: 'width', value: '200', isCustom: true }, - { id: 'displayMode', value: 'gradient-gauge', isCustom: true }, + { id: 'width', value: '200' }, + { id: 'displayMode', value: 'gradient-gauge' }, { id: 'min', value: '0' }, { id: 'max', value: '100' }, ], @@ -141,8 +141,8 @@ export const ColoredCells = () => { { matcher: { id: FieldMatcherID.byName, options: 'Progress' }, properties: [ - { id: 'width', value: '80', isCustom: true }, - { id: 'displayMode', value: 'color-background', isCustom: true }, + { id: 'width', value: '80' }, + { id: 'displayMode', value: 'color-background' }, { id: 'min', value: '0' }, { id: 'max', value: '100' }, { id: 'thresholds', value: defaultThresholds }, diff --git a/packages/grafana-ui/src/utils/standardEditors.tsx b/packages/grafana-ui/src/utils/standardEditors.tsx index 3fabc7fd04b..32025c9a90f 100644 --- a/packages/grafana-ui/src/utils/standardEditors.tsx +++ b/packages/grafana-ui/src/utils/standardEditors.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { DataLink, dataLinksOverrideProcessor, - FieldPropertyEditorItem, + FieldConfigPropertyItem, FieldType, identityOverrideProcessor, NumberFieldConfigSettings, @@ -31,7 +31,7 @@ import { StatsPickerEditor } from '../components/OptionsUI/stats'; * Returns collection of common field config properties definitions */ export const getStandardFieldConfigs = () => { - const title: FieldPropertyEditorItem = { + const title: FieldConfigPropertyItem = { id: 'title', path: 'title', name: 'Title', @@ -46,7 +46,7 @@ export const getStandardFieldConfigs = () => { shouldApply: field => field.type !== FieldType.time, }; - const unit: FieldPropertyEditorItem = { + const unit: FieldConfigPropertyItem = { id: 'unit', path: 'unit', name: 'Unit', @@ -63,7 +63,7 @@ export const getStandardFieldConfigs = () => { shouldApply: field => field.type === FieldType.number, }; - const min: FieldPropertyEditorItem = { + const min: FieldConfigPropertyItem = { id: 'min', path: 'min', name: 'Min', @@ -79,7 +79,7 @@ export const getStandardFieldConfigs = () => { shouldApply: field => field.type === FieldType.number, }; - const max: FieldPropertyEditorItem = { + const max: FieldConfigPropertyItem = { id: 'max', path: 'max', name: 'Max', @@ -96,7 +96,7 @@ export const getStandardFieldConfigs = () => { shouldApply: field => field.type === FieldType.number, }; - const decimals: FieldPropertyEditorItem = { + const decimals: FieldConfigPropertyItem = { id: 'decimals', path: 'decimals', name: 'Decimals', @@ -116,7 +116,7 @@ export const getStandardFieldConfigs = () => { shouldApply: field => field.type === FieldType.number, }; - const thresholds: FieldPropertyEditorItem = { + const thresholds: FieldConfigPropertyItem = { id: 'thresholds', path: 'thresholds', name: 'Thresholds', @@ -136,7 +136,7 @@ export const getStandardFieldConfigs = () => { shouldApply: field => field.type === FieldType.number, }; - const mappings: FieldPropertyEditorItem = { + const mappings: FieldConfigPropertyItem = { id: 'mappings', path: 'mappings', name: 'Value mappings', @@ -150,7 +150,7 @@ export const getStandardFieldConfigs = () => { shouldApply: field => field.type === FieldType.number, }; - const noValue: FieldPropertyEditorItem = { + const noValue: FieldConfigPropertyItem = { id: 'noValue', path: 'noValue', name: 'No Value', @@ -167,7 +167,7 @@ export const getStandardFieldConfigs = () => { shouldApply: () => true, }; - const links: FieldPropertyEditorItem = { + const links: FieldConfigPropertyItem = { id: 'links', path: 'links', name: 'DataLinks', @@ -181,7 +181,7 @@ export const getStandardFieldConfigs = () => { shouldApply: () => true, }; - const color: FieldPropertyEditorItem = { + const color: FieldConfigPropertyItem = { id: 'color', path: 'color', name: 'Color', diff --git a/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx b/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx index 194a9219c4d..2d0aa0fe236 100644 --- a/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx @@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep'; import { FieldConfigSource, DataFrame, - FieldPropertyEditorItem, + FieldConfigPropertyItem, VariableSuggestionsScope, PanelPlugin, SelectableValue, @@ -142,7 +142,7 @@ export const DefaultFieldConfigEditor: React.FC = ({ data, onChange, conf ); const renderEditor = useCallback( - (item: FieldPropertyEditorItem) => { + (item: FieldConfigPropertyItem) => { const defaults = config.defaults; const value = item.isCustom ? defaults.custom diff --git a/public/app/features/dashboard/components/PanelEditor/OverrideEditor.tsx b/public/app/features/dashboard/components/PanelEditor/OverrideEditor.tsx index b34901f4750..707bbc543db 100644 --- a/public/app/features/dashboard/components/PanelEditor/OverrideEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OverrideEditor.tsx @@ -49,10 +49,9 @@ export const OverrideEditor: React.FC = ({ data, override, ); const onDynamicConfigValueAdd = useCallback( - (id: string, custom?: boolean) => { + (id: string) => { const propertyConfig: DynamicConfigValue = { id, - isCustom: custom, }; if (override.properties) { override.properties.push(propertyConfig); @@ -69,7 +68,6 @@ export const OverrideEditor: React.FC = ({ data, override, label: item.name, value: item.id, description: item.description, - custom: item.isCustom, }; }); @@ -117,7 +115,7 @@ export const OverrideEditor: React.FC = ({ data, override, options={configPropertiesOptions} variant={'link'} onChange={o => { - onDynamicConfigValueAdd(o.value, o.custom); + onDynamicConfigValueAdd(o.value); }} />