diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 17715ee256b..269d86ecdb7 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -24,12 +24,12 @@ export interface PanelEditorProps { /** * Called when a panel is first loaded with existing options */ -export type PanelMigrationHook = (exiting: any, oldVersion?: string) => Partial; +export type PanelMigrationHandler = (exiting: any, oldVersion?: string) => Partial; /** * Called before a panel is initalized */ -export type PanelTypeChangedHook = ( +export type PanelTypeChangedHandler = ( options: Partial, prevPluginId: string, prevOptions?: any @@ -39,6 +39,22 @@ export class ReactPanelPlugin { panel: ComponentClass>; editor?: ComponentClass>; defaults?: TOptions; + onPanelMigration?: PanelMigrationHandler; + onPanelTypeChanged?: PanelTypeChangedHandler; + + constructor(panel: ComponentClass>) { + this.panel = panel; + } + + setEditor(editor: ComponentClass>) { + 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 { * * This is a good place to support any changes to the options model */ - onPanelMigration?: PanelMigrationHook; + 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; - - constructor(panel: ComponentClass>, defaults?: TOptions) { - this.panel = panel; - this.defaults = defaults; + setPanelChangeHandler(handler: PanelTypeChangedHandler) { + this.onPanelTypeChanged = handler; + return this; } } diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index bb1794a5154..dcc20f19ea0 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/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 { diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 18646e2f9b3..ba9988fbca9 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/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 { 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 { } 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 }); } } diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index 82af0804029..1b75844809c 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/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); }); }); diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index fd85dd5e691..a7ce1fdb250 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/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)); } } diff --git a/public/app/features/plugins/__mocks__/pluginMocks.ts b/public/app/features/plugins/__mocks__/pluginMocks.ts index ab78f8094b3..5350aee0baa 100644 --- a/public/app/features/plugins/__mocks__/pluginMocks.ts +++ b/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 => { 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, }; }; diff --git a/public/app/plugins/panel/bargauge/module.tsx b/public/app/plugins/panel/bargauge/module.tsx index d921fae236a..e84c64f3710 100644 --- a/public/app/plugins/panel/bargauge/module.tsx +++ b/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(BarGaugePanel, defaults); - -reactPanel.editor = BarGaugePanelEditor; -reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck; +export const reactPanel = new ReactPanelPlugin(BarGaugePanel) + .setDefaults(defaults) + .setEditor(BarGaugePanelEditor) + .setPanelChangeHandler(singleStatBaseOptionsCheck); diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 54c1465372b..d811d29029a 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/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(GaugePanel, defaults); - -reactPanel.editor = GaugePanelEditor; -reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck; -reactPanel.onPanelMigration = singleStatMigrationCheck; +export const reactPanel = new ReactPanelPlugin(GaugePanel) + .setDefaults(defaults) + .setEditor(GaugePanelEditor) + .setPanelChangeHandler(singleStatBaseOptionsCheck) + .setMigrationHandler(singleStatMigrationCheck); diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 67ed1ae6c7a..8d27c36492f 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/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(GraphPanel, defaults); - -reactPanel.editor = GraphPanelEditor; +export const reactPanel = new ReactPanelPlugin(GraphPanel).setDefaults(defaults).setEditor(GraphPanelEditor); diff --git a/public/app/plugins/panel/piechart/PieChartPanelEditor.tsx b/public/app/plugins/panel/piechart/PieChartPanelEditor.tsx index 7a8aae8b7c9..d76a35dbf68 100644 --- a/public/app/plugins/panel/piechart/PieChartPanelEditor.tsx +++ b/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> { +export class PieChartPanelEditor extends PureComponent> { onValueMappingsChanged = (valueMappings: ValueMapping[]) => this.props.onOptionsChange({ ...this.props.options, diff --git a/public/app/plugins/panel/piechart/module.tsx b/public/app/plugins/panel/piechart/module.tsx index 5384a425958..fd9c14a4760 100644 --- a/public/app/plugins/panel/piechart/module.tsx +++ b/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(PieChartPanel, defaults); - -reactPanel.editor = PieChartPanelEditor; -reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck; +export const reactPanel = new ReactPanelPlugin(PieChartPanel) + .setDefaults(defaults) + .setEditor(PieChartPanelEditor); diff --git a/public/app/plugins/panel/singlestat2/module.tsx b/public/app/plugins/panel/singlestat2/module.tsx index 04e6b529eaa..55e777abfe0 100644 --- a/public/app/plugins/panel/singlestat2/module.tsx +++ b/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(SingleStatPanel, defaults); - -reactPanel.editor = SingleStatEditor; -reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck; -reactPanel.onPanelMigration = singleStatMigrationCheck; +export const reactPanel = new ReactPanelPlugin(SingleStatPanel) + .setDefaults(defaults) + .setEditor(SingleStatEditor) + .setPanelChangeHandler(singleStatMigrationCheck) + .setMigrationHandler(singleStatMigrationCheck); diff --git a/public/app/plugins/panel/table2/module.tsx b/public/app/plugins/panel/table2/module.tsx index 56135936540..ed04e6867b5 100644 --- a/public/app/plugins/panel/table2/module.tsx +++ b/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(TablePanel, defaults); -reactPanel.editor = TablePanelEditor; +export const reactPanel = new ReactPanelPlugin(TablePanel).setDefaults(defaults).setEditor(TablePanelEditor); diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index d40071c983d..29d5167463e 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/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(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(TextPanel) + .setDefaults(defaults) + .setEditor(TextPanelEditor) + .setPanelChangeHandler((options: TextOptions, prevPluginId: string, prevOptions: any) => { + if (prevPluginId === 'text') { + return prevOptions as TextOptions; + } + return options; + });