Browse Source

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
Dominik Prokop 6 years ago
parent
commit
73e4178aef

+ 97 - 81
packages/grafana-ui/src/components/SingleStatShared/FieldPropertiesEditor.tsx

@@ -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"
+      />
+    </>
+  );
+};

+ 1 - 1
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -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}

+ 2 - 2
public/app/features/dashboard/panel_editor/VisualizationTab.tsx

@@ -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() {

+ 61 - 39
public/app/features/dashboard/state/PanelModel.test.ts

@@ -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);
-      });
-    });
   });
 });

+ 12 - 4
public/app/features/dashboard/state/PanelModel.ts

@@ -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 || {};