mirror of
https://github.com/grafana/grafana.git
synced 2025-01-08 15:13:30 -06:00
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
This commit is contained in:
parent
5f1b2691a3
commit
5a0cf1a83c
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
public/app/features/plugins/PluginSettingsCache.ts
Normal file
25
public/app/features/plugins/PluginSettingsCache.ts
Normal file
@ -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');
|
||||
});
|
||||
}
|
@ -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],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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
public/app/plugins/app/example-app/README.md
Normal file
4
public/app/plugins/app/example-app/README.md
Normal file
@ -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
Normal file
BIN
public/app/plugins/app/example-app/img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
<h3 class="page-heading">
|
||||
Example Page
|
||||
</h3>
|
||||
|
||||
<p>this is in angular</p>
|
||||
|
@ -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
public/app/plugins/app/example-app/legacy/config.html
Normal file
22
public/app/plugins/app/example-app/legacy/config.html
Normal file
@ -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
public/app/plugins/app/example-app/legacy/config.ts
Normal file
36
public/app/plugins/app/example-app/legacy/config.ts
Normal file
@ -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
public/app/plugins/app/example-app/module.ts
Normal file
9
public/app/plugins/app/example-app/module.ts
Normal file
@ -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
public/app/plugins/app/example-app/plugin.json
Normal file
28
public/app/plugins/app/example-app/plugin.json
Normal file
@ -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
|
||||
}
|
||||
]
|
||||
}
|
@ -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)"
|
||||
|
Loading…
Reference in New Issue
Block a user