| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- // Libraries
- import React, { PureComponent } from 'react';
- import { hot } from 'react-hot-loader';
- import { connect } from 'react-redux';
- import find from 'lodash/find';
- // Types
- import { UrlQueryMap } from '@grafana/runtime';
- import { StoreState, AppNotificationSeverity } from 'app/types';
- import {
- Alert,
- PluginType,
- GrafanaPlugin,
- PluginInclude,
- PluginDependencies,
- PluginMeta,
- PluginMetaInfo,
- Tooltip,
- AppPlugin,
- PluginIncludeType,
- } from '@grafana/ui';
- import { NavModel, NavModelItem } from '@grafana/data';
- 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';
- import { config } from 'app/core/config';
- import { ContextSrv } from '../../core/services/context_srv';
- 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
- $contextSrv: ContextSrv;
- }
- interface State {
- loading: boolean;
- plugin?: GrafanaPlugin;
- nav: NavModel;
- defaultPage: string; // The first configured one or readme
- }
- const PAGE_ID_README = 'readme';
- const PAGE_ID_DASHBOARDS = 'dashboards';
- const PAGE_ID_CONFIG_CTRL = 'config';
- class PluginPage extends PureComponent<Props, State> {
- constructor(props: Props) {
- super(props);
- this.state = {
- loading: true,
- nav: getLoadingNav(),
- defaultPage: PAGE_ID_README,
- };
- }
- async componentDidMount() {
- const { pluginId, path, query, $contextSrv } = this.props;
- const { appSubUrl } = config;
- const plugin = await loadPlugin(pluginId);
- if (!plugin) {
- this.setState({
- loading: false,
- nav: getNotFoundNav(),
- });
- return; // 404
- }
- const { defaultPage, nav } = getPluginTabsNav(plugin, appSubUrl, path, query, $contextSrv.hasRole('Admin'));
- this.setState({
- loading: false,
- plugin,
- defaultPage,
- nav,
- });
- }
- componentDidUpdate(prevProps: Props) {
- const prevPage = prevProps.query.page as string;
- const page = this.props.query.page as string;
- if (prevPage !== page) {
- const { nav, defaultPage } = this.state;
- const node = {
- ...nav.node,
- children: setActivePage(page, nav.node.children, defaultPage),
- };
- this.setState({
- nav: {
- node: node,
- main: node,
- },
- });
- }
- }
- renderBody() {
- const { query } = this.props;
- const { plugin, nav } = this.state;
- if (!plugin) {
- return <Alert severity={AppNotificationSeverity.Error} title="Plugin Not Found" />;
- }
- const active = nav.main.children.find(tab => tab.active);
- if (active) {
- // Find the current config tab
- if (plugin.configPages) {
- for (const tab of plugin.configPages) {
- if (tab.id === active.id) {
- return <tab.body plugin={plugin} query={query} />;
- }
- }
- }
- // Apps have some special behavior
- if (plugin.meta.type === PluginType.app) {
- if (active.id === PAGE_ID_DASHBOARDS) {
- return <PluginDashboards plugin={plugin.meta} />;
- }
- if (active.id === PAGE_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" rel="noopener">
- {link.name}
- </a>
- </li>
- );
- })}
- </ul>
- </section>
- );
- }
- render() {
- const { loading, nav, plugin } = this.state;
- const { $contextSrv } = this.props;
- const isAdmin = $contextSrv.hasRole('Admin');
- return (
- <Page navModel={nav}>
- <Page.Contents isLoading={loading}>
- {!loading && (
- <div className="sidebar-container">
- <div className="sidebar-content">
- {plugin.loadError && (
- <Alert
- severity={AppNotificationSeverity.Error}
- title="Error Loading Plugin"
- children={
- <>
- Check the server startup logs for more information. <br />
- If this plugin was loaded from git, make sure it was compiled.
- </>
- }
- />
- )}
- {this.renderBody()}
- </div>
- <aside className="page-sidebar">
- {plugin && (
- <section className="page-sidebar-section">
- {this.renderVersionInfo(plugin.meta)}
- {isAdmin && this.renderSidebarIncludes(plugin.meta.includes)}
- {this.renderSidebarDependencies(plugin.meta.dependencies)}
- {this.renderSidebarLinks(plugin.meta.info)}
- </section>
- )}
- </aside>
- </div>
- )}
- </Page.Contents>
- </Page>
- );
- }
- }
- function getPluginTabsNav(
- plugin: GrafanaPlugin,
- appSubUrl: string,
- path: string,
- query: UrlQueryMap,
- isAdmin: boolean
- ): { defaultPage: string; nav: NavModel } {
- const { meta } = plugin;
- let defaultPage: string;
- const pages: NavModelItem[] = [];
- if (true) {
- pages.push({
- text: 'Readme',
- icon: 'fa fa-fw fa-file-text-o',
- url: `${appSubUrl}${path}?page=${PAGE_ID_README}`,
- id: PAGE_ID_README,
- });
- }
- // We allow non admins to see plugins but only their readme. Config is hidden even though the API needs to be
- // public for plugins to work properly.
- if (isAdmin) {
- // Only show Config/Pages for app
- if (meta.type === PluginType.app) {
- // Legacy App Config
- if (plugin.angularConfigCtrl) {
- pages.push({
- text: 'Config',
- icon: 'gicon gicon-cog',
- url: `${appSubUrl}${path}?page=${PAGE_ID_CONFIG_CTRL}`,
- id: PAGE_ID_CONFIG_CTRL,
- });
- defaultPage = PAGE_ID_CONFIG_CTRL;
- }
- if (plugin.configPages) {
- for (const page of plugin.configPages) {
- pages.push({
- text: page.title,
- icon: page.icon,
- url: path + '?page=' + page.id,
- id: page.id,
- });
- if (!defaultPage) {
- defaultPage = page.id;
- }
- }
- }
- // Check for the dashboard pages
- if (find(meta.includes, { type: PluginIncludeType.dashboard })) {
- pages.push({
- text: 'Dashboards',
- icon: 'gicon gicon-dashboard',
- url: `${appSubUrl}${path}?page=${PAGE_ID_DASHBOARDS}`,
- id: PAGE_ID_DASHBOARDS,
- });
- }
- }
- }
- if (!defaultPage) {
- defaultPage = pages[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: `${appSubUrl}${path}`,
- children: setActivePage(query.page as string, pages, defaultPage),
- };
- return {
- defaultPage,
- nav: {
- node: node,
- main: node,
- },
- };
- }
- function setActivePage(pageId: string, pages: NavModelItem[], defaultPageId: string): NavModelItem[] {
- let found = false;
- const selected = pageId || defaultPageId;
- const changed = pages.map(p => {
- const active = !found && selected === p.id;
- if (active) {
- found = true;
- }
- return { ...p, active };
- });
- if (!found) {
- changed[0].active = true;
- }
- return changed;
- }
- 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));
|