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;
|
||||
}
|
||||
|
||||
setConfigCtrl(ConfigCtrl: any) {
|
||||
this.components.ConfigCtrl = ConfigCtrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
setQueryCtrl(QueryCtrl: any) {
|
||||
this.components.QueryCtrl = QueryCtrl;
|
||||
return this;
|
||||
@ -60,14 +55,15 @@ export class DataSourcePlugin<TOptions = {}, TQuery extends DataQuery = DataQuer
|
||||
return this;
|
||||
}
|
||||
|
||||
setComponentsFromLegacyExports(exports: any) {
|
||||
this.components.ConfigCtrl = exports.ConfigCtrl;
|
||||
this.components.QueryCtrl = exports.QueryCtrl;
|
||||
this.components.AnnotationsQueryCtrl = exports.AnnotationsQueryCtrl;
|
||||
this.components.ExploreQueryField = exports.ExploreQueryField;
|
||||
this.components.ExploreStartPage = exports.ExploreStartPage;
|
||||
this.components.QueryEditor = exports.QueryEditor;
|
||||
this.components.VariableQueryEditor = exports.VariableQueryEditor;
|
||||
setComponentsFromLegacyExports(pluginExports: any) {
|
||||
this.angularConfigCtrl = pluginExports.ConfigCtrl;
|
||||
|
||||
this.components.QueryCtrl = pluginExports.QueryCtrl;
|
||||
this.components.AnnotationsQueryCtrl = pluginExports.AnnotationsQueryCtrl;
|
||||
this.components.ExploreQueryField = pluginExports.ExploreQueryField;
|
||||
this.components.ExploreStartPage = pluginExports.ExploreStartPage;
|
||||
this.components.QueryEditor = pluginExports.QueryEditor;
|
||||
this.components.VariableQueryEditor = pluginExports.VariableQueryEditor;
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,7 +87,6 @@ interface PluginMetaQueryOptions {
|
||||
|
||||
export interface DataSourcePluginComponents<TOptions = {}, TQuery extends DataQuery = DataQuery> {
|
||||
QueryCtrl?: any;
|
||||
ConfigCtrl?: any;
|
||||
AnnotationsQueryCtrl?: any;
|
||||
VariableQueryEditor?: any;
|
||||
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi, TQuery>>;
|
||||
|
@ -2,6 +2,7 @@ export * from './data';
|
||||
export * from './time';
|
||||
export * from './panel';
|
||||
export * from './plugin';
|
||||
export * from './app';
|
||||
export * from './datasource';
|
||||
export * from './theme';
|
||||
export * from './graph';
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { ComponentClass } from 'react';
|
||||
|
||||
export enum PluginState {
|
||||
alpha = 'alpha', // Only included it `enable_alpha` is true
|
||||
beta = 'beta', // Will show a warning banner
|
||||
@ -21,8 +23,12 @@ export interface PluginMeta {
|
||||
module: string;
|
||||
baseUrl: string;
|
||||
|
||||
// Define plugin requirements
|
||||
dependencies?: PluginDependencies;
|
||||
|
||||
// Filled in by the backend
|
||||
jsonData?: { [str: string]: any };
|
||||
secureJsonData?: { [str: string]: any };
|
||||
enabled?: boolean;
|
||||
defaultNavUrl?: string;
|
||||
hasUpdate?: boolean;
|
||||
@ -30,6 +36,18 @@ export interface PluginMeta {
|
||||
pinned?: boolean;
|
||||
}
|
||||
|
||||
interface PluginDependencyInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
type: PluginType;
|
||||
}
|
||||
|
||||
export interface PluginDependencies {
|
||||
grafanaVersion: string;
|
||||
plugins: PluginDependencyInfo[];
|
||||
}
|
||||
|
||||
export enum PluginIncludeType {
|
||||
dashboard = 'dashboard',
|
||||
page = 'page',
|
||||
@ -44,6 +62,10 @@ export interface PluginInclude {
|
||||
name: string;
|
||||
path?: string;
|
||||
icon?: string;
|
||||
|
||||
role?: string; // "Viewer", Admin, editor???
|
||||
addToNav?: boolean; // Show in the sidebar... only if type=page?
|
||||
|
||||
// Angular app pages
|
||||
component?: string;
|
||||
}
|
||||
@ -69,44 +91,35 @@ export interface PluginMetaInfo {
|
||||
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?: T;
|
||||
|
||||
// Soon this will also include common config options
|
||||
}
|
||||
// Config control (app/datasource)
|
||||
angularConfigCtrl?: any;
|
||||
|
||||
export class AppPlugin extends GrafanaPlugin<PluginMeta> {
|
||||
angular?: {
|
||||
ConfigCtrl?: any;
|
||||
pages: { [component: string]: any };
|
||||
};
|
||||
// Show configuration tabs on the plugin page
|
||||
configTabs?: Array<PluginConfigTab<T>>;
|
||||
|
||||
setComponentsFromLegacyExports(pluginExports: any) {
|
||||
const legacy = {
|
||||
ConfigCtrl: undefined,
|
||||
pages: {} as any,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Tabs on the plugin page
|
||||
addConfigTab(tab: PluginConfigTab<T>) {
|
||||
if (!this.configTabs) {
|
||||
this.configTabs = [];
|
||||
}
|
||||
this.configTabs.push(tab);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
@ -59,8 +59,10 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/styleguide", 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("/a/:id/*", reqSignedIn, hs.Index) // App Root Page
|
||||
|
||||
r.Get("/d/:uid/:slug", 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 {
|
||||
listItem.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + listItem.Id + "/edit"
|
||||
listItem.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + listItem.Id + "/"
|
||||
}
|
||||
|
||||
// filter out disabled
|
||||
|
@ -49,18 +49,25 @@ export class NavModelSrv {
|
||||
}
|
||||
|
||||
getNotFoundNav() {
|
||||
const node = {
|
||||
text: 'Page not found',
|
||||
icon: 'fa fa-fw fa-warning',
|
||||
subTitle: '404 Error',
|
||||
};
|
||||
|
||||
return {
|
||||
breadcrumbs: [node],
|
||||
node: node,
|
||||
main: node,
|
||||
};
|
||||
return getNotFoundNav(); // the exported function
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
breadcrumbs: [node],
|
||||
node: node,
|
||||
main: node,
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.service('navModelSrv', NavModelSrv);
|
||||
|
@ -3,8 +3,8 @@ import { PluginDashboard } from '../../types';
|
||||
|
||||
export interface Props {
|
||||
dashboards: PluginDashboard[];
|
||||
onImport: (dashboard, overwrite) => void;
|
||||
onRemove: (dashboard) => void;
|
||||
onImport: (dashboard: PluginDashboard, overwrite: boolean) => void;
|
||||
onRemove: (dashboard: PluginDashboard) => void;
|
||||
}
|
||||
|
||||
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 (
|
||||
<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-type">
|
||||
<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
|
||||
className="card-item"
|
||||
href="plugins/1/edit"
|
||||
href="plugins/1/"
|
||||
>
|
||||
<div
|
||||
className="card-item-header"
|
||||
@ -55,7 +55,7 @@ exports[`Render should render has plugin section 1`] = `
|
||||
>
|
||||
<a
|
||||
className="card-item"
|
||||
href="plugins/1/edit"
|
||||
href="plugins/1/"
|
||||
>
|
||||
<div
|
||||
className="card-item-header"
|
||||
|
@ -1,6 +1,4 @@
|
||||
import './plugin_edit_ctrl';
|
||||
import './plugin_page_ctrl';
|
||||
import './import_list/import_list';
|
||||
import './datasource_srv';
|
||||
import './plugin_component';
|
||||
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="gf-form-group">
|
||||
<p>Type the following on the command line to update {{plugin.name}}.</p>
|
||||
<pre><code>grafana-cli plugins update {{plugin.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>
|
||||
<p>Type the following on the command line to update {{model.name}}.</p>
|
||||
<pre><code>grafana-cli plugins update {{model.id}}</code></pre>
|
||||
<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>
|
||||
<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>
|
||||
|
@ -147,7 +147,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
||||
name: 'ds-config-' + dsMeta.id,
|
||||
bindings: { meta: '=', 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,
|
||||
bindings: { appModel: '=', appEditCtrl: '=' },
|
||||
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,
|
||||
bindings: { 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
|
||||
import { ExampleConfigCtrl } from './legacy/config';
|
||||
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 {
|
||||
ExampleConfigCtrl as ConfigCtrl,
|
||||
// Must match `pages.component` in plugin.json
|
||||
AngularExamplePageCtrl,
|
||||
AngularExamplePageCtrl, // Must match `pages.component` in plugin.json
|
||||
};
|
||||
|
||||
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",
|
||||
"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?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -22,6 +22,8 @@ import DataSourceSettingsPage from '../features/datasources/settings/DataSourceS
|
||||
import OrgDetailsPage from '../features/org/OrgDetailsPage';
|
||||
import SoloPanelPage from '../features/dashboard/containers/SoloPanelPage';
|
||||
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';
|
||||
|
||||
// Types
|
||||
@ -164,6 +166,14 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
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', {
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
@ -301,10 +311,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
component: () => PluginListPage,
|
||||
},
|
||||
})
|
||||
.when('/plugins/:pluginId/edit', {
|
||||
templateUrl: 'public/app/features/plugins/partials/plugin_edit.html',
|
||||
controller: 'PluginEditCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
.when('/plugins/:pluginId/', {
|
||||
template: '<react-container />',
|
||||
reloadOnSearch: false, // tabs from query parameters
|
||||
resolve: {
|
||||
component: () => PluginPage,
|
||||
},
|
||||
})
|
||||
.when('/plugins/:pluginId/page/:slug', {
|
||||
templateUrl: 'public/app/features/plugins/partials/plugin_page.html',
|
||||
|
Loading…
Reference in New Issue
Block a user