ソースを参照

Panels: Refactoring how panel plugins sets hooks and components, #16166

Torkel Ödegaard 6 年 前
コミット
0d55141a2d

+ 25 - 8
packages/grafana-ui/src/types/panel.ts

@@ -24,12 +24,12 @@ export interface PanelEditorProps<T = any> {
 /**
  * Called when a panel is first loaded with existing options
  */
-export type PanelMigrationHook<TOptions = any> = (exiting: any, oldVersion?: string) => Partial<TOptions>;
+export type PanelMigrationHandler<TOptions = any> = (exiting: any, oldVersion?: string) => 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
@@ -39,6 +39,22 @@ export class ReactPanelPlugin<TOptions = any> {
   panel: ComponentClass<PanelProps<TOptions>>;
   editor?: ComponentClass<PanelEditorProps<TOptions>>;
   defaults?: TOptions;
+  onPanelMigration?: PanelMigrationHandler<TOptions>;
+  onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>;
+
+  constructor(panel: ComponentClass<PanelProps<TOptions>>) {
+    this.panel = panel;
+  }
+
+  setEditor(editor: ComponentClass<PanelEditorProps<TOptions>>) {
+    this.editor = editor;
+    return this;
+  }
+
+  setDefaults(defaults: TOptions) {
+    this.defaults = defaults;
+    return this;
+  }
 
   /**
    * This function is called before the panel first loads if
@@ -46,17 +62,18 @@ export class ReactPanelPlugin<TOptions = any> {
    *
    * This is a good place to support any changes to the options model
    */
-  onPanelMigration?: PanelMigrationHook<TOptions>;
+  setMigrationHandler(handler: PanelMigrationHandler) {
+    this.onPanelMigration = handler;
+    return this;
+  }
 
   /**
    * This function is called when the visualization was changed.  This
    * passes in the options that were used in the previous visualization
    */
-  onPanelTypeChanged?: PanelTypeChangedHook<TOptions>;
-
-  constructor(panel: ComponentClass<PanelProps<TOptions>>, defaults?: TOptions) {
-    this.panel = panel;
-    this.defaults = defaults;
+  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 {

+ 4 - 24
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -1,7 +1,6 @@
 import React, { PureComponent } from 'react';
 import config from 'app/core/config';
 import classNames from 'classnames';
-import get from 'lodash/get';
 
 import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
 import { importPluginModule } from 'app/features/plugins/plugin_loader';
@@ -15,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;
@@ -72,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();
 
@@ -87,26 +82,11 @@ 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.onPanelTypeChanged;
-          }
-          panel.changeType(pluginId, hook);
-        }
-      } else if (plugin.exports && plugin.exports.reactPanel && panel.options) {
-        const pluginVersion = get(plugin, 'info.version') || config.buildInfo.version;
-        const hook = plugin.exports.reactPanel.onPanelMigration;
-        if (hook && panel.pluginVersion !== pluginVersion) {
-          panel.options = hook(panel.options, panel.pluginVersion);
-          panel.pluginVersion = pluginVersion;
-        }
+        panel.changePlugin(plugin);
+      } else {
+        panel.pluginLoaded(plugin);
       }
+
       this.setState({ plugin, angularPanel: null });
     }
   }

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

@@ -1,4 +1,5 @@
 import { PanelModel } from './PanelModel';
+import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
 
 describe('PanelModel', () => {
   describe('when creating new panel model', () => {
@@ -76,7 +77,7 @@ describe('PanelModel', () => {
 
     describe('when changing panel type', () => {
       beforeEach(() => {
-        model.changeType('graph');
+        model.changePlugin(getPanelPlugin({ id: 'graph', exports: {} }));
         model.alert = { id: 2 };
       });
 
@@ -85,12 +86,12 @@ 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);
       });
     });

+ 29 - 7
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
@@ -112,6 +113,7 @@ export class PanelModel {
   cacheTimeout?: any;
   cachedPluginOptions?: any;
   legend?: { show: boolean };
+  plugin?: PanelPlugin;
 
   constructor(model: any) {
     this.events = new Emitter();
@@ -242,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.options, this.pluginVersion);
+      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)) {
@@ -260,12 +278,16 @@ export class PanelModel {
     this.cachedPluginOptions[oldPluginId] = oldOptions;
     this.restorePanelOptions(pluginId);
 
+    // switch
+    this.type = pluginId;
+    this.plugin = newPlugin;
+
     // Callback that can validate and migrate any existing settings
-    if (hook) {
+    const onPanelTypeChanged = reactPanel ? reactPanel.onPanelTypeChanged : null;
+    if (onPanelTypeChanged) {
       this.options = this.options || {};
       const old = oldOptions ? oldOptions.options : null;
-
-      Object.assign(this.options, hook(this.options, oldPluginId, old));
+      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 - 4
public/app/plugins/panel/bargauge/module.tsx

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

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

@@ -5,8 +5,8 @@ import { GaugePanel } from './GaugePanel';
 import { GaugeOptions, defaults } from './types';
 import { singleStatBaseOptionsCheck, singleStatMigrationCheck } from '../singlestat2/module';
 
-export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel, defaults);
-
-reactPanel.editor = GaugePanelEditor;
-reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck;
-reactPanel.onPanelMigration = singleStatMigrationCheck;
+export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel)
+  .setDefaults(defaults)
+  .setEditor(GaugePanelEditor)
+  .setPanelChangeHandler(singleStatBaseOptionsCheck)
+  .setMigrationHandler(singleStatMigrationCheck);

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

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

+ 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 - 7
public/app/plugins/panel/piechart/module.tsx

@@ -1,11 +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, defaults);
 
-reactPanel.editor = PieChartPanelEditor;
-reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck;
+export const reactPanel = new ReactPanelPlugin<PieChartOptions>(PieChartPanel)
+  .setDefaults(defaults)
+  .setEditor(PieChartPanelEditor);

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

@@ -35,8 +35,8 @@ export const singleStatMigrationCheck = (exiting: any, oldVersion?: string) => {
   return options;
 };
 
-export const reactPanel = new ReactPanelPlugin<SingleStatOptions>(SingleStatPanel, defaults);
-
-reactPanel.editor = SingleStatEditor;
-reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck;
-reactPanel.onPanelMigration = singleStatMigrationCheck;
+export const reactPanel = new ReactPanelPlugin<SingleStatOptions>(SingleStatPanel)
+  .setDefaults(defaults)
+  .setEditor(SingleStatEditor)
+  .setPanelChangeHandler(singleStatMigrationCheck)
+  .setMigrationHandler(singleStatMigrationCheck);

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

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

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

@@ -4,12 +4,12 @@ import { TextPanelEditor } from './TextPanelEditor';
 import { TextPanel } from './TextPanel';
 import { TextOptions, defaults } from './types';
 
-export const reactPanel = new ReactPanelPlugin<TextOptions>(TextPanel, defaults);
-
-reactPanel.editor = TextPanelEditor;
-reactPanel.onPanelTypeChanged = (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;
+  });