mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
App Plugins: support react pages and tabs (#16586)
This commit is contained in:
parent
31ea0122a0
commit
013f1b8d19
62
packages/grafana-ui/src/types/app.ts
Normal file
62
packages/grafana-ui/src/types/app.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { ComponentClass } from 'react';
|
||||||
|
import { NavModel } from './navModel';
|
||||||
|
import { PluginMeta, PluginIncludeType, GrafanaPlugin } from './plugin';
|
||||||
|
|
||||||
|
export interface AppRootProps {
|
||||||
|
meta: AppPluginMeta;
|
||||||
|
|
||||||
|
path: string; // The URL path to this page
|
||||||
|
query: { [s: string]: any }; // The URL query parameters
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass the nav model to the container... is there a better way?
|
||||||
|
*/
|
||||||
|
onNavChanged: (nav: NavModel) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppPluginMeta extends PluginMeta {
|
||||||
|
// TODO anything specific to apps?
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AppPlugin extends GrafanaPlugin<AppPluginMeta> {
|
||||||
|
// Content under: /a/${plugin-id}/*
|
||||||
|
root?: ComponentClass<AppRootProps>;
|
||||||
|
rootNav?: NavModel; // Initial navigation model
|
||||||
|
|
||||||
|
// Old style pages
|
||||||
|
angularPages?: { [component: string]: any };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the component displayed under:
|
||||||
|
* /a/${plugin-id}/*
|
||||||
|
*/
|
||||||
|
setRootPage(root: ComponentClass<AppRootProps>, rootNav?: NavModel) {
|
||||||
|
this.root = root;
|
||||||
|
this.rootNav = rootNav;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setComponentsFromLegacyExports(pluginExports: any) {
|
||||||
|
if (pluginExports.ConfigCtrl) {
|
||||||
|
this.angularConfigCtrl = pluginExports.ConfigCtrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { meta } = this;
|
||||||
|
if (meta && 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;
|
||||||
|
}
|
||||||
|
if (!this.angularPages) {
|
||||||
|
this.angularPages = {};
|
||||||
|
}
|
||||||
|
this.angularPages[component] = exp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,11 +25,6 @@ export class DataSourcePlugin<TOptions = {}, TQuery extends DataQuery = DataQuer
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfigCtrl(ConfigCtrl: any) {
|
|
||||||
this.components.ConfigCtrl = ConfigCtrl;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
setQueryCtrl(QueryCtrl: any) {
|
setQueryCtrl(QueryCtrl: any) {
|
||||||
this.components.QueryCtrl = QueryCtrl;
|
this.components.QueryCtrl = QueryCtrl;
|
||||||
return this;
|
return this;
|
||||||
@ -60,14 +55,15 @@ export class DataSourcePlugin<TOptions = {}, TQuery extends DataQuery = DataQuer
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
setComponentsFromLegacyExports(exports: any) {
|
setComponentsFromLegacyExports(pluginExports: any) {
|
||||||
this.components.ConfigCtrl = exports.ConfigCtrl;
|
this.angularConfigCtrl = pluginExports.ConfigCtrl;
|
||||||
this.components.QueryCtrl = exports.QueryCtrl;
|
|
||||||
this.components.AnnotationsQueryCtrl = exports.AnnotationsQueryCtrl;
|
this.components.QueryCtrl = pluginExports.QueryCtrl;
|
||||||
this.components.ExploreQueryField = exports.ExploreQueryField;
|
this.components.AnnotationsQueryCtrl = pluginExports.AnnotationsQueryCtrl;
|
||||||
this.components.ExploreStartPage = exports.ExploreStartPage;
|
this.components.ExploreQueryField = pluginExports.ExploreQueryField;
|
||||||
this.components.QueryEditor = exports.QueryEditor;
|
this.components.ExploreStartPage = pluginExports.ExploreStartPage;
|
||||||
this.components.VariableQueryEditor = exports.VariableQueryEditor;
|
this.components.QueryEditor = pluginExports.QueryEditor;
|
||||||
|
this.components.VariableQueryEditor = pluginExports.VariableQueryEditor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +87,6 @@ interface PluginMetaQueryOptions {
|
|||||||
|
|
||||||
export interface DataSourcePluginComponents<TOptions = {}, TQuery extends DataQuery = DataQuery> {
|
export interface DataSourcePluginComponents<TOptions = {}, TQuery extends DataQuery = DataQuery> {
|
||||||
QueryCtrl?: any;
|
QueryCtrl?: any;
|
||||||
ConfigCtrl?: any;
|
|
||||||
AnnotationsQueryCtrl?: any;
|
AnnotationsQueryCtrl?: any;
|
||||||
VariableQueryEditor?: any;
|
VariableQueryEditor?: any;
|
||||||
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi, TQuery>>;
|
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi, TQuery>>;
|
||||||
|
@ -2,6 +2,7 @@ export * from './data';
|
|||||||
export * from './time';
|
export * from './time';
|
||||||
export * from './panel';
|
export * from './panel';
|
||||||
export * from './plugin';
|
export * from './plugin';
|
||||||
|
export * from './app';
|
||||||
export * from './datasource';
|
export * from './datasource';
|
||||||
export * from './theme';
|
export * from './theme';
|
||||||
export * from './graph';
|
export * from './graph';
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { ComponentClass } from 'react';
|
||||||
|
|
||||||
export enum PluginState {
|
export enum PluginState {
|
||||||
alpha = 'alpha', // Only included it `enable_alpha` is true
|
alpha = 'alpha', // Only included it `enable_alpha` is true
|
||||||
beta = 'beta', // Will show a warning banner
|
beta = 'beta', // Will show a warning banner
|
||||||
@ -21,8 +23,12 @@ export interface PluginMeta {
|
|||||||
module: string;
|
module: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
|
||||||
|
// Define plugin requirements
|
||||||
|
dependencies?: PluginDependencies;
|
||||||
|
|
||||||
// Filled in by the backend
|
// Filled in by the backend
|
||||||
jsonData?: { [str: string]: any };
|
jsonData?: { [str: string]: any };
|
||||||
|
secureJsonData?: { [str: string]: any };
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
defaultNavUrl?: string;
|
defaultNavUrl?: string;
|
||||||
hasUpdate?: boolean;
|
hasUpdate?: boolean;
|
||||||
@ -30,6 +36,18 @@ export interface PluginMeta {
|
|||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PluginDependencyInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
type: PluginType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginDependencies {
|
||||||
|
grafanaVersion: string;
|
||||||
|
plugins: PluginDependencyInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
export enum PluginIncludeType {
|
export enum PluginIncludeType {
|
||||||
dashboard = 'dashboard',
|
dashboard = 'dashboard',
|
||||||
page = 'page',
|
page = 'page',
|
||||||
@ -44,6 +62,10 @@ export interface PluginInclude {
|
|||||||
name: string;
|
name: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
|
||||||
|
role?: string; // "Viewer", Admin, editor???
|
||||||
|
addToNav?: boolean; // Show in the sidebar... only if type=page?
|
||||||
|
|
||||||
// Angular app pages
|
// Angular app pages
|
||||||
component?: string;
|
component?: string;
|
||||||
}
|
}
|
||||||
@ -69,44 +91,35 @@ export interface PluginMetaInfo {
|
|||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GrafanaPlugin<T extends PluginMeta> {
|
export interface PluginConfigTabProps<T extends PluginMeta> {
|
||||||
|
meta: T;
|
||||||
|
query: { [s: string]: any }; // The URL query parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginConfigTab<T extends PluginMeta> {
|
||||||
|
title: string; // Display
|
||||||
|
icon?: string;
|
||||||
|
id: string; // Unique, in URL
|
||||||
|
|
||||||
|
body: ComponentClass<PluginConfigTabProps<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GrafanaPlugin<T extends PluginMeta = PluginMeta> {
|
||||||
// Meta is filled in by the plugin loading system
|
// Meta is filled in by the plugin loading system
|
||||||
meta?: T;
|
meta?: T;
|
||||||
|
|
||||||
// Soon this will also include common config options
|
// Config control (app/datasource)
|
||||||
}
|
angularConfigCtrl?: any;
|
||||||
|
|
||||||
export class AppPlugin extends GrafanaPlugin<PluginMeta> {
|
// Show configuration tabs on the plugin page
|
||||||
angular?: {
|
configTabs?: Array<PluginConfigTab<T>>;
|
||||||
ConfigCtrl?: any;
|
|
||||||
pages: { [component: string]: any };
|
|
||||||
};
|
|
||||||
|
|
||||||
setComponentsFromLegacyExports(pluginExports: any) {
|
// Tabs on the plugin page
|
||||||
const legacy = {
|
addConfigTab(tab: PluginConfigTab<T>) {
|
||||||
ConfigCtrl: undefined,
|
if (!this.configTabs) {
|
||||||
pages: {} as any,
|
this.configTabs = [];
|
||||||
};
|
|
||||||
|
|
||||||
if (pluginExports.ConfigCtrl) {
|
|
||||||
legacy.ConfigCtrl = pluginExports.ConfigCtrl;
|
|
||||||
this.angular = legacy;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { meta } = this;
|
|
||||||
if (meta && 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.configTabs.push(tab);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,8 +59,10 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
r.Get("/styleguide", reqSignedIn, hs.Index)
|
r.Get("/styleguide", reqSignedIn, hs.Index)
|
||||||
|
|
||||||
r.Get("/plugins", reqSignedIn, hs.Index)
|
r.Get("/plugins", reqSignedIn, hs.Index)
|
||||||
r.Get("/plugins/:id/edit", reqSignedIn, hs.Index)
|
r.Get("/plugins/:id/", reqSignedIn, hs.Index)
|
||||||
|
r.Get("/plugins/:id/edit", reqSignedIn, hs.Index) // deprecated
|
||||||
r.Get("/plugins/:id/page/:page", reqSignedIn, hs.Index)
|
r.Get("/plugins/:id/page/:page", reqSignedIn, hs.Index)
|
||||||
|
r.Get("/a/:id/*", reqSignedIn, hs.Index) // App Root Page
|
||||||
|
|
||||||
r.Get("/d/:uid/:slug", reqSignedIn, hs.Index)
|
r.Get("/d/:uid/:slug", reqSignedIn, hs.Index)
|
||||||
r.Get("/d/:uid", reqSignedIn, hs.Index)
|
r.Get("/d/:uid", reqSignedIn, hs.Index)
|
||||||
|
@ -60,7 +60,7 @@ func (hs *HTTPServer) GetPluginList(c *m.ReqContext) Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if listItem.DefaultNavUrl == "" || !listItem.Enabled {
|
if listItem.DefaultNavUrl == "" || !listItem.Enabled {
|
||||||
listItem.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + listItem.Id + "/edit"
|
listItem.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + listItem.Id + "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter out disabled
|
// filter out disabled
|
||||||
|
@ -49,18 +49,25 @@ export class NavModelSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getNotFoundNav() {
|
getNotFoundNav() {
|
||||||
const node = {
|
return getNotFoundNav(); // the exported function
|
||||||
text: 'Page not found',
|
}
|
||||||
icon: 'fa fa-fw fa-warning',
|
}
|
||||||
subTitle: '404 Error',
|
|
||||||
};
|
|
||||||
|
|
||||||
|
export function getNotFoundNav(): NavModel {
|
||||||
|
return getWarningNav('Page not found', '404 Error');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWarningNav(text: string, subTitle?: string): NavModel {
|
||||||
|
const node = {
|
||||||
|
text,
|
||||||
|
subTitle,
|
||||||
|
icon: 'fa fa-fw fa-warning',
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
breadcrumbs: [node],
|
breadcrumbs: [node],
|
||||||
node: node,
|
node: node,
|
||||||
main: node,
|
main: node,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
coreModule.service('navModelSrv', NavModelSrv);
|
coreModule.service('navModelSrv', NavModelSrv);
|
||||||
|
@ -3,8 +3,8 @@ import { PluginDashboard } from '../../types';
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
dashboards: PluginDashboard[];
|
dashboards: PluginDashboard[];
|
||||||
onImport: (dashboard, overwrite) => void;
|
onImport: (dashboard: PluginDashboard, overwrite: boolean) => void;
|
||||||
onRemove: (dashboard) => void;
|
onRemove: (dashboard: PluginDashboard) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardsTable: FC<Props> = ({ dashboards, onImport, onRemove }) => {
|
const DashboardsTable: FC<Props> = ({ dashboards, onImport, onRemove }) => {
|
||||||
|
103
public/app/features/plugins/AppRootPage.tsx
Normal file
103
public/app/features/plugins/AppRootPage.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// Libraries
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { StoreState, UrlQueryMap } from 'app/types';
|
||||||
|
|
||||||
|
import Page from 'app/core/components/Page/Page';
|
||||||
|
import { getPluginSettings } from './PluginSettingsCache';
|
||||||
|
import { importAppPlugin } from './plugin_loader';
|
||||||
|
import { AppPlugin, NavModel, AppPluginMeta, PluginType } from '@grafana/ui';
|
||||||
|
import { getLoadingNav } from './PluginPage';
|
||||||
|
import { getNotFoundNav, getWarningNav } from 'app/core/nav_model_srv';
|
||||||
|
import { appEvents } from 'app/core/core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pluginId: string; // From the angular router
|
||||||
|
query: UrlQueryMap;
|
||||||
|
path: string;
|
||||||
|
slug?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
loading: boolean;
|
||||||
|
plugin?: AppPlugin;
|
||||||
|
nav: NavModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAppPluginPageError(meta: AppPluginMeta) {
|
||||||
|
if (!meta) {
|
||||||
|
return 'Unknown Plugin';
|
||||||
|
}
|
||||||
|
if (meta.type !== PluginType.app) {
|
||||||
|
return 'Plugin must be an app';
|
||||||
|
}
|
||||||
|
if (!meta.enabled) {
|
||||||
|
return 'Applicaiton Not Enabled';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppRootPage extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: true,
|
||||||
|
nav: getLoadingNav(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const { pluginId } = this.props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const app = await getPluginSettings(pluginId).then(info => {
|
||||||
|
const error = getAppPluginPageError(info);
|
||||||
|
if (error) {
|
||||||
|
appEvents.emit('alert-error', [error]);
|
||||||
|
this.setState({ nav: getWarningNav(error) });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return importAppPlugin(info);
|
||||||
|
});
|
||||||
|
this.setState({ plugin: app, loading: false });
|
||||||
|
} catch (err) {
|
||||||
|
this.setState({ plugin: null, loading: false, nav: getNotFoundNav() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNavChanged = (nav: NavModel) => {
|
||||||
|
this.setState({ nav });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { path, query } = this.props;
|
||||||
|
const { loading, plugin, nav } = this.state;
|
||||||
|
|
||||||
|
if (plugin && !plugin.root) {
|
||||||
|
// TODO? redirect to plugin page?
|
||||||
|
return <div>No Root App</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page navModel={nav}>
|
||||||
|
<Page.Contents isLoading={loading}>
|
||||||
|
{!loading && plugin && (
|
||||||
|
<plugin.root meta={plugin.meta} query={query} path={path} onNavChanged={this.onNavChanged} />
|
||||||
|
)}
|
||||||
|
</Page.Contents>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StoreState) => ({
|
||||||
|
pluginId: state.location.routeParams.pluginId,
|
||||||
|
slug: state.location.routeParams.slug,
|
||||||
|
query: state.location.query,
|
||||||
|
path: state.location.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps)(AppRootPage));
|
112
public/app/features/plugins/PluginDashboards.tsx
Normal file
112
public/app/features/plugins/PluginDashboards.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import extend from 'lodash/extend';
|
||||||
|
|
||||||
|
import { PluginMeta, DataSourceApi } from '@grafana/ui';
|
||||||
|
import { PluginDashboard } from 'app/types';
|
||||||
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
|
import { appEvents } from 'app/core/core';
|
||||||
|
import DashboardsTable from 'app/features/datasources/DashboardsTable';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
plugin: PluginMeta;
|
||||||
|
datasource?: DataSourceApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
dashboards: PluginDashboard[];
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginDashboards extends PureComponent<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: true,
|
||||||
|
dashboards: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const pluginId = this.props.plugin.id;
|
||||||
|
getBackendSrv()
|
||||||
|
.get(`/api/plugins/${pluginId}/dashboards`)
|
||||||
|
.then((dashboards: any) => {
|
||||||
|
this.setState({ dashboards, loading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
importAll = () => {
|
||||||
|
this.importNext(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
private importNext = (index: number) => {
|
||||||
|
const { dashboards } = this.state;
|
||||||
|
return this.import(dashboards[index], true).then(() => {
|
||||||
|
if (index + 1 < dashboards.length) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.importNext(index + 1).then(() => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
import = (dash: PluginDashboard, overwrite: boolean) => {
|
||||||
|
const { plugin, datasource } = this.props;
|
||||||
|
|
||||||
|
const installCmd = {
|
||||||
|
pluginId: plugin.id,
|
||||||
|
path: dash.path,
|
||||||
|
overwrite: overwrite,
|
||||||
|
inputs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (datasource) {
|
||||||
|
installCmd.inputs.push({
|
||||||
|
name: '*',
|
||||||
|
type: 'datasource',
|
||||||
|
pluginId: datasource.meta.id,
|
||||||
|
value: datasource.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return getBackendSrv()
|
||||||
|
.post(`/api/dashboards/import`, installCmd)
|
||||||
|
.then((res: PluginDashboard) => {
|
||||||
|
appEvents.emit('alert-success', ['Dashboard Imported', dash.title]);
|
||||||
|
extend(dash, res);
|
||||||
|
this.setState({ dashboards: [...this.state.dashboards] });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
remove = (dash: PluginDashboard) => {
|
||||||
|
getBackendSrv()
|
||||||
|
.delete('/api/dashboards/' + dash.importedUri)
|
||||||
|
.then(() => {
|
||||||
|
dash.imported = false;
|
||||||
|
this.setState({ dashboards: [...this.state.dashboards] });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, dashboards } = this.state;
|
||||||
|
if (loading) {
|
||||||
|
return <div>loading...</div>;
|
||||||
|
}
|
||||||
|
if (!dashboards || !dashboards.length) {
|
||||||
|
return <div>No dashboards are included with this plugin</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gf-form-group">
|
||||||
|
<DashboardsTable dashboards={dashboards} onImport={this.import} onRemove={this.remove} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,7 @@ const PluginListItem: FC<Props> = props => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="card-item-wrapper">
|
<li className="card-item-wrapper">
|
||||||
<a className="card-item" href={`plugins/${plugin.id}/edit`}>
|
<a className="card-item" href={`plugins/${plugin.id}/`}>
|
||||||
<div className="card-item-header">
|
<div className="card-item-header">
|
||||||
<div className="card-item-type">
|
<div className="card-item-type">
|
||||||
<i className={icon} />
|
<i className={icon} />
|
||||||
|
415
public/app/features/plugins/PluginPage.tsx
Normal file
415
public/app/features/plugins/PluginPage.tsx
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
// Libraries
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import find from 'lodash/find';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { StoreState, UrlQueryMap } from 'app/types';
|
||||||
|
import {
|
||||||
|
NavModel,
|
||||||
|
NavModelItem,
|
||||||
|
PluginType,
|
||||||
|
GrafanaPlugin,
|
||||||
|
PluginInclude,
|
||||||
|
PluginDependencies,
|
||||||
|
PluginMeta,
|
||||||
|
PluginMetaInfo,
|
||||||
|
Tooltip,
|
||||||
|
AppPlugin,
|
||||||
|
PluginIncludeType,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
|
||||||
|
import Page from 'app/core/components/Page/Page';
|
||||||
|
import { getPluginSettings } from './PluginSettingsCache';
|
||||||
|
import { importAppPlugin, importDataSourcePlugin, importPanelPlugin } from './plugin_loader';
|
||||||
|
import { getNotFoundNav } from 'app/core/nav_model_srv';
|
||||||
|
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||||
|
import { AppConfigCtrlWrapper } from './wrappers/AppConfigWrapper';
|
||||||
|
import { PluginDashboards } from './PluginDashboards';
|
||||||
|
import { appEvents } from 'app/core/core';
|
||||||
|
|
||||||
|
export function getLoadingNav(): NavModel {
|
||||||
|
const node = {
|
||||||
|
text: 'Loading...',
|
||||||
|
icon: 'icon-gf icon-gf-panel',
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
node: node,
|
||||||
|
main: node,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPlugin(pluginId: string): Promise<GrafanaPlugin> {
|
||||||
|
return getPluginSettings(pluginId).then(info => {
|
||||||
|
if (info.type === PluginType.app) {
|
||||||
|
return importAppPlugin(info);
|
||||||
|
}
|
||||||
|
if (info.type === PluginType.datasource) {
|
||||||
|
return importDataSourcePlugin(info);
|
||||||
|
}
|
||||||
|
if (info.type === PluginType.panel) {
|
||||||
|
return importPanelPlugin(pluginId).then(plugin => {
|
||||||
|
// Panel Meta does not have the *full* settings meta
|
||||||
|
return getPluginSettings(pluginId).then(meta => {
|
||||||
|
plugin.meta = {
|
||||||
|
...meta, // Set any fields that do not exist
|
||||||
|
...plugin.meta,
|
||||||
|
};
|
||||||
|
return plugin;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.reject('Unknown Plugin type: ' + info.type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pluginId: string;
|
||||||
|
query: UrlQueryMap;
|
||||||
|
path: string; // the URL path
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
loading: boolean;
|
||||||
|
plugin?: GrafanaPlugin;
|
||||||
|
nav: NavModel;
|
||||||
|
defaultTab: string; // The first configured one or readme
|
||||||
|
}
|
||||||
|
|
||||||
|
const TAB_ID_README = 'readme';
|
||||||
|
const TAB_ID_DASHBOARDS = 'dashboards';
|
||||||
|
const TAB_ID_CONFIG_CTRL = 'config';
|
||||||
|
|
||||||
|
class PluginPage extends PureComponent<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: true,
|
||||||
|
nav: getLoadingNav(),
|
||||||
|
defaultTab: TAB_ID_README,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const { pluginId, path, query } = this.props;
|
||||||
|
const plugin = await loadPlugin(pluginId);
|
||||||
|
if (!plugin) {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
nav: getNotFoundNav(),
|
||||||
|
});
|
||||||
|
return; // 404
|
||||||
|
}
|
||||||
|
const { meta } = plugin;
|
||||||
|
|
||||||
|
let defaultTab: string;
|
||||||
|
const tabs: NavModelItem[] = [];
|
||||||
|
if (true) {
|
||||||
|
tabs.push({
|
||||||
|
text: 'Readme',
|
||||||
|
icon: 'fa fa-fw fa-file-text-o',
|
||||||
|
url: path + '?tab=' + TAB_ID_README,
|
||||||
|
id: TAB_ID_README,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show Config/Pages for app
|
||||||
|
if (meta.type === PluginType.app) {
|
||||||
|
// Legacy App Config
|
||||||
|
if (plugin.angularConfigCtrl) {
|
||||||
|
tabs.push({
|
||||||
|
text: 'Config',
|
||||||
|
icon: 'gicon gicon-cog',
|
||||||
|
url: path + '?tab=' + TAB_ID_CONFIG_CTRL,
|
||||||
|
id: TAB_ID_CONFIG_CTRL,
|
||||||
|
});
|
||||||
|
defaultTab = TAB_ID_CONFIG_CTRL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.configTabs) {
|
||||||
|
for (const tab of plugin.configTabs) {
|
||||||
|
tabs.push({
|
||||||
|
text: tab.title,
|
||||||
|
icon: tab.icon,
|
||||||
|
url: path + '?tab=' + tab.id,
|
||||||
|
id: tab.id,
|
||||||
|
});
|
||||||
|
if (!defaultTab) {
|
||||||
|
defaultTab = tab.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for the dashboard tabs
|
||||||
|
if (find(meta.includes, { type: 'dashboard' })) {
|
||||||
|
tabs.push({
|
||||||
|
text: 'Dashboards',
|
||||||
|
icon: 'gicon gicon-dashboard',
|
||||||
|
url: path + '?tab=' + TAB_ID_DASHBOARDS,
|
||||||
|
id: TAB_ID_DASHBOARDS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defaultTab) {
|
||||||
|
defaultTab = tabs[0].id; // the first tab
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = {
|
||||||
|
text: meta.name,
|
||||||
|
img: meta.info.logos.large,
|
||||||
|
subTitle: meta.info.author.name,
|
||||||
|
breadcrumbs: [{ title: 'Plugins', url: '/plugins' }],
|
||||||
|
url: path,
|
||||||
|
children: this.setActiveTab(query.tab as string, tabs, defaultTab),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
plugin,
|
||||||
|
defaultTab,
|
||||||
|
nav: {
|
||||||
|
node: node,
|
||||||
|
main: node,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveTab(tabId: string, tabs: NavModelItem[], defaultTabId: string): NavModelItem[] {
|
||||||
|
let found = false;
|
||||||
|
const selected = tabId || defaultTabId;
|
||||||
|
const changed = tabs.map(tab => {
|
||||||
|
const active = !found && selected === tab.id;
|
||||||
|
if (active) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
return { ...tab, active };
|
||||||
|
});
|
||||||
|
if (!found) {
|
||||||
|
changed[0].active = true;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
const prevTab = prevProps.query.tab as string;
|
||||||
|
const tab = this.props.query.tab as string;
|
||||||
|
if (prevTab !== tab) {
|
||||||
|
const { nav, defaultTab } = this.state;
|
||||||
|
const node = {
|
||||||
|
...nav.node,
|
||||||
|
children: this.setActiveTab(tab, nav.node.children, defaultTab),
|
||||||
|
};
|
||||||
|
this.setState({
|
||||||
|
nav: {
|
||||||
|
node: node,
|
||||||
|
main: node,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBody() {
|
||||||
|
const { query } = this.props;
|
||||||
|
const { plugin, nav } = this.state;
|
||||||
|
|
||||||
|
if (!plugin) {
|
||||||
|
return <div>Plugin not found.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const active = nav.main.children.find(tab => tab.active);
|
||||||
|
if (active) {
|
||||||
|
// Find the current config tab
|
||||||
|
if (plugin.configTabs) {
|
||||||
|
for (const tab of plugin.configTabs) {
|
||||||
|
if (tab.id === active.id) {
|
||||||
|
return <tab.body meta={plugin.meta} query={query} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apps have some special behavior
|
||||||
|
if (plugin.meta.type === PluginType.app) {
|
||||||
|
if (active.id === TAB_ID_DASHBOARDS) {
|
||||||
|
return <PluginDashboards plugin={plugin.meta} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active.id === TAB_ID_CONFIG_CTRL && plugin.angularConfigCtrl) {
|
||||||
|
return <AppConfigCtrlWrapper app={plugin as AppPlugin} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PluginHelp plugin={plugin.meta} type="help" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
showUpdateInfo = () => {
|
||||||
|
appEvents.emit('show-modal', {
|
||||||
|
src: 'public/app/features/plugins/partials/update_instructions.html',
|
||||||
|
model: this.state.plugin.meta,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderVersionInfo(meta: PluginMeta) {
|
||||||
|
if (!meta.info.version) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="page-sidebar-section">
|
||||||
|
<h4>Version</h4>
|
||||||
|
<span>{meta.info.version}</span>
|
||||||
|
{meta.hasUpdate && (
|
||||||
|
<div>
|
||||||
|
<Tooltip content={meta.latestVersion} theme="info" placement="top">
|
||||||
|
<a href="#" onClick={this.showUpdateInfo}>
|
||||||
|
Update Available!
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSidebarIncludeBody(item: PluginInclude) {
|
||||||
|
if (item.type === PluginIncludeType.page) {
|
||||||
|
const pluginId = this.state.plugin.meta.id;
|
||||||
|
const page = item.name.toLowerCase().replace(' ', '-');
|
||||||
|
return (
|
||||||
|
<a href={`plugins/${pluginId}/page/${page}`}>
|
||||||
|
<i className={getPluginIcon(item.type)} />
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<i className={getPluginIcon(item.type)} />
|
||||||
|
{item.name}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSidebarIncludes(includes: PluginInclude[]) {
|
||||||
|
if (!includes || !includes.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="page-sidebar-section">
|
||||||
|
<h4>Includes</h4>
|
||||||
|
<ul className="ui-list plugin-info-list">
|
||||||
|
{includes.map(include => {
|
||||||
|
return (
|
||||||
|
<li className="plugin-info-list-item" key={include.name}>
|
||||||
|
{this.renderSidebarIncludeBody(include)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSidebarDependencies(dependencies: PluginDependencies) {
|
||||||
|
if (!dependencies) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="page-sidebar-section">
|
||||||
|
<h4>Dependencies</h4>
|
||||||
|
<ul className="ui-list plugin-info-list">
|
||||||
|
<li className="plugin-info-list-item">
|
||||||
|
<img src="public/img/grafana_icon.svg" />
|
||||||
|
Grafana {dependencies.grafanaVersion}
|
||||||
|
</li>
|
||||||
|
{dependencies.plugins &&
|
||||||
|
dependencies.plugins.map(plug => {
|
||||||
|
return (
|
||||||
|
<li className="plugin-info-list-item" key={plug.name}>
|
||||||
|
<i className={getPluginIcon(plug.type)} />
|
||||||
|
{plug.name} {plug.version}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSidebarLinks(info: PluginMetaInfo) {
|
||||||
|
if (!info.links || !info.links.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="page-sidebar-section">
|
||||||
|
<h4>Links</h4>
|
||||||
|
<ul className="ui-list">
|
||||||
|
{info.links.map(link => {
|
||||||
|
return (
|
||||||
|
<li key={link.url}>
|
||||||
|
<a href={link.url} className="external-link" target="_blank">
|
||||||
|
{link.name}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, nav, plugin } = this.state;
|
||||||
|
return (
|
||||||
|
<Page navModel={nav}>
|
||||||
|
<Page.Contents isLoading={loading}>
|
||||||
|
{!loading && (
|
||||||
|
<div className="sidebar-container">
|
||||||
|
<div className="sidebar-content">{this.renderBody()}</div>
|
||||||
|
<aside className="page-sidebar">
|
||||||
|
{plugin && (
|
||||||
|
<section className="page-sidebar-section">
|
||||||
|
{this.renderVersionInfo(plugin.meta)}
|
||||||
|
{this.renderSidebarIncludes(plugin.meta.includes)}
|
||||||
|
{this.renderSidebarDependencies(plugin.meta.dependencies)}
|
||||||
|
{this.renderSidebarLinks(plugin.meta.info)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Page.Contents>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPluginIcon(type: string) {
|
||||||
|
switch (type) {
|
||||||
|
case 'datasource':
|
||||||
|
return 'gicon gicon-datasources';
|
||||||
|
case 'panel':
|
||||||
|
return 'icon-gf icon-gf-panel';
|
||||||
|
case 'app':
|
||||||
|
return 'icon-gf icon-gf-apps';
|
||||||
|
case 'page':
|
||||||
|
return 'icon-gf icon-gf-endpoint-tiny';
|
||||||
|
case 'dashboard':
|
||||||
|
return 'gicon gicon-dashboard';
|
||||||
|
default:
|
||||||
|
return 'icon-gf icon-gf-apps';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StoreState) => ({
|
||||||
|
pluginId: state.location.routeParams.pluginId,
|
||||||
|
query: state.location.query,
|
||||||
|
path: state.location.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps)(PluginPage));
|
@ -6,7 +6,7 @@ exports[`Render should render component 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className="card-item"
|
className="card-item"
|
||||||
href="plugins/1/edit"
|
href="plugins/1/"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="card-item-header"
|
className="card-item-header"
|
||||||
@ -55,7 +55,7 @@ exports[`Render should render has plugin section 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className="card-item"
|
className="card-item"
|
||||||
href="plugins/1/edit"
|
href="plugins/1/"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="card-item-header"
|
className="card-item-header"
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import './plugin_edit_ctrl';
|
|
||||||
import './plugin_page_ctrl';
|
import './plugin_page_ctrl';
|
||||||
import './import_list/import_list';
|
|
||||||
import './datasource_srv';
|
import './datasource_srv';
|
||||||
import './plugin_component';
|
import './plugin_component';
|
||||||
import './variableQueryEditorLoader';
|
import './variableQueryEditorLoader';
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
<div class="gf-form-group" ng-if="ctrl.dashboards.length">
|
|
||||||
<table class="filter-table">
|
|
||||||
<tbody>
|
|
||||||
<tr ng-repeat="dash in ctrl.dashboards">
|
|
||||||
<td class="width-1">
|
|
||||||
<i class="gicon gicon-dashboard"></i>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="{{dash.importedUrl}}" ng-show="dash.imported">
|
|
||||||
{{dash.title}}
|
|
||||||
</a>
|
|
||||||
<span ng-show="!dash.imported">{{dash.title}}</span>
|
|
||||||
</td>
|
|
||||||
<td style="text-align: right">
|
|
||||||
<button class="btn btn-secondary btn-small" ng-click="ctrl.import(dash, false)" ng-show="!dash.imported">
|
|
||||||
Import
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary btn-small" ng-click="ctrl.import(dash, true)" ng-show="dash.imported">
|
|
||||||
<span ng-if="dash.revision !== dash.importedRevision">Update</span>
|
|
||||||
<span ng-if="dash.revision === dash.importedRevision">Re-import</span>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger btn-small" ng-click="ctrl.remove(dash)" ng-show="dash.imported">
|
|
||||||
<i class="fa fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import coreModule from 'app/core/core_module';
|
|
||||||
import appEvents from 'app/core/app_events';
|
|
||||||
|
|
||||||
export class DashImportListCtrl {
|
|
||||||
dashboards: any[];
|
|
||||||
plugin: any;
|
|
||||||
datasource: any;
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope, private backendSrv, private $rootScope) {
|
|
||||||
this.dashboards = [];
|
|
||||||
|
|
||||||
backendSrv.get(`/api/plugins/${this.plugin.id}/dashboards`).then(dashboards => {
|
|
||||||
this.dashboards = dashboards;
|
|
||||||
});
|
|
||||||
|
|
||||||
appEvents.on('dashboard-list-import-all', this.importAll.bind(this), $scope);
|
|
||||||
}
|
|
||||||
|
|
||||||
importAll(payload) {
|
|
||||||
return this.importNext(0)
|
|
||||||
.then(() => {
|
|
||||||
payload.resolve('All dashboards imported');
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
payload.reject(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
importNext(index) {
|
|
||||||
return this.import(this.dashboards[index], true).then(() => {
|
|
||||||
if (index + 1 < this.dashboards.length) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.importNext(index + 1).then(() => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
import(dash, overwrite) {
|
|
||||||
const installCmd = {
|
|
||||||
pluginId: this.plugin.id,
|
|
||||||
path: dash.path,
|
|
||||||
overwrite: overwrite,
|
|
||||||
inputs: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.datasource) {
|
|
||||||
installCmd.inputs.push({
|
|
||||||
name: '*',
|
|
||||||
type: 'datasource',
|
|
||||||
pluginId: this.datasource.type,
|
|
||||||
value: this.datasource.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.backendSrv.post(`/api/dashboards/import`, installCmd).then(res => {
|
|
||||||
this.$rootScope.appEvent('alert-success', ['Dashboard Imported', dash.title]);
|
|
||||||
_.extend(dash, res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(dash) {
|
|
||||||
this.backendSrv.delete('/api/dashboards/' + dash.importedUri).then(() => {
|
|
||||||
this.$rootScope.appEvent('alert-success', ['Dashboard Deleted', dash.title]);
|
|
||||||
dash.imported = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dashboardImportList() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
templateUrl: 'public/app/features/plugins/import_list/import_list.html',
|
|
||||||
controller: DashImportListCtrl,
|
|
||||||
bindToController: true,
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
scope: {
|
|
||||||
plugin: '=',
|
|
||||||
datasource: '=',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.directive('dashboardImportList', dashboardImportList);
|
|
@ -1,69 +0,0 @@
|
|||||||
<div ng-if="ctrl.navModel">
|
|
||||||
<page-header model="ctrl.navModel"></page-header>
|
|
||||||
|
|
||||||
<div class="page-container page-body">
|
|
||||||
<div class="sidebar-container">
|
|
||||||
<div class="tab-content sidebar-content" ng-if="ctrl.tab === 'readme'">
|
|
||||||
<div ng-bind-html="ctrl.readmeHtml" class="markdown-html">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content sidebar-content" ng-if="ctrl.tab === 'config'">
|
|
||||||
<div ng-if="ctrl.model.id">
|
|
||||||
<plugin-component type="app-config-ctrl"></plugin-component>
|
|
||||||
|
|
||||||
<div class="gf-form-button-row">
|
|
||||||
<button type="submit" class="btn btn-primary" ng-click="ctrl.enable()" ng-show="!ctrl.model.enabled">Enable</button>
|
|
||||||
<button type="submit" class="btn btn-primary" ng-click="ctrl.update()" ng-show="ctrl.model.enabled">Update</button>
|
|
||||||
<button type="submit" class="btn btn-danger" ng-click="ctrl.disable()" ng-show="ctrl.model.enabled">Disable</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content sidebar-content" ng-if="ctrl.tab === 'dashboards'">
|
|
||||||
<dashboard-import-list plugin="ctrl.model"></dashboard-import-list>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<aside class="page-sidebar">
|
|
||||||
<section class="page-sidebar-section" ng-if="ctrl.model.info.version">
|
|
||||||
<h4>Version</h4>
|
|
||||||
<span>{{ctrl.model.info.version}}</span>
|
|
||||||
<div ng-show="ctrl.model.hasUpdate">
|
|
||||||
<a ng-click="ctrl.updateAvailable()" bs-tooltip="ctrl.model.latestVersion">Update Available!</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="page-sidebar-section" ng-show="ctrl.model.type === 'app'">
|
|
||||||
<h5>Includes</h4>
|
|
||||||
<ul class="ui-list plugin-info-list">
|
|
||||||
<li ng-repeat="plug in ctrl.includes" class="plugin-info-list-item">
|
|
||||||
<i class="{{plug.icon}}"></i>
|
|
||||||
{{plug.name}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section class="page-sidebar-section">
|
|
||||||
<h5>Dependencies</h4>
|
|
||||||
<ul class="ui-list plugin-info-list">
|
|
||||||
<li class="plugin-info-list-item">
|
|
||||||
<img src="public/img/grafana_icon.svg"></img>
|
|
||||||
Grafana {{ctrl.model.dependencies.grafanaVersion}}
|
|
||||||
</li>
|
|
||||||
<li ng-repeat="plugDep in ctrl.model.dependencies.plugins" class="plugin-info-list-item">
|
|
||||||
<i class="{{plugDep.icon}}"></i>
|
|
||||||
{{plugDep.name}} {{plugDep.version}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section class="page-sidebar-section" ng-if="ctrl.model.info.links">
|
|
||||||
<h5>Links</h4>
|
|
||||||
<ul class="ui-list">
|
|
||||||
<li ng-repeat="link in ctrl.model.info.links">
|
|
||||||
<a href="{{link.url}}" class="external-link" target="_blank">{{link.name}}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -12,9 +12,9 @@
|
|||||||
|
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<p>Type the following on the command line to update {{plugin.name}}.</p>
|
<p>Type the following on the command line to update {{model.name}}.</p>
|
||||||
<pre><code>grafana-cli plugins update {{plugin.id}}</code></pre>
|
<pre><code>grafana-cli plugins update {{model.id}}</code></pre>
|
||||||
<span class="small">Check out {{plugin.name}} on <a href="https://grafana.com/plugins/{{plugin.id}}">Grafana.com</a> for README and changelog. If you do not have access to the command line, ask your Grafana administator.</span>
|
<span class="small">Check out {{model.name}} on <a href="https://grafana.com/plugins/{{model.id}}">Grafana.com</a> for README and changelog. If you do not have access to the command line, ask your Grafana administator.</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="pluginlist-none-installed"><img class="pluginlist-inline-logo" src="public/img/grafana_icon.svg"><strong>Pro tip</strong>: To update all plugins at once, type <code class="code--small">grafana-cli plugins update-all</code> on the command line.</div>
|
<p class="pluginlist-none-installed"><img class="pluginlist-inline-logo" src="public/img/grafana_icon.svg"><strong>Pro tip</strong>: To update all plugins at once, type <code class="code--small">grafana-cli plugins update-all</code> on the command line.</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -147,7 +147,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
name: 'ds-config-' + dsMeta.id,
|
name: 'ds-config-' + dsMeta.id,
|
||||||
bindings: { meta: '=', current: '=' },
|
bindings: { meta: '=', current: '=' },
|
||||||
attrs: { meta: 'ctrl.datasourceMeta', current: 'ctrl.current' },
|
attrs: { meta: 'ctrl.datasourceMeta', current: 'ctrl.current' },
|
||||||
Component: dsPlugin.components.ConfigCtrl,
|
Component: dsPlugin.angularConfigCtrl,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -160,7 +160,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
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.angular.ConfigCtrl,
|
Component: appPlugin.angularConfigCtrl,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -173,7 +173,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
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.angular.pages[scope.ctrl.page.component],
|
Component: appPlugin.angularPages[scope.ctrl.page.component],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,180 +0,0 @@
|
|||||||
import angular from 'angular';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import Remarkable from 'remarkable';
|
|
||||||
import { getPluginSettings } from './PluginSettingsCache';
|
|
||||||
|
|
||||||
export class PluginEditCtrl {
|
|
||||||
model: any;
|
|
||||||
pluginIcon: string;
|
|
||||||
pluginId: any;
|
|
||||||
includes: any;
|
|
||||||
readmeHtml: any;
|
|
||||||
includedDatasources: any;
|
|
||||||
tab: string;
|
|
||||||
navModel: any;
|
|
||||||
hasDashboards: any;
|
|
||||||
preUpdateHook: () => any;
|
|
||||||
postUpdateHook: () => any;
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor(private $scope, private $rootScope, private backendSrv, private $sce, private $routeParams, navModelSrv) {
|
|
||||||
this.pluginId = $routeParams.pluginId;
|
|
||||||
this.preUpdateHook = () => Promise.resolve();
|
|
||||||
this.postUpdateHook = () => Promise.resolve();
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
setNavModel(model) {
|
|
||||||
let defaultTab = 'readme';
|
|
||||||
|
|
||||||
this.navModel = {
|
|
||||||
main: {
|
|
||||||
img: model.info.logos.large,
|
|
||||||
subTitle: model.info.author.name,
|
|
||||||
url: '',
|
|
||||||
text: model.name,
|
|
||||||
breadcrumbs: [{ title: 'Plugins', url: 'plugins' }],
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
icon: 'fa fa-fw fa-file-text-o',
|
|
||||||
id: 'readme',
|
|
||||||
text: 'Readme',
|
|
||||||
url: `plugins/${this.model.id}/edit?tab=readme`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (model.type === 'app') {
|
|
||||||
this.navModel.main.children.push({
|
|
||||||
icon: 'gicon gicon-cog',
|
|
||||||
id: 'config',
|
|
||||||
text: 'Config',
|
|
||||||
url: `plugins/${this.model.id}/edit?tab=config`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasDashboards: any = _.find(model.includes, { type: 'dashboard' });
|
|
||||||
|
|
||||||
if (hasDashboards) {
|
|
||||||
this.navModel.main.children.push({
|
|
||||||
icon: 'gicon gicon-dashboard',
|
|
||||||
id: 'dashboards',
|
|
||||||
text: 'Dashboards',
|
|
||||||
url: `plugins/${this.model.id}/edit?tab=dashboards`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultTab = 'config';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tab = this.$routeParams.tab || defaultTab;
|
|
||||||
|
|
||||||
for (const tab of this.navModel.main.children) {
|
|
||||||
if (tab.id === this.tab) {
|
|
||||||
tab.active = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
return getPluginSettings(this.pluginId).then(result => {
|
|
||||||
this.model = result;
|
|
||||||
this.pluginIcon = this.getPluginIcon(this.model.type);
|
|
||||||
|
|
||||||
this.model.dependencies.plugins.forEach(plug => {
|
|
||||||
plug.icon = this.getPluginIcon(plug.type);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.includes = _.map(result.includes, plug => {
|
|
||||||
plug.icon = this.getPluginIcon(plug.type);
|
|
||||||
return plug;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setNavModel(this.model);
|
|
||||||
return this.initReadme();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initReadme() {
|
|
||||||
return this.backendSrv.get(`/api/plugins/${this.pluginId}/markdown/readme`).then(res => {
|
|
||||||
const md = new Remarkable({
|
|
||||||
linkify: true,
|
|
||||||
});
|
|
||||||
this.readmeHtml = this.$sce.trustAsHtml(md.render(res));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getPluginIcon(type) {
|
|
||||||
switch (type) {
|
|
||||||
case 'datasource':
|
|
||||||
return 'gicon gicon-datasources';
|
|
||||||
case 'panel':
|
|
||||||
return 'icon-gf icon-gf-panel';
|
|
||||||
case 'app':
|
|
||||||
return 'icon-gf icon-gf-apps';
|
|
||||||
case 'page':
|
|
||||||
return 'icon-gf icon-gf-endpoint-tiny';
|
|
||||||
case 'dashboard':
|
|
||||||
return 'gicon gicon-dashboard';
|
|
||||||
default:
|
|
||||||
return 'icon-gf icon-gf-apps';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
this.preUpdateHook()
|
|
||||||
.then(() => {
|
|
||||||
const updateCmd = _.extend(
|
|
||||||
{
|
|
||||||
enabled: this.model.enabled,
|
|
||||||
pinned: this.model.pinned,
|
|
||||||
jsonData: this.model.jsonData,
|
|
||||||
secureJsonData: this.model.secureJsonData,
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
return this.backendSrv.post(`/api/plugins/${this.pluginId}/settings`, updateCmd);
|
|
||||||
})
|
|
||||||
.then(this.postUpdateHook)
|
|
||||||
.then(res => {
|
|
||||||
window.location.href = window.location.href;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
importDashboards() {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
setPreUpdateHook(callback: () => any) {
|
|
||||||
this.preUpdateHook = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPostUpdateHook(callback: () => any) {
|
|
||||||
this.postUpdateHook = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAvailable() {
|
|
||||||
const modalScope = this.$scope.$new(true);
|
|
||||||
modalScope.plugin = this.model;
|
|
||||||
|
|
||||||
this.$rootScope.appEvent('show-modal', {
|
|
||||||
src: 'public/app/features/plugins/partials/update_instructions.html',
|
|
||||||
scope: modalScope,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
enable() {
|
|
||||||
this.model.enabled = true;
|
|
||||||
this.model.pinned = true;
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
disable() {
|
|
||||||
this.model.enabled = false;
|
|
||||||
this.model.pinned = false;
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.module('grafana.controllers').controller('PluginEditCtrl', PluginEditCtrl);
|
|
139
public/app/features/plugins/wrappers/AppConfigWrapper.tsx
Normal file
139
public/app/features/plugins/wrappers/AppConfigWrapper.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
// Libraries
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
|
import extend from 'lodash/extend';
|
||||||
|
|
||||||
|
import { PluginMeta, AppPlugin, Button } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
|
||||||
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
|
import { ButtonVariant } from '@grafana/ui/src/components/Button/AbstractButton';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
app: AppPlugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
angularCtrl: AngularComponent;
|
||||||
|
refresh: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AppConfigCtrlWrapper extends PureComponent<Props, State> {
|
||||||
|
element: HTMLElement; // for angular ctrl
|
||||||
|
|
||||||
|
// Needed for angular scope
|
||||||
|
preUpdateHook = () => Promise.resolve();
|
||||||
|
postUpdateHook = () => Promise.resolve();
|
||||||
|
model: PluginMeta;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
angularCtrl: null,
|
||||||
|
refresh: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// Force a reload after the first mount -- is there a better way to do this?
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({ refresh: this.state.refresh + 1 });
|
||||||
|
}, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
if (!this.element || this.state.angularCtrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a copy of the meta
|
||||||
|
this.model = cloneDeep(this.props.app.meta);
|
||||||
|
|
||||||
|
const loader = getAngularLoader();
|
||||||
|
const template = '<plugin-component type="app-config-ctrl"></plugin-component>';
|
||||||
|
const scopeProps = { ctrl: this };
|
||||||
|
const angularCtrl = loader.load(this.element, scopeProps, template);
|
||||||
|
|
||||||
|
this.setState({ angularCtrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const model = this.model;
|
||||||
|
|
||||||
|
const withRightMargin = css({ marginRight: '8px' });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div ref={element => (this.element = element)} />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
{model && (
|
||||||
|
<div className="gf-form">
|
||||||
|
{!model.enabled && (
|
||||||
|
<Button variant={ButtonVariant.Primary} onClick={this.enable} className={withRightMargin}>
|
||||||
|
Enable
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{model.enabled && (
|
||||||
|
<Button variant={ButtonVariant.Primary} onClick={this.update} className={withRightMargin}>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{model.enabled && (
|
||||||
|
<Button variant={ButtonVariant.Danger} onClick={this.disable} className={withRightMargin}>
|
||||||
|
Disable
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Copied from plugin_edit_ctrl
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
|
||||||
|
update = () => {
|
||||||
|
const pluginId = this.model.id;
|
||||||
|
|
||||||
|
this.preUpdateHook()
|
||||||
|
.then(() => {
|
||||||
|
const updateCmd = extend(
|
||||||
|
{
|
||||||
|
enabled: this.model.enabled,
|
||||||
|
pinned: this.model.pinned,
|
||||||
|
jsonData: this.model.jsonData,
|
||||||
|
secureJsonData: this.model.secureJsonData,
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return getBackendSrv().post(`/api/plugins/${pluginId}/settings`, updateCmd);
|
||||||
|
})
|
||||||
|
.then(this.postUpdateHook)
|
||||||
|
.then(res => {
|
||||||
|
window.location.href = window.location.href;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setPreUpdateHook = (callback: () => any) => {
|
||||||
|
this.preUpdateHook = callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
setPostUpdateHook = (callback: () => any) => {
|
||||||
|
this.postUpdateHook = callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
enable = () => {
|
||||||
|
this.model.enabled = true;
|
||||||
|
this.model.pinned = true;
|
||||||
|
this.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
disable = () => {
|
||||||
|
this.model.enabled = false;
|
||||||
|
this.model.pinned = false;
|
||||||
|
this.update();
|
||||||
|
};
|
||||||
|
}
|
102
public/app/plugins/app/example-app/ExampleRootPage.tsx
Normal file
102
public/app/plugins/app/example-app/ExampleRootPage.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// Libraries
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { AppRootProps, NavModelItem } from '@grafana/ui';
|
||||||
|
|
||||||
|
interface Props extends AppRootProps {}
|
||||||
|
|
||||||
|
const TAB_ID_A = 'A';
|
||||||
|
const TAB_ID_B = 'B';
|
||||||
|
const TAB_ID_C = 'C';
|
||||||
|
|
||||||
|
export class ExampleRootPage 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 } = 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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
25
public/app/plugins/app/example-app/config/ExampleTab1.tsx
Normal file
25
public/app/plugins/app/example-app/config/ExampleTab1.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Libraries
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { PluginConfigTabProps, AppPluginMeta } from '@grafana/ui';
|
||||||
|
|
||||||
|
interface Props extends PluginConfigTabProps<AppPluginMeta> {}
|
||||||
|
|
||||||
|
export class ExampleTab1 extends PureComponent<Props> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { query } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
11111111111111111111111111111111
|
||||||
|
<pre>{JSON.stringify(query)}</pre>
|
||||||
|
11111111111111111111111111111111
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
25
public/app/plugins/app/example-app/config/ExampleTab2.tsx
Normal file
25
public/app/plugins/app/example-app/config/ExampleTab2.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Libraries
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { PluginConfigTabProps, AppPluginMeta } from '@grafana/ui';
|
||||||
|
|
||||||
|
interface Props extends PluginConfigTabProps<AppPluginMeta> {}
|
||||||
|
|
||||||
|
export class ExampleTab2 extends PureComponent<Props> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { query } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
22222222222222222222222222222222
|
||||||
|
<pre>{JSON.stringify(query)}</pre>
|
||||||
|
22222222222222222222222222222222
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
110
public/app/plugins/app/example-app/dashboards/stats.json
Normal file
110
public/app/plugins/app/example-app/dashboards/stats.json
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"__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
|
||||||
|
}
|
83
public/app/plugins/app/example-app/dashboards/streaming.json
Normal file
83
public/app/plugins/app/example-app/dashboards/streaming.json
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"__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
|
||||||
|
}
|
@ -1,9 +1,28 @@
|
|||||||
// Angular pages
|
// Angular pages
|
||||||
import { ExampleConfigCtrl } from './legacy/config';
|
import { ExampleConfigCtrl } from './legacy/config';
|
||||||
import { AngularExamplePageCtrl } from './legacy/angular_example_page';
|
import { AngularExamplePageCtrl } from './legacy/angular_example_page';
|
||||||
|
import { AppPlugin } from '@grafana/ui';
|
||||||
|
import { ExampleTab1 } from './config/ExampleTab1';
|
||||||
|
import { ExampleTab2 } from './config/ExampleTab2';
|
||||||
|
import { ExampleRootPage } from './ExampleRootPage';
|
||||||
|
|
||||||
|
// Legacy exports just for testing
|
||||||
export {
|
export {
|
||||||
ExampleConfigCtrl as ConfigCtrl,
|
ExampleConfigCtrl as ConfigCtrl,
|
||||||
// Must match `pages.component` in plugin.json
|
AngularExamplePageCtrl, // Must match `pages.component` in plugin.json
|
||||||
AngularExamplePageCtrl,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const plugin = new AppPlugin()
|
||||||
|
.setRootPage(ExampleRootPage)
|
||||||
|
.addConfigTab({
|
||||||
|
title: 'Tab 1',
|
||||||
|
icon: 'fa fa-info',
|
||||||
|
body: ExampleTab1,
|
||||||
|
id: 'tab1',
|
||||||
|
})
|
||||||
|
.addConfigTab({
|
||||||
|
title: 'Tab 2',
|
||||||
|
icon: 'fa fa-user',
|
||||||
|
body: ExampleTab2,
|
||||||
|
id: 'tab2',
|
||||||
|
});
|
||||||
|
@ -23,6 +23,20 @@
|
|||||||
"role": "Viewer",
|
"role": "Viewer",
|
||||||
"addToNav": true,
|
"addToNav": true,
|
||||||
"defaultNav": 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?"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,8 @@ import DataSourceSettingsPage from '../features/datasources/settings/DataSourceS
|
|||||||
import OrgDetailsPage from '../features/org/OrgDetailsPage';
|
import OrgDetailsPage from '../features/org/OrgDetailsPage';
|
||||||
import SoloPanelPage from '../features/dashboard/containers/SoloPanelPage';
|
import SoloPanelPage from '../features/dashboard/containers/SoloPanelPage';
|
||||||
import DashboardPage from '../features/dashboard/containers/DashboardPage';
|
import DashboardPage from '../features/dashboard/containers/DashboardPage';
|
||||||
|
import PluginPage from '../features/plugins/PluginPage';
|
||||||
|
import AppRootPage from 'app/features/plugins/AppRootPage';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
@ -164,6 +166,14 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
component: () => import(/* webpackChunkName: "explore" */ 'app/features/explore/Wrapper'),
|
component: () => import(/* webpackChunkName: "explore" */ 'app/features/explore/Wrapper'),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
.when('/a/:pluginId/', {
|
||||||
|
// Someday * and will get a ReactRouter under that path!
|
||||||
|
template: '<react-container />',
|
||||||
|
reloadOnSearch: false,
|
||||||
|
resolve: {
|
||||||
|
component: () => AppRootPage,
|
||||||
|
},
|
||||||
|
})
|
||||||
.when('/org', {
|
.when('/org', {
|
||||||
template: '<react-container />',
|
template: '<react-container />',
|
||||||
resolve: {
|
resolve: {
|
||||||
@ -301,10 +311,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
component: () => PluginListPage,
|
component: () => PluginListPage,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.when('/plugins/:pluginId/edit', {
|
.when('/plugins/:pluginId/', {
|
||||||
templateUrl: 'public/app/features/plugins/partials/plugin_edit.html',
|
template: '<react-container />',
|
||||||
controller: 'PluginEditCtrl',
|
reloadOnSearch: false, // tabs from query parameters
|
||||||
controllerAs: 'ctrl',
|
resolve: {
|
||||||
|
component: () => PluginPage,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.when('/plugins/:pluginId/page/:slug', {
|
.when('/plugins/:pluginId/page/:slug', {
|
||||||
templateUrl: 'public/app/features/plugins/partials/plugin_page.html',
|
templateUrl: 'public/app/features/plugins/partials/plugin_page.html',
|
||||||
|
Loading…
Reference in New Issue
Block a user