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

Refactor ReactPanelPlugin change hooks -> handler & add panel version to json
pull/16183/head
Torkel Ödegaard 6 years ago committed by GitHub
commit 090b3f6c6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 40
      packages/grafana-ui/src/types/panel.ts
  2. 2
      packages/grafana-ui/src/types/plugin.ts
  3. 6
      public/app/core/utils/emitter.ts
  4. 24
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  5. 46
      public/app/features/dashboard/state/PanelModel.test.ts
  6. 42
      public/app/features/dashboard/state/PanelModel.ts
  7. 3
      public/app/features/plugins/__mocks__/pluginMocks.ts
  8. 9
      public/app/plugins/panel/bargauge/module.tsx
  9. 12
      public/app/plugins/panel/gauge/module.tsx
  10. 6
      public/app/plugins/panel/graph2/module.tsx
  11. 6
      public/app/plugins/panel/graph2/types.ts
  12. 2
      public/app/plugins/panel/piechart/PieChartPanelEditor.tsx
  13. 12
      public/app/plugins/panel/piechart/module.tsx
  14. 42
      public/app/plugins/panel/singlestat2/module.tsx
  15. 4
      public/app/plugins/panel/table2/module.tsx
  16. 20
      public/app/plugins/panel/text2/module.tsx

@ -21,27 +21,32 @@ export interface PanelEditorProps<T = any> {
onOptionsChange: (options: T) => void; 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 * Called before a panel is initalized
*/ */
export type PanelTypeChangedHook<TOptions = any> = ( export type PanelTypeChangedHandler<TOptions = any> = (
options: Partial<TOptions>, options: Partial<TOptions>,
prevPluginId: string, prevPluginId: string,
prevOptions?: any prevOptions: any
) => Partial<TOptions>; ) => Partial<TOptions>;
export class ReactPanelPlugin<TOptions = any> { export class ReactPanelPlugin<TOptions = any> {
panel: ComponentClass<PanelProps<TOptions>>; panel: ComponentClass<PanelProps<TOptions>>;
editor?: ComponentClass<PanelEditorProps<TOptions>>; editor?: ComponentClass<PanelEditorProps<TOptions>>;
defaults?: TOptions; defaults?: TOptions;
onPanelMigration?: PanelMigrationHandler<TOptions>;
panelMigrationHook?: PanelMigrationHook<TOptions>; onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>;
panelTypeChangedHook?: PanelTypeChangedHook<TOptions>;
constructor(panel: ComponentClass<PanelProps<TOptions>>) { constructor(panel: ComponentClass<PanelProps<TOptions>>) {
this.panel = panel; this.panel = panel;
@ -49,25 +54,32 @@ export class ReactPanelPlugin<TOptions = any> {
setEditor(editor: ComponentClass<PanelEditorProps<TOptions>>) { setEditor(editor: ComponentClass<PanelEditorProps<TOptions>>) {
this.editor = editor; this.editor = editor;
return this;
} }
setDefaults(defaults: TOptions) { setDefaults(defaults: TOptions) {
this.defaults = defaults; 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>) { setMigrationHandler(handler: PanelMigrationHandler) {
this.panelMigrationHook = v; this.onPanelMigration = handler;
return this;
} }
/** /**
* Called when the visualization changes. * This function is called when the visualization was changed. This
* Lets you keep whatever settings made sense in the previous panel * passes in the options that were used in the previous visualization
*/ */
setPanelTypeChangedHook(v: PanelTypeChangedHook<TOptions>) { setPanelChangeHandler(handler: PanelTypeChangedHandler) {
this.panelTypeChangedHook = v; this.onPanelTypeChanged = handler;
return this;
} }
} }

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

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

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

@ -1,4 +1,6 @@
import { PanelModel } from './PanelModel'; import { PanelModel } from './PanelModel';
import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
import { ReactPanelPlugin } from '@grafana/ui/src/types/panel';
describe('PanelModel', () => { describe('PanelModel', () => {
describe('when creating new panel model', () => { describe('when creating new panel model', () => {
@ -76,7 +78,7 @@ describe('PanelModel', () => {
describe('when changing panel type', () => { describe('when changing panel type', () => {
beforeEach(() => { beforeEach(() => {
model.changeType('graph'); model.changePlugin(getPanelPlugin({ id: 'graph', exports: {} }));
model.alert = { id: 2 }; model.alert = { id: 2 };
}); });
@ -85,16 +87,54 @@ describe('PanelModel', () => {
}); });
it('should restore table properties when changing back', () => { it('should restore table properties when changing back', () => {
model.changeType('table'); model.changePlugin(getPanelPlugin({ id: 'table', exports: {} }));
expect(model.showColumns).toBe(true); expect(model.showColumns).toBe(true);
}); });
it('should remove alert rule when changing type that does not support it', () => { 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); 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', () => { describe('get panel options', () => {
it('should apply defaults', () => { it('should apply defaults', () => {
model.options = { existingProp: 10 }; model.options = { existingProp: 10 };

@ -6,8 +6,8 @@ import { Emitter } from 'app/core/utils/emitter';
import { getNextRefIdChar } from 'app/core/utils/query'; import { getNextRefIdChar } from 'app/core/utils/query';
// Types // Types
import { DataQuery, TimeSeries, Threshold, ScopedVars, PanelTypeChangedHook } from '@grafana/ui'; import { DataQuery, TimeSeries, Threshold, ScopedVars, TableData } from '@grafana/ui';
import { TableData } from '@grafana/ui/src'; import { PanelPlugin } from 'app/types';
export interface GridPos { export interface GridPos {
x: number; x: number;
@ -23,6 +23,7 @@ const notPersistedProperties: { [str: string]: boolean } = {
isEditing: true, isEditing: true,
hasRefreshed: true, hasRefreshed: true,
cachedPluginOptions: true, cachedPluginOptions: true,
plugin: true,
}; };
// For angular panels we need to clean up properties when changing type // For angular panels we need to clean up properties when changing type
@ -58,6 +59,7 @@ const mustKeepProps: { [str: string]: boolean } = {
cacheTimeout: true, cacheTimeout: true,
cachedPluginOptions: true, cachedPluginOptions: true,
transparent: true, transparent: true,
pluginVersion: true,
}; };
const defaults: any = { const defaults: any = {
@ -87,6 +89,7 @@ export class PanelModel {
targets: DataQuery[]; targets: DataQuery[];
datasource: string; datasource: string;
thresholds?: any; thresholds?: any;
pluginVersion?: string;
snapshotData?: TimeSeries[] | [TableData]; snapshotData?: TimeSeries[] | [TableData];
timeFrom?: any; timeFrom?: any;
@ -110,6 +113,7 @@ export class PanelModel {
cacheTimeout?: any; cacheTimeout?: any;
cachedPluginOptions?: any; cachedPluginOptions?: any;
legend?: { show: boolean }; legend?: { show: boolean };
plugin?: PanelPlugin;
constructor(model: any) { constructor(model: any) {
this.events = new Emitter(); 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 oldOptions: any = this.getOptionsToRemember();
const oldPluginId = this.type; 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 // remove panel type specific options
for (const key of _.keys(this)) { for (const key of _.keys(this)) {
@ -258,12 +278,16 @@ export class PanelModel {
this.cachedPluginOptions[oldPluginId] = oldOptions; this.cachedPluginOptions[oldPluginId] = oldOptions;
this.restorePanelOptions(pluginId); this.restorePanelOptions(pluginId);
// Callback that can validate and migrate any existing settings // switch
if (hook) { this.type = pluginId;
this.options = this.options || {}; this.plugin = newPlugin;
const old = oldOptions ? oldOptions.options : null;
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));
} }
} }

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save