diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index 05df2734299..625f6bc75c0 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -12,15 +12,19 @@ export enum PluginType { export interface PluginMeta { id: string; name: string; - info: PluginMetaInfo; - module: string; - includes?: PluginInclude[]; - baseUrl?: string; - type: PluginType; - enabled?: boolean; + info: PluginMetaInfo; + includes?: PluginInclude[]; state?: PluginState; + // System.load & relative URLS + module: string; + baseUrl: string; + + // Filled in by the backend + jsonData?: { [str: string]: any }; + enabled?: boolean; + // Datasource-specific builtIn?: boolean; metrics?: 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; + } + } + } } } diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 7d9c95ecc5f..a329a781baf 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/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('LOAD_DATA_SOURCE').create(); @@ -50,7 +51,7 @@ export function loadDataSources(): ThunkResult { export function loadDataSource(id: number): ThunkResult { 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))); diff --git a/public/app/features/plugins/PluginSettingsCache.ts b/public/app/features/plugins/PluginSettingsCache.ts new file mode 100644 index 00000000000..21161c5cacf --- /dev/null +++ b/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 { + 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'); + }); +} diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 6ae93f5ec3e..4429085fcff 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/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; diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index f208de5a9f8..690d423197a 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/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], }; }); } diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts index 177986cb331..61faae07690 100644 --- a/public/app/features/plugins/plugin_edit_ctrl.ts +++ b/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); diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index a59fdb3caeb..4d96bef2f39 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/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 }); } -export function importAppPlugin(path: string): Promise { - return importPluginModule(path).then(pluginExports => { - return new AppPlugin(pluginExports.ConfigCtrl); +export function importAppPlugin(meta: PluginMeta): Promise { + return importPluginModule(meta.module).then(pluginExports => { + return new AppPlugin(meta, pluginExports); }); } diff --git a/public/app/features/plugins/plugin_page_ctrl.ts b/public/app/features/plugins/plugin_page_ctrl.ts index 8ea099e1905..d07de832e24 100644 --- a/public/app/features/plugins/plugin_page_ctrl.ts +++ b/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); diff --git a/public/app/plugins/app/example-app/README.md b/public/app/plugins/app/example-app/README.md new file mode 100644 index 00000000000..8dc5db52a34 --- /dev/null +++ b/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. + diff --git a/public/app/plugins/app/example-app/img/logo.png b/public/app/plugins/app/example-app/img/logo.png new file mode 100644 index 00000000000..46099d58095 Binary files /dev/null and b/public/app/plugins/app/example-app/img/logo.png differ diff --git a/public/app/plugins/app/example-app/legacy/angular_example_page.html b/public/app/plugins/app/example-app/legacy/angular_example_page.html new file mode 100644 index 00000000000..850b1d22087 --- /dev/null +++ b/public/app/plugins/app/example-app/legacy/angular_example_page.html @@ -0,0 +1,8 @@ + + +

+ Example Page +

+ +

this is in angular

+ diff --git a/public/app/plugins/app/example-app/legacy/angular_example_page.ts b/public/app/plugins/app/example-app/legacy/angular_example_page.ts new file mode 100644 index 00000000000..b5ebe173ad2 --- /dev/null +++ b/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); + } +} diff --git a/public/app/plugins/app/example-app/legacy/config.html b/public/app/plugins/app/example-app/legacy/config.html new file mode 100644 index 00000000000..162fc16a74c --- /dev/null +++ b/public/app/plugins/app/example-app/legacy/config.html @@ -0,0 +1,22 @@ +

Example Application

+ +

+Angular based config: +

+ +
+
+
+
+ json Data property + +
+
+ +
+
+
+
diff --git a/public/app/plugins/app/example-app/legacy/config.ts b/public/app/plugins/app/example-app/legacy/config.ts new file mode 100644 index 00000000000..3892d61c14c --- /dev/null +++ b/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); + } +} diff --git a/public/app/plugins/app/example-app/module.ts b/public/app/plugins/app/example-app/module.ts new file mode 100644 index 00000000000..1e2a393d17a --- /dev/null +++ b/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, +}; diff --git a/public/app/plugins/app/example-app/plugin.json b/public/app/plugins/app/example-app/plugin.json new file mode 100644 index 00000000000..8864468768f --- /dev/null +++ b/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 + } + ] +} diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index f6fbf830fd3..ce71bf0243a 100644 --- a/public/app/types/plugins.ts +++ b/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; diff --git a/scripts/ci-frontend-metrics.sh b/scripts/ci-frontend-metrics.sh index 007f5e64486..70960b10abf 100755 --- a/scripts/ci-frontend-metrics.sh +++ b/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)"