PanelOptions: Refactoring applying panel and field options out of PanelModel and add property clean up for properties not in field config registry (#30389)

* PanelOptions: Refactoring on applying panel and field options from PanelModel

* Progress

* Filtering out props

* downgraded prettier

* Fixes

* Initial simple remember and restore for custom and overrides

* clearing custom options and overrides and restoring works

* actually use the function

* Added type for options cache

* minor fix

* Updated with new prettier

* Added old field config to panel type change handler

* Update public/app/features/dashboard/state/getPanelOptionsWithDefaults.test.ts

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Torkel Ödegaard 2021-01-20 16:06:29 +01:00 committed by GitHub
parent b40b134e4c
commit 15033d0011
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 763 additions and 201 deletions

View File

@ -18,5 +18,5 @@ export {
BasicValueMatcherOptions,
RangeValueMatcherOptions,
} from './transformations/matchers/valueMatchers/types';
export { PanelPlugin, SetFieldConfigOptionsArgs } from './panel/PanelPlugin';
export { PanelPlugin, SetFieldConfigOptionsArgs, StandardOptionConfig } from './panel/PanelPlugin';
export { createFieldConfigRegistry } from './panel/registryFactories';

View File

@ -16,7 +16,8 @@ import { deprecationWarning } from '../utils';
import { FieldConfigOptionsRegistry } from '../field';
import { createFieldConfigRegistry } from './registryFactories';
type StandardOptionConfig = {
/** @beta */
export type StandardOptionConfig = {
defaultValue?: any;
settings?: any;
};
@ -130,6 +131,7 @@ export class PanelPlugin<
set(result, editor.id, editor.defaultValue);
}
}
return result;
}
@ -138,6 +140,10 @@ export class PanelPlugin<
configDefaults.custom = {} as TFieldConfigOptions;
for (const option of this.fieldConfigRegistry.list()) {
if (option.defaultValue === undefined) {
continue;
}
set(configDefaults, option.id, option.defaultValue);
}

View File

@ -131,7 +131,8 @@ export type PanelMigrationHandler<TOptions = any> = (panel: PanelModel<TOptions>
export type PanelTypeChangedHandler<TOptions = any> = (
panel: PanelModel<TOptions>,
prevPluginId: string,
prevOptions: any
prevOptions: Record<string, any>,
prevFieldConfig: FieldConfigSource
) => Partial<TOptions>;
export type PanelOptionEditorsRegistry = Registry<PanelOptionsEditorItem>;

View File

@ -6,8 +6,6 @@ import {
standardEditorsRegistry,
standardFieldConfigEditorRegistry,
PanelData,
FieldColorModeId,
FieldColorConfigSettings,
DataLinkBuiltInVars,
VariableModel,
} from '@grafana/data';
@ -51,6 +49,41 @@ describe('PanelModel', () => {
let modelJson: any;
let persistedOptionsMock;
const tablePlugin = getPanelPlugin(
{
id: 'table',
},
(null as unknown) as ComponentClass<PanelProps>, // react
{} // angular
);
tablePlugin.setPanelOptions((builder) => {
builder.addBooleanSwitch({
name: 'Show thresholds',
path: 'showThresholds',
defaultValue: true,
description: '',
});
});
tablePlugin.useFieldConfig({
standardOptions: {
[FieldConfigProperty.Unit]: {
defaultValue: 'flop',
},
[FieldConfigProperty.Decimals]: {
defaultValue: 2,
},
},
useCustomConfig: (builder) => {
builder.addBooleanSwitch({
name: 'CustomProp',
path: 'customProp',
defaultValue: false,
});
},
});
beforeEach(() => {
persistedOptionsMock = {
fieldOptions: {
@ -112,35 +145,7 @@ describe('PanelModel', () => {
};
model = new PanelModel(modelJson);
const panelPlugin = getPanelPlugin(
{
id: 'table',
},
(null as unknown) as ComponentClass<PanelProps>, // react
{} // angular
);
panelPlugin.setPanelOptions((builder) => {
builder.addBooleanSwitch({
name: 'Show thresholds',
path: 'showThresholds',
defaultValue: true,
description: '',
});
});
panelPlugin.useFieldConfig({
standardOptions: {
[FieldConfigProperty.Unit]: {
defaultValue: 'flop',
},
[FieldConfigProperty.Decimals]: {
defaultValue: 2,
},
},
});
model.pluginLoaded(panelPlugin);
model.pluginLoaded(tablePlugin);
});
it('should apply defaults', () => {
@ -240,6 +245,13 @@ describe('PanelModel', () => {
},
},
},
useCustomConfig: (builder) => {
builder.addNumberInput({
path: 'customProp',
name: 'customProp',
defaultValue: 100,
});
},
});
newPlugin.setPanelOptions((builder) => {
@ -252,6 +264,25 @@ describe('PanelModel', () => {
});
model.editSourceId = 1001;
model.fieldConfig.defaults.decimals = 3;
model.fieldConfig.defaults.custom = {
customProp: true,
};
model.fieldConfig.overrides = [
{
matcher: { id: 'byName', options: 'D-series' },
properties: [
{
id: 'custom.customProp',
value: false,
},
{
id: 'decimals',
value: 0,
},
],
},
];
model.changePlugin(newPlugin);
model.alert = { id: 2 };
});
@ -268,6 +299,21 @@ describe('PanelModel', () => {
expect(model.interval).toBe('5m');
});
it('should preseve standard field config', () => {
expect(model.fieldConfig.defaults.decimals).toEqual(3);
});
it('should clear custom field config and apply new defaults', () => {
expect(model.fieldConfig.defaults.custom).toEqual({
customProp: 100,
});
});
it('should remove overrides with custom props', () => {
expect(model.fieldConfig.overrides.length).toEqual(1);
expect(model.fieldConfig.overrides[0].properties[0].id).toEqual('decimals');
});
it('should apply next panel option defaults', () => {
expect(model.getOptions().showThresholdLabels).toBeFalsy();
expect(model.getOptions().showThresholds).toBeUndefined();
@ -278,76 +324,21 @@ describe('PanelModel', () => {
});
it('should restore table properties when changing back', () => {
model.changePlugin(getPanelPlugin({ id: 'table' }));
model.changePlugin(tablePlugin);
expect(model.showColumns).toBe(true);
});
it('should restore custom field config to what it was and preseve standard options', () => {
model.changePlugin(tablePlugin);
expect(model.fieldConfig.defaults.custom.customProp).toBe(true);
});
it('should remove alert rule when changing type that does not support it', () => {
model.changePlugin(getPanelPlugin({ id: 'table' }));
expect(model.alert).toBe(undefined);
});
});
describe('when changing panel type to one that does not support by value color mode', () => {
beforeEach(() => {
model.fieldConfig.defaults.color = { mode: FieldColorModeId.Thresholds };
const newPlugin = getPanelPlugin({ id: 'graph' });
newPlugin.useFieldConfig({
standardOptions: {
[FieldConfigProperty.Color]: {
settings: {
byValueSupport: false,
},
},
},
});
model.editSourceId = 1001;
model.changePlugin(newPlugin);
model.alert = { id: 2 };
});
it('should change color mode', () => {
expect(model.fieldConfig.defaults.color.mode).toBe(FieldColorModeId.PaletteClassic);
});
});
describe('when changing panel type from one not supporting by value color mode to one that supports it', () => {
const prepareModel = (colorOptions?: FieldColorConfigSettings) => {
const newModel = new PanelModel(modelJson);
newModel.fieldConfig.defaults.color = { mode: FieldColorModeId.PaletteClassic };
const newPlugin = getPanelPlugin({ id: 'graph' });
newPlugin.useFieldConfig({
standardOptions: {
[FieldConfigProperty.Color]: {
settings: {
byValueSupport: true,
...colorOptions,
},
},
},
});
newModel.editSourceId = 1001;
newModel.changePlugin(newPlugin);
newModel.alert = { id: 2 };
return newModel;
};
it('should keep supported mode', () => {
const testModel = prepareModel();
expect(testModel.fieldConfig.defaults.color!.mode).toBe(FieldColorModeId.PaletteClassic);
});
it('should change to thresholds mode when it prefers to', () => {
const testModel = prepareModel({ preferThresholdsMode: true });
expect(testModel.fieldConfig.defaults.color!.mode).toBe(FieldColorModeId.Thresholds);
});
});
describe('when changing to react panel from angular panel', () => {
let panelQueryRunner: any;

View File

@ -9,15 +9,9 @@ import {
DataLink,
DataQuery,
DataTransformerConfig,
FieldColorConfigSettings,
FieldColorModeId,
fieldColorModeRegistry,
FieldConfigProperty,
FieldConfigSource,
PanelPlugin,
ScopedVars,
ThresholdsConfig,
ThresholdsMode,
EventBusSrv,
DataFrameDTO,
urlUtil,
@ -35,6 +29,12 @@ import {
} from 'app/types/events';
import { getTimeSrv } from '../services/TimeSrv';
import { getAllVariableValuesForUrl } from '../../variables/getAllVariableValuesForUrl';
import {
filterFieldConfigOverrides,
getPanelOptionsWithDefaults,
isStandardFieldProp,
restoreCustomOverrideRules,
} from './getPanelOptionsWithDefaults';
export interface GridPos {
x: number;
@ -96,6 +96,7 @@ const mustKeepProps: { [str: string]: boolean } = {
editSourceId: true,
maxDataPoints: true,
interval: true,
replaceVariables: true,
};
const defaults: any = {
@ -154,7 +155,7 @@ export class PanelModel implements DataConfigSource {
hasRefreshed: boolean;
events: EventBusSrv;
cacheTimeout?: any;
cachedPluginOptions?: any;
cachedPluginOptions: Record<string, PanelOptionsCache>;
legend?: { show: boolean; sort?: string; sortDesc?: boolean };
plugin?: PanelPlugin;
@ -294,53 +295,32 @@ export class PanelModel implements DataConfigSource {
}
private restorePanelOptions(pluginId: string) {
const prevOptions = this.cachedPluginOptions[pluginId] || {};
Object.keys(prevOptions).map((property) => {
(this as any)[property] = prevOptions[property];
});
}
private applyPluginOptionDefaults(plugin: PanelPlugin) {
if (plugin.angularConfigCtrl) {
if (!this.cachedPluginOptions) {
return;
}
this.options = _.mergeWith({}, plugin.defaults, this.options || {}, (objValue: any, srcValue: any): any => {
if (_.isArray(srcValue)) {
return srcValue;
}
const prevOptions = this.cachedPluginOptions[pluginId];
if (!prevOptions) {
return;
}
Object.keys(prevOptions.properties).map((property) => {
(this as any)[property] = prevOptions.properties[property];
});
this.fieldConfig = applyFieldConfigDefaults(this.fieldConfig, plugin.fieldConfigDefaults);
this.validateFieldColorMode(plugin);
this.fieldConfig = restoreCustomOverrideRules(this.fieldConfig, prevOptions.fieldConfig);
}
private validateFieldColorMode(plugin: PanelPlugin) {
// adjust to prefered field color setting if needed
const color = plugin.fieldConfigRegistry.getIfExists(FieldConfigProperty.Color);
applyPluginOptionDefaults(plugin: PanelPlugin) {
const options = getPanelOptionsWithDefaults({
plugin,
currentOptions: this.options,
currentFieldConfig: this.fieldConfig,
});
if (color && color.settings) {
const colorSettings = color.settings as FieldColorConfigSettings;
const mode = fieldColorModeRegistry.getIfExists(this.fieldConfig.defaults.color?.mode);
// When no support fo value colors, use classic palette
if (!colorSettings.byValueSupport) {
if (!mode || mode.isByValue) {
this.fieldConfig.defaults.color = { mode: FieldColorModeId.PaletteClassic };
return;
}
}
// When supporting value colors and prefering thresholds, use Thresholds mode.
// Otherwise keep current mode
if (colorSettings.byValueSupport && colorSettings.preferThresholdsMode) {
if (!mode || !mode.isByValue) {
this.fieldConfig.defaults.color = { mode: FieldColorModeId.Thresholds };
return;
}
}
}
this.fieldConfig = options.fieldConfig;
this.options = options.options;
}
pluginLoaded(plugin: PanelPlugin) {
@ -359,35 +339,54 @@ export class PanelModel implements DataConfigSource {
this.resendLastResult();
}
changePlugin(newPlugin: PanelPlugin) {
const pluginId = newPlugin.meta.id;
const oldOptions: any = this.getOptionsToRemember();
const oldPluginId = this.type;
const wasAngular = this.isAngularPlugin();
clearPropertiesBeforePluginChange() {
// remove panel type specific options
for (const key of _.keys(this)) {
if (mustKeepProps[key]) {
continue;
}
delete (this as any)[key];
}
this.cachedPluginOptions[oldPluginId] = oldOptions;
this.options = {};
// clear custom options
this.fieldConfig = {
defaults: {
...this.fieldConfig.defaults,
custom: {},
},
// filter out custom overrides
overrides: filterFieldConfigOverrides(this.fieldConfig.overrides, isStandardFieldProp),
};
}
changePlugin(newPlugin: PanelPlugin) {
const pluginId = newPlugin.meta.id;
const oldOptions: any = this.getOptionsToRemember();
const oldFieldConfig = this.fieldConfig;
const oldPluginId = this.type;
const wasAngular = this.isAngularPlugin();
this.cachedPluginOptions[oldPluginId] = {
properties: oldOptions,
fieldConfig: oldFieldConfig,
};
this.clearPropertiesBeforePluginChange();
this.restorePanelOptions(pluginId);
// Let panel plugins inspect options from previous panel and keep any that it can use
if (newPlugin.onPanelTypeChanged) {
let old: any = {};
let oldOptions: any = {};
if (wasAngular) {
old = { angular: oldOptions };
oldOptions = { angular: oldOptions };
} else if (oldOptions && oldOptions.options) {
old = oldOptions.options;
oldOptions = oldOptions.options;
}
this.options = this.options || {};
Object.assign(this.options, newPlugin.onPanelTypeChanged(this, oldPluginId, old));
Object.assign(this.options, newPlugin.onPanelTypeChanged(this, oldPluginId, oldOptions, oldFieldConfig));
}
// switch
@ -532,51 +531,11 @@ export class PanelModel implements DataConfigSource {
}
}
function applyFieldConfigDefaults(fieldConfig: FieldConfigSource, defaults: FieldConfigSource): FieldConfigSource {
const result: FieldConfigSource = {
defaults: _.mergeWith(
{},
defaults.defaults,
fieldConfig ? fieldConfig.defaults : {},
(objValue: any, srcValue: any): any => {
if (_.isArray(srcValue)) {
return srcValue;
}
}
),
overrides: fieldConfig?.overrides ?? [],
};
// Thresholds base values are null in JSON but need to be converted to -Infinity
if (result.defaults.thresholds) {
fixThresholds(result.defaults.thresholds);
}
for (const override of result.overrides) {
for (const property of override.properties) {
if (property.id === 'thresholds') {
fixThresholds(property.value as ThresholdsConfig);
}
}
}
return result;
}
function fixThresholds(thresholds: ThresholdsConfig) {
if (!thresholds.mode) {
thresholds.mode = ThresholdsMode.Absolute;
}
if (!thresholds.steps) {
thresholds.steps = [];
} else if (thresholds.steps.length) {
// First value is always -Infinity
// JSON saves it as null
thresholds.steps[0].value = -Infinity;
}
}
function getPluginVersion(plugin: PanelPlugin): string {
return plugin && plugin.meta.info.version ? plugin.meta.info.version : config.buildInfo.version;
}
interface PanelOptionsCache {
properties: any;
fieldConfig: FieldConfigSource;
}

View File

@ -0,0 +1,390 @@
import {
ConfigOverrideRule,
FieldColorModeId,
FieldConfig,
FieldConfigProperty,
FieldConfigSource,
PanelPlugin,
standardEditorsRegistry,
standardFieldConfigEditorRegistry,
StandardOptionConfig,
} from '@grafana/data';
import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
import { mockStandardFieldConfigOptions } from 'test/helpers/fieldConfig';
import { getPanelOptionsWithDefaults, restoreCustomOverrideRules } from './getPanelOptionsWithDefaults';
standardFieldConfigEditorRegistry.setInit(() => mockStandardFieldConfigOptions());
standardEditorsRegistry.setInit(() => mockStandardFieldConfigOptions());
const pluginA = getPanelPlugin({ id: 'graph' });
pluginA.useFieldConfig({
useCustomConfig: (builder) => {
builder.addBooleanSwitch({
name: 'Hide lines',
path: 'hideLines',
defaultValue: false,
});
},
});
pluginA.setPanelOptions((builder) => {
builder.addBooleanSwitch({
name: 'Show thresholds',
path: 'showThresholds',
defaultValue: true,
});
builder.addTextInput({
name: 'Name',
path: 'name',
defaultValue: 'hello',
});
builder.addNumberInput({
name: 'Number',
path: 'number',
defaultValue: 10,
});
});
describe('getPanelOptionsWithDefaults', () => {
describe('When panel plugin has no options', () => {
it('Should set defaults', () => {
const result = runScenario({
plugin: getPanelPlugin({ id: 'graph' }),
options: {},
defaults: {},
overrides: [],
});
expect(result).toMatchInlineSnapshot(`
Object {
"fieldConfig": Object {
"defaults": Object {
"custom": Object {},
},
"overrides": Array [],
},
"options": Object {},
}
`);
});
});
describe('When current options are emtpy', () => {
it('Should set defaults', () => {
const result = getPanelOptionsWithDefaults({
plugin: pluginA,
currentOptions: {},
currentFieldConfig: {
defaults: {},
overrides: [],
},
});
expect(result).toMatchInlineSnapshot(`
Object {
"fieldConfig": Object {
"defaults": Object {
"custom": Object {
"hideLines": false,
},
"thresholds": Object {
"mode": "absolute",
"steps": Array [
Object {
"color": "green",
"value": -Infinity,
},
Object {
"color": "red",
"value": 80,
},
],
},
},
"overrides": Array [],
},
"options": Object {
"name": "hello",
"number": 10,
"showThresholds": true,
},
}
`);
});
});
describe('When there are current options and overrides', () => {
it('Should set defaults', () => {
const result = getPanelOptionsWithDefaults({
plugin: pluginA,
currentOptions: {
number: 20,
showThresholds: false,
},
currentFieldConfig: {
defaults: {
unit: 'bytes',
decimals: 2,
},
overrides: [],
},
});
expect(result).toMatchInlineSnapshot(`
Object {
"fieldConfig": Object {
"defaults": Object {
"custom": Object {
"hideLines": false,
},
"decimals": 2,
"thresholds": Object {
"mode": "absolute",
"steps": Array [
Object {
"color": "green",
"value": -Infinity,
},
Object {
"color": "red",
"value": 80,
},
],
},
"unit": "bytes",
},
"overrides": Array [],
},
"options": Object {
"name": "hello",
"number": 20,
"showThresholds": false,
},
}
`);
});
});
describe('when changing panel type to one that does not support by value color mode', () => {
it('should change color mode', () => {
const plugin = getPanelPlugin({ id: 'graph' }).useFieldConfig({
standardOptions: {
[FieldConfigProperty.Color]: {
settings: {
byValueSupport: false,
},
},
},
});
const result = getPanelOptionsWithDefaults({
plugin,
currentOptions: {},
currentFieldConfig: {
defaults: {
color: { mode: FieldColorModeId.Thresholds },
},
overrides: [],
},
});
expect(result.fieldConfig.defaults.color!.mode).toBe(FieldColorModeId.PaletteClassic);
});
});
describe('when changing panel type from one not supporting by value color mode to one that supports it', () => {
it('should keep supported mode', () => {
const result = runScenario({
defaults: {
color: { mode: FieldColorModeId.PaletteClassic },
},
standardOptions: {
[FieldConfigProperty.Color]: {
settings: {
byValueSupport: true,
},
},
},
});
expect(result.fieldConfig.defaults.color!.mode).toBe(FieldColorModeId.PaletteClassic);
});
it('should change to thresholds mode when it prefers to', () => {
const result = runScenario({
defaults: {
color: { mode: FieldColorModeId.PaletteClassic },
},
standardOptions: {
[FieldConfigProperty.Color]: {
settings: {
byValueSupport: true,
preferThresholdsMode: true,
},
},
},
});
expect(result.fieldConfig.defaults.color!.mode).toBe(FieldColorModeId.Thresholds);
});
});
describe('when applying defaults clean properties that are no longer part of the registry', () => {
it('should remove custom defaults that no longer exist', () => {
const result = runScenario({
defaults: {
unit: 'bytes',
custom: {
customProp: 20,
customPropNoExist: true,
nested: {
nestedA: 'A',
nestedB: 'B',
},
},
},
});
expect(result.fieldConfig.defaults).toMatchInlineSnapshot(`
Object {
"custom": Object {
"customProp": 20,
"nested": Object {
"nestedA": "A",
},
},
"thresholds": Object {
"mode": "absolute",
"steps": Array [
Object {
"color": "green",
"value": -Infinity,
},
Object {
"color": "red",
"value": 80,
},
],
},
"unit": "bytes",
}
`);
});
it('should remove custom overrides that no longer exist', () => {
const result = runScenario({
defaults: {},
overrides: [
{
matcher: { id: 'byName', options: 'D-series' },
properties: [
{
id: 'custom.customPropNoExist',
value: 'google',
},
],
},
{
matcher: { id: 'byName', options: 'D-series' },
properties: [
{
id: 'custom.customProp',
value: 30,
},
],
},
],
});
expect(result.fieldConfig.overrides.length).toBe(1);
expect(result.fieldConfig.overrides[0].properties[0].id).toBe('custom.customProp');
});
});
});
describe('restoreCustomOverrideRules', () => {
it('should add back custom rules', () => {
const current = {
defaults: {},
overrides: [
{
matcher: { id: 'byName', options: 'SeriesA' },
properties: [
{
id: 'decimals',
value: 2,
},
],
},
],
};
const old = {
defaults: {},
overrides: [
{
matcher: { id: 'byName', options: 'SeriesA' },
properties: [
{
id: 'custom.propName',
value: 10,
},
],
},
{
matcher: { id: 'byName', options: 'SeriesB' },
properties: [
{
id: 'custom.propName',
value: 20,
},
],
},
],
};
const result = restoreCustomOverrideRules(current, old);
expect(result.overrides.length).toBe(2);
expect(result.overrides[0].properties[0].id).toBe('decimals');
expect(result.overrides[0].properties[1].id).toBe('custom.propName');
expect(result.overrides[1].properties.length).toBe(1);
expect(result.overrides[1].matcher.options).toBe('SeriesB');
});
});
interface ScenarioOptions {
defaults?: FieldConfig<any>;
overrides?: ConfigOverrideRule[];
disabledStandardOptions?: FieldConfigProperty[];
standardOptions?: Partial<Record<FieldConfigProperty, StandardOptionConfig>>;
plugin?: PanelPlugin;
options?: any;
}
function runScenario(options: ScenarioOptions) {
const fieldConfig: FieldConfigSource = {
defaults: options.defaults || {},
overrides: options.overrides || [],
};
const plugin =
options.plugin ??
getPanelPlugin({ id: 'graph' }).useFieldConfig({
standardOptions: options.standardOptions,
useCustomConfig: (builder) => {
builder.addNumberInput({
name: 'Custom prop',
path: 'customProp',
defaultValue: 10,
});
builder.addTextInput({
name: 'Nested prop',
path: 'nested.nestedA',
});
},
});
return getPanelOptionsWithDefaults({
plugin,
currentOptions: options.options || {},
currentFieldConfig: fieldConfig,
});
}

View File

@ -0,0 +1,195 @@
import {
ConfigOverrideRule,
DynamicConfigValue,
FieldColorConfigSettings,
FieldColorModeId,
fieldColorModeRegistry,
FieldConfigOptionsRegistry,
FieldConfigProperty,
FieldConfigSource,
PanelPlugin,
ThresholdsConfig,
ThresholdsMode,
} from '@grafana/data';
import { mergeWith, isArray, isObject, unset, isEqual } from 'lodash';
export interface Props {
plugin: PanelPlugin;
currentFieldConfig: FieldConfigSource;
currentOptions: Record<string, any>;
}
export interface OptionDefaults {
options: any;
fieldConfig: FieldConfigSource;
}
export function getPanelOptionsWithDefaults({ plugin, currentOptions, currentFieldConfig }: Props): OptionDefaults {
const optionsWithDefaults = mergeWith(
{},
plugin.defaults,
currentOptions || {},
(objValue: any, srcValue: any): any => {
if (isArray(srcValue)) {
return srcValue;
}
}
);
const fieldConfigWithDefaults = applyFieldConfigDefaults(currentFieldConfig, plugin);
const fieldConfigWithOptimalColorMode = adaptFieldColorMode(plugin, fieldConfigWithDefaults);
return { options: optionsWithDefaults, fieldConfig: fieldConfigWithOptimalColorMode };
}
function applyFieldConfigDefaults(existingFieldConfig: FieldConfigSource, plugin: PanelPlugin): FieldConfigSource {
const pluginDefaults = plugin.fieldConfigDefaults;
const result: FieldConfigSource = {
defaults: mergeWith(
{},
pluginDefaults.defaults,
existingFieldConfig ? existingFieldConfig.defaults : {},
(objValue: any, srcValue: any): any => {
if (isArray(srcValue)) {
return srcValue;
}
}
),
overrides: existingFieldConfig?.overrides ?? [],
};
cleanProperties(result.defaults, '', plugin.fieldConfigRegistry);
// Thresholds base values are null in JSON but need to be converted to -Infinity
if (result.defaults.thresholds) {
fixThresholds(result.defaults.thresholds);
}
// Filter out overrides for properties that cannot be found in registry
result.overrides = filterFieldConfigOverrides(result.overrides, (prop) => {
return plugin.fieldConfigRegistry.getIfExists(prop.id) !== undefined;
});
for (const override of result.overrides) {
for (const property of override.properties) {
if (property.id === 'thresholds') {
fixThresholds(property.value as ThresholdsConfig);
}
}
}
return result;
}
export function filterFieldConfigOverrides(
overrides: ConfigOverrideRule[],
condition: (value: DynamicConfigValue) => boolean
): ConfigOverrideRule[] {
return overrides
.map((x) => {
const properties = x.properties.filter(condition);
return {
...x,
properties,
};
})
.filter((x) => x.properties.length > 0);
}
function cleanProperties(obj: any, parentPath: string, fieldConfigRegistry: FieldConfigOptionsRegistry) {
for (const propName of Object.keys(obj)) {
const value = obj[propName];
const fullPath = `${parentPath}${propName}`;
const existsInRegistry = !!fieldConfigRegistry.getIfExists(fullPath);
// need to check early here as some standard properties have nested properies
if (existsInRegistry) {
continue;
}
if (isArray(value) || !isObject(value)) {
if (!existsInRegistry) {
unset(obj, propName);
}
} else {
cleanProperties(value, `${fullPath}.`, fieldConfigRegistry);
}
}
}
function adaptFieldColorMode(plugin: PanelPlugin, fieldConfig: FieldConfigSource): FieldConfigSource {
// adjust to prefered field color setting if needed
const color = plugin.fieldConfigRegistry.getIfExists(FieldConfigProperty.Color);
if (color && color.settings) {
const colorSettings = color.settings as FieldColorConfigSettings;
const mode = fieldColorModeRegistry.getIfExists(fieldConfig.defaults.color?.mode);
// When no support fo value colors, use classic palette
if (!colorSettings.byValueSupport) {
if (!mode || mode.isByValue) {
fieldConfig.defaults.color = { mode: FieldColorModeId.PaletteClassic };
return fieldConfig;
}
}
// When supporting value colors and prefering thresholds, use Thresholds mode.
// Otherwise keep current mode
if (colorSettings.byValueSupport && colorSettings.preferThresholdsMode) {
if (!mode || !mode.isByValue) {
fieldConfig.defaults.color = { mode: FieldColorModeId.Thresholds };
return fieldConfig;
}
}
}
return fieldConfig;
}
function fixThresholds(thresholds: ThresholdsConfig) {
if (!thresholds.mode) {
thresholds.mode = ThresholdsMode.Absolute;
}
if (!thresholds.steps) {
thresholds.steps = [];
} else if (thresholds.steps.length) {
// First value is always -Infinity
// JSON saves it as null
thresholds.steps[0].value = -Infinity;
}
}
export function restoreCustomOverrideRules(current: FieldConfigSource, old: FieldConfigSource): FieldConfigSource {
const result = {
defaults: {
...current.defaults,
custom: old.defaults.custom,
},
overrides: [...current.overrides],
};
for (const override of old.overrides) {
for (const prop of override.properties) {
if (isCustomFieldProp(prop)) {
const currentOverride = result.overrides.find((o) => isEqual(o.matcher, override.matcher));
if (currentOverride) {
currentOverride.properties.push(prop);
} else {
result.overrides.push(override);
}
}
}
}
return result;
}
export function isCustomFieldProp(prop: DynamicConfigValue): boolean {
return prop.id.startsWith('custom.');
}
export function isStandardFieldProp(prop: DynamicConfigValue): boolean {
return !isCustomFieldProp(prop);
}

View File

@ -1,4 +1,4 @@
import { identityOverrideProcessor } from '@grafana/data';
import { identityOverrideProcessor, ThresholdsMode } from '@grafana/data';
export function mockStandardFieldConfigOptions() {
const unit = {
@ -79,5 +79,25 @@ export function mockStandardFieldConfigOptions() {
shouldApply: () => true,
};
return [unit, decimals, boolean, fieldColor, text, number];
const thresholds = {
id: 'thresholds',
path: 'thresholds',
name: 'thresholds',
description: '',
// @ts-ignore
editor: () => null,
// @ts-ignore
override: () => null,
process: identityOverrideProcessor,
shouldApply: () => true,
defaultValue: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: 80, color: 'red' },
],
},
};
return [unit, decimals, boolean, fieldColor, text, number, thresholds];
}