From 82be27a42a50c303a6632155d2164f20b5f741e8 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 22 Mar 2019 13:12:35 -0700 Subject: [PATCH 1/6] remove panel plugin setters --- packages/grafana-ui/src/types/panel.ts | 30 +++---------------- .../dashboard/dashgrid/DashboardPanel.tsx | 4 +-- public/app/plugins/panel/bargauge/module.tsx | 7 ++--- public/app/plugins/panel/gauge/module.tsx | 10 +++---- public/app/plugins/panel/graph2/module.tsx | 7 +++-- public/app/plugins/panel/graph2/types.ts | 6 ++++ public/app/plugins/panel/piechart/module.tsx | 5 ++-- .../app/plugins/panel/singlestat2/module.tsx | 11 ++++--- public/app/plugins/panel/table2/module.tsx | 5 ++-- public/app/plugins/panel/text2/module.tsx | 10 +++---- 10 files changed, 37 insertions(+), 58 deletions(-) diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index bd535655551..9341ef376e2 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -24,7 +24,7 @@ export interface PanelEditorProps { /** * Called when a panel is first loaded with existing options */ -export type PanelMigrationHook = (options: Partial) => Partial; +export type PanelMigrationHook = (options: any) => Partial; /** * Called before a panel is initalized @@ -40,35 +40,13 @@ export class ReactPanelPlugin { editor?: ComponentClass>; defaults?: TOptions; - panelMigrationHook?: PanelMigrationHook; - panelTypeChangedHook?: PanelTypeChangedHook; + onPanelMigration?: PanelMigrationHook; + onPanelTypeChanged?: PanelTypeChangedHook; - constructor(panel: ComponentClass>) { + constructor(panel: ComponentClass>, defaults?: TOptions) { this.panel = panel; - } - - setEditor(editor: ComponentClass>) { - this.editor = editor; - } - - setDefaults(defaults: TOptions) { this.defaults = defaults; } - - /** - * Called when the panel first loaded with - */ - setPanelMigrationHook(v: PanelMigrationHook) { - this.panelMigrationHook = v; - } - - /** - * Called when the visualization changes. - * Lets you keep whatever settings made sense in the previous panel - */ - setPanelTypeChangedHook(v: PanelTypeChangedHook) { - this.panelTypeChangedHook = v; - } } export interface PanelSize { diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 243380c617e..e2111e4892d 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -94,12 +94,12 @@ export class DashboardPanel extends PureComponent { } else { let hook: PanelTypeChangedHook | null = null; if (plugin.exports.reactPanel) { - hook = plugin.exports.reactPanel.panelTypeChangedHook; + hook = plugin.exports.reactPanel.onPanelTypeChanged; } panel.changeType(pluginId, hook); } } else if (plugin.exports && plugin.exports.reactPanel && panel.options) { - const hook = plugin.exports.reactPanel.panelMigrationHook; + const hook = plugin.exports.reactPanel.onPanelMigration; if (hook) { panel.options = hook(panel.options); } diff --git a/public/app/plugins/panel/bargauge/module.tsx b/public/app/plugins/panel/bargauge/module.tsx index 3c46adeb4f9..d921fae236a 100644 --- a/public/app/plugins/panel/bargauge/module.tsx +++ b/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(BarGaugePanel); +export const reactPanel = new ReactPanelPlugin(BarGaugePanel, defaults); -reactPanel.setEditor(BarGaugePanelEditor); -reactPanel.setDefaults(defaults); -reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck); +reactPanel.editor = BarGaugePanelEditor; +reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck; diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 340af06a080..54c1465372b 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/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(GaugePanel); +export const reactPanel = new ReactPanelPlugin(GaugePanel, defaults); -reactPanel.setEditor(GaugePanelEditor); -reactPanel.setDefaults(defaults); -reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck); +reactPanel.editor = GaugePanelEditor; +reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck; +reactPanel.onPanelMigration = singleStatMigrationCheck; diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 6dd6d4a77c4..67ed1ae6c7a 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -2,7 +2,8 @@ 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(GraphPanel); -reactPanel.setEditor(GraphPanelEditor); +export const reactPanel = new ReactPanelPlugin(GraphPanel, defaults); + +reactPanel.editor = GraphPanelEditor; diff --git a/public/app/plugins/panel/graph2/types.ts b/public/app/plugins/panel/graph2/types.ts index b9baaa09cd7..1f6e9c093dd 100644 --- a/public/app/plugins/panel/graph2/types.ts +++ b/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, +}; diff --git a/public/app/plugins/panel/piechart/module.tsx b/public/app/plugins/panel/piechart/module.tsx index 3e0ef90dc6c..8336cd6803f 100644 --- a/public/app/plugins/panel/piechart/module.tsx +++ b/public/app/plugins/panel/piechart/module.tsx @@ -4,7 +4,6 @@ import PieChartPanelEditor from './PieChartPanelEditor'; import { PieChartPanel } from './PieChartPanel'; import { PieChartOptions, defaults } from './types'; -export const reactPanel = new ReactPanelPlugin(PieChartPanel); +export const reactPanel = new ReactPanelPlugin(PieChartPanel, defaults); -reactPanel.setEditor(PieChartPanelEditor); -reactPanel.setDefaults(defaults); +reactPanel.editor = PieChartPanelEditor; diff --git a/public/app/plugins/panel/singlestat2/module.tsx b/public/app/plugins/panel/singlestat2/module.tsx index 4b2c27c360a..42142c0f673 100644 --- a/public/app/plugins/panel/singlestat2/module.tsx +++ b/public/app/plugins/panel/singlestat2/module.tsx @@ -4,8 +4,6 @@ import { SingleStatPanel } from './SingleStatPanel'; import cloneDeep from 'lodash/cloneDeep'; import { SingleStatEditor } from './SingleStatEditor'; -export const reactPanel = new ReactPanelPlugin(SingleStatPanel); - const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings']; export const singleStatBaseOptionsCheck = ( @@ -33,7 +31,8 @@ export const singleStatMigrationCheck = (options: Partial return options; }; -reactPanel.setEditor(SingleStatEditor); -reactPanel.setDefaults(defaults); -reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck); -reactPanel.setPanelMigrationHook(singleStatMigrationCheck); +export const reactPanel = new ReactPanelPlugin(SingleStatPanel, defaults); + +reactPanel.editor = SingleStatEditor; +reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck; +reactPanel.onPanelMigration = singleStatMigrationCheck; diff --git a/public/app/plugins/panel/table2/module.tsx b/public/app/plugins/panel/table2/module.tsx index d93e7911074..56135936540 100644 --- a/public/app/plugins/panel/table2/module.tsx +++ b/public/app/plugins/panel/table2/module.tsx @@ -4,6 +4,5 @@ import { TablePanelEditor } from './TablePanelEditor'; import { TablePanel } from './TablePanel'; import { Options, defaults } from './types'; -export const reactPanel = new ReactPanelPlugin(TablePanel); -reactPanel.setEditor(TablePanelEditor); -reactPanel.setDefaults(defaults); +export const reactPanel = new ReactPanelPlugin(TablePanel, defaults); +reactPanel.editor = TablePanelEditor; diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index b2e3057de34..d40071c983d 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/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(TextPanel); +export const reactPanel = new ReactPanelPlugin(TextPanel, defaults); -reactPanel.setEditor(TextPanelEditor); -reactPanel.setDefaults(defaults); -reactPanel.setPanelTypeChangedHook((options: TextOptions, prevPluginId: string, prevOptions: any) => { +reactPanel.editor = TextPanelEditor; +reactPanel.onPanelTypeChanged = (options: TextOptions, prevPluginId: string, prevOptions: any) => { if (prevPluginId === 'text') { return prevOptions as TextOptions; } - return options; -}); +}; From 6da2f132c7f460522adc373cc4cf5aec45839aee Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 22 Mar 2019 13:45:09 -0700 Subject: [PATCH 2/6] keep plugin versions --- packages/grafana-ui/src/types/panel.ts | 13 ++++++++++++- .../dashboard/dashgrid/DashboardPanel.tsx | 8 +++++--- .../app/features/dashboard/state/PanelModel.ts | 2 ++ public/app/plugins/panel/singlestat2/module.tsx | 16 ++++++++++------ 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 9341ef376e2..17715ee256b 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -24,7 +24,7 @@ export interface PanelEditorProps { /** * Called when a panel is first loaded with existing options */ -export type PanelMigrationHook = (options: any) => Partial; +export type PanelMigrationHook = (exiting: any, oldVersion?: string) => Partial; /** * Called before a panel is initalized @@ -40,7 +40,18 @@ export class ReactPanelPlugin { editor?: ComponentClass>; defaults?: TOptions; + /** + * 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 + */ onPanelMigration?: PanelMigrationHook; + + /** + * 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) { diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index e2111e4892d..18646e2f9b3 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,6 +1,7 @@ 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'; @@ -99,12 +100,13 @@ export class DashboardPanel extends PureComponent { 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.options = hook(panel.options); + if (hook && panel.pluginVersion !== pluginVersion) { + panel.options = hook(panel.options, panel.pluginVersion); + panel.pluginVersion = pluginVersion; } } - this.setState({ plugin, angularPanel: null }); } } diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 68f017adb9e..fd85dd5e691 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -58,6 +58,7 @@ const mustKeepProps: { [str: string]: boolean } = { cacheTimeout: true, cachedPluginOptions: true, transparent: true, + pluginVersion: true, }; const defaults: any = { @@ -87,6 +88,7 @@ export class PanelModel { targets: DataQuery[]; datasource: string; thresholds?: any; + pluginVersion?: string; snapshotData?: TimeSeries[] | [TableData]; timeFrom?: any; diff --git a/public/app/plugins/panel/singlestat2/module.tsx b/public/app/plugins/panel/singlestat2/module.tsx index 42142c0f673..04e6b529eaa 100644 --- a/public/app/plugins/panel/singlestat2/module.tsx +++ b/public/app/plugins/panel/singlestat2/module.tsx @@ -21,12 +21,16 @@ export const singleStatBaseOptionsCheck = ( return options; }; -export const singleStatMigrationCheck = (options: Partial) => { - // 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 = (exiting: any, oldVersion?: string) => { + const options = exiting as Partial; + 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; }; From 0d55141a2d41651c8ec9889806034a660fada71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 24 Mar 2019 15:56:32 +0100 Subject: [PATCH 3/6] Panels: Refactoring how panel plugins sets hooks and components, #16166 --- packages/grafana-ui/src/types/panel.ts | 33 ++++++++++++----- packages/grafana-ui/src/types/plugin.ts | 2 +- .../dashboard/dashgrid/DashboardPanel.tsx | 28 +++------------ .../dashboard/state/PanelModel.test.ts | 7 ++-- .../features/dashboard/state/PanelModel.ts | 36 +++++++++++++++---- .../features/plugins/__mocks__/pluginMocks.ts | 3 +- public/app/plugins/panel/bargauge/module.tsx | 8 ++--- public/app/plugins/panel/gauge/module.tsx | 10 +++--- public/app/plugins/panel/graph2/module.tsx | 5 +-- .../panel/piechart/PieChartPanelEditor.tsx | 2 +- public/app/plugins/panel/piechart/module.tsx | 11 +++--- .../app/plugins/panel/singlestat2/module.tsx | 10 +++--- public/app/plugins/panel/table2/module.tsx | 3 +- public/app/plugins/panel/text2/module.tsx | 18 +++++----- 14 files changed, 95 insertions(+), 81 deletions(-) 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; + }); From 20fec4d261f67ad7ff4e7b4cfbbd10fe6b3eba38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 24 Mar 2019 16:39:55 +0100 Subject: [PATCH 4/6] Panels: Added more tests for change panel plugin --- packages/grafana-ui/src/types/panel.ts | 2 +- public/app/core/utils/emitter.ts | 6 ++- .../dashboard/state/PanelModel.test.ts | 39 +++++++++++++++++++ .../features/dashboard/state/PanelModel.ts | 4 +- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 269d86ecdb7..6f008f31bcb 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -32,7 +32,7 @@ export type PanelMigrationHandler = (exiting: any, oldVersion?: export type PanelTypeChangedHandler = ( options: Partial, prevPluginId: string, - prevOptions?: any + prevOptions: any ) => Partial; export class ReactPanelPlugin { diff --git a/public/app/core/utils/emitter.ts b/public/app/core/utils/emitter.ts index 9a5c671f574..95258e7552e 100644 --- a/public/app/core/utils/emitter.ts +++ b/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; + } } diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index 1b75844809c..1da0c786753 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/public/app/features/dashboard/state/PanelModel.test.ts @@ -1,5 +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', () => { @@ -96,6 +97,44 @@ describe('PanelModel', () => { }); }); + 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 }; diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index a7ce1fdb250..dc6b502f447 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -282,11 +282,11 @@ export class PanelModel { this.type = pluginId; this.plugin = newPlugin; - // Callback that can validate and migrate any existing settings + // 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 : null; + const old = oldOptions ? oldOptions.options : {}; Object.assign(this.options, onPanelTypeChanged(this.options, oldPluginId, old)); } } From e03c7bf1a35fa47fd3afdb2f9f37424a0791c0c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 24 Mar 2019 17:03:27 +0100 Subject: [PATCH 5/6] Panels: Support angular -> react migration via PanelMigrationHandler --- packages/grafana-ui/src/types/panel.ts | 10 ++++++++-- public/app/features/dashboard/state/PanelModel.ts | 2 +- public/app/plugins/panel/singlestat2/module.tsx | 7 +++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 6f008f31bcb..d6f3879d358 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -21,10 +21,16 @@ export interface PanelEditorProps { onOptionsChange: (options: T) => void; } +export interface PanelModel { + 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 PanelMigrationHandler = (exiting: any, oldVersion?: string) => Partial; +export type PanelMigrationHandler = (panel: PanelModel) => Partial; /** * Called before a panel is initalized diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index dc6b502f447..7e67e280241 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -250,7 +250,7 @@ export class PanelModel { const { reactPanel } = plugin.exports; if (reactPanel && reactPanel.onPanelMigration) { - this.options = reactPanel.onPanelMigration(this.options, this.pluginVersion); + this.options = reactPanel.onPanelMigration(this); this.pluginVersion = plugin.info ? plugin.info.version : '1.0.0'; } } diff --git a/public/app/plugins/panel/singlestat2/module.tsx b/public/app/plugins/panel/singlestat2/module.tsx index 55e777abfe0..d9a92727e5a 100644 --- a/public/app/plugins/panel/singlestat2/module.tsx +++ b/public/app/plugins/panel/singlestat2/module.tsx @@ -1,4 +1,4 @@ -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'; @@ -21,12 +21,11 @@ export const singleStatBaseOptionsCheck = ( return options; }; -export const singleStatMigrationCheck = (exiting: any, oldVersion?: string) => { - const options = exiting as Partial; +export const singleStatMigrationCheck = (panel: PanelModel) => { + 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]; From f4f5eeeb18282acb51b135cf983ccfbaee1c50f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 24 Mar 2019 18:34:43 +0100 Subject: [PATCH 6/6] Pamels: Options are always there --- public/app/plugins/panel/singlestat2/module.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/panel/singlestat2/module.tsx b/public/app/plugins/panel/singlestat2/module.tsx index d9a92727e5a..d7e2667d605 100644 --- a/public/app/plugins/panel/singlestat2/module.tsx +++ b/public/app/plugins/panel/singlestat2/module.tsx @@ -9,15 +9,13 @@ const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'threshol export const singleStatBaseOptionsCheck = ( options: Partial, 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; };