瀏覽代碼

Refactor: Plugin exports & data source / panel types (#16364)

* wip: began work off removing meta and pluginExports from DataSourceApi interface

* WIP: changing how plugins are exports and loaded

* Down the refactoring rabit hole that keeps expanding

* TestData now returns DataSourcePlugin

* Refactoring: fixed app config page loading, type renamings and more typings

* Refactor: Correct casing on DatasourceStatus => DataSourceStatus
Torkel Ödegaard 6 年之前
父節點
當前提交
47e51cb6b3
共有 31 個文件被更改,包括 411 次插入251 次删除
  1. 178 0
      packages/grafana-ui/src/types/datasource.ts
  2. 10 0
      packages/grafana-ui/src/types/panel.ts
  3. 17 102
      packages/grafana-ui/src/types/plugin.ts
  4. 2 2
      public/app/core/config.ts
  5. 33 12
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  6. 2 2
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  7. 2 4
      public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx
  8. 4 3
      public/app/features/dashboard/panel_editor/QueryEditorRow.tsx
  9. 3 3
      public/app/features/dashboard/panel_editor/VisualizationTab.tsx
  10. 15 10
      public/app/features/dashboard/state/PanelModel.test.ts
  11. 6 7
      public/app/features/dashboard/state/PanelModel.ts
  12. 2 0
      public/app/features/datasources/state/navModel.ts
  13. 6 4
      public/app/features/explore/QueryRow.tsx
  14. 1 1
      public/app/features/explore/state/reducers.test.ts
  15. 2 1
      public/app/features/explore/state/reducers.ts
  16. 1 1
      public/app/features/manage-dashboards/DashboardImportCtrl.test.ts
  17. 2 1
      public/app/features/plugins/__mocks__/pluginMocks.ts
  18. 10 11
      public/app/features/plugins/datasource_srv.ts
  19. 32 40
      public/app/features/plugins/plugin_component.ts
  20. 34 2
      public/app/features/plugins/plugin_loader.ts
  21. 11 4
      public/app/features/plugins/specs/datasource_srv.test.ts
  22. 4 4
      public/app/features/plugins/variableQueryEditorLoader.tsx
  23. 4 4
      public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx
  24. 4 4
      public/app/plugins/datasource/loki/components/useLokiLabels.test.ts
  25. 5 5
      public/app/plugins/datasource/loki/components/useLokiLabels.ts
  26. 4 4
      public/app/plugins/datasource/loki/components/useLokiSyntax.test.ts
  27. 2 2
      public/app/plugins/datasource/loki/components/useLokiSyntax.ts
  28. 6 6
      public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
  29. 4 9
      public/app/plugins/datasource/testdata/module.ts
  30. 4 2
      public/app/types/plugins.ts
  31. 1 1
      scripts/ci-frontend-metrics.sh

+ 178 - 0
packages/grafana-ui/src/types/datasource.ts

@@ -1,7 +1,159 @@
+import { ComponentClass } from 'react';
 import { TimeRange, RawTimeRange } from './time';
 import { PluginMeta } from './plugin';
 import { TableData, TimeSeries, SeriesData } from './data';
 
+export class DataSourcePlugin<TQuery extends DataQuery = DataQuery> {
+  DataSourceClass: DataSourceConstructor<TQuery>;
+  components: DataSourcePluginComponents<TQuery>;
+
+  constructor(DataSourceClass: DataSourceConstructor<TQuery>) {
+    this.DataSourceClass = DataSourceClass;
+    this.components = {};
+  }
+
+  setConfigCtrl(ConfigCtrl: any) {
+    this.components.ConfigCtrl = ConfigCtrl;
+    return this;
+  }
+
+  setQueryCtrl(QueryCtrl: any) {
+    this.components.QueryCtrl = QueryCtrl;
+    return this;
+  }
+
+  setAnnotationQueryCtrl(AnnotationsQueryCtrl: any) {
+    this.components.AnnotationsQueryCtrl = AnnotationsQueryCtrl;
+    return this;
+  }
+
+  setQueryEditor(QueryEditor: ComponentClass<QueryEditorProps<DataSourceApi, TQuery>>) {
+    this.components.QueryEditor = QueryEditor;
+    return this;
+  }
+
+  setExploreQueryField(ExploreQueryField: ComponentClass<ExploreQueryFieldProps<DataSourceApi, TQuery>>) {
+    this.components.ExploreQueryField = ExploreQueryField;
+    return this;
+  }
+
+  setExploreStartPage(ExploreStartPage: ComponentClass<ExploreStartPageProps>) {
+    this.components.ExploreStartPage = ExploreStartPage;
+    return this;
+  }
+
+  setVariableQueryEditor(VariableQueryEditor: any) {
+    this.components.VariableQueryEditor = VariableQueryEditor;
+    return this;
+  }
+
+  setComponentsFromLegacyExports(exports: any) {
+    this.components.ConfigCtrl = exports.ConfigCtrl;
+    this.components.QueryCtrl = exports.QueryCtrl;
+    this.components.AnnotationsQueryCtrl = exports.AnnotationsQueryCtrl;
+    this.components.ExploreQueryField = exports.ExploreQueryField;
+    this.components.ExploreStartPage = exports.ExploreStartPage;
+    this.components.QueryEditor = exports.QueryEditor;
+  }
+}
+
+export interface DataSourcePluginComponents<TQuery extends DataQuery = DataQuery> {
+  QueryCtrl?: any;
+  ConfigCtrl?: any;
+  AnnotationsQueryCtrl?: any;
+  VariableQueryEditor?: any;
+  QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi, TQuery>>;
+  ExploreQueryField?: ComponentClass<ExploreQueryFieldProps<DataSourceApi, TQuery>>;
+  ExploreStartPage?: ComponentClass<ExploreStartPageProps>;
+}
+
+interface DataSourceConstructor<TQuery extends DataQuery = DataQuery> {
+  new (instanceSettings: DataSourceInstanceSettings, ...args: any[]): DataSourceApi<TQuery>;
+}
+
+/**
+ * The main data source abstraction interface, represents an instance of a data source
+ */
+export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
+  /**
+   *  min interval range
+   */
+  interval?: string;
+
+  /**
+   * Imports queries from a different datasource
+   */
+  importQueries?(queries: TQuery[], originMeta: PluginMeta): Promise<TQuery[]>;
+
+  /**
+   * Initializes a datasource after instantiation
+   */
+  init?: () => void;
+
+  /**
+   * Main metrics / data query action
+   */
+  query(options: DataQueryOptions<TQuery>): Promise<DataQueryResponse>;
+
+  /**
+   * Test & verify datasource settings & connection details
+   */
+  testDatasource(): Promise<any>;
+
+  /**
+   *  Get hints for query improvements
+   */
+  getQueryHints?(query: TQuery, results: any[], ...rest: any): QueryHint[];
+
+  /**
+   *  Set after constructor is called by Grafana
+   */
+  name?: string;
+
+  /**
+   * Set after constructor call, as the data source instance is the most common thing to pass around
+   * we attach the components to this instance for easy access
+   */
+  components?: DataSourcePluginComponents;
+  meta?: PluginMeta;
+}
+
+export interface ExploreDataSourceApi<TQuery extends DataQuery = DataQuery> extends DataSourceApi {
+  modifyQuery?(query: TQuery, action: QueryFixAction): TQuery;
+  getHighlighterExpression?(query: TQuery): string;
+  languageProvider?: any;
+}
+
+export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
+  datasource: DSType;
+  query: TQuery;
+  onRunQuery: () => void;
+  onChange: (value: TQuery) => void;
+  queryResponse?: SeriesData[];
+  queryError?: DataQueryError;
+}
+
+export enum DataSourceStatus {
+  Connected,
+  Disconnected,
+}
+
+export interface ExploreQueryFieldProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
+  datasource: DSType;
+  datasourceStatus: DataSourceStatus;
+  query: TQuery;
+  error?: string | JSX.Element;
+  hint?: QueryHint;
+  history: any[];
+  onExecuteQuery?: () => void;
+  onQueryChange?: (value: TQuery) => void;
+  onExecuteHint?: (action: QueryFixAction) => void;
+}
+
+export interface ExploreStartPageProps {
+  onClickExample: (query: DataQuery) => void;
+}
+
 /**
  * Starting in v6.2 SeriesData can represent both TimeSeries and TableData
  */
@@ -89,6 +241,9 @@ export interface QueryHint {
   fix?: QueryFix;
 }
 
+/**
+ * Data Source instance edit model
+ */
 export interface DataSourceSettings {
   id: number;
   orgId: number;
@@ -109,6 +264,29 @@ export interface DataSourceSettings {
   withCredentials: boolean;
 }
 
+/**
+ * Frontend settings model that is passed to Datasource constructor. This differs a bit from the model above
+ * as this data model is available to every user who has access to a data source (Viewers+).
+ */
+export interface DataSourceInstanceSettings {
+  type: string;
+  name: string;
+  meta: PluginMeta;
+  url?: string;
+  jsonData: { [str: string]: any };
+  username?: string;
+  password?: string; // when access is direct, for some legacy datasources
+
+  /**
+   * This is the full Authorization header if basic auth is ennabled.
+   * Only available here when access is Browser (direct), when acess is Server (proxy)
+   * The basic auth header, username & password is never exposted to browser/Frontend
+   * so this will be emtpy then.
+   */
+  basicAuth?: string;
+  withCredentials?: boolean;
+}
+
 export interface DataSourceSelectItem {
   name: string;
   value: string | null;

+ 10 - 0
packages/grafana-ui/src/types/panel.ts

@@ -83,6 +83,16 @@ export class ReactPanelPlugin<TOptions = any> {
   }
 }
 
+export class AngularPanelPlugin {
+  components: {
+    PanelCtrl: any;
+  };
+
+  constructor(PanelCtrl: any) {
+    this.components = { PanelCtrl: PanelCtrl };
+  }
+}
+
 export interface PanelSize {
   width: number;
   height: number;

+ 17 - 102
packages/grafana-ui/src/types/plugin.ts

@@ -1,110 +1,10 @@
-import { ComponentClass } from 'react';
-import { ReactPanelPlugin } from './panel';
-import {
-  DataQueryOptions,
-  DataQuery,
-  DataQueryResponse,
-  QueryHint,
-  QueryFixAction,
-  DataQueryError,
-} from './datasource';
-import { SeriesData } from './data';
-
-export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
-  /**
-   *  min interval range
-   */
-  interval?: string;
-
-  /**
-   * Imports queries from a different datasource
-   */
-  importQueries?(queries: TQuery[], originMeta: PluginMeta): Promise<TQuery[]>;
-
-  /**
-   * Initializes a datasource after instantiation
-   */
-  init?: () => void;
-
-  /**
-   * Main metrics / data query action
-   */
-  query(options: DataQueryOptions<TQuery>): Promise<DataQueryResponse>;
-
-  /**
-   * Test & verify datasource settings & connection details
-   */
-  testDatasource(): Promise<any>;
-
-  /**
-   *  Get hints for query improvements
-   */
-  getQueryHints?(query: TQuery, results: any[], ...rest: any): QueryHint[];
-
-  /**
-   *  Set after constructor is called by Grafana
-   */
-  name?: string;
-  meta?: PluginMeta;
-  pluginExports?: PluginExports;
-}
-
-export interface ExploreDataSourceApi<TQuery extends DataQuery = DataQuery> extends DataSourceApi {
-  modifyQuery?(query: TQuery, action: QueryFixAction): TQuery;
-  getHighlighterExpression?(query: TQuery): string;
-  languageProvider?: any;
-}
-
-export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
-  datasource: DSType;
-  query: TQuery;
-  onRunQuery: () => void;
-  onChange: (value: TQuery) => void;
-  queryResponse?: SeriesData[];
-  queryError?: DataQueryError;
-}
-
-export enum DatasourceStatus {
-  Connected,
-  Disconnected,
-}
-
-export interface ExploreQueryFieldProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
-  datasource: DSType;
-  datasourceStatus: DatasourceStatus;
-  query: TQuery;
-  error?: string | JSX.Element;
-  hint?: QueryHint;
-  history: any[];
-  onExecuteQuery?: () => void;
-  onQueryChange?: (value: TQuery) => void;
-  onExecuteHint?: (action: QueryFixAction) => void;
-}
-
-export interface ExploreStartPageProps {
-  onClickExample: (query: DataQuery) => void;
-}
-
-export interface PluginExports {
-  Datasource?: DataSourceApi;
-  QueryCtrl?: any;
-  QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi, DataQuery>>;
-  ConfigCtrl?: any;
-  AnnotationsQueryCtrl?: any;
-  VariableQueryEditor?: any;
-  ExploreQueryField?: ComponentClass<ExploreQueryFieldProps<DataSourceApi, DataQuery>>;
-  ExploreStartPage?: ComponentClass<ExploreStartPageProps>;
-
-  // Panel plugin
-  PanelCtrl?: any;
-  reactPanel?: ReactPanelPlugin;
-}
-
 export interface PluginMeta {
   id: string;
   name: string;
   info: PluginMetaInfo;
   includes: PluginInclude[];
+  module: string;
+  baseUrl: string;
 
   // Datasource-specific
   builtIn?: boolean;
@@ -150,3 +50,18 @@ export interface PluginMetaInfo {
   updated: string;
   version: string;
 }
+
+export class AppPlugin {
+  components: {
+    ConfigCtrl?: any;
+  };
+
+  pages: { [str: string]: any };
+
+  constructor(ConfigCtrl: any) {
+    this.components = {
+      ConfigCtrl: ConfigCtrl,
+    };
+    this.pages = {};
+  }
+}

+ 2 - 2
public/app/core/config.ts

@@ -1,6 +1,6 @@
 import _ from 'lodash';
 import { PanelPlugin } from 'app/types/plugins';
-import { GrafanaTheme, getTheme, GrafanaThemeType } from '@grafana/ui';
+import { GrafanaTheme, getTheme, GrafanaThemeType, DataSourceInstanceSettings } from '@grafana/ui';
 
 export interface BuildInfo {
   version: string;
@@ -12,7 +12,7 @@ export interface BuildInfo {
 }
 
 export class Settings {
-  datasources: any;
+  datasources: { [str: string]: DataSourceInstanceSettings };
   panels: { [key: string]: PanelPlugin };
   appSubUrl: string;
   windowTitlePrefix: string;

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

@@ -1,19 +1,24 @@
+// Libraries
 import React, { PureComponent } from 'react';
 import config from 'app/core/config';
 import classNames from 'classnames';
 
+// Utils & Services
 import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
-import { importPluginModule } from 'app/features/plugins/plugin_loader';
+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';
+import { PanelResizer } from './PanelResizer';
 
+// Types
 import { PanelModel, DashboardModel } from '../state';
 import { PanelPlugin } from 'app/types';
-import { PanelResizer } from './PanelResizer';
+import { AngularPanelPlugin, ReactPanelPlugin } from '@grafana/ui/src/types/panel';
 
 export interface Props {
   panel: PanelModel;
@@ -73,13 +78,8 @@ export class DashboardPanel extends PureComponent<Props, State> {
       // unmount angular panel
       this.cleanUpAngularPanel();
 
-      if (!plugin.exports) {
-        try {
-          plugin.exports = await importPluginModule(plugin.module);
-        } catch (e) {
-          plugin = getPanelPluginNotFound(pluginId);
-        }
-      }
+      // load the actual plugin code
+      plugin = await this.importPanelPluginModule(plugin);
 
       if (panel.type !== pluginId) {
         panel.changePlugin(plugin);
@@ -91,6 +91,27 @@ export class DashboardPanel extends PureComponent<Props, State> {
     }
   }
 
+  async importPanelPluginModule(plugin: PanelPlugin): Promise<PanelPlugin> {
+    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 ReactPanelPlugin) {
+        plugin.reactPlugin = importedPlugin as ReactPanelPlugin;
+      }
+    } 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);
   }
@@ -155,7 +176,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
     }
 
     // if we have not loaded plugin exports yet, wait
-    if (!plugin || !plugin.exports) {
+    if (!plugin || !plugin.hasBeenImported) {
       return null;
     }
 
@@ -178,8 +199,8 @@ export class DashboardPanel extends PureComponent<Props, State> {
               onMouseLeave={this.onMouseLeave}
               style={styles}
             >
-              {plugin.exports.reactPanel && this.renderReactPanel()}
-              {plugin.exports.PanelCtrl && this.renderAngularPanel()}
+              {plugin.reactPlugin && this.renderReactPanel()}
+              {plugin.angularPlugin && this.renderAngularPanel()}
             </div>
           )}
         />

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

@@ -158,7 +158,7 @@ export class PanelChrome extends PureComponent<Props, State> {
   renderPanelPlugin(loading: LoadingState, data: SeriesData[], width: number, height: number): JSX.Element {
     const { panel, plugin } = this.props;
     const { timeRange, renderCounter } = this.state;
-    const PanelComponent = plugin.exports.reactPanel.panel;
+    const PanelComponent = plugin.reactPlugin.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
@@ -172,7 +172,7 @@ export class PanelChrome extends PureComponent<Props, State> {
           loading={loading}
           data={data}
           timeRange={timeRange}
-          options={panel.getOptions(plugin.exports.reactPanel.defaults)}
+          options={panel.getOptions(plugin.reactPlugin.defaults)}
           width={width - 2 * config.theme.panelPadding.horizontal}
           height={height - PANEL_HEADER_HEIGHT - config.theme.panelPadding.vertical}
           renderCounter={renderCounter}

+ 2 - 4
public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx

@@ -62,9 +62,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin {
       updated: '',
       version: '',
     },
-
-    exports: {
-      reactPanel: new ReactPanelPlugin(NotFound),
-    },
+    reactPlugin: new ReactPanelPlugin(NotFound),
+    angularPlugin: null,
   };
 }

+ 4 - 3
public/app/features/dashboard/panel_editor/QueryEditorRow.tsx

@@ -176,12 +176,13 @@ export class QueryEditorRow extends PureComponent<Props, State> {
     const { query, onChange } = this.props;
     const { datasource, queryResponse, queryError } = this.state;
 
-    if (datasource.pluginExports.QueryCtrl) {
+    if (datasource.components.QueryCtrl) {
       return <div ref={element => (this.element = element)} />;
     }
 
-    if (datasource.pluginExports.QueryEditor) {
-      const QueryEditor = datasource.pluginExports.QueryEditor;
+    if (datasource.components.QueryEditor) {
+      const QueryEditor = datasource.components.QueryEditor;
+
       return (
         <QueryEditor
           query={query}

+ 3 - 3
public/app/features/dashboard/panel_editor/VisualizationTab.tsx

@@ -53,7 +53,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
 
   getReactPanelOptions = () => {
     const { panel, plugin } = this.props;
-    return panel.getOptions(plugin.exports.reactPanel.defaults);
+    return panel.getOptions(plugin.reactPlugin.defaults);
   };
 
   renderPanelOptions() {
@@ -63,8 +63,8 @@ export class VisualizationTab extends PureComponent<Props, State> {
       return <div ref={element => (this.element = element)} />;
     }
 
-    if (plugin.exports.reactPanel) {
-      const PanelEditor = plugin.exports.reactPanel.editor;
+    if (plugin.reactPlugin) {
+      const PanelEditor = plugin.reactPlugin.editor;
 
       if (PanelEditor) {
         return <PanelEditor options={this.getReactPanelOptions()} onOptionsChange={this.onPanelOptionsChanged} />;

+ 15 - 10
public/app/features/dashboard/state/PanelModel.test.ts

@@ -1,6 +1,8 @@
 import { PanelModel } from './PanelModel';
 import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
-import { ReactPanelPlugin } from '@grafana/ui/src/types/panel';
+import { ReactPanelPlugin, AngularPanelPlugin } from '@grafana/ui/src/types/panel';
+
+class TablePanelCtrl {}
 
 describe('PanelModel', () => {
   describe('when creating new panel model', () => {
@@ -28,7 +30,12 @@ describe('PanelModel', () => {
         },
       };
       model = new PanelModel(modelJson);
-      model.pluginLoaded(getPanelPlugin({ id: 'table', exports: { PanelCtrl: {} as any } }));
+      model.pluginLoaded(
+        getPanelPlugin({
+          id: 'table',
+          angularPlugin: new AngularPanelPlugin(TablePanelCtrl),
+        })
+      );
     });
 
     it('should apply defaults', () => {
@@ -79,7 +86,7 @@ describe('PanelModel', () => {
 
     describe('when changing panel type', () => {
       beforeEach(() => {
-        model.changePlugin(getPanelPlugin({ id: 'graph', exports: {} }));
+        model.changePlugin(getPanelPlugin({ id: 'graph' }));
         model.alert = { id: 2 };
       });
 
@@ -88,12 +95,12 @@ describe('PanelModel', () => {
       });
 
       it('should restore table properties when changing back', () => {
-        model.changePlugin(getPanelPlugin({ id: 'table', exports: {} }));
+        model.changePlugin(getPanelPlugin({ id: 'table' }));
         expect(model.showColumns).toBe(true);
       });
 
       it('should remove alert rule when changing type that does not support it', () => {
-        model.changePlugin(getPanelPlugin({ id: 'table', exports: {} }));
+        model.changePlugin(getPanelPlugin({ id: 'table' }));
         expect(model.alert).toBe(undefined);
       });
     });
@@ -105,7 +112,7 @@ describe('PanelModel', () => {
         model.events.on('panel-teardown', () => {
           tearDownPublished = true;
         });
-        model.changePlugin(getPanelPlugin({ id: 'graph', exports: {} }));
+        model.changePlugin(getPanelPlugin({ id: 'graph' }));
       });
 
       it('should teardown / destroy panel so angular panels event subscriptions are removed', () => {
@@ -116,15 +123,13 @@ describe('PanelModel', () => {
 
     describe('when changing to react panel', () => {
       const onPanelTypeChanged = jest.fn();
-      const reactPanel = new ReactPanelPlugin({} as any).setPanelChangeHandler(onPanelTypeChanged as any);
+      const reactPlugin = new ReactPanelPlugin({} as any).setPanelChangeHandler(onPanelTypeChanged as any);
 
       beforeEach(() => {
         model.changePlugin(
           getPanelPlugin({
             id: 'react',
-            exports: {
-              reactPanel,
-            },
+            reactPlugin: reactPlugin,
           })
         );
       });

+ 6 - 7
public/app/features/dashboard/state/PanelModel.ts

@@ -252,13 +252,10 @@ export class PanelModel {
   pluginLoaded(plugin: PanelPlugin) {
     this.plugin = plugin;
 
-    const { reactPanel } = plugin.exports;
-
-    // Call PanelMigration Handler if the version has changed
-    if (reactPanel && reactPanel.onPanelMigration) {
+    if (plugin.reactPlugin && plugin.reactPlugin.onPanelMigration) {
       const version = this.getPluginVersion(plugin);
       if (version !== this.pluginVersion) {
-        this.options = reactPanel.onPanelMigration(this);
+        this.options = plugin.reactPlugin.onPanelMigration(this);
         this.pluginVersion = version;
       }
     }
@@ -268,10 +265,9 @@ export class PanelModel {
     const pluginId = newPlugin.id;
     const oldOptions: any = this.getOptionsToRemember();
     const oldPluginId = this.type;
-    const reactPanel = newPlugin.exports.reactPanel;
 
     // for angular panels we must remove all events and let angular panels do some cleanup
-    if (this.plugin.exports.PanelCtrl) {
+    if (this.plugin.angularPlugin) {
       this.destroy();
     }
 
@@ -292,12 +288,15 @@ export class PanelModel {
     this.plugin = newPlugin;
 
     // Let panel plugins inspect options from previous panel and keep any that it can use
+    const reactPanel = newPlugin.reactPlugin;
+
     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 (reactPanel.onPanelMigration) {
         this.pluginVersion = this.getPluginVersion(newPlugin);
       }

+ 2 - 0
public/app/features/datasources/state/navModel.ts

@@ -84,6 +84,8 @@ export function getDataSourceLoadingNav(pageName: string): NavModel {
         version: '',
       },
       includes: [{ type: '', name: '', path: '' }],
+      module: '',
+      baseUrl: '',
     }
   );
 

+ 6 - 4
public/app/features/explore/QueryRow.tsx

@@ -21,7 +21,7 @@ import {
   ExploreDataSourceApi,
   QueryHint,
   QueryFixAction,
-  DatasourceStatus,
+  DataSourceStatus,
 } from '@grafana/ui';
 import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore';
 import { Emitter } from 'app/core/utils/emitter';
@@ -41,7 +41,7 @@ interface QueryRowProps {
   className?: string;
   exploreId: ExploreId;
   datasourceInstance: ExploreDataSourceApi;
-  datasourceStatus: DatasourceStatus;
+  datasourceStatus: DataSourceStatus;
   highlightLogsExpressionAction: typeof highlightLogsExpressionAction;
   history: HistoryItem[];
   index: number;
@@ -115,11 +115,13 @@ export class QueryRow extends PureComponent<QueryRowProps> {
       range,
       datasourceStatus,
     } = this.props;
+
     const transactions = queryTransactions.filter(t => t.rowIndex === index);
     const transactionWithError = transactions.find(t => t.error !== undefined);
     const hint = getFirstHintFromTransactions(transactions);
     const queryError = transactionWithError ? transactionWithError.error : null;
-    const QueryField = datasourceInstance.pluginExports.ExploreQueryField;
+    const QueryField = datasourceInstance.components.ExploreQueryField;
+
     return (
       <div className="query-row">
         <div className="query-row-status">
@@ -183,7 +185,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
     query,
     queryTransactions,
     range,
-    datasourceStatus: datasourceError ? DatasourceStatus.Disconnected : DatasourceStatus.Connected,
+    datasourceStatus: datasourceError ? DataSourceStatus.Disconnected : DataSourceStatus.Connected,
   };
 }
 

+ 1 - 1
public/app/features/explore/state/reducers.test.ts

@@ -136,7 +136,7 @@ describe('Explore item reducer', () => {
               logs: {},
               tables: {},
             },
-            pluginExports: {
+            components: {
               ExploreStartPage: StartPage,
             },
           } as DataSourceApi;

+ 2 - 1
public/app/features/explore/state/reducers.ts

@@ -226,8 +226,9 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       const supportsGraph = datasourceInstance.meta.metrics;
       const supportsLogs = datasourceInstance.meta.logs;
       const supportsTable = datasourceInstance.meta.tables;
+
       // Custom components
-      const StartPage = datasourceInstance.pluginExports.ExploreStartPage;
+      const StartPage = datasourceInstance.components.ExploreStartPage;
 
       return {
         ...state,

+ 1 - 1
public/app/features/manage-dashboards/DashboardImportCtrl.test.ts

@@ -31,7 +31,7 @@ describe('DashboardImportCtrl', () => {
       config.datasources = {
         ds: {
           type: 'test-db',
-        },
+        } as any,
       };
 
       ctx.ctrl.onUpload({

+ 2 - 1
public/app/features/plugins/__mocks__/pluginMocks.ts

@@ -56,7 +56,8 @@ export const getPanelPlugin = (options: Partial<PanelPlugin>): PanelPlugin => {
     hideFromList: options.hideFromList === true,
     module: '',
     baseUrl: '',
-    exports: options.exports,
+    reactPlugin: options.reactPlugin,
+    angularPlugin: options.angularPlugin,
   };
 };
 

+ 10 - 11
public/app/features/plugins/datasource_srv.ts

@@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module';
 
 // Services & Utils
 import config from 'app/core/config';
-import { importPluginModule } from './plugin_loader';
+import { importDataSourcePlugin } from './plugin_loader';
 
 // Types
 import { DataSourceApi, DataSourceSelectItem, ScopedVars } from '@grafana/ui/src/types';
@@ -52,25 +52,24 @@ export class DatasourceSrv {
     }
 
     const deferred = this.$q.defer();
-    const pluginDef = dsConfig.meta;
 
-    importPluginModule(pluginDef.module)
-      .then(plugin => {
+    importDataSourcePlugin(dsConfig.meta.module)
+      .then(dsPlugin => {
         // check if its in cache now
         if (this.datasources[name]) {
           deferred.resolve(this.datasources[name]);
           return;
         }
 
-        // plugin module needs to export a constructor function named Datasource
-        if (!plugin.Datasource) {
-          throw new Error('Plugin module is missing Datasource constructor');
-        }
+        const instance: DataSourceApi = this.$injector.instantiate(dsPlugin.DataSourceClass, {
+          instanceSettings: dsConfig,
+        });
 
-        const instance: DataSourceApi = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
-        instance.meta = pluginDef;
         instance.name = name;
-        instance.pluginExports = plugin;
+        instance.components = dsPlugin.components;
+        instance.meta = dsConfig.meta;
+
+        // store in instance cache
         this.datasources[name] = instance;
         deferred.resolve(instance);
       })

+ 32 - 40
public/app/features/plugins/plugin_component.ts

@@ -3,7 +3,9 @@ import _ from 'lodash';
 
 import config from 'app/core/config';
 import coreModule from 'app/core/core_module';
-import { importPluginModule } from './plugin_loader';
+
+import { AngularPanelPlugin, DataSourceApi } from '@grafana/ui/src/types';
+import { importPanelPlugin, importDataSourcePlugin, importAppPlugin } from './plugin_loader';
 
 /** @ngInject */
 function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache, $timeout) {
@@ -67,14 +69,9 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
     };
 
     const panelInfo = config.panels[scope.panel.type];
-    let panelCtrlPromise = Promise.resolve(null);
-    if (panelInfo) {
-      panelCtrlPromise = importPluginModule(panelInfo.module).then(panelModule => {
-        return panelModule.PanelCtrl;
-      });
-    }
-
-    return panelCtrlPromise.then((PanelCtrl: any) => {
+    return importPanelPlugin(panelInfo.module).then(panelPlugin => {
+      const angularPanelPlugin = panelPlugin as AngularPanelPlugin;
+      const PanelCtrl = angularPanelPlugin.components.PanelCtrl;
       componentInfo.Component = PanelCtrl;
 
       if (!PanelCtrl || PanelCtrl.registered) {
@@ -101,11 +98,12 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
     });
   }
 
-  function getModule(scope, attrs) {
+  function getModule(scope: any, attrs: any) {
     switch (attrs.type) {
       // QueryCtrl
       case 'query-ctrl': {
-        const ds = scope.ctrl.datasource;
+        const ds: DataSourceApi = scope.ctrl.datasource as DataSourceApi;
+
         return $q.when({
           baseUrl: ds.meta.baseUrl,
           name: 'query-ctrl-' + ds.meta.id,
@@ -115,12 +113,12 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
             'panel-ctrl': 'ctrl',
             datasource: 'ctrl.datasource',
           },
-          Component: ds.pluginExports.QueryCtrl,
+          Component: ds.components.QueryCtrl,
         });
       }
       // Annotations
       case 'annotations-query-ctrl': {
-        return importPluginModule(scope.ctrl.currentDatasource.meta.module).then(dsModule => {
+        return importDataSourcePlugin(scope.ctrl.currentDatasource.meta.module).then(dsPlugin => {
           return {
             baseUrl: scope.ctrl.currentDatasource.meta.baseUrl,
             name: 'annotations-query-ctrl-' + scope.ctrl.currentDatasource.meta.id,
@@ -129,60 +127,54 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
               annotation: 'ctrl.currentAnnotation',
               datasource: 'ctrl.currentDatasource',
             },
-            Component: dsModule.AnnotationsQueryCtrl,
+            Component: dsPlugin.components.AnnotationsQueryCtrl,
           };
         });
       }
       // Datasource ConfigCtrl
       case 'datasource-config-ctrl': {
         const dsMeta = scope.ctrl.datasourceMeta;
-        return importPluginModule(dsMeta.module).then(
-          (dsModule): any => {
-            if (!dsModule.ConfigCtrl) {
-              return { notFound: true };
-            }
-
-            scope.$watch(
-              'ctrl.current',
-              () => {
-                scope.onModelChanged(scope.ctrl.current);
-              },
-              true
-            );
+        return importDataSourcePlugin(dsMeta.module).then(dsPlugin => {
+          scope.$watch(
+            'ctrl.current',
+            () => {
+              scope.onModelChanged(scope.ctrl.current);
+            },
+            true
+          );
 
-            return {
-              baseUrl: dsMeta.baseUrl,
-              name: 'ds-config-' + dsMeta.id,
-              bindings: { meta: '=', current: '=' },
-              attrs: { meta: 'ctrl.datasourceMeta', current: 'ctrl.current' },
-              Component: dsModule.ConfigCtrl,
-            };
-          }
-        );
+          return {
+            baseUrl: dsMeta.baseUrl,
+            name: 'ds-config-' + dsMeta.id,
+            bindings: { meta: '=', current: '=' },
+            attrs: { meta: 'ctrl.datasourceMeta', current: 'ctrl.current' },
+            Component: dsPlugin.components.ConfigCtrl,
+          };
+        });
       }
       // AppConfigCtrl
       case 'app-config-ctrl': {
         const model = scope.ctrl.model;
-        return importPluginModule(model.module).then(appModule => {
+        return importAppPlugin(model.module).then(appPlugin => {
           return {
             baseUrl: model.baseUrl,
             name: 'app-config-' + model.id,
             bindings: { appModel: '=', appEditCtrl: '=' },
             attrs: { 'app-model': 'ctrl.model', 'app-edit-ctrl': 'ctrl' },
-            Component: appModule.ConfigCtrl,
+            Component: appPlugin.components.ConfigCtrl,
           };
         });
       }
       // App Page
       case 'app-page': {
         const appModel = scope.ctrl.appModel;
-        return importPluginModule(appModel.module).then(appModule => {
+        return importAppPlugin(appModel.module).then(appPlugin => {
           return {
             baseUrl: appModel.baseUrl,
             name: 'app-page-' + appModel.id + '-' + scope.ctrl.page.slug,
             bindings: { appModel: '=' },
             attrs: { 'app-model': 'ctrl.appModel' },
-            Component: appModule[scope.ctrl.page.component],
+            Component: appPlugin.pages[scope.ctrl.page.component],
           };
         });
       }

+ 34 - 2
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 { PluginExports } from '@grafana/ui';
+import { DataSourcePlugin, AppPlugin, ReactPanelPlugin, AngularPanelPlugin } 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';
@@ -141,7 +141,7 @@ for (const flotDep of flotDeps) {
   exposeToPlugin(flotDep, { fakeDep: 1 });
 }
 
-export function importPluginModule(path: string): Promise<PluginExports> {
+function importPluginModule(path: string): Promise<any> {
   const builtIn = builtInPlugins[path];
   if (builtIn) {
     return Promise.resolve(builtIn);
@@ -149,6 +149,38 @@ export function importPluginModule(path: string): Promise<PluginExports> {
   return System.import(path);
 }
 
+export function importDataSourcePlugin(path: string): Promise<DataSourcePlugin> {
+  return importPluginModule(path).then(pluginExports => {
+    if (pluginExports.plugin) {
+      return pluginExports.plugin as DataSourcePlugin;
+    }
+
+    if (pluginExports.Datasource) {
+      const dsPlugin = new DataSourcePlugin(pluginExports.Datasource);
+      dsPlugin.setComponentsFromLegacyExports(pluginExports);
+      return dsPlugin;
+    }
+
+    throw new Error('Plugin module is missing DataSourcePlugin or Datasource constructor export');
+  });
+}
+
+export function importAppPlugin(path: string): Promise<AppPlugin> {
+  return importPluginModule(path).then(pluginExports => {
+    return new AppPlugin(pluginExports.ConfigCtrl);
+  });
+}
+
+export function importPanelPlugin(path: string): Promise<AngularPanelPlugin | ReactPanelPlugin> {
+  return importPluginModule(path).then(pluginExports => {
+    if (pluginExports.reactPanel) {
+      return pluginExports.reactPanel as ReactPanelPlugin;
+    } else {
+      return new AngularPanelPlugin(pluginExports.PanelCtrl);
+    }
+  });
+}
+
 export function loadPluginCss(options) {
   if (config.bootData.user.lightTheme) {
     System.import(options.light + '!css');

+ 11 - 4
public/app/features/plugins/specs/datasource_srv.test.ts

@@ -1,6 +1,7 @@
 import config from 'app/core/config';
 import 'app/features/plugins/datasource_srv';
 import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
+import { PluginMeta } from '@grafana/ui/src/types';
 
 // Datasource variable $datasource with current value 'BBB'
 const templateSrv = {
@@ -22,16 +23,22 @@ describe('datasource_srv', () => {
     beforeEach(() => {
       config.datasources = {
         buildInDs: {
+          type: 'b',
           name: 'buildIn',
-          meta: { builtIn: true },
+          meta: { builtIn: true } as PluginMeta,
+          jsonData: {},
         },
         nonBuildIn: {
+          type: 'e',
           name: 'external1',
-          meta: { builtIn: false },
+          meta: { builtIn: false } as PluginMeta,
+          jsonData: {},
         },
         nonExplore: {
+          type: 'e2',
           name: 'external2',
-          meta: {},
+          meta: {} as PluginMeta,
+          jsonData: {},
         },
       };
     });
@@ -73,7 +80,7 @@ describe('datasource_srv', () => {
       },
     };
     beforeEach(() => {
-      config.datasources = unsortedDatasources;
+      config.datasources = unsortedDatasources as any;
       metricSources = _datasourceSrv.getMetricSources({});
       config.defaultDatasource = 'BBB';
     });

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

@@ -1,13 +1,13 @@
 import coreModule from 'app/core/core_module';
-import { importPluginModule } from './plugin_loader';
+import { importDataSourcePlugin } from './plugin_loader';
 import React from 'react';
 import ReactDOM from 'react-dom';
 import DefaultVariableQueryEditor from '../templating/DefaultVariableQueryEditor';
 
 async function loadComponent(module) {
-  const component = await importPluginModule(module);
-  if (component && component.VariableQueryEditor) {
-    return component.VariableQueryEditor;
+  const dsPlugin = await importDataSourcePlugin(module);
+  if (dsPlugin.components.VariableQueryEditor) {
+    return dsPlugin.components.VariableQueryEditor;
   } else {
     return DefaultVariableQueryEditor;
   }

+ 4 - 4
public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx

@@ -17,10 +17,10 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
 // Types
 import { LokiQuery } from '../types';
 import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
-import { ExploreDataSourceApi, ExploreQueryFieldProps, DatasourceStatus } from '@grafana/ui';
+import { ExploreDataSourceApi, ExploreQueryFieldProps, DataSourceStatus } from '@grafana/ui';
 
-function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DatasourceStatus) {
-  if (datasourceStatus === DatasourceStatus.Disconnected) {
+function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) {
+  if (datasourceStatus === DataSourceStatus.Disconnected) {
     return '(Disconnected)';
   }
   if (!hasSyntax) {
@@ -169,7 +169,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
     const cleanText = datasource.languageProvider ? datasource.languageProvider.cleanText : undefined;
     const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
     const chooserText = getChooserText(syntaxLoaded, hasLogLabels, datasourceStatus);
-    const buttonDisabled = !syntaxLoaded || datasourceStatus === DatasourceStatus.Disconnected;
+    const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected;
 
     return (
       <>

+ 4 - 4
public/app/plugins/datasource/loki/components/useLokiLabels.test.ts

@@ -1,7 +1,7 @@
 import { renderHook, act } from 'react-hooks-testing-library';
 import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
 import { useLokiLabels } from './useLokiLabels';
-import { DatasourceStatus } from '@grafana/ui/src/types/plugin';
+import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
 
 describe('useLokiLabels hook', () => {
   it('should refresh labels', async () => {
@@ -17,7 +17,7 @@ describe('useLokiLabels hook', () => {
     };
 
     const { result, waitForNextUpdate } = renderHook(() =>
-      useLokiLabels(languageProvider, true, [], DatasourceStatus.Connected, DatasourceStatus.Connected)
+      useLokiLabels(languageProvider, true, [], DataSourceStatus.Connected, DataSourceStatus.Connected)
     );
     act(() => result.current.refreshLabels());
     expect(result.current.logLabelOptions).toEqual([]);
@@ -33,7 +33,7 @@ describe('useLokiLabels hook', () => {
     languageProvider.refreshLogLabels = jest.fn();
 
     renderHook(() =>
-      useLokiLabels(languageProvider, true, [], DatasourceStatus.Connected, DatasourceStatus.Disconnected)
+      useLokiLabels(languageProvider, true, [], DataSourceStatus.Connected, DataSourceStatus.Disconnected)
     );
 
     expect(languageProvider.refreshLogLabels).toBeCalledTimes(1);
@@ -48,7 +48,7 @@ describe('useLokiLabels hook', () => {
     languageProvider.refreshLogLabels = jest.fn();
 
     renderHook(() =>
-      useLokiLabels(languageProvider, true, [], DatasourceStatus.Disconnected, DatasourceStatus.Connected)
+      useLokiLabels(languageProvider, true, [], DataSourceStatus.Disconnected, DataSourceStatus.Connected)
     );
 
     expect(languageProvider.refreshLogLabels).not.toBeCalled();

+ 5 - 5
public/app/plugins/datasource/loki/components/useLokiLabels.ts

@@ -1,5 +1,5 @@
 import { useState, useEffect } from 'react';
-import { DatasourceStatus } from '@grafana/ui/src/types/plugin';
+import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
 
 import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
 import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
@@ -17,8 +17,8 @@ export const useLokiLabels = (
   languageProvider: LokiLanguageProvider,
   languageProviderInitialised: boolean,
   activeOption: CascaderOption[],
-  datasourceStatus: DatasourceStatus,
-  initialDatasourceStatus?: DatasourceStatus // used for test purposes
+  datasourceStatus: DataSourceStatus,
+  initialDatasourceStatus?: DataSourceStatus // used for test purposes
 ) => {
   const mounted = useRefMounted();
 
@@ -26,7 +26,7 @@ export const useLokiLabels = (
   const [logLabelOptions, setLogLabelOptions] = useState([]);
   const [shouldTryRefreshLabels, setRefreshLabels] = useState(false);
   const [prevDatasourceStatus, setPrevDatasourceStatus] = useState(
-    initialDatasourceStatus || DatasourceStatus.Connected
+    initialDatasourceStatus || DataSourceStatus.Connected
   );
   const [shouldForceRefreshLabels, setForceRefreshLabels] = useState(false);
 
@@ -84,7 +84,7 @@ export const useLokiLabels = (
   // This effect is performed on datasourceStatus state change only.
   // We want to make sure to only force refresh AFTER a disconnected state thats why we store the previous datasourceStatus in state
   useEffect(() => {
-    if (datasourceStatus === DatasourceStatus.Connected && prevDatasourceStatus === DatasourceStatus.Disconnected) {
+    if (datasourceStatus === DataSourceStatus.Connected && prevDatasourceStatus === DataSourceStatus.Disconnected) {
       setForceRefreshLabels(true);
     }
     setPrevDatasourceStatus(datasourceStatus);

+ 4 - 4
public/app/plugins/datasource/loki/components/useLokiSyntax.test.ts

@@ -1,5 +1,5 @@
 import { renderHook, act } from 'react-hooks-testing-library';
-import { DatasourceStatus } from '@grafana/ui/src/types/plugin';
+import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
 
 import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
 import { useLokiSyntax } from './useLokiSyntax';
@@ -30,7 +30,7 @@ describe('useLokiSyntax hook', () => {
   };
 
   it('should provide Loki syntax when used', async () => {
-    const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DatasourceStatus.Connected));
+    const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DataSourceStatus.Connected));
     expect(result.current.syntax).toEqual(null);
 
     await waitForNextUpdate();
@@ -39,7 +39,7 @@ describe('useLokiSyntax hook', () => {
   });
 
   it('should fetch labels on first call', async () => {
-    const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DatasourceStatus.Connected));
+    const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DataSourceStatus.Connected));
     expect(result.current.isSyntaxReady).toBeFalsy();
     expect(result.current.logLabelOptions).toEqual([]);
 
@@ -50,7 +50,7 @@ describe('useLokiSyntax hook', () => {
   });
 
   it('should try to fetch missing options when active option changes', async () => {
-    const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DatasourceStatus.Connected));
+    const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DataSourceStatus.Connected));
     await waitForNextUpdate();
     expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2);
 

+ 2 - 2
public/app/plugins/datasource/loki/components/useLokiSyntax.ts

@@ -1,7 +1,7 @@
 import { useState, useEffect } from 'react';
 // @ts-ignore
 import Prism from 'prismjs';
-import { DatasourceStatus } from '@grafana/ui/src/types/plugin';
+import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
 
 import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
 import { useLokiLabels } from 'app/plugins/datasource/loki/components/useLokiLabels';
@@ -15,7 +15,7 @@ const PRISM_SYNTAX = 'promql';
  * @param languageProvider
  * @description Initializes given language provider, exposes Loki syntax and enables loading label option values
  */
-export const useLokiSyntax = (languageProvider: LokiLanguageProvider, datasourceStatus: DatasourceStatus) => {
+export const useLokiSyntax = (languageProvider: LokiLanguageProvider, datasourceStatus: DataSourceStatus) => {
   const mounted = useRefMounted();
   // State
   const [languageProviderInitialized, setLanguageProviderInitilized] = useState(false);

+ 6 - 6
public/app/plugins/datasource/prometheus/components/PromQueryField.tsx

@@ -17,15 +17,15 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
 import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
 import { PromQuery } from '../types';
 import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
-import { ExploreDataSourceApi, ExploreQueryFieldProps, DatasourceStatus } from '@grafana/ui';
+import { ExploreDataSourceApi, ExploreQueryFieldProps, DataSourceStatus } from '@grafana/ui';
 
 const HISTOGRAM_GROUP = '__histograms__';
 const METRIC_MARK = 'metric';
 const PRISM_SYNTAX = 'promql';
 export const RECORDING_RULES_GROUP = '__recording_rules__';
 
-function getChooserText(hasSyntax: boolean, datasourceStatus: DatasourceStatus) {
-  if (datasourceStatus === DatasourceStatus.Disconnected) {
+function getChooserText(hasSyntax: boolean, datasourceStatus: DataSourceStatus) {
+  if (datasourceStatus === DataSourceStatus.Disconnected) {
     return '(Disconnected)';
   }
   if (!hasSyntax) {
@@ -153,8 +153,8 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
 
   componentDidUpdate(prevProps: PromQueryFieldProps) {
     const reconnected =
-      prevProps.datasourceStatus === DatasourceStatus.Disconnected &&
-      this.props.datasourceStatus === DatasourceStatus.Connected;
+      prevProps.datasourceStatus === DataSourceStatus.Disconnected &&
+      this.props.datasourceStatus === DataSourceStatus.Connected;
     if (!reconnected) {
       return;
     }
@@ -278,7 +278,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
     const { metricsOptions, syntaxLoaded } = this.state;
     const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
     const chooserText = getChooserText(syntaxLoaded, datasourceStatus);
-    const buttonDisabled = !syntaxLoaded || datasourceStatus === DatasourceStatus.Disconnected;
+    const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected;
 
     return (
       <>

+ 4 - 9
public/app/plugins/datasource/testdata/module.ts

@@ -1,18 +1,13 @@
+import { DataSourcePlugin } from '@grafana/ui';
 import { TestDataDatasource } from './datasource';
 import { TestDataQueryCtrl } from './query_ctrl';
-// import { QueryEditor } from './QueryEditor';
 
 class TestDataAnnotationsQueryCtrl {
   annotation: any;
-
   constructor() {}
-
   static template = '<h2>Annotation scenario</h2>';
 }
 
-export {
-  // QueryEditor,
-  TestDataDatasource as Datasource,
-  TestDataQueryCtrl as QueryCtrl,
-  TestDataAnnotationsQueryCtrl as AnnotationsQueryCtrl,
-};
+export const plugin = new DataSourcePlugin(TestDataDatasource)
+  .setQueryCtrl(TestDataQueryCtrl)
+  .setAnnotationQueryCtrl(TestDataAnnotationsQueryCtrl);

+ 4 - 2
public/app/types/plugins.ts

@@ -1,4 +1,4 @@
-import { PluginExports, PluginMetaInfo } from '@grafana/ui/src/types';
+import { AngularPanelPlugin, ReactPanelPlugin, PluginMetaInfo } from '@grafana/ui/src/types';
 
 export interface PanelPlugin {
   id: string;
@@ -8,7 +8,9 @@ export interface PanelPlugin {
   baseUrl: string;
   info: PluginMetaInfo;
   sort: number;
-  exports?: PluginExports;
+  angularPlugin: AngularPanelPlugin | null;
+  reactPlugin: ReactPanelPlugin | null;
+  hasBeenImported?: boolean;
   dataFormats: PanelDataFormat[];
 }
 

+ 1 - 1
scripts/ci-frontend-metrics.sh

@@ -4,7 +4,7 @@ echo -e "Collecting code stats (typescript errors & more)"
 
 ERROR_COUNT_LIMIT=6843
 DIRECTIVES_LIMIT=173
-CONTROLLERS_LIMIT=136
+CONTROLLERS_LIMIT=137
 
 ERROR_COUNT="$(./node_modules/.bin/tsc --project tsconfig.json --noEmit --noImplicitAny true | grep -oP 'Found \K(\d+)')"
 DIRECTIVES="$(grep -r -o  directive public/app/**/*  | wc -l)"