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 {
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;
}
}
}
}
}
......@@ -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)));
......
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';
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;
......@@ -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],
};
});
}
......
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);
......
......@@ -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);
});
}
......
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);
# 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 {
TimeSeries = 'time_series',
}
/**
* Values we don't want in the public API
*/
export interface Plugin extends PluginMeta {
defaultNavUrl: string;
hasUpdate: boolean;
......
......@@ -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)"
......
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