Commit c5da1864 by Ryan McKinley Committed by GitHub

AppPlugin: remove simple app from the core repo (#21526)

parent f6130db0
...@@ -55,5 +55,5 @@ If possible a link to a dashboard or custom page should be shown after enabling ...@@ -55,5 +55,5 @@ If possible a link to a dashboard or custom page should be shown after enabling
> Our goal is not to have a very extensive documentation but rather have actual > Our goal is not to have a very extensive documentation but rather have actual
> code that people can look at. An example implementation of an app can be found > code that people can look at. An example implementation of an app can be found
> in this [example app repo](https://github.com/grafana/grafana/tree/master/public/app/plugins/app/example-app) > in this [example app repo](https://github.com/grafana/simple-app-plugin)
...@@ -54,8 +54,6 @@ import * as barGaugePanel from 'app/plugins/panel/bargauge/module'; ...@@ -54,8 +54,6 @@ import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
import * as logsPanel from 'app/plugins/panel/logs/module'; import * as logsPanel from 'app/plugins/panel/logs/module';
import * as newsPanel from 'app/plugins/panel/news/module'; import * as newsPanel from 'app/plugins/panel/news/module';
const exampleApp = async () => await import(/* webpackChunkName: "exampleApp" */ 'app/plugins/app/example-app/module');
const builtInPlugins: any = { const builtInPlugins: any = {
'app/plugins/datasource/graphite/module': graphitePlugin, 'app/plugins/datasource/graphite/module': graphitePlugin,
'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin, 'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin,
...@@ -94,8 +92,6 @@ const builtInPlugins: any = { ...@@ -94,8 +92,6 @@ const builtInPlugins: any = {
'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/panel/logs/module': logsPanel, 'app/plugins/panel/logs/module': logsPanel,
'app/plugins/app/example-app/module': exampleApp,
}; };
export default builtInPlugins; export default builtInPlugins;
// Use the real plugin_loader (stubbed by default)
jest.unmock('app/features/plugins/plugin_loader');
(global as any).ace = {
define: jest.fn(),
};
jest.mock('app/core/core', () => {
return {
coreModule: {
directive: jest.fn(),
},
};
});
import { SystemJS } from '@grafana/runtime';
import { AppPluginMeta, PluginMetaInfo, PluginType, PluginIncludeType, AppPlugin } from '@grafana/data';
import { importAppPlugin } from './plugin_loader';
class MyCustomApp extends AppPlugin {
initWasCalled = false;
calledTwice = false;
init(meta: AppPluginMeta) {
this.initWasCalled = true;
this.calledTwice = this.meta === meta;
}
}
describe('Load App', () => {
const app = new MyCustomApp();
const modulePath = 'my/custom/plugin/module';
beforeAll(() => {
SystemJS.set(modulePath, SystemJS.newModule({ plugin: app }));
});
afterAll(() => {
SystemJS.delete(modulePath);
});
it('should call init and set meta', async () => {
const meta: AppPluginMeta = {
id: 'test-app',
module: modulePath,
baseUrl: 'xxx',
info: {} as PluginMetaInfo,
type: PluginType.app,
name: 'test',
};
// Check that we mocked the import OK
const m = await SystemJS.import(modulePath);
expect(m.plugin).toBe(app);
const loaded = await importAppPlugin(meta);
expect(loaded).toBe(app);
expect(app.meta).toBe(meta);
expect(app.initWasCalled).toBeTruthy();
expect(app.calledTwice).toBeFalsy();
const again = await importAppPlugin(meta);
expect(again).toBe(app);
expect(app.calledTwice).toBeTruthy();
});
});
import { ExampleConfigCtrl as ConfigCtrl } from 'app/plugins/app/example-app/legacy/config';
import { AngularExamplePageCtrl } from 'app/plugins/app/example-app/legacy/angular_example_page';
describe('Load Legacy App', () => {
const app = {
ConfigCtrl,
AngularExamplePageCtrl, // Must match `pages.component` in plugin.json
};
const modulePath = 'my/custom/legacy/plugin/module';
beforeAll(() => {
SystemJS.set(modulePath, SystemJS.newModule(app));
});
afterAll(() => {
SystemJS.delete(modulePath);
});
it('should call init and set meta for legacy app', async () => {
const meta: AppPluginMeta = {
id: 'test-app',
module: modulePath,
baseUrl: 'xxx',
info: {} as PluginMetaInfo,
type: PluginType.app,
name: 'test',
includes: [
{
type: PluginIncludeType.page,
name: 'Example Page',
component: 'AngularExamplePageCtrl',
role: 'Viewer',
addToNav: false,
},
],
};
const loaded = await importAppPlugin(meta);
expect(loaded).toHaveProperty('angularPages');
expect(loaded.angularPages).toHaveProperty('AngularExamplePageCtrl', AngularExamplePageCtrl);
});
});
// Libraries
import React, { PureComponent } from 'react';
// Types
import { NavModelItem, AppRootProps } from '@grafana/data';
interface Props extends AppRootProps {}
const TAB_ID_A = 'A';
const TAB_ID_B = 'B';
const TAB_ID_C = 'C';
export class ExampleRootPage<ExampleAppSettings> extends PureComponent<Props> {
constructor(props: Props) {
super(props);
}
componentDidMount() {
this.updateNav();
}
componentDidUpdate(prevProps: Props) {
if (this.props.query !== prevProps.query) {
if (this.props.query.tab !== prevProps.query.tab) {
this.updateNav();
}
}
}
updateNav() {
const { path, onNavChanged, query, meta } = this.props;
const tabs: NavModelItem[] = [];
tabs.push({
text: 'Tab A',
icon: 'fa fa-fw fa-file-text-o',
url: path + '?tab=' + TAB_ID_A,
id: TAB_ID_A,
});
tabs.push({
text: 'Tab B',
icon: 'fa fa-fw fa-file-text-o',
url: path + '?tab=' + TAB_ID_B,
id: TAB_ID_B,
});
tabs.push({
text: 'Tab C',
icon: 'fa fa-fw fa-file-text-o',
url: path + '?tab=' + TAB_ID_C,
id: TAB_ID_C,
});
// Set the active tab
let found = false;
const selected = query.tab || TAB_ID_B;
for (const tab of tabs) {
tab.active = !found && selected === tab.id;
if (tab.active) {
found = true;
}
}
if (!found) {
tabs[0].active = true;
}
const node = {
text: 'This is the Page title',
img: meta.info.logos.large,
subTitle: 'subtitle here',
url: path,
children: tabs,
};
// Update the page header
onNavChanged({
node: node,
main: node,
});
}
render() {
const { path, query, meta } = this.props;
return (
<div>
QUERY: <pre>{JSON.stringify(query)}</pre>
<br />
<ul>
<li>
<a href={path + '?x=1'}>111</a>
</li>
<li>
<a href={path + '?x=AAA'}>AAA</a>
</li>
<li>
<a href={path + '?x=1&y=2&y=3'}>ZZZ</a>
</li>
</ul>
<pre>{JSON.stringify(meta.jsonData)}</pre>
</div>
);
}
}
# Example App - Native Plugin
This is an example app. It has no real use other than making sure external apps are supported.
// Libraries
import React, { PureComponent } from 'react';
// Types
import { PluginConfigPageProps, AppPluginMeta } from '@grafana/data';
import { ExampleAppSettings } from '../types';
interface Props extends PluginConfigPageProps<AppPluginMeta<ExampleAppSettings>> {}
export class ExamplePage1 extends PureComponent<Props> {
constructor(props: Props) {
super(props);
}
render() {
const { query } = this.props;
return (
<div>
11111111111111111111111111111111
<pre>{JSON.stringify(query)}</pre>
11111111111111111111111111111111
</div>
);
}
}
// Libraries
import React, { PureComponent } from 'react';
// Types
import { PluginConfigPageProps, AppPluginMeta } from '@grafana/data';
import { ExampleAppSettings } from '../types';
interface Props extends PluginConfigPageProps<AppPluginMeta<ExampleAppSettings>> {}
export class ExamplePage2 extends PureComponent<Props> {
constructor(props: Props) {
super(props);
}
render() {
const { query } = this.props;
return (
<div>
22222222222222222222222222222222
<pre>{JSON.stringify(query)}</pre>
22222222222222222222222222222222
</div>
);
}
}
{
"__inputs": [],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "6.2.0-pre"
},
{
"type": "panel",
"id": "singlestat2",
"name": "Singlestat (react)",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"gridPos": {
"h": 4,
"w": 24,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"orientation": "auto",
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "red",
"index": 1,
"value": 80
}
],
"valueMappings": [],
"valueOptions": {
"decimals": null,
"prefix": "",
"stat": "mean",
"suffix": "",
"unit": "none"
}
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "random_walk_table",
"stringInput": ""
},
{
"refId": "B",
"scenarioId": "random_walk_table",
"stringInput": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"type": "singlestat2"
}
],
"schemaVersion": 18,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
},
"timezone": "",
"title": "stats",
"uid": "YeBxHjzWz",
"version": 1
}
{
"__inputs": [],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "6.2.0-pre"
},
{
"type": "panel",
"id": "graph2",
"name": "React Graph",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"description": "",
"gridPos": {
"h": 6,
"w": 24,
"x": 0,
"y": 0
},
"id": 2,
"links": [],
"targets": [
{
"refId": "A",
"scenarioId": "streaming_client",
"stream": {
"noise": 10,
"speed": 100,
"spread": 20,
"type": "signal"
},
"stringInput": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Simple dummy streaming example",
"type": "graph2"
}
],
"schemaVersion": 18,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-1m",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
},
"timezone": "",
"title": "simple streaming",
"uid": "TbbEZjzWz",
"version": 1
}
<h3 class="page-heading">
Example Page
</h3>
<p>this is in angular</p>
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
export class AngularExamplePageCtrl {
static templateUrl = 'legacy/angular_example_page.html';
/** @ngInject */
constructor($scope: any, $rootScope: GrafanaRootScope) {
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/data';
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';
import { AppPlugin } from '@grafana/data';
import { ExamplePage1 } from './config/ExamplePage1';
import { ExamplePage2 } from './config/ExamplePage2';
import { ExampleRootPage } from './ExampleRootPage';
import { ExampleAppSettings } from './types';
// Legacy exports just for testing
export {
ExampleConfigCtrl as ConfigCtrl,
AngularExamplePageCtrl, // Must match `pages.component` in plugin.json
};
export const plugin = new AppPlugin<ExampleAppSettings>()
.setRootPage(ExampleRootPage)
.addConfigPage({
title: 'Page 1',
icon: 'fa fa-info',
body: ExamplePage1,
id: 'page1',
})
.addConfigPage({
title: 'Page 2',
icon: 'fa fa-user',
body: ExamplePage2,
id: 'page2',
});
{
"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
},
{
"type": "dashboard",
"name": "Streaming Example",
"path": "dashboards/streaming.json"
},
{
"type": "dashboard",
"name": "Lots of Stats",
"path": "dashboards/stats.json"
},
{
"type": "panel",
"name": "Anything -- just display?"
}
]
}
export interface ExampleAppSettings {
customText?: string;
customCheckbox?: boolean;
}
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