Commit 2ea4a36b by Dominik Prokop Committed by GitHub

Field color: handling color changes when switching panel types (#28875)

* FieldColor: Per panel settings to filter out supported modes

* Updates

* Updated solution

* Update panel plugin API for standard options support

* Update FieldColorConfigSettings interface

* Change color mode correctly when changing plugin type

* Render only applicable color modes in field color config editor

* Apply field config API changes

* TS fixes

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
parent 2887f3f6
......@@ -24,6 +24,7 @@ export const fieldColorModeRegistry = new Registry<FieldColorMode>(() => {
{
id: FieldColorModeId.Thresholds,
name: 'From thresholds',
isByValue: true,
description: 'Derive colors from thresholds',
getCalculator: (_field, theme) => {
return (_value, _percent, threshold) => {
......
......@@ -306,7 +306,6 @@ const processFieldConfigValue = (
const currentConfig = get(destination, fieldConfigProperty.path);
if (currentConfig === null || currentConfig === undefined) {
const item = context.fieldConfigRegistry.getIfExists(fieldConfigProperty.id);
// console.log(item);
if (!item) {
return;
}
......
......@@ -120,8 +120,15 @@ export const booleanOverrideProcessor = (
return value; // !!!! likely not !!!!
};
export interface ColorFieldConfigSettings {
allowUndefined?: boolean;
textWhenUndefined?: string; // Pick Color
disableNamedColors?: boolean;
export interface FieldColorConfigSettings {
/**
* When switching to a visualization that does not support by value coloring then Grafana will
* switch to a by series palette based color mode
*/
byValueSupport?: boolean;
/**
* When switching to a visualization that has this set to true then Grafana will change color mode
* to from thresholds if it was set to a by series palette
*/
preferThresholdsMode?: boolean;
}
......@@ -9,11 +9,11 @@ describe('PanelPlugin', () => {
standardFieldConfigEditorRegistry.setInit(() => {
return [
{
id: 'min',
id: FieldConfigProperty.Min,
path: 'min',
},
{
id: 'max',
id: FieldConfigProperty.Max,
path: 'max',
},
] as any;
......@@ -210,15 +210,15 @@ describe('PanelPlugin', () => {
expect(panel.fieldConfigRegistry.list()).toHaveLength(2);
});
test('selected standard config', () => {
test('disabling standard config properties', () => {
const panel = new PanelPlugin(() => {
return <div>Panel</div>;
});
panel.useFieldConfig({
standardOptions: [FieldConfigProperty.Min, FieldConfigProperty.Max],
disableStandardOptions: [FieldConfigProperty.Min],
});
expect(panel.fieldConfigRegistry.list()).toHaveLength(2);
expect(panel.fieldConfigRegistry.list()).toHaveLength(1);
});
describe('default values', () => {
......@@ -228,10 +228,9 @@ describe('PanelPlugin', () => {
});
panel.useFieldConfig({
standardOptions: [FieldConfigProperty.Max, FieldConfigProperty.Min],
standardOptionsDefaults: {
[FieldConfigProperty.Max]: 20,
[FieldConfigProperty.Min]: 10,
standardOptions: {
[FieldConfigProperty.Max]: { defaultValue: 20 },
[FieldConfigProperty.Min]: { defaultValue: 10 },
},
});
......@@ -247,17 +246,16 @@ describe('PanelPlugin', () => {
});
});
it('should ignore defaults that are not specified as available properties', () => {
it('should disable properties independently from the default values settings', () => {
const panel = new PanelPlugin(() => {
return <div>Panel</div>;
});
panel.useFieldConfig({
standardOptions: [FieldConfigProperty.Max],
standardOptionsDefaults: {
[FieldConfigProperty.Max]: 20,
[FieldConfigProperty.Min]: 10,
standardOptions: {
[FieldConfigProperty.Max]: { defaultValue: 20 },
},
disableStandardOptions: [FieldConfigProperty.Min],
});
expect(panel.fieldConfigRegistry.list()).toHaveLength(1);
......
......@@ -15,33 +15,38 @@ import set from 'lodash/set';
import { deprecationWarning } from '../utils';
import { FieldConfigOptionsRegistry, standardFieldConfigEditorRegistry } from '../field';
type StandardOptionConfig = {
defaultValue?: any;
settings?: any;
};
export interface SetFieldConfigOptionsArgs<TFieldConfigOptions = any> {
/**
* Array of standard field config properties
* Configuration object of the standard field config properites
*
* @example
* ```typescript
* {
* standardOptions: [FieldConfigProperty.Min, FieldConfigProperty.Max, FieldConfigProperty.Unit]
* standardOptions: {
* [FieldConfigProperty.Decimals]: {
* defaultValue: 3
* }
* }
* }
* ```
*/
standardOptions?: FieldConfigProperty[];
standardOptions?: Partial<Record<FieldConfigProperty, StandardOptionConfig>>;
/**
* Object specifying standard option properties default values
*
* Array of standard field config properties that should not be available in the panel
* @example
* ```typescript
* {
* standardOptionsDefaults: {
* [FieldConfigProperty.Min]: 20,
* [FieldConfigProperty.Max]: 100
* }
* disableStandardOptions: [FieldConfigProperty.Min, FieldConfigProperty.Max, FieldConfigProperty.Unit]
* }
* ```
*/
standardOptionsDefaults?: Partial<Record<FieldConfigProperty, any>>;
disableStandardOptions?: FieldConfigProperty[];
/**
* Function that allows custom field config properties definition.
......@@ -305,13 +310,13 @@ export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = an
*
* @public
*/
useFieldConfig(config?: SetFieldConfigOptionsArgs<TFieldConfigOptions>) {
useFieldConfig(config: SetFieldConfigOptionsArgs<TFieldConfigOptions> = {}) {
// builder is applied lazily when custom field configs are accessed
this._initConfigRegistry = () => {
const registry = new FieldConfigOptionsRegistry();
// Add custom options
if (config && config.useCustomConfig) {
if (config.useCustomConfig) {
const builder = new FieldConfigEditorBuilder<TFieldConfigOptions>();
config.useCustomConfig(builder);
......@@ -326,20 +331,32 @@ export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = an
}
}
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,
});
for (let fieldConfigProp of standardFieldConfigEditorRegistry.list()) {
if (config.disableStandardOptions) {
const isDisabled = config.disableStandardOptions.indexOf(fieldConfigProp.id as FieldConfigProperty) > -1;
if (isDisabled) {
continue;
}
}
} else {
for (const fieldConfigProp of standardFieldConfigEditorRegistry.list()) {
registry.register(fieldConfigProp);
if (config.standardOptions) {
const customDefault: any = config.standardOptions[fieldConfigProp.id as FieldConfigProperty]?.defaultValue;
const customSettings: any = config.standardOptions[fieldConfigProp.id as FieldConfigProperty]?.settings;
if (customDefault) {
fieldConfigProp = {
...fieldConfigProp,
defaultValue: customDefault,
};
}
if (customSettings) {
fieldConfigProp = {
...fieldConfigProp,
settings: fieldConfigProp.settings ? { ...fieldConfigProp.settings, ...customSettings } : customSettings,
};
}
}
registry.register(fieldConfigProp);
}
return registry;
......
......@@ -13,12 +13,10 @@ import {
StringFieldConfigSettings,
NumberFieldConfigSettings,
SliderFieldConfigSettings,
ColorFieldConfigSettings,
identityOverrideProcessor,
UnitFieldConfigSettings,
unitOverrideProcessor,
} from '../field';
import { FieldColor } from '../types';
/**
* Fluent API for declarative creation of field config option editors
......@@ -104,9 +102,7 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
});
}
addColorPicker<TSettings = any>(
config: FieldConfigEditorConfig<TOptions, TSettings & ColorFieldConfigSettings, FieldColor>
) {
addColorPicker<TSettings = any>(config: FieldConfigEditorConfig<TOptions, TSettings, string>) {
return this.addCustomEditor({
...config,
id: config.path,
......@@ -203,9 +199,7 @@ export class PanelOptionsEditorBuilder<TOptions> extends OptionsUIRegistryBuilde
});
}
addColorPicker<TSettings = any>(
config: PanelOptionsEditorConfig<TOptions, TSettings & ColorFieldConfigSettings, string>
): this {
addColorPicker<TSettings = any>(config: PanelOptionsEditorConfig<TOptions, TSettings, string>): this {
return this.addCustomEditor({
...config,
id: config.path,
......
......@@ -545,7 +545,7 @@ export function getBarGradient(props: Props, maxSize: number): string {
return gradient + ')';
}
if (mode.colors) {
if (mode.isContinuous && mode.colors) {
const scheme = mode.colors.map(item => getColorForTheme(item, theme));
for (let i = 0; i < scheme.length; i++) {
const color = scheme[i];
......
......@@ -28,12 +28,6 @@ export const ColorValueEditor: React.FC<Props> = ({ value, onChange }) => {
color={value ? getColorForTheme(value, theme) : theme.colors.formInputBorder}
/>
</div>
{/* <div className={styles.colorText} onClick={showColorPicker}>
{value ?? settings?.textWhenUndefined ?? 'Pick Color'}
</div>
{value && settings?.allowUndefined && (
<Icon className={styles.trashIcon} name="trash-alt" onClick={() => onChange(undefined)} />
)} */}
</div>
);
}}
......
......@@ -8,13 +8,14 @@ import {
FieldColorMode,
GrafanaTheme,
getColorForTheme,
FieldColorConfigSettings,
} from '@grafana/data';
import { Select } from '../Select/Select';
import { ColorValueEditor } from './color';
import { useStyles, useTheme } from '../../themes/ThemeContext';
import { css } from 'emotion';
export const FieldColorEditor: React.FC<FieldConfigEditorProps<FieldColor | undefined, {}>> = ({
export const FieldColorEditor: React.FC<FieldConfigEditorProps<FieldColor | undefined, FieldColorConfigSettings>> = ({
value,
onChange,
item,
......@@ -22,7 +23,11 @@ export const FieldColorEditor: React.FC<FieldConfigEditorProps<FieldColor | unde
const theme = useTheme();
const styles = useStyles(getStyles);
const options = fieldColorModeRegistry.list().map(mode => {
const availableOptions = item.settings?.byValueSupport
? fieldColorModeRegistry.list()
: fieldColorModeRegistry.list().filter(m => !m.isByValue);
const options = availableOptions.map(mode => {
let suffix = mode.isByValue ? ' (by value)' : '';
return {
......
......@@ -20,6 +20,7 @@ import {
identityOverrideProcessor,
TimeZone,
FieldColor,
FieldColorConfigSettings,
} from '@grafana/data';
import { Switch } from '../components/Switch/Switch';
......@@ -204,15 +205,18 @@ export const getStandardFieldConfigs = () => {
getItemsCount: value => (value ? value.length : 0),
};
const color: FieldConfigPropertyItem<any, FieldColor | undefined, {}> = {
const color: FieldConfigPropertyItem<any, FieldColor | undefined, FieldColorConfigSettings> = {
id: 'color',
path: 'color',
name: 'Color scheme',
description: 'Select palette, gradient or single color',
editor: standardEditorsRegistry.get('fieldColor').editor as any,
override: standardEditorsRegistry.get('fieldColor').editor as any,
process: identityOverrideProcessor,
shouldApply: () => true,
settings: {
byValueSupport: true,
preferThresholdsMode: true,
},
category,
};
......
......@@ -8,6 +8,8 @@ import {
standardFieldConfigEditorRegistry,
PanelData,
DataSourceInstanceSettings,
FieldColorModeId,
FieldColorConfigSettings,
} from '@grafana/data';
import { ComponentClass } from 'react';
import { PanelQueryRunner } from './PanelQueryRunner';
......@@ -55,7 +57,20 @@ export const mockStandardProperties = () => {
shouldApply: () => true,
};
return [unit, decimals, boolean];
const fieldColor = {
id: 'color',
path: 'color',
name: 'color',
description: '',
// @ts-ignore
editor: () => null,
// @ts-ignore
override: () => null,
process: identityOverrideProcessor,
shouldApply: () => true,
};
return [unit, decimals, boolean, fieldColor];
};
standardFieldConfigEditorRegistry.setInit(() => mockStandardProperties());
......@@ -147,10 +162,13 @@ describe('PanelModel', () => {
});
panelPlugin.useFieldConfig({
standardOptions: [FieldConfigProperty.Unit, FieldConfigProperty.Decimals],
standardOptionsDefaults: {
[FieldConfigProperty.Unit]: 'flop',
[FieldConfigProperty.Decimals]: 2,
standardOptions: {
[FieldConfigProperty.Unit]: {
defaultValue: 'flop',
},
[FieldConfigProperty.Decimals]: {
defaultValue: 2,
},
},
});
model.pluginLoaded(panelPlugin);
......@@ -239,6 +257,17 @@ describe('PanelModel', () => {
describe('when changing panel type', () => {
beforeEach(() => {
const newPlugin = getPanelPlugin({ id: 'graph' });
newPlugin.useFieldConfig({
standardOptions: {
[FieldConfigProperty.Color]: {
settings: {
byThresholdsSupport: true,
},
},
},
});
newPlugin.setPanelOptions(builder => {
builder.addBooleanSwitch({
name: 'Show thresholds labels',
......@@ -285,6 +314,66 @@ describe('PanelModel', () => {
});
});
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;
......
......@@ -12,6 +12,10 @@ import {
DataQueryResponseData,
DataTransformerConfig,
eventFactory,
FieldColorConfigSettings,
FieldColorModeId,
fieldColorModeRegistry,
FieldConfigProperty,
FieldConfigSource,
PanelEvents,
PanelPlugin,
......@@ -322,7 +326,35 @@ export class PanelModel implements DataConfigSource {
}
});
this.fieldConfig = applyFieldConfigDefaults(this.fieldConfig, this.plugin!.fieldConfigDefaults);
this.fieldConfig = applyFieldConfigDefaults(this.fieldConfig, plugin.fieldConfigDefaults);
this.validateFieldColorMode(plugin);
}
private validateFieldColorMode(plugin: PanelPlugin) {
// 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(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;
}
}
}
}
pluginLoaded(plugin: PanelPlugin) {
......
......@@ -12,11 +12,11 @@ import { axesEditorComponent } from './axes_editor';
import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2';
import { getProcessedDataFrames } from 'app/features/dashboard/state/runRequest';
import { PanelEvents, PanelPlugin, DataFrame, FieldConfigProperty, getColorForTheme } from '@grafana/data';
import { DataFrame, FieldConfigProperty, getColorForTheme, PanelEvents, PanelPlugin } from '@grafana/data';
import { GraphContextMenuCtrl } from './GraphContextMenuCtrl';
import { graphPanelMigrationHandler } from './GraphMigrations';
import { DataWarning, GraphPanelOptions, GraphFieldConfig } from './types';
import { DataWarning, GraphFieldConfig, GraphPanelOptions } from './types';
import { auto } from 'angular';
import { AnnotationsSrv } from 'app/features/annotations/all';
......@@ -388,10 +388,14 @@ export class GraphCtrl extends MetricsPanelCtrl {
// Use new react style configuration
export const plugin = new PanelPlugin<GraphPanelOptions, GraphFieldConfig>(null)
.useFieldConfig({
standardOptions: [
FieldConfigProperty.DisplayName,
FieldConfigProperty.Unit,
FieldConfigProperty.Links, // previously saved as dataLinks on options
disableStandardOptions: [
FieldConfigProperty.NoValue,
FieldConfigProperty.Thresholds,
FieldConfigProperty.Max,
FieldConfigProperty.Min,
FieldConfigProperty.Decimals,
FieldConfigProperty.Color,
FieldConfigProperty.Mappings,
],
})
.setMigrationHandler(graphPanelMigrationHandler);
......
import { FieldConfigProperty, PanelPlugin } from '@grafana/data';
import { PanelPlugin } from '@grafana/data';
import { GraphPanel } from './GraphPanel';
import { Options } from './types';
export const plugin = new PanelPlugin<Options>(GraphPanel)
.useFieldConfig({ standardOptions: [FieldConfigProperty.Unit, FieldConfigProperty.Decimals] })
.setPanelOptions(builder => {
builder
.addBooleanSwitch({
path: 'graph.showBars',
name: 'Show bars',
description: '',
defaultValue: false,
})
.addBooleanSwitch({
path: 'graph.showLines',
name: 'Show lines',
description: '',
defaultValue: true,
})
.addBooleanSwitch({
path: 'graph.showPoints',
name: 'Show poins',
description: '',
defaultValue: false,
})
.addBooleanSwitch({
path: 'legend.isVisible',
name: 'Show legend',
description: '',
defaultValue: true,
})
.addBooleanSwitch({
path: 'legend.asTable',
name: 'Display legend as table',
description: '',
defaultValue: false,
})
.addRadio({
path: 'legend.placement',
name: 'Legend placement',
description: '',
defaultValue: 'under',
settings: {
options: [
{ value: 'under', label: 'Below graph' },
{ value: 'right', label: 'Right to the graph' },
],
},
})
.addRadio({
path: 'tooltipOptions.mode',
name: 'Tooltip mode',
description: '',
defaultValue: 'single',
settings: {
options: [
{ value: 'single', label: 'Single series' },
{ value: 'multi', label: 'All series' },
],
},
});
});
export const plugin = new PanelPlugin<Options>(GraphPanel).useFieldConfig().setPanelOptions(builder => {
builder
.addBooleanSwitch({
path: 'graph.showBars',
name: 'Show bars',
description: '',
defaultValue: false,
})
.addBooleanSwitch({
path: 'graph.showLines',
name: 'Show lines',
description: '',
defaultValue: true,
})
.addBooleanSwitch({
path: 'graph.showPoints',
name: 'Show poins',
description: '',
defaultValue: false,
})
.addBooleanSwitch({
path: 'legend.isVisible',
name: 'Show legend',
description: '',
defaultValue: true,
})
.addBooleanSwitch({
path: 'legend.asTable',
name: 'Display legend as table',
description: '',
defaultValue: false,
})
.addRadio({
path: 'legend.placement',
name: 'Legend placement',
description: '',
defaultValue: 'under',
settings: {
options: [
{ value: 'under', label: 'Below graph' },
{ value: 'right', label: 'Right to the graph' },
],
},
})
.addRadio({
path: 'tooltipOptions.mode',
name: 'Tooltip mode',
description: '',
defaultValue: 'single',
settings: {
options: [
{ value: 'single', label: 'Single series' },
{ value: 'multi', label: 'All series' },
],
},
});
});
import { FieldConfigProperty, PanelPlugin } from '@grafana/data';
import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data';
import { AxisSide, GraphCustomFieldConfig } from '@grafana/ui';
import { GraphPanel } from './GraphPanel';
import { Options } from './types';
export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPanel)
.useFieldConfig({
standardOptions: [
// FieldConfigProperty.Min,
// FieldConfigProperty.Max,
FieldConfigProperty.Color,
FieldConfigProperty.Unit,
FieldConfigProperty.DisplayName,
FieldConfigProperty.Decimals,
// NOT: FieldConfigProperty.Thresholds,
FieldConfigProperty.Mappings,
],
standardOptions: {
[FieldConfigProperty.Color]: {
settings: {
byValueSupport: false,
},
defaultValue: {
mode: FieldColorModeId.PaletteClassic,
},
},
},
useCustomConfig: builder => {
builder
.addBooleanSwitch({
......
import { sharedSingleStatMigrationHandler, BigValueTextMode } from '@grafana/ui';
import { BigValueTextMode, sharedSingleStatMigrationHandler } from '@grafana/ui';
import { PanelPlugin } from '@grafana/data';
import { StatPanelOptions, addStandardDataReduceOptions } from './types';
import { addStandardDataReduceOptions, StatPanelOptions } from './types';
import { StatPanel } from './StatPanel';
import { statPanelChangedHandler } from './StatMigrations';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment