Commit 73e4178a by Dominik Prokop Committed by Torkel Ödegaard

Panel: Apply option defaults on panel init and on save model retrieval (#17174)

* Apply panel options defaults on panel init and on save model retrieval

* Remove unnecessary argument, added tests

* Make FieldPropertiesEditor statefull to enable onBlur changes

* Remove unnecessary import

* Post-review updates

Fixes #17154
parent 87403999
// Libraries
import React, { PureComponent, ChangeEvent } from 'react';
import React, { ChangeEvent, useState, useCallback } from 'react';
// Components
import { FormField } from '../FormField/FormField';
......@@ -8,7 +8,7 @@ import { UnitPicker } from '../UnitPicker/UnitPicker';
// Types
import { Field } from '../../types/data';
import { toNumberString, toIntegerOrUndefined } from '../../utils';
import { toIntegerOrUndefined } from '../../utils';
import { SelectOptionItem } from '../Select/Select';
import { VAR_SERIES_NAME, VAR_FIELD_NAME, VAR_CALC, VAR_CELL_PREFIX } from '../../utils/fieldDisplay';
......@@ -21,92 +21,108 @@ export interface Props {
onChange: (value: Partial<Field>, event?: React.SyntheticEvent<HTMLElement>) => void;
}
export class FieldPropertiesEditor extends PureComponent<Props> {
onTitleChange = (event: ChangeEvent<HTMLInputElement>) =>
this.props.onChange({ ...this.props.value, title: event.target.value });
export const FieldPropertiesEditor: React.FC<Props> = ({ value, onChange, showMinMax }) => {
const { unit, title } = value;
// @ts-ignore
onUnitChange = (unit: SelectOptionItem<string>) => this.props.onChange({ ...this.props.value, unit: unit.value });
const [decimals, setDecimals] = useState(
value.decimals !== undefined && value.decimals !== null ? value.decimals.toString() : ''
);
const [min, setMin] = useState(value.min !== undefined && value.min !== null ? value.min.toString() : '');
const [max, setMax] = useState(value.max !== undefined && value.max !== null ? value.max.toString() : '');
onDecimalChange = (event: ChangeEvent<HTMLInputElement>) => {
this.props.onChange({
...this.props.value,
decimals: toIntegerOrUndefined(event.target.value),
});
const onTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
onChange({ ...value, title: event.target.value });
};
onMinChange = (event: ChangeEvent<HTMLInputElement>) => {
this.props.onChange({
...this.props.value,
min: toIntegerOrUndefined(event.target.value),
});
const onDecimalChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
setDecimals(event.target.value);
},
[value.decimals, onChange]
);
const onMinChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
setMin(event.target.value);
},
[value.min, onChange]
);
const onMaxChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
setMax(event.target.value);
},
[value.max, onChange]
);
const onUnitChange = (unit: SelectOptionItem<string>) => {
onChange({ ...value, unit: unit.value });
};
onMaxChange = (event: ChangeEvent<HTMLInputElement>) => {
this.props.onChange({
...this.props.value,
max: toIntegerOrUndefined(event.target.value),
const commitChanges = useCallback(() => {
onChange({
...value,
decimals: toIntegerOrUndefined(decimals),
min: toIntegerOrUndefined(min),
max: toIntegerOrUndefined(max),
});
};
}, [min, max, decimals]);
render() {
const { showMinMax } = this.props;
const { unit, decimals, min, max } = this.props.value;
const titleTooltip = (
<div>
Template Variables:
<br />
{'$' + VAR_SERIES_NAME}
<br />
{'$' + VAR_FIELD_NAME}
<br />
{'$' + VAR_CELL_PREFIX + '{N}'} / {'$' + VAR_CALC}
</div>
);
return (
<>
<FormField
label="Title"
labelWidth={labelWidth}
onChange={onTitleChange}
value={title}
tooltip={titleTooltip}
placeholder="Auto"
/>
const titleTooltip = (
<div>
Template Variables:
<br />
{'$' + VAR_SERIES_NAME}
<br />
{'$' + VAR_FIELD_NAME}
<br />
{'$' + VAR_CELL_PREFIX + '{N}'} / {'$' + VAR_CALC}
<div className="gf-form">
<FormLabel width={labelWidth}>Unit</FormLabel>
<UnitPicker defaultValue={unit} onChange={onUnitChange} />
</div>
);
return (
<>
<FormField
label="Title"
labelWidth={labelWidth}
onChange={this.onTitleChange}
value={this.props.value.title}
tooltip={titleTooltip}
placeholder="Auto"
/>
<div className="gf-form">
<FormLabel width={labelWidth}>Unit</FormLabel>
<UnitPicker defaultValue={unit} onChange={this.onUnitChange} />
</div>
{showMinMax && (
<>
<FormField
label="Min"
labelWidth={labelWidth}
onChange={this.onMinChange}
value={toNumberString(min)}
type="number"
/>
<FormField
label="Max"
labelWidth={labelWidth}
onChange={this.onMaxChange}
value={toNumberString(max)}
type="number"
/>
</>
)}
<FormField
label="Decimals"
labelWidth={labelWidth}
placeholder="auto"
onChange={this.onDecimalChange}
value={toNumberString(decimals)}
type="number"
/>
</>
);
}
}
{showMinMax && (
<>
<FormField
label="Min"
labelWidth={labelWidth}
onChange={onMinChange}
onBlur={commitChanges}
value={min}
type="number"
/>
<FormField
label="Max"
labelWidth={labelWidth}
onChange={onMaxChange}
onBlur={commitChanges}
value={max}
type="number"
/>
</>
)}
<FormField
label="Decimals"
labelWidth={labelWidth}
placeholder="auto"
onChange={onDecimalChange}
onBlur={commitChanges}
value={decimals}
type="number"
/>
</>
);
};
......@@ -253,7 +253,7 @@ export class PanelChrome extends PureComponent<Props, State> {
id={panel.id}
data={data}
timeRange={data.request ? data.request.range : this.timeSrv.timeRange()}
options={panel.getOptions(plugin.defaults)}
options={panel.getOptions()}
width={width - theme.panelPadding * 2}
height={innerPanelHeight}
renderCounter={renderCounter}
......
......@@ -53,8 +53,8 @@ export class VisualizationTab extends PureComponent<Props, State> {
}
getReactPanelOptions = () => {
const { panel, plugin } = this.props;
return panel.getOptions(plugin.defaults);
const { panel } = this.props;
return panel.getOptions();
};
renderPanelOptions() {
......
......@@ -7,45 +7,70 @@ describe('PanelModel', () => {
describe('when creating new panel model', () => {
let model;
let modelJson;
let persistedOptionsMock;
const defaultOptionsMock = {
fieldOptions: {
thresholds: [
{
color: '#F2495C',
index: 1,
value: 50,
},
{
color: '#73BF69',
index: 0,
value: null,
},
],
},
showThresholds: true,
};
beforeEach(() => {
persistedOptionsMock = {
fieldOptions: {
thresholds: [
{
color: '#F2495C',
index: 1,
value: 50,
},
{
color: '#73BF69',
index: 0,
value: null,
},
],
},
};
modelJson = {
type: 'table',
showColumns: true,
targets: [{ refId: 'A' }, { noRefId: true }],
options: {
fieldOptions: {
thresholds: [
{
color: '#F2495C',
index: 1,
value: 50,
},
{
color: '#73BF69',
index: 0,
value: null,
},
],
},
},
options: persistedOptionsMock,
};
model = new PanelModel(modelJson);
model.pluginLoaded(
getPanelPlugin(
{
id: 'table',
},
null, // react
TablePanelCtrl // angular
)
const panelPlugin = getPanelPlugin(
{
id: 'table',
},
null, // react
TablePanelCtrl // angular
);
panelPlugin.setDefaults(defaultOptionsMock);
model.pluginLoaded(panelPlugin);
});
it('should apply defaults', () => {
expect(model.gridPos.h).toBe(3);
});
it('should apply option defaults', () => {
expect(model.getOptions().showThresholds).toBeTruthy();
});
it('should set model props on instance', () => {
expect(model.showColumns).toBe(true);
});
......@@ -89,11 +114,22 @@ describe('PanelModel', () => {
});
describe('when changing panel type', () => {
const newPanelPluginDefaults = {
showThresholdLabels: false,
};
beforeEach(() => {
model.changePlugin(getPanelPlugin({ id: 'graph' }));
const newPlugin = getPanelPlugin({ id: 'graph' });
newPlugin.setDefaults(newPanelPluginDefaults);
model.changePlugin(newPlugin);
model.alert = { id: 2 };
});
it('should apply next panel option defaults', () => {
expect(model.getOptions().showThresholdLabels).toBeFalsy();
expect(model.getOptions().showThresholds).toBeUndefined();
});
it('should remove table properties but keep core props', () => {
expect(model.showColumns).toBe(undefined);
});
......@@ -153,19 +189,5 @@ describe('PanelModel', () => {
expect(panelQueryRunner).toBe(sameQueryRunner);
});
});
describe('get panel options', () => {
it('should apply defaults', () => {
model.options = { existingProp: 10 };
const options = model.getOptions({
defaultProp: true,
existingProp: 0,
});
expect(options.defaultProp).toBe(true);
expect(options.existingProp).toBe(10);
expect(model.options).toBe(options);
});
});
});
});
......@@ -157,8 +157,8 @@ export class PanelModel {
}
}
getOptions(panelDefaults: any) {
return _.defaultsDeep(this.options || {}, panelDefaults);
getOptions() {
return this.options;
}
updateOptions(options: object) {
......@@ -179,7 +179,6 @@ export class PanelModel {
model[property] = _.cloneDeep(this[property]);
}
return model;
}
......@@ -247,9 +246,18 @@ export class PanelModel {
});
}
private applyPluginOptionDefaults(plugin: PanelPlugin) {
if (plugin.angularConfigCtrl) {
return;
}
this.options = _.defaultsDeep({}, this.options || {}, plugin.defaults);
}
pluginLoaded(plugin: PanelPlugin) {
this.plugin = plugin;
this.applyPluginOptionDefaults(plugin);
if (plugin.panel && plugin.onPanelMigration) {
const version = getPluginVersion(plugin);
if (version !== this.pluginVersion) {
......@@ -284,7 +292,7 @@ export class PanelModel {
// switch
this.type = pluginId;
this.plugin = newPlugin;
this.applyPluginOptionDefaults(newPlugin);
// Let panel plugins inspect options from previous panel and keep any that it can use
if (newPlugin.onPanelTypeChanged) {
this.options = this.options || {};
......
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