mirror of
https://github.com/grafana/grafana.git
synced 2025-01-26 16:27:02 -06:00
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
This commit is contained in:
parent
874039992f
commit
73e4178aef
@ -1,5 +1,5 @@
|
||||
// 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 || {};
|
||||
|
Loading…
Reference in New Issue
Block a user