ソースを参照

Refactor: consistant plugin/meta usage (#16834)

Ryan McKinley 6 年 前
コミット
51a98565dc

+ 5 - 2
packages/grafana-ui/src/types/datasource.ts

@@ -1,6 +1,6 @@
 import { ComponentClass } from 'react';
 import { TimeRange } from './time';
-import { PluginMeta } from './plugin';
+import { PluginMeta, GrafanaPlugin } from './plugin';
 import { TableData, TimeSeries, SeriesData, LoadingState } from './data';
 import { PanelData } from './panel';
 
@@ -8,11 +8,14 @@ export interface DataSourcePluginOptionsEditorProps<TOptions> {
   options: TOptions;
   onOptionsChange: (options: TOptions) => void;
 }
-export class DataSourcePlugin<TOptions = {}, TQuery extends DataQuery = DataQuery> {
+export class DataSourcePlugin<TOptions = {}, TQuery extends DataQuery = DataQuery> extends GrafanaPlugin<
+  DataSourcePluginMeta
+> {
   DataSourceClass: DataSourceConstructor<TQuery>;
   components: DataSourcePluginComponents<TOptions, TQuery>;
 
   constructor(DataSourceClass: DataSourceConstructor<TQuery>) {
+    super();
     this.DataSourceClass = DataSourceClass;
     this.components = {};
   }

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

@@ -2,16 +2,13 @@ import { ComponentClass, ComponentType } from 'react';
 import { LoadingState, SeriesData } from './data';
 import { TimeRange } from './time';
 import { ScopedVars, DataQueryRequest, DataQueryError, LegacyResponseData } from './datasource';
-import { PluginMeta } from './plugin';
+import { PluginMeta, GrafanaPlugin } from './plugin';
 
 export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
 
 export interface PanelPluginMeta extends PluginMeta {
   hideFromList?: boolean;
   sort: number;
-  angularPlugin: AngularPanelPlugin | null;
-  panelPlugin: PanelPlugin | null;
-  hasBeenImported?: boolean;
 
   // if length>0 the query tab will show up
   // Before 6.2 this could be table and/or series, but 6.2+ supports both transparently
@@ -72,14 +69,20 @@ export type PanelTypeChangedHandler<TOptions = any> = (
   prevOptions: any
 ) => Partial<TOptions>;
 
-export class PanelPlugin<TOptions = any> {
+export class PanelPlugin<TOptions = any> extends GrafanaPlugin<PanelPluginMeta> {
   panel: ComponentType<PanelProps<TOptions>>;
   editor?: ComponentClass<PanelEditorProps<TOptions>>;
   defaults?: TOptions;
   onPanelMigration?: PanelMigrationHandler<TOptions>;
   onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>;
 
+  /**
+   * Legacy angular ctrl.  If this exists it will be used instead of the panel
+   */
+  angularPanelCtrl?: any;
+
   constructor(panel: ComponentType<PanelProps<TOptions>>) {
+    super();
     this.panel = panel;
   }
 
@@ -114,16 +117,6 @@ export class PanelPlugin<TOptions = any> {
   }
 }
 
-export class AngularPanelPlugin {
-  components: {
-    PanelCtrl: any;
-  };
-
-  constructor(PanelCtrl: any) {
-    this.components = { PanelCtrl: PanelCtrl };
-  }
-}
-
 export interface PanelSize {
   width: number;
   height: number;

+ 10 - 5
packages/grafana-ui/src/types/plugin.ts

@@ -69,16 +69,20 @@ export interface PluginMetaInfo {
   version: string;
 }
 
-export class AppPlugin {
-  meta: PluginMeta;
+export class GrafanaPlugin<T extends PluginMeta> {
+  // Meta is filled in by the plugin loading system
+  meta?: T;
 
+  // Soon this will also include common config options
+}
+
+export class AppPlugin extends GrafanaPlugin<PluginMeta> {
   angular?: {
     ConfigCtrl?: any;
     pages: { [component: string]: any };
   };
 
-  constructor(meta: PluginMeta, pluginExports: any) {
-    this.meta = meta;
+  setComponentsFromLegacyExports(pluginExports: any) {
     const legacy = {
       ConfigCtrl: undefined,
       pages: {} as any,
@@ -89,7 +93,8 @@ export class AppPlugin {
       this.angular = legacy;
     }
 
-    if (meta.includes) {
+    const { meta } = this;
+    if (meta && meta.includes) {
       for (const include of meta.includes) {
         const { type, component } = include;
         if (type === PluginIncludeType.page && component) {

+ 6 - 33
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -1,6 +1,5 @@
 // Libraries
 import React, { PureComponent } from 'react';
-import config from 'app/core/config';
 import classNames from 'classnames';
 
 // Utils & Services
@@ -9,7 +8,6 @@ import { importPanelPlugin } from 'app/features/plugins/plugin_loader';
 
 // Components
 import { AddPanelWidget } from '../components/AddPanelWidget';
-import { getPanelPluginNotFound } from './PanelPluginNotFound';
 import { DashboardRow } from '../components/DashboardRow';
 import { PanelChrome } from './PanelChrome';
 import { PanelEditor } from '../panel_editor/PanelEditor';
@@ -17,7 +15,7 @@ import { PanelResizer } from './PanelResizer';
 
 // Types
 import { PanelModel, DashboardModel } from '../state';
-import { PanelPluginMeta, AngularPanelPlugin, PanelPlugin } from '@grafana/ui/src/types/panel';
+import { PanelPluginMeta, PanelPlugin } from '@grafana/ui/src/types/panel';
 import { AutoSizer } from 'react-virtualized';
 
 export interface Props {
@@ -28,7 +26,7 @@ export interface Props {
 }
 
 export interface State {
-  plugin: PanelPluginMeta;
+  plugin: PanelPlugin;
   angularPanel: AngularComponent;
 }
 
@@ -72,15 +70,12 @@ export class DashboardPanel extends PureComponent<Props, State> {
     const { panel } = this.props;
 
     // handle plugin loading & changing of plugin type
-    if (!this.state.plugin || this.state.plugin.id !== pluginId) {
-      let plugin = config.panels[pluginId] || getPanelPluginNotFound(pluginId);
+    if (!this.state.plugin || this.state.plugin.meta.id !== pluginId) {
+      const plugin = await importPanelPlugin(pluginId);
 
       // unmount angular panel
       this.cleanUpAngularPanel();
 
-      // load the actual plugin code
-      plugin = await this.importPanelPluginModule(plugin);
-
       if (panel.type !== pluginId) {
         panel.changePlugin(plugin);
       } else {
@@ -91,27 +86,6 @@ export class DashboardPanel extends PureComponent<Props, State> {
     }
   }
 
-  async importPanelPluginModule(plugin: PanelPluginMeta): Promise<PanelPluginMeta> {
-    if (plugin.hasBeenImported) {
-      return plugin;
-    }
-
-    try {
-      const importedPlugin = await importPanelPlugin(plugin.module);
-      if (importedPlugin instanceof AngularPanelPlugin) {
-        plugin.angularPlugin = importedPlugin as AngularPanelPlugin;
-      } else if (importedPlugin instanceof PanelPlugin) {
-        plugin.panelPlugin = importedPlugin as PanelPlugin;
-      }
-    } catch (e) {
-      plugin = getPanelPluginNotFound(plugin.id);
-      console.log('Failed to import plugin module', e);
-    }
-
-    plugin.hasBeenImported = true;
-    return plugin;
-  }
-
   componentDidMount() {
     this.loadPlugin(this.props.panel.type);
   }
@@ -186,7 +160,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
     }
 
     // if we have not loaded plugin exports yet, wait
-    if (!plugin || !plugin.hasBeenImported) {
+    if (!plugin) {
       return null;
     }
 
@@ -209,8 +183,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
               onMouseLeave={this.onMouseLeave}
               style={styles}
             >
-              {plugin.panelPlugin && this.renderReactPanel()}
-              {plugin.angularPlugin && this.renderAngularPanel()}
+              {plugin.angularPanelCtrl ? this.renderAngularPanel() : this.renderReactPanel()}
             </div>
           )}
         />

+ 5 - 5
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -16,7 +16,7 @@ import config from 'app/core/config';
 
 // Types
 import { DashboardModel, PanelModel } from '../state';
-import { PanelPluginMeta, LoadingState, PanelData } from '@grafana/ui';
+import { LoadingState, PanelData, PanelPlugin } from '@grafana/ui';
 import { ScopedVars } from '@grafana/ui';
 
 import templateSrv from 'app/features/templating/template_srv';
@@ -29,7 +29,7 @@ const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
 export interface Props {
   panel: PanelModel;
   dashboard: DashboardModel;
-  plugin: PanelPluginMeta;
+  plugin: PanelPlugin;
   isFullscreen: boolean;
   width: number;
   height: number;
@@ -209,13 +209,13 @@ export class PanelChrome extends PureComponent<Props, State> {
   }
 
   get wantsQueryExecution() {
-    return this.props.plugin.dataFormats.length > 0 && !this.hasPanelSnapshot;
+    return this.props.plugin.meta.dataFormats.length > 0 && !this.hasPanelSnapshot;
   }
 
   renderPanel(width: number, height: number): JSX.Element {
     const { panel, plugin } = this.props;
     const { renderCounter, data, isFirstLoad } = this.state;
-    const PanelComponent = plugin.panelPlugin.panel;
+    const PanelComponent = plugin.panel;
 
     // This is only done to increase a counter that is used by backend
     // image rendering (phantomjs/headless chrome) to know when to capture image
@@ -236,7 +236,7 @@ export class PanelChrome extends PureComponent<Props, State> {
           <PanelComponent
             data={data}
             timeRange={data.request ? data.request.range : this.timeSrv.timeRange()}
-            options={panel.getOptions(plugin.panelPlugin.defaults)}
+            options={panel.getOptions(plugin.defaults)}
             width={width - 2 * config.theme.panelPadding.horizontal}
             height={height - PANEL_HEADER_HEIGHT - config.theme.panelPadding.vertical}
             renderCounter={renderCounter}

+ 6 - 6
public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx

@@ -7,14 +7,14 @@ import { AlertBox } from 'app/core/components/AlertBox/AlertBox';
 
 // Types
 import { AppNotificationSeverity } from 'app/types';
-import { PanelPluginMeta, PanelProps, PanelPlugin, PluginType } from '@grafana/ui';
+import { PanelProps, PanelPlugin, PluginType } from '@grafana/ui';
 
 interface Props {
   pluginId: string;
 }
 
 class PanelPluginNotFound extends PureComponent<Props> {
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
   }
 
@@ -34,14 +34,15 @@ class PanelPluginNotFound extends PureComponent<Props> {
   }
 }
 
-export function getPanelPluginNotFound(id: string): PanelPluginMeta {
+export function getPanelPluginNotFound(id: string): PanelPlugin {
   const NotFound = class NotFound extends PureComponent<PanelProps> {
     render() {
       return <PanelPluginNotFound pluginId={id} />;
     }
   };
 
-  return {
+  const plugin = new PanelPlugin(NotFound);
+  plugin.meta = {
     id: id,
     name: id,
     sort: 100,
@@ -63,7 +64,6 @@ export function getPanelPluginNotFound(id: string): PanelPluginMeta {
       updated: '',
       version: '',
     },
-    panelPlugin: new PanelPlugin(NotFound),
-    angularPlugin: null,
   };
+  return plugin;
 }

+ 5 - 5
public/app/features/dashboard/panel_editor/PanelEditor.tsx

@@ -13,12 +13,12 @@ import { AngularComponent } from 'app/core/services/AngularLoader';
 
 import { PanelModel } from '../state/PanelModel';
 import { DashboardModel } from '../state/DashboardModel';
-import { PanelPluginMeta, Tooltip } from '@grafana/ui';
+import { Tooltip, PanelPlugin, PanelPluginMeta } from '@grafana/ui';
 
 interface PanelEditorProps {
   panel: PanelModel;
   dashboard: DashboardModel;
-  plugin: PanelPluginMeta;
+  plugin: PanelPlugin;
   angularPanel?: AngularComponent;
   onTypeChanged: (newType: PanelPluginMeta) => void;
 }
@@ -55,7 +55,7 @@ const getPanelEditorTab = (tabId: PanelEditorTabIds): PanelEditorTab => {
 };
 
 export class PanelEditor extends PureComponent<PanelEditorProps> {
-  constructor(props) {
+  constructor(props: PanelEditorProps) {
     super(props);
   }
 
@@ -105,7 +105,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
     ];
 
     // handle panels that do not have queries tab
-    if (plugin.dataFormats.length === 0) {
+    if (plugin.meta.dataFormats.length === 0) {
       // remove queries tab
       tabs.shift();
       // switch tab
@@ -114,7 +114,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
       }
     }
 
-    if (config.alertingEnabled && plugin.id === 'graph') {
+    if (config.alertingEnabled && plugin.meta.id === 'graph') {
       tabs.push(getPanelEditorTab(PanelEditorTabIds.Alert));
     }
 

+ 15 - 17
public/app/features/dashboard/panel_editor/VisualizationTab.tsx

@@ -18,12 +18,12 @@ import { PanelModel } from '../state';
 import { DashboardModel } from '../state';
 import { VizPickerSearch } from './VizPickerSearch';
 import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
-import { PanelPluginMeta } from '@grafana/ui';
+import { PanelPlugin, PanelPluginMeta } from '@grafana/ui';
 
 interface Props {
   panel: PanelModel;
   dashboard: DashboardModel;
-  plugin: PanelPluginMeta;
+  plugin: PanelPlugin;
   angularPanel?: AngularComponent;
   onTypeChanged: (newType: PanelPluginMeta) => void;
   updateLocation: typeof updateLocation;
@@ -41,7 +41,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
   element: HTMLElement;
   angularOptions: AngularComponent;
 
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
 
     this.state = {
@@ -54,7 +54,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
 
   getReactPanelOptions = () => {
     const { panel, plugin } = this.props;
-    return panel.getOptions(plugin.panelPlugin.defaults);
+    return panel.getOptions(plugin.defaults);
   };
 
   renderPanelOptions() {
@@ -64,12 +64,8 @@ export class VisualizationTab extends PureComponent<Props, State> {
       return <div ref={element => (this.element = element)} />;
     }
 
-    if (plugin.panelPlugin) {
-      const PanelEditor = plugin.panelPlugin.editor;
-
-      if (PanelEditor) {
-        return <PanelEditor options={this.getReactPanelOptions()} onOptionsChange={this.onPanelOptionsChanged} />;
-      }
+    if (plugin.editor) {
+      return <plugin.editor options={this.getReactPanelOptions()} onOptionsChange={this.onPanelOptionsChanged} />;
     }
 
     return <p>Visualization has no options</p>;
@@ -176,11 +172,12 @@ export class VisualizationTab extends PureComponent<Props, State> {
   renderToolbar = (): JSX.Element => {
     const { plugin } = this.props;
     const { isVizPickerOpen, searchQuery } = this.state;
+    const { meta } = plugin;
 
     if (isVizPickerOpen) {
       return (
         <VizPickerSearch
-          plugin={plugin}
+          plugin={meta}
           searchQuery={searchQuery}
           onChange={this.onSearchQueryChange}
           onClose={this.onCloseVizPicker}
@@ -189,8 +186,8 @@ export class VisualizationTab extends PureComponent<Props, State> {
     } else {
       return (
         <div className="toolbar__main" onClick={this.onOpenVizPicker}>
-          <img className="toolbar__main-image" src={plugin.info.logos.small} />
-          <div className="toolbar__main-name">{plugin.name}</div>
+          <img className="toolbar__main-image" src={meta.info.logos.small} />
+          <div className="toolbar__main-name">{meta.name}</div>
           <i className="fa fa-caret-down" />
         </div>
       );
@@ -198,14 +195,14 @@ export class VisualizationTab extends PureComponent<Props, State> {
   };
 
   onTypeChanged = (plugin: PanelPluginMeta) => {
-    if (plugin.id === this.props.plugin.id) {
+    if (plugin.id === this.props.plugin.meta.id) {
       this.setState({ isVizPickerOpen: false });
     } else {
       this.props.onTypeChanged(plugin);
     }
   };
 
-  renderHelp = () => <PluginHelp plugin={this.props.plugin} type="help" />;
+  renderHelp = () => <PluginHelp plugin={this.props.plugin.meta} type="help" />;
 
   setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
     const target = event.target as HTMLElement;
@@ -215,6 +212,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
   render() {
     const { plugin } = this.props;
     const { isVizPickerOpen, searchQuery, scrollTop } = this.state;
+    const { meta } = plugin;
 
     const pluginHelp: EditorToolbarView = {
       heading: 'Help',
@@ -233,13 +231,13 @@ export class VisualizationTab extends PureComponent<Props, State> {
         <>
           <FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true} onExited={this.clearQuery}>
             <VizTypePicker
-              current={plugin}
+              current={meta}
               onTypeChanged={this.onTypeChanged}
               searchQuery={searchQuery}
               onClose={this.onCloseVizPicker}
             />
           </FadeIn>
-          <PluginStateinfo state={plugin.state} />
+          <PluginStateinfo state={meta.state} />
           {this.renderPanelOptions()}
         </>
       </EditorTabBody>

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

@@ -1,6 +1,5 @@
 import { PanelModel } from './PanelModel';
 import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
-import { PanelPlugin, AngularPanelPlugin } from '@grafana/ui/src/types/panel';
 
 class TablePanelCtrl {}
 
@@ -31,10 +30,13 @@ describe('PanelModel', () => {
       };
       model = new PanelModel(modelJson);
       model.pluginLoaded(
-        getPanelPlugin({
-          id: 'table',
-          angularPlugin: new AngularPanelPlugin(TablePanelCtrl),
-        })
+        getPanelPlugin(
+          {
+            id: 'table',
+          },
+          null, // react
+          TablePanelCtrl // angular
+        )
       );
     });
 
@@ -123,15 +125,10 @@ describe('PanelModel', () => {
 
     describe('when changing to react panel', () => {
       const onPanelTypeChanged = jest.fn();
-      const reactPlugin = new PanelPlugin({} as any).setPanelChangeHandler(onPanelTypeChanged as any);
+      const reactPlugin = getPanelPlugin({ id: 'react' }).setPanelChangeHandler(onPanelTypeChanged as any);
 
       beforeEach(() => {
-        model.changePlugin(
-          getPanelPlugin({
-            id: 'react',
-            panelPlugin: reactPlugin,
-          })
-        );
+        model.changePlugin(reactPlugin);
       });
 
       it('should call react onPanelTypeChanged', () => {

+ 20 - 24
public/app/features/dashboard/state/PanelModel.ts

@@ -6,7 +6,7 @@ import { Emitter } from 'app/core/utils/emitter';
 import { getNextRefIdChar } from 'app/core/utils/query';
 
 // Types
-import { PanelPluginMeta, DataQuery, Threshold, ScopedVars, DataQueryResponseData } from '@grafana/ui';
+import { DataQuery, Threshold, ScopedVars, DataQueryResponseData, PanelPlugin } from '@grafana/ui';
 import config from 'app/core/config';
 
 import { PanelQueryRunner } from './PanelQueryRunner';
@@ -116,7 +116,7 @@ export class PanelModel {
   cacheTimeout?: any;
   cachedPluginOptions?: any;
   legend?: { show: boolean };
-  plugin?: PanelPluginMeta;
+  plugin?: PanelPlugin;
   private queryRunner?: PanelQueryRunner;
 
   constructor(model: any) {
@@ -248,29 +248,25 @@ export class PanelModel {
     });
   }
 
-  private getPluginVersion(plugin: PanelPluginMeta): string {
-    return this.plugin && this.plugin.info.version ? this.plugin.info.version : config.buildInfo.version;
-  }
-
-  pluginLoaded(plugin: PanelPluginMeta) {
+  pluginLoaded(plugin: PanelPlugin) {
     this.plugin = plugin;
 
-    if (plugin.panelPlugin && plugin.panelPlugin.onPanelMigration) {
-      const version = this.getPluginVersion(plugin);
+    if (plugin.panel && plugin.onPanelMigration) {
+      const version = getPluginVersion(plugin);
       if (version !== this.pluginVersion) {
-        this.options = plugin.panelPlugin.onPanelMigration(this);
+        this.options = plugin.onPanelMigration(this);
         this.pluginVersion = version;
       }
     }
   }
 
-  changePlugin(newPlugin: PanelPluginMeta) {
-    const pluginId = newPlugin.id;
+  changePlugin(newPlugin: PanelPlugin) {
+    const pluginId = newPlugin.meta.id;
     const oldOptions: any = this.getOptionsToRemember();
     const oldPluginId = this.type;
 
     // for angular panels we must remove all events and let angular panels do some cleanup
-    if (this.plugin.angularPlugin) {
+    if (this.plugin.angularPanelCtrl) {
       this.destroy();
     }
 
@@ -291,18 +287,14 @@ export class PanelModel {
     this.plugin = newPlugin;
 
     // Let panel plugins inspect options from previous panel and keep any that it can use
-    const reactPanel = newPlugin.panelPlugin;
-
-    if (reactPanel) {
-      if (reactPanel.onPanelTypeChanged) {
-        this.options = this.options || {};
-        const old = oldOptions && oldOptions.options ? oldOptions.options : {};
-        Object.assign(this.options, reactPanel.onPanelTypeChanged(this.options, oldPluginId, old));
-      }
+    if (newPlugin.onPanelTypeChanged) {
+      this.options = this.options || {};
+      const old = oldOptions && oldOptions.options ? oldOptions.options : {};
+      Object.assign(this.options, newPlugin.onPanelTypeChanged(this.options, oldPluginId, old));
+    }
 
-      if (reactPanel.onPanelMigration) {
-        this.pluginVersion = this.getPluginVersion(newPlugin);
-      }
+    if (newPlugin.onPanelMigration) {
+      this.pluginVersion = getPluginVersion(newPlugin);
     }
   }
 
@@ -341,3 +333,7 @@ export class PanelModel {
     }
   }
 }
+
+function getPluginVersion(plugin: PanelPlugin): string {
+  return plugin && plugin.meta.info.version ? plugin.meta.info.version : config.buildInfo.version;
+}

+ 1 - 1
public/app/features/datasources/settings/DataSourceSettingsPage.tsx

@@ -63,7 +63,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
     let importedPlugin: DataSourcePlugin;
 
     try {
-      importedPlugin = await importDataSourcePlugin(dataSourceMeta.module);
+      importedPlugin = await importDataSourcePlugin(dataSourceMeta);
     } catch (e) {
       console.log('Failed to import plugin module', e);
     }

+ 11 - 5
public/app/features/plugins/__mocks__/pluginMocks.ts

@@ -1,4 +1,5 @@
-import { PanelPluginMeta, PluginMeta, PluginType, PanelDataFormat } from '@grafana/ui';
+import { PanelPluginMeta, PluginMeta, PluginType, PanelDataFormat, PanelPlugin, PanelProps } from '@grafana/ui';
+import { ComponentType } from 'enzyme';
 
 export const getMockPlugins = (amount: number): PluginMeta[] => {
   const plugins = [];
@@ -33,8 +34,14 @@ export const getMockPlugins = (amount: number): PluginMeta[] => {
   return plugins;
 };
 
-export const getPanelPlugin = (options: Partial<PanelPluginMeta>): PanelPluginMeta => {
-  return {
+export const getPanelPlugin = (
+  options: Partial<PanelPluginMeta>,
+  reactPanel?: ComponentType<PanelProps>,
+  angularPanel?: any
+): PanelPlugin => {
+  const plugin = new PanelPlugin(reactPanel);
+  plugin.angularPanelCtrl = angularPanel;
+  plugin.meta = {
     id: options.id,
     type: PluginType.panel,
     name: options.id,
@@ -57,9 +64,8 @@ export const getPanelPlugin = (options: Partial<PanelPluginMeta>): PanelPluginMe
     hideFromList: options.hideFromList === true,
     module: '',
     baseUrl: '',
-    panelPlugin: options.panelPlugin,
-    angularPlugin: options.angularPlugin,
   };
+  return plugin;
 };
 
 export const getMockPlugin = () => {

+ 1 - 1
public/app/features/plugins/datasource_srv.ts

@@ -53,7 +53,7 @@ export class DatasourceSrv {
 
     const deferred = this.$q.defer();
 
-    importDataSourcePlugin(dsConfig.meta.module)
+    importDataSourcePlugin(dsConfig.meta)
       .then(dsPlugin => {
         // check if its in cache now
         if (this.datasources[name]) {

+ 6 - 7
public/app/features/plugins/plugin_component.ts

@@ -4,7 +4,7 @@ import _ from 'lodash';
 import config from 'app/core/config';
 import coreModule from 'app/core/core_module';
 
-import { AngularPanelPlugin, DataSourceApi } from '@grafana/ui/src/types';
+import { DataSourceApi } from '@grafana/ui/src/types';
 import { importPanelPlugin, importDataSourcePlugin, importAppPlugin } from './plugin_loader';
 
 /** @ngInject */
@@ -22,7 +22,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
     });
   }
 
-  function relativeTemplateUrlToAbs(templateUrl, baseUrl) {
+  function relativeTemplateUrlToAbs(templateUrl: string, baseUrl: string) {
     if (!templateUrl) {
       return undefined;
     }
@@ -69,9 +69,8 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
     };
 
     const panelInfo = config.panels[scope.panel.type];
-    return importPanelPlugin(panelInfo.module).then(panelPlugin => {
-      const angularPanelPlugin = panelPlugin as AngularPanelPlugin;
-      const PanelCtrl = angularPanelPlugin.components.PanelCtrl;
+    return importPanelPlugin(panelInfo.id).then(panelPlugin => {
+      const PanelCtrl = panelPlugin.angularPanelCtrl;
       componentInfo.Component = PanelCtrl;
 
       if (!PanelCtrl || PanelCtrl.registered) {
@@ -118,7 +117,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
       }
       // Annotations
       case 'annotations-query-ctrl': {
-        return importDataSourcePlugin(scope.ctrl.currentDatasource.meta.module).then(dsPlugin => {
+        return importDataSourcePlugin(scope.ctrl.currentDatasource.meta).then(dsPlugin => {
           return {
             baseUrl: scope.ctrl.currentDatasource.meta.baseUrl,
             name: 'annotations-query-ctrl-' + scope.ctrl.currentDatasource.meta.id,
@@ -134,7 +133,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
       // Datasource ConfigCtrl
       case 'datasource-config-ctrl': {
         const dsMeta = scope.ctrl.datasourceMeta;
-        return importDataSourcePlugin(dsMeta.module).then(dsPlugin => {
+        return importDataSourcePlugin(dsMeta).then(dsPlugin => {
           scope.$watch(
             'ctrl.current',
             () => {

+ 48 - 13
public/app/features/plugins/plugin_loader.ts

@@ -18,7 +18,7 @@ import config from 'app/core/config';
 import TimeSeries from 'app/core/time_series2';
 import TableModel from 'app/core/table_model';
 import { coreModule, appEvents, contextSrv } from 'app/core/core';
-import { DataSourcePlugin, AppPlugin, PanelPlugin, AngularPanelPlugin, PluginMeta } from '@grafana/ui/src/types';
+import { DataSourcePlugin, AppPlugin, PanelPlugin, PluginMeta, DataSourcePluginMeta } from '@grafana/ui/src/types';
 import * as datemath from 'app/core/utils/datemath';
 import * as fileExport from 'app/core/utils/file_export';
 import * as flatten from 'app/core/utils/flatten';
@@ -160,15 +160,18 @@ export function importPluginModule(path: string): Promise<any> {
   return System.import(path);
 }
 
-export function importDataSourcePlugin(path: string): Promise<DataSourcePlugin<any>> {
-  return importPluginModule(path).then(pluginExports => {
+export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<DataSourcePlugin<any>> {
+  return importPluginModule(meta.module).then(pluginExports => {
     if (pluginExports.plugin) {
-      return pluginExports.plugin as DataSourcePlugin<any>;
+      const dsPlugin = pluginExports.plugin as DataSourcePlugin<any>;
+      dsPlugin.meta = meta;
+      return dsPlugin;
     }
 
     if (pluginExports.Datasource) {
       const dsPlugin = new DataSourcePlugin(pluginExports.Datasource);
       dsPlugin.setComponentsFromLegacyExports(pluginExports);
+      dsPlugin.meta = meta;
       return dsPlugin;
     }
 
@@ -178,18 +181,50 @@ export function importDataSourcePlugin(path: string): Promise<DataSourcePlugin<a
 
 export function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
   return importPluginModule(meta.module).then(pluginExports => {
-    return new AppPlugin(meta, pluginExports);
+    const plugin = pluginExports.plugin ? (pluginExports.plugin as AppPlugin) : new AppPlugin();
+    plugin.meta = meta;
+    plugin.setComponentsFromLegacyExports(pluginExports);
+    return plugin;
   });
 }
 
-export function importPanelPlugin(path: string): Promise<AngularPanelPlugin | PanelPlugin> {
-  return importPluginModule(path).then(pluginExports => {
-    if (pluginExports.plugin) {
-      return pluginExports.plugin as PanelPlugin;
-    } else {
-      return new AngularPanelPlugin(pluginExports.PanelCtrl);
-    }
-  });
+import { getPanelPluginNotFound } from '../dashboard/dashgrid/PanelPluginNotFound';
+
+interface PanelCache {
+  [key: string]: PanelPlugin;
+}
+const panelCache: PanelCache = {};
+
+export function importPanelPlugin(id: string): Promise<PanelPlugin> {
+  const loaded = panelCache[id];
+  if (loaded) {
+    return Promise.resolve(loaded);
+  }
+  const meta = config.panels[id];
+  if (!meta) {
+    return Promise.resolve(getPanelPluginNotFound(id));
+  }
+
+  return importPluginModule(meta.module)
+    .then(pluginExports => {
+      if (pluginExports.plugin) {
+        return pluginExports.plugin as PanelPlugin;
+      } else if (pluginExports.PanelCtrl) {
+        const plugin = new PanelPlugin(null);
+        plugin.angularPanelCtrl = pluginExports.PanelCtrl;
+        return plugin;
+      }
+      throw new Error('missing export: plugin or PanelCtrl');
+    })
+    .then(plugin => {
+      plugin.meta = meta;
+      return (panelCache[meta.id] = plugin);
+    })
+    .catch(err => {
+      // TODO, maybe a different error plugin
+      console.log('Error loading panel plugin', err);
+      return getPanelPluginNotFound(id);
+    });
 }
 
 export function loadPluginCss(options) {

+ 6 - 4
public/app/features/plugins/variableQueryEditorLoader.tsx

@@ -3,9 +3,11 @@ import { importDataSourcePlugin } from './plugin_loader';
 import React from 'react';
 import ReactDOM from 'react-dom';
 import DefaultVariableQueryEditor from '../templating/DefaultVariableQueryEditor';
+import { DataSourcePluginMeta } from '@grafana/ui';
+import { TemplateSrv } from '../templating/template_srv';
 
-async function loadComponent(module) {
-  const dsPlugin = await importDataSourcePlugin(module);
+async function loadComponent(meta: DataSourcePluginMeta) {
+  const dsPlugin = await importDataSourcePlugin(meta);
   if (dsPlugin.components.VariableQueryEditor) {
     return dsPlugin.components.VariableQueryEditor;
   } else {
@@ -14,11 +16,11 @@ async function loadComponent(module) {
 }
 
 /** @ngInject */
-function variableQueryEditorLoader(templateSrv) {
+function variableQueryEditorLoader(templateSrv: TemplateSrv) {
   return {
     restrict: 'E',
     link: async (scope, elem) => {
-      const Component = await loadComponent(scope.currentDatasource.meta.module);
+      const Component = await loadComponent(scope.currentDatasource.meta);
       const props = {
         datasource: scope.currentDatasource,
         query: scope.current.query,