Commit 5a0cf1a8 by Ryan McKinley Committed by Torkel Ödegaard

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
parent 5f1b2691
...@@ -12,14 +12,18 @@ export enum PluginType { ...@@ -12,14 +12,18 @@ export enum PluginType {
export interface PluginMeta { export interface PluginMeta {
id: string; id: string;
name: string; name: string;
type: PluginType;
info: PluginMetaInfo; info: PluginMetaInfo;
module: string;
includes?: PluginInclude[]; 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; enabled?: boolean;
state?: PluginState;
// Datasource-specific // Datasource-specific
builtIn?: boolean; builtIn?: boolean;
...@@ -51,7 +55,10 @@ export enum PluginIncludeType { ...@@ -51,7 +55,10 @@ export enum PluginIncludeType {
export interface PluginInclude { export interface PluginInclude {
type: PluginIncludeType; type: PluginIncludeType;
name: string; name: string;
path: string; path?: string;
// Angular app pages
component?: string;
} }
interface PluginMetaInfoLink { interface PluginMetaInfoLink {
...@@ -76,16 +83,38 @@ export interface PluginMetaInfo { ...@@ -76,16 +83,38 @@ export interface PluginMetaInfo {
} }
export class AppPlugin { export class AppPlugin {
components: { meta: PluginMeta;
angular?: {
ConfigCtrl?: any; ConfigCtrl?: any;
pages: { [component: string]: any };
}; };
pages: { [str: string]: any }; constructor(meta: PluginMeta, pluginExports: any) {
this.meta = meta;
constructor(ConfigCtrl: any) { const legacy = {
this.components = { ConfigCtrl: undefined,
ConfigCtrl: ConfigCtrl, 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;
}
}
}
} }
} }
...@@ -9,6 +9,7 @@ import { DataSourceSettings } from '@grafana/ui/src/types'; ...@@ -9,6 +9,7 @@ import { DataSourceSettings } from '@grafana/ui/src/types';
import { Plugin, StoreState, LocationUpdate } from 'app/types'; import { Plugin, StoreState, LocationUpdate } from 'app/types';
import { actionCreatorFactory } from 'app/core/redux'; import { actionCreatorFactory } from 'app/core/redux';
import { ActionOf, noPayloadActionCreatorFactory } from 'app/core/redux/actionCreatorFactory'; import { ActionOf, noPayloadActionCreatorFactory } from 'app/core/redux/actionCreatorFactory';
import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache';
export const dataSourceLoaded = actionCreatorFactory<DataSourceSettings>('LOAD_DATA_SOURCE').create(); export const dataSourceLoaded = actionCreatorFactory<DataSourceSettings>('LOAD_DATA_SOURCE').create();
...@@ -50,7 +51,7 @@ export function loadDataSources(): ThunkResult<void> { ...@@ -50,7 +51,7 @@ export function loadDataSources(): ThunkResult<void> {
export function loadDataSource(id: number): ThunkResult<void> { export function loadDataSource(id: number): ThunkResult<void> {
return async dispatch => { return async dispatch => {
const dataSource = await getBackendSrv().get(`/api/datasources/${id}`); 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(dataSourceLoaded(dataSource));
dispatch(dataSourceMetaLoaded(pluginInfo)); dispatch(dataSourceMetaLoaded(pluginInfo));
dispatch(updateNavIndex(buildNavModel(dataSource, pluginInfo))); dispatch(updateNavIndex(buildNavModel(dataSource, pluginInfo)));
......
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');
});
}
...@@ -32,6 +32,8 @@ import * as gaugePanel from 'app/plugins/panel/gauge/module'; ...@@ -32,6 +32,8 @@ import * as gaugePanel from 'app/plugins/panel/gauge/module';
import * as pieChartPanel from 'app/plugins/panel/piechart/module'; import * as pieChartPanel from 'app/plugins/panel/piechart/module';
import * as barGaugePanel from 'app/plugins/panel/bargauge/module'; import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
import * as exampleApp from 'app/plugins/app/example-app/module';
const builtInPlugins = { const builtInPlugins = {
'app/plugins/datasource/graphite/module': graphitePlugin, 'app/plugins/datasource/graphite/module': graphitePlugin,
'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin, 'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin,
...@@ -66,6 +68,8 @@ const builtInPlugins = { ...@@ -66,6 +68,8 @@ const builtInPlugins = {
'app/plugins/panel/gauge/module': gaugePanel, 'app/plugins/panel/gauge/module': gaugePanel,
'app/plugins/panel/piechart/module': pieChartPanel, 'app/plugins/panel/piechart/module': pieChartPanel,
'app/plugins/panel/bargauge/module': barGaugePanel, 'app/plugins/panel/bargauge/module': barGaugePanel,
'app/plugins/app/example-app/module': exampleApp,
}; };
export default builtInPlugins; export default builtInPlugins;
...@@ -155,26 +155,26 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ ...@@ -155,26 +155,26 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
// AppConfigCtrl // AppConfigCtrl
case 'app-config-ctrl': { case 'app-config-ctrl': {
const model = scope.ctrl.model; const model = scope.ctrl.model;
return importAppPlugin(model.module).then(appPlugin => { return importAppPlugin(model).then(appPlugin => {
return { return {
baseUrl: model.baseUrl, baseUrl: model.baseUrl,
name: 'app-config-' + model.id, name: 'app-config-' + model.id,
bindings: { appModel: '=', appEditCtrl: '=' }, bindings: { appModel: '=', appEditCtrl: '=' },
attrs: { 'app-model': 'ctrl.model', 'app-edit-ctrl': 'ctrl' }, attrs: { 'app-model': 'ctrl.model', 'app-edit-ctrl': 'ctrl' },
Component: appPlugin.components.ConfigCtrl, Component: appPlugin.angular.ConfigCtrl,
}; };
}); });
} }
// App Page // App Page
case 'app-page': { case 'app-page': {
const appModel = scope.ctrl.appModel; const appModel = scope.ctrl.appModel;
return importAppPlugin(appModel.module).then(appPlugin => { return importAppPlugin(appModel).then(appPlugin => {
return { return {
baseUrl: appModel.baseUrl, baseUrl: appModel.baseUrl,
name: 'app-page-' + appModel.id + '-' + scope.ctrl.page.slug, name: 'app-page-' + appModel.id + '-' + scope.ctrl.page.slug,
bindings: { appModel: '=' }, bindings: { appModel: '=' },
attrs: { 'app-model': 'ctrl.appModel' }, attrs: { 'app-model': 'ctrl.appModel' },
Component: appPlugin.pages[scope.ctrl.page.component], Component: appPlugin.angular.pages[scope.ctrl.page.component],
}; };
}); });
} }
......
import angular from 'angular'; import angular from 'angular';
import _ from 'lodash'; import _ from 'lodash';
import Remarkable from 'remarkable'; import Remarkable from 'remarkable';
import { getPluginSettings } from './PluginSettingsCache';
export class PluginEditCtrl { export class PluginEditCtrl {
model: any; model: any;
...@@ -77,7 +78,7 @@ export class PluginEditCtrl { ...@@ -77,7 +78,7 @@ export class PluginEditCtrl {
} }
init() { init() {
return this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(result => { return getPluginSettings(this.pluginId).then(result => {
this.model = result; this.model = result;
this.pluginIcon = this.getPluginIcon(this.model.type); this.pluginIcon = this.getPluginIcon(this.model.type);
......
...@@ -18,7 +18,7 @@ import config from 'app/core/config'; ...@@ -18,7 +18,7 @@ import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';
import TableModel from 'app/core/table_model'; import TableModel from 'app/core/table_model';
import { coreModule, appEvents, contextSrv } from 'app/core/core'; 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 datemath from 'app/core/utils/datemath';
import * as fileExport from 'app/core/utils/file_export'; import * as fileExport from 'app/core/utils/file_export';
import * as flatten from 'app/core/utils/flatten'; import * as flatten from 'app/core/utils/flatten';
...@@ -176,9 +176,9 @@ export function importDataSourcePlugin(path: string): Promise<DataSourcePlugin> ...@@ -176,9 +176,9 @@ export function importDataSourcePlugin(path: string): Promise<DataSourcePlugin>
}); });
} }
export function importAppPlugin(path: string): Promise<AppPlugin> { export function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
return importPluginModule(path).then(pluginExports => { return importPluginModule(meta.module).then(pluginExports => {
return new AppPlugin(pluginExports.ConfigCtrl); return new AppPlugin(meta, pluginExports);
}); });
} }
......
import angular from 'angular'; import angular from 'angular';
import _ from 'lodash'; import _ from 'lodash';
const pluginInfoCache = {}; import { getPluginSettings } from './PluginSettingsCache';
import { PluginMeta } from '@grafana/ui';
export class AppPageCtrl { export class AppPageCtrl {
page: any; page: any;
...@@ -10,25 +11,30 @@ export class AppPageCtrl { ...@@ -10,25 +11,30 @@ export class AppPageCtrl {
navModel: any; navModel: any;
/** @ngInject */ /** @ngInject */
constructor(private backendSrv, private $routeParams: any, private $rootScope, private navModelSrv) { constructor(private $routeParams: any, private $rootScope, private navModelSrv) {
this.pluginId = $routeParams.pluginId; this.pluginId = $routeParams.pluginId;
if (pluginInfoCache[this.pluginId]) { getPluginSettings(this.pluginId)
this.initPage(pluginInfoCache[this.pluginId]); .then(settings => {
} else { this.initPage(settings);
this.loadPluginInfo(); })
} .catch(err => {
this.$rootScope.appEvent('alert-error', ['Unknown Plugin', '']);
this.navModel = this.navModelSrv.getNotFoundNav();
});
} }
initPage(app) { initPage(app: PluginMeta) {
this.appModel = app; this.appModel = app;
this.page = _.find(app.includes, { slug: this.$routeParams.slug }); this.page = _.find(app.includes, { slug: this.$routeParams.slug });
pluginInfoCache[this.pluginId] = app;
if (!this.page) { if (!this.page) {
this.$rootScope.appEvent('alert-error', ['App Page Not Found', '']); 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(); this.navModel = this.navModelSrv.getNotFoundNav();
return; return;
} }
...@@ -45,12 +51,6 @@ export class AppPageCtrl { ...@@ -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); angular.module('grafana.controllers').controller('AppPageCtrl', AppPageCtrl);
# Example App - Native Plugin
This is an example app. It has no real use other than making sure external apps are supported.
<h3 class="page-heading">
Example Page
</h3>
<p>this is in angular</p>
export class AngularExamplePageCtrl {
static templateUrl = 'legacy/angular_example_page.html';
/** @ngInject */
constructor($scope: any, $rootScope: any) {
console.log('AngularExamplePageCtrl:', this);
}
}
<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>
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);
}
}
// 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,
};
{
"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
}
]
}
...@@ -16,6 +16,9 @@ export enum PanelDataFormat { ...@@ -16,6 +16,9 @@ export enum PanelDataFormat {
TimeSeries = 'time_series', TimeSeries = 'time_series',
} }
/**
* Values we don't want in the public API
*/
export interface Plugin extends PluginMeta { export interface Plugin extends PluginMeta {
defaultNavUrl: string; defaultNavUrl: string;
hasUpdate: boolean; hasUpdate: boolean;
......
...@@ -4,7 +4,7 @@ echo -e "Collecting code stats (typescript errors & more)" ...@@ -4,7 +4,7 @@ echo -e "Collecting code stats (typescript errors & more)"
ERROR_COUNT_LIMIT=5977 ERROR_COUNT_LIMIT=5977
DIRECTIVES_LIMIT=175 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+)')" 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)" DIRECTIVES="$(grep -r -o directive public/app/**/* | wc -l)"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment