Kaynağa Gözat

Merge pull request #16166 from ryantxu/drop-panel-plugin-setters

Refactor ReactPanelPlugin change hooks -> handler & add panel version to json
Torkel Ödegaard 6 yıl önce
ebeveyn
işleme
090b3f6c6d

+ 26 - 14
packages/grafana-ui/src/types/panel.ts

@@ -21,27 +21,32 @@ export interface PanelEditorProps<T = any> {
   onOptionsChange: (options: T) => void;
 }
 
+export interface PanelModel<TOptions = any> {
+  id: number;
+  options: TOptions;
+  pluginVersion?: string;
+}
+
 /**
- * Called when a panel is first loaded with existing options
+ * Called when a panel is first loaded with current panel model
  */
-export type PanelMigrationHook<TOptions = any> = (options: Partial<TOptions>) => Partial<TOptions>;
+export type PanelMigrationHandler<TOptions = any> = (panel: PanelModel<TOptions>) => Partial<TOptions>;
 
 /**
  * Called before a panel is initalized
  */
-export type PanelTypeChangedHook<TOptions = any> = (
+export type PanelTypeChangedHandler<TOptions = any> = (
   options: Partial<TOptions>,
   prevPluginId: string,
-  prevOptions?: any
+  prevOptions: any
 ) => Partial<TOptions>;
 
 export class ReactPanelPlugin<TOptions = any> {
   panel: ComponentClass<PanelProps<TOptions>>;
   editor?: ComponentClass<PanelEditorProps<TOptions>>;
   defaults?: TOptions;
-
-  panelMigrationHook?: PanelMigrationHook<TOptions>;
-  panelTypeChangedHook?: PanelTypeChangedHook<TOptions>;
+  onPanelMigration?: PanelMigrationHandler<TOptions>;
+  onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>;
 
   constructor(panel: ComponentClass<PanelProps<TOptions>>) {
     this.panel = panel;
@@ -49,25 +54,32 @@ export class ReactPanelPlugin<TOptions = any> {
 
   setEditor(editor: ComponentClass<PanelEditorProps<TOptions>>) {
     this.editor = editor;
+    return this;
   }
 
   setDefaults(defaults: TOptions) {
     this.defaults = defaults;
+    return this;
   }
 
   /**
-   * Called when the panel first loaded with
+   * This function is called before the panel first loads if
+   * the current version is different than the version that was saved.
+   *
+   * This is a good place to support any changes to the options model
    */
-  setPanelMigrationHook(v: PanelMigrationHook<TOptions>) {
-    this.panelMigrationHook = v;
+  setMigrationHandler(handler: PanelMigrationHandler) {
+    this.onPanelMigration = handler;
+    return this;
   }
 
   /**
-   * Called when the visualization changes.
-   * Lets you keep whatever settings made sense in the previous panel
+   * This function is called when the visualization was changed.  This
+   * passes in the options that were used in the previous visualization
    */
-  setPanelTypeChangedHook(v: PanelTypeChangedHook<TOptions>) {
-    this.panelTypeChangedHook = v;
+  setPanelChangeHandler(handler: PanelTypeChangedHandler) {
+    this.onPanelTypeChanged = handler;
+    return this;
   }
 }
 

+ 1 - 1
packages/grafana-ui/src/types/plugin.ts

@@ -81,7 +81,7 @@ export interface PluginExports {
 
   // Panel plugin
   PanelCtrl?: any;
-  reactPanel: ReactPanelPlugin;
+  reactPanel?: ReactPanelPlugin;
 }
 
 export interface PluginMeta {

+ 5 - 1
public/app/core/utils/emitter.ts

@@ -1,7 +1,7 @@
 import { EventEmitter } from 'eventemitter3';
 
 export class Emitter {
-  emitter: any;
+  private emitter: EventEmitter;
 
   constructor() {
     this.emitter = new EventEmitter();
@@ -29,4 +29,8 @@ export class Emitter {
   off(name, handler) {
     this.emitter.off(name, handler);
   }
+
+  getEventCount(): number {
+    return (this.emitter as any)._eventsCount;
+  }
 }

+ 3 - 21
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -14,7 +14,6 @@ import { PanelEditor } from '../panel_editor/PanelEditor';
 import { PanelModel, DashboardModel } from '../state';
 import { PanelPlugin } from 'app/types';
 import { PanelResizer } from './PanelResizer';
-import { PanelTypeChangedHook } from '@grafana/ui';
 
 export interface Props {
   panel: PanelModel;
@@ -71,9 +70,6 @@ export class DashboardPanel extends PureComponent<Props, State> {
     if (!this.state.plugin || this.state.plugin.id !== pluginId) {
       let plugin = config.panels[pluginId] || getPanelPluginNotFound(pluginId);
 
-      // remember if this is from an angular panel
-      const fromAngularPanel = this.state.angularPanel != null;
-
       // unmount angular panel
       this.cleanUpAngularPanel();
 
@@ -86,23 +82,9 @@ export class DashboardPanel extends PureComponent<Props, State> {
       }
 
       if (panel.type !== pluginId) {
-        if (fromAngularPanel) {
-          // for angular panels only we need to remove all events and let angular panels do some cleanup
-          panel.destroy();
-
-          this.props.panel.changeType(pluginId);
-        } else {
-          let hook: PanelTypeChangedHook | null = null;
-          if (plugin.exports.reactPanel) {
-            hook = plugin.exports.reactPanel.panelTypeChangedHook;
-          }
-          panel.changeType(pluginId, hook);
-        }
-      } else if (plugin.exports && plugin.exports.reactPanel && panel.options) {
-        const hook = plugin.exports.reactPanel.panelMigrationHook;
-        if (hook) {
-          panel.options = hook(panel.options);
-        }
+        panel.changePlugin(plugin);
+      } else {
+        panel.pluginLoaded(plugin);
       }
 
       this.setState({ plugin, angularPanel: null });

+ 43 - 3
public/app/features/dashboard/state/PanelModel.test.ts

@@ -1,4 +1,6 @@
 import { PanelModel } from './PanelModel';
+import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
+import { ReactPanelPlugin } from '@grafana/ui/src/types/panel';
 
 describe('PanelModel', () => {
   describe('when creating new panel model', () => {
@@ -76,7 +78,7 @@ describe('PanelModel', () => {
 
     describe('when changing panel type', () => {
       beforeEach(() => {
-        model.changeType('graph');
+        model.changePlugin(getPanelPlugin({ id: 'graph', exports: {} }));
         model.alert = { id: 2 };
       });
 
@@ -85,16 +87,54 @@ describe('PanelModel', () => {
       });
 
       it('should restore table properties when changing back', () => {
-        model.changeType('table');
+        model.changePlugin(getPanelPlugin({ id: 'table', exports: {} }));
         expect(model.showColumns).toBe(true);
       });
 
       it('should remove alert rule when changing type that does not support it', () => {
-        model.changeType('table');
+        model.changePlugin(getPanelPlugin({ id: 'table', exports: {} }));
         expect(model.alert).toBe(undefined);
       });
     });
 
+    describe('when changing from angular panel', () => {
+      let tearDownPublished = false;
+
+      beforeEach(() => {
+        model.events.on('panel-teardown', () => {
+          tearDownPublished = true;
+        });
+        model.changePlugin(getPanelPlugin({ id: 'graph', exports: {} }));
+      });
+
+      it('should teardown / destroy panel so angular panels event subscriptions are removed', () => {
+        expect(tearDownPublished).toBe(true);
+        expect(model.events.getEventCount()).toBe(0);
+      });
+    });
+
+    describe('when changing to react panel', () => {
+      const onPanelTypeChanged = jest.fn();
+      const reactPanel = new ReactPanelPlugin({} as any).setPanelChangeHandler(onPanelTypeChanged as any);
+
+      beforeEach(() => {
+        model.changePlugin(
+          getPanelPlugin({
+            id: 'react',
+            exports: {
+              reactPanel,
+            },
+          })
+        );
+      });
+
+      it('should call react onPanelTypeChanged', () => {
+        expect(onPanelTypeChanged.mock.calls.length).toBe(1);
+        expect(onPanelTypeChanged.mock.calls[0][1]).toBe('table');
+        expect(onPanelTypeChanged.mock.calls[0][2].thresholds).toBeDefined();
+      });
+    });
+
     describe('get panel options', () => {
       it('should apply defaults', () => {
         model.options = { existingProp: 10 };

+ 33 - 9
public/app/features/dashboard/state/PanelModel.ts

@@ -6,8 +6,8 @@ import { Emitter } from 'app/core/utils/emitter';
 import { getNextRefIdChar } from 'app/core/utils/query';
 
 // Types
-import { DataQuery, TimeSeries, Threshold, ScopedVars, PanelTypeChangedHook } from '@grafana/ui';
-import { TableData } from '@grafana/ui/src';
+import { DataQuery, TimeSeries, Threshold, ScopedVars, TableData } from '@grafana/ui';
+import { PanelPlugin } from 'app/types';
 
 export interface GridPos {
   x: number;
@@ -23,6 +23,7 @@ const notPersistedProperties: { [str: string]: boolean } = {
   isEditing: true,
   hasRefreshed: true,
   cachedPluginOptions: true,
+  plugin: true,
 };
 
 // For angular panels we need to clean up properties when changing type
@@ -58,6 +59,7 @@ const mustKeepProps: { [str: string]: boolean } = {
   cacheTimeout: true,
   cachedPluginOptions: true,
   transparent: true,
+  pluginVersion: true,
 };
 
 const defaults: any = {
@@ -87,6 +89,7 @@ export class PanelModel {
   targets: DataQuery[];
   datasource: string;
   thresholds?: any;
+  pluginVersion?: string;
 
   snapshotData?: TimeSeries[] | [TableData];
   timeFrom?: any;
@@ -110,6 +113,7 @@ export class PanelModel {
   cacheTimeout?: any;
   cachedPluginOptions?: any;
   legend?: { show: boolean };
+  plugin?: PanelPlugin;
 
   constructor(model: any) {
     this.events = new Emitter();
@@ -240,11 +244,27 @@ export class PanelModel {
     });
   }
 
-  changeType(pluginId: string, hook?: PanelTypeChangedHook) {
+  pluginLoaded(plugin: PanelPlugin) {
+    this.plugin = plugin;
+
+    const { reactPanel } = plugin.exports;
+
+    if (reactPanel && reactPanel.onPanelMigration) {
+      this.options = reactPanel.onPanelMigration(this);
+      this.pluginVersion = plugin.info ? plugin.info.version : '1.0.0';
+    }
+  }
+
+  changePlugin(newPlugin: PanelPlugin) {
+    const pluginId = newPlugin.id;
     const oldOptions: any = this.getOptionsToRemember();
     const oldPluginId = this.type;
+    const reactPanel = newPlugin.exports.reactPanel;
 
-    this.type = pluginId;
+    // for angular panels we must remove all events and let angular panels do some cleanup
+    if (!reactPanel) {
+      this.destroy();
+    }
 
     // remove panel type specific  options
     for (const key of _.keys(this)) {
@@ -258,12 +278,16 @@ export class PanelModel {
     this.cachedPluginOptions[oldPluginId] = oldOptions;
     this.restorePanelOptions(pluginId);
 
-    // Callback that can validate and migrate any existing settings
-    if (hook) {
-      this.options = this.options || {};
-      const old = oldOptions ? oldOptions.options : null;
+    // switch
+    this.type = pluginId;
+    this.plugin = newPlugin;
 
-      Object.assign(this.options, hook(this.options, oldPluginId, old));
+    // Let panel plugins inspect options from previous panel and keep any that it can use
+    const onPanelTypeChanged = reactPanel ? reactPanel.onPanelTypeChanged : null;
+    if (onPanelTypeChanged) {
+      this.options = this.options || {};
+      const old = oldOptions ? oldOptions.options : {};
+      Object.assign(this.options, onPanelTypeChanged(this.options, oldPluginId, old));
     }
   }
 

+ 2 - 1
public/app/features/plugins/__mocks__/pluginMocks.ts

@@ -33,7 +33,7 @@ export const getMockPlugins = (amount: number): Plugin[] => {
   return plugins;
 };
 
-export const getPanelPlugin = (options: { id: string; sort?: number; hideFromList?: boolean }): PanelPlugin => {
+export const getPanelPlugin = (options: Partial<PanelPlugin>): PanelPlugin => {
   return {
     id: options.id,
     name: options.id,
@@ -56,6 +56,7 @@ export const getPanelPlugin = (options: { id: string; sort?: number; hideFromLis
     hideFromList: options.hideFromList === true,
     module: '',
     baseUrl: '',
+    exports: options.exports,
   };
 };
 

+ 4 - 5
public/app/plugins/panel/bargauge/module.tsx

@@ -5,8 +5,7 @@ import { BarGaugePanelEditor } from './BarGaugePanelEditor';
 import { BarGaugeOptions, defaults } from './types';
 import { singleStatBaseOptionsCheck } from '../singlestat2/module';
 
-export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel);
-
-reactPanel.setEditor(BarGaugePanelEditor);
-reactPanel.setDefaults(defaults);
-reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
+export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel)
+  .setDefaults(defaults)
+  .setEditor(BarGaugePanelEditor)
+  .setPanelChangeHandler(singleStatBaseOptionsCheck);

+ 6 - 6
public/app/plugins/panel/gauge/module.tsx

@@ -3,10 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui';
 import { GaugePanelEditor } from './GaugePanelEditor';
 import { GaugePanel } from './GaugePanel';
 import { GaugeOptions, defaults } from './types';
-import { singleStatBaseOptionsCheck } from '../singlestat2/module';
+import { singleStatBaseOptionsCheck, singleStatMigrationCheck } from '../singlestat2/module';
 
-export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel);
-
-reactPanel.setEditor(GaugePanelEditor);
-reactPanel.setDefaults(defaults);
-reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
+export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel)
+  .setDefaults(defaults)
+  .setEditor(GaugePanelEditor)
+  .setPanelChangeHandler(singleStatBaseOptionsCheck)
+  .setMigrationHandler(singleStatMigrationCheck);

+ 2 - 4
public/app/plugins/panel/graph2/module.tsx

@@ -1,8 +1,6 @@
 import { ReactPanelPlugin } from '@grafana/ui';
-
 import { GraphPanelEditor } from './GraphPanelEditor';
 import { GraphPanel } from './GraphPanel';
-import { Options } from './types';
+import { Options, defaults } from './types';
 
-export const reactPanel = new ReactPanelPlugin<Options>(GraphPanel);
-reactPanel.setEditor(GraphPanelEditor);
+export const reactPanel = new ReactPanelPlugin<Options>(GraphPanel).setDefaults(defaults).setEditor(GraphPanelEditor);

+ 6 - 0
public/app/plugins/panel/graph2/types.ts

@@ -3,3 +3,9 @@ export interface Options {
   showLines: boolean;
   showPoints: boolean;
 }
+
+export const defaults: Options = {
+  showBars: false,
+  showLines: true,
+  showPoints: false,
+};

+ 1 - 1
public/app/plugins/panel/piechart/PieChartPanelEditor.tsx

@@ -6,7 +6,7 @@ import { PieChartOptions } from './types';
 import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor';
 import { SingleStatValueOptions } from '../singlestat2/types';
 
-export default class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChartOptions>> {
+export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChartOptions>> {
   onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
     this.props.onOptionsChange({
       ...this.props.options,

+ 4 - 8
public/app/plugins/panel/piechart/module.tsx

@@ -1,12 +1,8 @@
 import { ReactPanelPlugin } from '@grafana/ui';
-
-import PieChartPanelEditor from './PieChartPanelEditor';
+import { PieChartPanelEditor } from './PieChartPanelEditor';
 import { PieChartPanel } from './PieChartPanel';
 import { PieChartOptions, defaults } from './types';
-import { singleStatBaseOptionsCheck } from '../singlestat2/module';
-
-export const reactPanel = new ReactPanelPlugin<PieChartOptions>(PieChartPanel);
 
-reactPanel.setEditor(PieChartPanelEditor);
-reactPanel.setDefaults(defaults);
-reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
+export const reactPanel = new ReactPanelPlugin<PieChartOptions>(PieChartPanel)
+  .setDefaults(defaults)
+  .setEditor(PieChartPanelEditor);

+ 21 - 21
public/app/plugins/panel/singlestat2/module.tsx

@@ -1,39 +1,39 @@
-import { ReactPanelPlugin, getStatsCalculators } from '@grafana/ui';
+import { ReactPanelPlugin, getStatsCalculators, PanelModel } from '@grafana/ui';
 import { SingleStatOptions, defaults, SingleStatBaseOptions } from './types';
 import { SingleStatPanel } from './SingleStatPanel';
 import cloneDeep from 'lodash/cloneDeep';
 import { SingleStatEditor } from './SingleStatEditor';
 
-export const reactPanel = new ReactPanelPlugin<SingleStatOptions>(SingleStatPanel);
-
 const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings'];
 
 export const singleStatBaseOptionsCheck = (
   options: Partial<SingleStatBaseOptions>,
   prevPluginId: string,
-  prevOptions?: any
+  prevOptions: any
 ) => {
-  if (prevOptions) {
-    optionsToKeep.forEach(v => {
-      if (prevOptions.hasOwnProperty(v)) {
-        options[v] = cloneDeep(prevOptions.display);
-      }
-    });
-  }
+  optionsToKeep.forEach(v => {
+    if (prevOptions.hasOwnProperty(v)) {
+      options[v] = cloneDeep(prevOptions.display);
+    }
+  });
   return options;
 };
 
-export const singleStatMigrationCheck = (options: Partial<SingleStatBaseOptions>) => {
-  // 6.1 renamed some stats, This makes sure they are up to date
-  // avg -> mean, current -> last, total -> sum
-  const { valueOptions } = options;
-  if (valueOptions && valueOptions.stat) {
-    valueOptions.stat = getStatsCalculators([valueOptions.stat]).map(s => s.id)[0];
+export const singleStatMigrationCheck = (panel: PanelModel<SingleStatOptions>) => {
+  const options = panel.options;
+  if (options.valueOptions) {
+    // 6.1 renamed some stats, This makes sure they are up to date
+    // avg -> mean, current -> last, total -> sum
+    const { valueOptions } = options;
+    if (valueOptions && valueOptions.stat) {
+      valueOptions.stat = getStatsCalculators([valueOptions.stat]).map(s => s.id)[0];
+    }
   }
   return options;
 };
 
-reactPanel.setEditor(SingleStatEditor);
-reactPanel.setDefaults(defaults);
-reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
-reactPanel.setPanelMigrationHook(singleStatMigrationCheck);
+export const reactPanel = new ReactPanelPlugin<SingleStatOptions>(SingleStatPanel)
+  .setDefaults(defaults)
+  .setEditor(SingleStatEditor)
+  .setPanelChangeHandler(singleStatMigrationCheck)
+  .setMigrationHandler(singleStatMigrationCheck);

+ 1 - 3
public/app/plugins/panel/table2/module.tsx

@@ -4,6 +4,4 @@ import { TablePanelEditor } from './TablePanelEditor';
 import { TablePanel } from './TablePanel';
 import { Options, defaults } from './types';
 
-export const reactPanel = new ReactPanelPlugin<Options>(TablePanel);
-reactPanel.setEditor(TablePanelEditor);
-reactPanel.setDefaults(defaults);
+export const reactPanel = new ReactPanelPlugin<Options>(TablePanel).setDefaults(defaults).setEditor(TablePanelEditor);

+ 9 - 11
public/app/plugins/panel/text2/module.tsx

@@ -4,14 +4,12 @@ import { TextPanelEditor } from './TextPanelEditor';
 import { TextPanel } from './TextPanel';
 import { TextOptions, defaults } from './types';
 
-export const reactPanel = new ReactPanelPlugin<TextOptions>(TextPanel);
-
-reactPanel.setEditor(TextPanelEditor);
-reactPanel.setDefaults(defaults);
-reactPanel.setPanelTypeChangedHook((options: TextOptions, prevPluginId: string, prevOptions: any) => {
-  if (prevPluginId === 'text') {
-    return prevOptions as TextOptions;
-  }
-
-  return options;
-});
+export const reactPanel = new ReactPanelPlugin<TextOptions>(TextPanel)
+  .setDefaults(defaults)
+  .setEditor(TextPanelEditor)
+  .setPanelChangeHandler((options: TextOptions, prevPluginId: string, prevOptions: any) => {
+    if (prevPluginId === 'text') {
+      return prevOptions as TextOptions;
+    }
+    return options;
+  });