mirror of
https://github.com/grafana/grafana.git
synced 2025-02-09 23:16:16 -06:00
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:
parent
b40b134e4c
commit
15033d0011
@ -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';
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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>;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
@ -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);
|
||||
}
|
@ -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];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user