// Libraries import React, { PureComponent } from 'react'; import { capitalize, find } from 'lodash'; // Types import { AppPlugin, GrafanaPlugin, GrafanaTheme2, NavModel, NavModelItem, PluginDependencies, PluginInclude, PluginIncludeType, PluginMeta, PluginMetaInfo, PluginSignatureStatus, PluginSignatureType, PluginType, UrlQueryMap, PanelPluginMeta, } from '@grafana/data'; import { AppNotificationSeverity } from 'app/types'; import { Alert, LinkButton, PluginSignatureBadge, Tooltip, Badge, useStyles2, Icon } from '@grafana/ui'; import Page from 'app/core/components/Page/Page'; import { getPluginSettings } from './PluginSettingsCache'; import { importAppPlugin, importDataSourcePlugin, importPanelPluginFromMeta } 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'; import { css } from '@emotion/css'; import { selectors } from '@grafana/e2e-selectors'; import { ShowModalReactEvent } from 'app/types/events'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { UpdatePluginModal } from './UpdatePluginModal'; interface Props extends GrafanaRouteComponentProps<{ pluginId: string }, UrlQueryMap> {} 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 { constructor(props: Props) { super(props); this.state = { loading: true, nav: getLoadingNav(), defaultPage: PAGE_ID_README, }; } async componentDidMount() { const { location, queryParams } = this.props; const { appSubUrl } = config; const plugin = await loadPlugin(this.props.match.params.pluginId); if (!plugin) { this.setState({ loading: false, nav: getNotFoundNav(), }); return; // 404 } const { defaultPage, nav } = getPluginTabsNav( plugin, appSubUrl, location.pathname, queryParams, contextSrv.hasRole('Admin') ); this.setState({ loading: false, plugin, defaultPage, nav, }); } componentDidUpdate(prevProps: Props) { const prevPage = prevProps.queryParams.page as string; const page = this.props.queryParams.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 { queryParams } = this.props; const { plugin, nav } = this.state; if (!plugin) { return ; } 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 ; } } } // Apps have some special behavior if (plugin.meta.type === PluginType.app) { if (active.id === PAGE_ID_DASHBOARDS) { return ; } if (active.id === PAGE_ID_CONFIG_CTRL && plugin.angularConfigCtrl) { return ; } } } return ; } showUpdateInfo = () => { const { id, name } = this.state.plugin!.meta; appEvents.publish( new ShowModalReactEvent({ props: { id, name, }, component: UpdatePluginModal, }) ); }; renderVersionInfo(meta: PluginMeta) { if (!meta.info.version) { return null; } return (

Version

{meta.info.version} {meta.hasUpdate && (
Update Available!
)}
); } renderSidebarIncludeBody(item: PluginInclude) { if (item.type === PluginIncludeType.page) { const pluginId = this.state.plugin!.meta.id; const page = item.name.toLowerCase().replace(' ', '-'); const url = item.path ?? `plugins/${pluginId}/page/${page}`; return ( {item.name} ); } return ( <> {item.name} ); } renderSidebarIncludes(includes?: PluginInclude[]) { if (!includes || !includes.length) { return null; } return (

Includes

    {includes.map((include) => { return (
  • {this.renderSidebarIncludeBody(include)}
  • ); })}
); } renderSidebarDependencies(dependencies?: PluginDependencies) { if (!dependencies) { return null; } return (

Dependencies

  • Grafana logo Grafana {dependencies.grafanaVersion}
  • {dependencies.plugins && dependencies.plugins.map((plug) => { return (
  • {plug.name} {plug.version}
  • ); })}
); } renderSidebarLinks(info: PluginMetaInfo) { if (!info.links || !info.links.length) { return null; } return (

Links

); } renderPluginNotice() { const { plugin } = this.state; if (!plugin) { return null; } const isSignatureValid = plugin.meta.signature === PluginSignatureStatus.valid; if (plugin.meta.signature === PluginSignatureStatus.internal) { return null; } return (
{isSignatureValid && ( )}

Grafana Labs checks each plugin to verify that it has a valid digital signature. Plugin signature verification is part of our security measures to ensure plugins are safe and trustworthy.{' '} {!isSignatureValid && 'Grafana Labs can’t guarantee the integrity of this unsigned plugin. Ask the plugin author to request it to be signed.'}

Read more about plugins signing.
); } render() { const { loading, nav, plugin } = this.state; const isAdmin = contextSrv.hasRole('Admin'); return ( {plugin && (
{plugin.loadError && ( <> Check the server startup logs for more information.
If this plugin was loaded from git, make sure it was compiled.
)} {this.renderPluginNotice()} {this.renderBody()}
)}
); } } function getPluginTabsNav( plugin: GrafanaPlugin, appSubUrl: string, path: string, query: UrlQueryMap, isAdmin: boolean ): { defaultPage: string; nav: NavModel } { const { meta } = plugin; let defaultPage: string | undefined; const pages: NavModelItem[] = []; pages.push({ text: 'Readme', icon: 'file-alt', 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: '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: `${appSubUrl}${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: 'apps', 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: 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'; } } export function getLoadingNav(): NavModel { const node = { text: 'Loading...', icon: 'icon-gf icon-gf-panel', }; return { node: node, main: node, }; } export function loadPlugin(pluginId: string): Promise { 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 importPanelPluginFromMeta(info as PanelPluginMeta); } if (info.type === PluginType.renderer) { return Promise.resolve({ meta: info } as GrafanaPlugin); } return Promise.reject('Unknown Plugin type: ' + info.type); }); } type PluginSignatureDetailsBadgeProps = { signatureType?: PluginSignatureType; signatureOrg?: string; }; const PluginSignatureDetailsBadge: React.FC = ({ signatureType, signatureOrg }) => { const styles = useStyles2(getDetailsBadgeStyles); if (!signatureType && !signatureOrg) { return null; } const signatureTypeIcon = signatureType === PluginSignatureType.grafana ? 'grafana' : signatureType === PluginSignatureType.commercial || signatureType === PluginSignatureType.community ? 'shield' : 'shield-exclamation'; const signatureTypeText = signatureType === PluginSignatureType.grafana ? 'Grafana Labs' : capitalize(signatureType); return ( <> {signatureType && ( Level:    {signatureTypeText} } /> )} {signatureOrg && ( Signed by: {signatureOrg} } /> )} ); }; const getDetailsBadgeStyles = (theme: GrafanaTheme2) => ({ badge: css` background-color: ${theme.colors.background.canvas}; border-color: ${theme.colors.border.strong}; color: ${theme.colors.text.secondary}; margin-left: ${theme.spacing()}; `, strong: css` color: ${theme.colors.text.primary}; `, icon: css` margin-right: ${theme.spacing(0.5)}; `, }); export default PluginPage;