mirror of https://github.com/grafana/grafana
App Plugins: support react pages and tabs (#16586)
parent
31ea0122a0
commit
013f1b8d19
@ -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; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -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)); |
||||
@ -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> |
||||
); |
||||
} |
||||
} |
||||
@ -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)); |
||||
@ -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> |
||||
@ -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); |
||||
@ -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(); |
||||
}; |
||||
} |
||||
@ -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> |
||||
); |
||||
} |
||||
} |
||||
@ -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> |
||||
); |
||||
} |
||||
} |
||||
@ -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> |
||||
); |
||||
} |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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', |
||||
}); |
||||
|
||||
Loading…
Reference in new issue