فهرست منبع

AppPlugins: fix app support and add an alpha example (#16528)

* app pages

* app pages

* workign example

* started alpha support

* bump controller limit

* bump controller limit

* existing plugin pages work again

* save Plugin in cache

* remove AppPage wip
Ryan McKinley 6 سال پیش
والد
کامیت
5a0cf1a83c

+ 41 - 12
packages/grafana-ui/src/types/plugin.ts

@@ -12,14 +12,18 @@ export enum PluginType {
 export interface PluginMeta {
   id: string;
   name: string;
+  type: PluginType;
   info: PluginMetaInfo;
-  module: string;
   includes?: PluginInclude[];
-  baseUrl?: string;
+  state?: PluginState;
 
-  type: PluginType;
+  // System.load & relative URLS
+  module: string;
+  baseUrl: string;
+
+  // Filled in by the backend
+  jsonData?: { [str: string]: any };
   enabled?: boolean;
-  state?: PluginState;
 
   // Datasource-specific
   builtIn?: boolean;
@@ -51,7 +55,10 @@ export enum PluginIncludeType {
 export interface PluginInclude {
   type: PluginIncludeType;
   name: string;
-  path: string;
+  path?: string;
+
+  // Angular app pages
+  component?: string;
 }
 
 interface PluginMetaInfoLink {
@@ -76,16 +83,38 @@ export interface PluginMetaInfo {
 }
 
 export class AppPlugin {
-  components: {
+  meta: PluginMeta;
+
+  angular?: {
     ConfigCtrl?: any;
+    pages: { [component: string]: any };
   };
 
-  pages: { [str: string]: any };
-
-  constructor(ConfigCtrl: any) {
-    this.components = {
-      ConfigCtrl: ConfigCtrl,
+  constructor(meta: PluginMeta, pluginExports: any) {
+    this.meta = meta;
+    const legacy = {
+      ConfigCtrl: undefined,
+      pages: {} as any,
     };
-    this.pages = {};
+
+    if (pluginExports.ConfigCtrl) {
+      legacy.ConfigCtrl = pluginExports.ConfigCtrl;
+      this.angular = legacy;
+    }
+
+    if (meta.includes) {
+      for (const include of meta.includes) {
+        const { type, component } = include;
+        if (type === PluginIncludeType.page && component) {
+          const exp = pluginExports[component];
+          if (!exp) {
+            console.warn('App Page uses unknown component: ', component, meta);
+            continue;
+          }
+          legacy.pages[component] = exp;
+          this.angular = legacy;
+        }
+      }
+    }
   }
 }

+ 2 - 1
public/app/features/datasources/state/actions.ts

@@ -9,6 +9,7 @@ import { DataSourceSettings } from '@grafana/ui/src/types';
 import { Plugin, StoreState, LocationUpdate } from 'app/types';
 import { actionCreatorFactory } from 'app/core/redux';
 import { ActionOf, noPayloadActionCreatorFactory } from 'app/core/redux/actionCreatorFactory';
+import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache';
 
 export const dataSourceLoaded = actionCreatorFactory<DataSourceSettings>('LOAD_DATA_SOURCE').create();
 
@@ -50,7 +51,7 @@ export function loadDataSources(): ThunkResult<void> {
 export function loadDataSource(id: number): ThunkResult<void> {
   return async dispatch => {
     const dataSource = await getBackendSrv().get(`/api/datasources/${id}`);
-    const pluginInfo = await getBackendSrv().get(`/api/plugins/${dataSource.type}/settings`);
+    const pluginInfo = await getPluginSettings(dataSource.type);
     dispatch(dataSourceLoaded(dataSource));
     dispatch(dataSourceMetaLoaded(pluginInfo));
     dispatch(updateNavIndex(buildNavModel(dataSource, pluginInfo)));

+ 25 - 0
public/app/features/plugins/PluginSettingsCache.ts

@@ -0,0 +1,25 @@
+import { getBackendSrv } from 'app/core/services/backend_srv';
+import { Plugin } from 'app/types';
+
+type PluginCache = {
+  [key: string]: Plugin;
+};
+
+const pluginInfoCache: PluginCache = {};
+
+export function getPluginSettings(pluginId: string): Promise<Plugin> {
+  const v = pluginInfoCache[pluginId];
+  if (v) {
+    return Promise.resolve(v);
+  }
+  return getBackendSrv()
+    .get(`/api/plugins/${pluginId}/settings`)
+    .then(settings => {
+      pluginInfoCache[pluginId] = settings;
+      return settings;
+    })
+    .catch(err => {
+      // err.isHandled = true;
+      return Promise.reject('Unknown Plugin');
+    });
+}

+ 4 - 0
public/app/features/plugins/built_in_plugins.ts

@@ -32,6 +32,8 @@ import * as gaugePanel from 'app/plugins/panel/gauge/module';
 import * as pieChartPanel from 'app/plugins/panel/piechart/module';
 import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
 
+import * as exampleApp from 'app/plugins/app/example-app/module';
+
 const builtInPlugins = {
   'app/plugins/datasource/graphite/module': graphitePlugin,
   'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin,
@@ -66,6 +68,8 @@ const builtInPlugins = {
   'app/plugins/panel/gauge/module': gaugePanel,
   'app/plugins/panel/piechart/module': pieChartPanel,
   'app/plugins/panel/bargauge/module': barGaugePanel,
+
+  'app/plugins/app/example-app/module': exampleApp,
 };
 
 export default builtInPlugins;

+ 4 - 4
public/app/features/plugins/plugin_component.ts

@@ -155,26 +155,26 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
       // AppConfigCtrl
       case 'app-config-ctrl': {
         const model = scope.ctrl.model;
-        return importAppPlugin(model.module).then(appPlugin => {
+        return importAppPlugin(model).then(appPlugin => {
           return {
             baseUrl: model.baseUrl,
             name: 'app-config-' + model.id,
             bindings: { appModel: '=', appEditCtrl: '=' },
             attrs: { 'app-model': 'ctrl.model', 'app-edit-ctrl': 'ctrl' },
-            Component: appPlugin.components.ConfigCtrl,
+            Component: appPlugin.angular.ConfigCtrl,
           };
         });
       }
       // App Page
       case 'app-page': {
         const appModel = scope.ctrl.appModel;
-        return importAppPlugin(appModel.module).then(appPlugin => {
+        return importAppPlugin(appModel).then(appPlugin => {
           return {
             baseUrl: appModel.baseUrl,
             name: 'app-page-' + appModel.id + '-' + scope.ctrl.page.slug,
             bindings: { appModel: '=' },
             attrs: { 'app-model': 'ctrl.appModel' },
-            Component: appPlugin.pages[scope.ctrl.page.component],
+            Component: appPlugin.angular.pages[scope.ctrl.page.component],
           };
         });
       }

+ 2 - 1
public/app/features/plugins/plugin_edit_ctrl.ts

@@ -1,6 +1,7 @@
 import angular from 'angular';
 import _ from 'lodash';
 import Remarkable from 'remarkable';
+import { getPluginSettings } from './PluginSettingsCache';
 
 export class PluginEditCtrl {
   model: any;
@@ -77,7 +78,7 @@ export class PluginEditCtrl {
   }
 
   init() {
-    return this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(result => {
+    return getPluginSettings(this.pluginId).then(result => {
       this.model = result;
       this.pluginIcon = this.getPluginIcon(this.model.type);
 

+ 4 - 4
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, ReactPanelPlugin, AngularPanelPlugin } from '@grafana/ui/src/types';
+import { DataSourcePlugin, AppPlugin, ReactPanelPlugin, AngularPanelPlugin, PluginMeta } 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';
@@ -176,9 +176,9 @@ export function importDataSourcePlugin(path: string): Promise<DataSourcePlugin>
   });
 }
 
-export function importAppPlugin(path: string): Promise<AppPlugin> {
-  return importPluginModule(path).then(pluginExports => {
-    return new AppPlugin(pluginExports.ConfigCtrl);
+export function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
+  return importPluginModule(meta.module).then(pluginExports => {
+    return new AppPlugin(meta, pluginExports);
   });
 }
 

+ 17 - 17
public/app/features/plugins/plugin_page_ctrl.ts

@@ -1,7 +1,8 @@
 import angular from 'angular';
 import _ from 'lodash';
 
-const pluginInfoCache = {};
+import { getPluginSettings } from './PluginSettingsCache';
+import { PluginMeta } from '@grafana/ui';
 
 export class AppPageCtrl {
   page: any;
@@ -10,25 +11,30 @@ export class AppPageCtrl {
   navModel: any;
 
   /** @ngInject */
-  constructor(private backendSrv, private $routeParams: any, private $rootScope, private navModelSrv) {
+  constructor(private $routeParams: any, private $rootScope, private navModelSrv) {
     this.pluginId = $routeParams.pluginId;
 
-    if (pluginInfoCache[this.pluginId]) {
-      this.initPage(pluginInfoCache[this.pluginId]);
-    } else {
-      this.loadPluginInfo();
-    }
+    getPluginSettings(this.pluginId)
+      .then(settings => {
+        this.initPage(settings);
+      })
+      .catch(err => {
+        this.$rootScope.appEvent('alert-error', ['Unknown Plugin', '']);
+        this.navModel = this.navModelSrv.getNotFoundNav();
+      });
   }
 
-  initPage(app) {
+  initPage(app: PluginMeta) {
     this.appModel = app;
     this.page = _.find(app.includes, { slug: this.$routeParams.slug });
 
-    pluginInfoCache[this.pluginId] = app;
-
     if (!this.page) {
       this.$rootScope.appEvent('alert-error', ['App Page Not Found', '']);
-
+      this.navModel = this.navModelSrv.getNotFoundNav();
+      return;
+    }
+    if (app.type !== 'app' || !app.enabled) {
+      this.$rootScope.appEvent('alert-error', ['Applicaiton Not Enabled', '']);
       this.navModel = this.navModelSrv.getNotFoundNav();
       return;
     }
@@ -45,12 +51,6 @@ export class AppPageCtrl {
       },
     };
   }
-
-  loadPluginInfo() {
-    this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(app => {
-      this.initPage(app);
-    });
-  }
 }
 
 angular.module('grafana.controllers').controller('AppPageCtrl', AppPageCtrl);

+ 4 - 0
public/app/plugins/app/example-app/README.md

@@ -0,0 +1,4 @@
+# Example App -  Native Plugin
+
+This is an example app.  It has no real use other than making sure external apps are supported.
+

BIN
public/app/plugins/app/example-app/img/logo.png


+ 8 - 0
public/app/plugins/app/example-app/legacy/angular_example_page.html

@@ -0,0 +1,8 @@
+
+
+<h3 class="page-heading">
+	Example Page
+</h3>
+
+<p>this is in angular</p>
+

+ 8 - 0
public/app/plugins/app/example-app/legacy/angular_example_page.ts

@@ -0,0 +1,8 @@
+export class AngularExamplePageCtrl {
+  static templateUrl = 'legacy/angular_example_page.html';
+
+  /** @ngInject */
+  constructor($scope: any, $rootScope: any) {
+    console.log('AngularExamplePageCtrl:', this);
+  }
+}

+ 22 - 0
public/app/plugins/app/example-app/legacy/config.html

@@ -0,0 +1,22 @@
+<h2>Example Application</h2>
+
+<p>
+Angular based config:
+</p>
+
+<div class="gf-form">
+  <div class="gf-form-group">
+    <div class="gf-form-inline">
+      <div class="gf-form">
+        <span class="gf-form-label">json Data property</span>
+        <input type="text" class="gf-form-input" ng-model="ctrl.appModel.jsonData.customText" >
+      </div>
+      <div class="gf-form">
+        <gf-form-checkbox class="gf-form" 
+          label="Custom Value" 
+          checked="ctrl.appModel.jsonData.customCheckbox" 
+          switch-class="max-width-6"></gf-form-checkbox>
+      </div>
+    </div>
+  </div>
+</div>

+ 36 - 0
public/app/plugins/app/example-app/legacy/config.ts

@@ -0,0 +1,36 @@
+import { PluginMeta } from '@grafana/ui';
+
+export class ExampleConfigCtrl {
+  static templateUrl = 'legacy/config.html';
+
+  appEditCtrl: any;
+  appModel: PluginMeta;
+
+  /** @ngInject */
+  constructor($scope: any, $injector: any) {
+    this.appEditCtrl.setPostUpdateHook(this.postUpdate.bind(this));
+
+    // Make sure it has a JSON Data spot
+    if (!this.appModel) {
+      this.appModel = {} as PluginMeta;
+    }
+
+    // Required until we get the types sorted on appModel :(
+    const appModel = this.appModel as any;
+    if (!appModel.jsonData) {
+      appModel.jsonData = {};
+    }
+
+    console.log('ExampleConfigCtrl', this);
+  }
+
+  postUpdate() {
+    if (!this.appModel.enabled) {
+      console.log('Not enabled...');
+      return;
+    }
+
+    // TODO, can do stuff after update
+    console.log('Post Update:', this);
+  }
+}

+ 9 - 0
public/app/plugins/app/example-app/module.ts

@@ -0,0 +1,9 @@
+// Angular pages
+import { ExampleConfigCtrl } from './legacy/config';
+import { AngularExamplePageCtrl } from './legacy/angular_example_page';
+
+export {
+  ExampleConfigCtrl as ConfigCtrl,
+  // Must match `pages.component` in plugin.json
+  AngularExamplePageCtrl,
+};

+ 28 - 0
public/app/plugins/app/example-app/plugin.json

@@ -0,0 +1,28 @@
+{
+  "type": "app",
+  "name": "Example App",
+  "id": "example-app",
+  "state": "alpha",
+
+  "info": {
+    "author": {
+      "name": "Grafana Project",
+      "url": "https://grafana.com"
+    },
+    "logos": {
+      "small": "img/logo.png",
+      "large": "img/logo.png"
+    }
+  },
+
+  "includes": [
+    {
+      "type": "page",
+      "name": "Angular Page",
+      "component": "AngularExamplePageCtrl",
+      "role": "Viewer",
+      "addToNav": true,
+      "defaultNav": true
+    }
+  ]
+}

+ 3 - 0
public/app/types/plugins.ts

@@ -16,6 +16,9 @@ export enum PanelDataFormat {
   TimeSeries = 'time_series',
 }
 
+/**
+ * Values we don't want in the public API
+ */
 export interface Plugin extends PluginMeta {
   defaultNavUrl: string;
   hasUpdate: boolean;

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

@@ -4,7 +4,7 @@ echo -e "Collecting code stats (typescript errors & more)"
 
 ERROR_COUNT_LIMIT=5977
 DIRECTIVES_LIMIT=175
-CONTROLLERS_LIMIT=138
+CONTROLLERS_LIMIT=140
 
 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)"