PluginPage.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. // Libraries
  2. import React, { PureComponent } from 'react';
  3. import { hot } from 'react-hot-loader';
  4. import { connect } from 'react-redux';
  5. import find from 'lodash/find';
  6. // Types
  7. import { UrlQueryMap } from '@grafana/runtime';
  8. import { StoreState, AppNotificationSeverity } from 'app/types';
  9. import {
  10. Alert,
  11. PluginType,
  12. GrafanaPlugin,
  13. PluginInclude,
  14. PluginDependencies,
  15. PluginMeta,
  16. PluginMetaInfo,
  17. Tooltip,
  18. AppPlugin,
  19. PluginIncludeType,
  20. } from '@grafana/ui';
  21. import { NavModel, NavModelItem } from '@grafana/data';
  22. import Page from 'app/core/components/Page/Page';
  23. import { getPluginSettings } from './PluginSettingsCache';
  24. import { importAppPlugin, importDataSourcePlugin, importPanelPlugin } from './plugin_loader';
  25. import { getNotFoundNav } from 'app/core/nav_model_srv';
  26. import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
  27. import { AppConfigCtrlWrapper } from './wrappers/AppConfigWrapper';
  28. import { PluginDashboards } from './PluginDashboards';
  29. import { appEvents } from 'app/core/core';
  30. import { config } from 'app/core/config';
  31. import { ContextSrv } from '../../core/services/context_srv';
  32. export function getLoadingNav(): NavModel {
  33. const node = {
  34. text: 'Loading...',
  35. icon: 'icon-gf icon-gf-panel',
  36. };
  37. return {
  38. node: node,
  39. main: node,
  40. };
  41. }
  42. function loadPlugin(pluginId: string): Promise<GrafanaPlugin> {
  43. return getPluginSettings(pluginId).then(info => {
  44. if (info.type === PluginType.app) {
  45. return importAppPlugin(info);
  46. }
  47. if (info.type === PluginType.datasource) {
  48. return importDataSourcePlugin(info);
  49. }
  50. if (info.type === PluginType.panel) {
  51. return importPanelPlugin(pluginId).then(plugin => {
  52. // Panel Meta does not have the *full* settings meta
  53. return getPluginSettings(pluginId).then(meta => {
  54. plugin.meta = {
  55. ...meta, // Set any fields that do not exist
  56. ...plugin.meta,
  57. };
  58. return plugin;
  59. });
  60. });
  61. }
  62. return Promise.reject('Unknown Plugin type: ' + info.type);
  63. });
  64. }
  65. interface Props {
  66. pluginId: string;
  67. query: UrlQueryMap;
  68. path: string; // the URL path
  69. $contextSrv: ContextSrv;
  70. }
  71. interface State {
  72. loading: boolean;
  73. plugin?: GrafanaPlugin;
  74. nav: NavModel;
  75. defaultPage: string; // The first configured one or readme
  76. }
  77. const PAGE_ID_README = 'readme';
  78. const PAGE_ID_DASHBOARDS = 'dashboards';
  79. const PAGE_ID_CONFIG_CTRL = 'config';
  80. class PluginPage extends PureComponent<Props, State> {
  81. constructor(props: Props) {
  82. super(props);
  83. this.state = {
  84. loading: true,
  85. nav: getLoadingNav(),
  86. defaultPage: PAGE_ID_README,
  87. };
  88. }
  89. async componentDidMount() {
  90. const { pluginId, path, query, $contextSrv } = this.props;
  91. const { appSubUrl } = config;
  92. const plugin = await loadPlugin(pluginId);
  93. if (!plugin) {
  94. this.setState({
  95. loading: false,
  96. nav: getNotFoundNav(),
  97. });
  98. return; // 404
  99. }
  100. const { defaultPage, nav } = getPluginTabsNav(plugin, appSubUrl, path, query, $contextSrv.hasRole('Admin'));
  101. this.setState({
  102. loading: false,
  103. plugin,
  104. defaultPage,
  105. nav,
  106. });
  107. }
  108. componentDidUpdate(prevProps: Props) {
  109. const prevPage = prevProps.query.page as string;
  110. const page = this.props.query.page as string;
  111. if (prevPage !== page) {
  112. const { nav, defaultPage } = this.state;
  113. const node = {
  114. ...nav.node,
  115. children: setActivePage(page, nav.node.children, defaultPage),
  116. };
  117. this.setState({
  118. nav: {
  119. node: node,
  120. main: node,
  121. },
  122. });
  123. }
  124. }
  125. renderBody() {
  126. const { query } = this.props;
  127. const { plugin, nav } = this.state;
  128. if (!plugin) {
  129. return <Alert severity={AppNotificationSeverity.Error} title="Plugin Not Found" />;
  130. }
  131. const active = nav.main.children.find(tab => tab.active);
  132. if (active) {
  133. // Find the current config tab
  134. if (plugin.configPages) {
  135. for (const tab of plugin.configPages) {
  136. if (tab.id === active.id) {
  137. return <tab.body plugin={plugin} query={query} />;
  138. }
  139. }
  140. }
  141. // Apps have some special behavior
  142. if (plugin.meta.type === PluginType.app) {
  143. if (active.id === PAGE_ID_DASHBOARDS) {
  144. return <PluginDashboards plugin={plugin.meta} />;
  145. }
  146. if (active.id === PAGE_ID_CONFIG_CTRL && plugin.angularConfigCtrl) {
  147. return <AppConfigCtrlWrapper app={plugin as AppPlugin} />;
  148. }
  149. }
  150. }
  151. return <PluginHelp plugin={plugin.meta} type="help" />;
  152. }
  153. showUpdateInfo = () => {
  154. appEvents.emit('show-modal', {
  155. src: 'public/app/features/plugins/partials/update_instructions.html',
  156. model: this.state.plugin.meta,
  157. });
  158. };
  159. renderVersionInfo(meta: PluginMeta) {
  160. if (!meta.info.version) {
  161. return null;
  162. }
  163. return (
  164. <section className="page-sidebar-section">
  165. <h4>Version</h4>
  166. <span>{meta.info.version}</span>
  167. {meta.hasUpdate && (
  168. <div>
  169. <Tooltip content={meta.latestVersion} theme="info" placement="top">
  170. <a href="#" onClick={this.showUpdateInfo}>
  171. Update Available!
  172. </a>
  173. </Tooltip>
  174. </div>
  175. )}
  176. </section>
  177. );
  178. }
  179. renderSidebarIncludeBody(item: PluginInclude) {
  180. if (item.type === PluginIncludeType.page) {
  181. const pluginId = this.state.plugin.meta.id;
  182. const page = item.name.toLowerCase().replace(' ', '-');
  183. return (
  184. <a href={`plugins/${pluginId}/page/${page}`}>
  185. <i className={getPluginIcon(item.type)} />
  186. {item.name}
  187. </a>
  188. );
  189. }
  190. return (
  191. <>
  192. <i className={getPluginIcon(item.type)} />
  193. {item.name}
  194. </>
  195. );
  196. }
  197. renderSidebarIncludes(includes: PluginInclude[]) {
  198. if (!includes || !includes.length) {
  199. return null;
  200. }
  201. return (
  202. <section className="page-sidebar-section">
  203. <h4>Includes</h4>
  204. <ul className="ui-list plugin-info-list">
  205. {includes.map(include => {
  206. return (
  207. <li className="plugin-info-list-item" key={include.name}>
  208. {this.renderSidebarIncludeBody(include)}
  209. </li>
  210. );
  211. })}
  212. </ul>
  213. </section>
  214. );
  215. }
  216. renderSidebarDependencies(dependencies: PluginDependencies) {
  217. if (!dependencies) {
  218. return null;
  219. }
  220. return (
  221. <section className="page-sidebar-section">
  222. <h4>Dependencies</h4>
  223. <ul className="ui-list plugin-info-list">
  224. <li className="plugin-info-list-item">
  225. <img src="public/img/grafana_icon.svg" />
  226. Grafana {dependencies.grafanaVersion}
  227. </li>
  228. {dependencies.plugins &&
  229. dependencies.plugins.map(plug => {
  230. return (
  231. <li className="plugin-info-list-item" key={plug.name}>
  232. <i className={getPluginIcon(plug.type)} />
  233. {plug.name} {plug.version}
  234. </li>
  235. );
  236. })}
  237. </ul>
  238. </section>
  239. );
  240. }
  241. renderSidebarLinks(info: PluginMetaInfo) {
  242. if (!info.links || !info.links.length) {
  243. return null;
  244. }
  245. return (
  246. <section className="page-sidebar-section">
  247. <h4>Links</h4>
  248. <ul className="ui-list">
  249. {info.links.map(link => {
  250. return (
  251. <li key={link.url}>
  252. <a href={link.url} className="external-link" target="_blank" rel="noopener">
  253. {link.name}
  254. </a>
  255. </li>
  256. );
  257. })}
  258. </ul>
  259. </section>
  260. );
  261. }
  262. render() {
  263. const { loading, nav, plugin } = this.state;
  264. const { $contextSrv } = this.props;
  265. const isAdmin = $contextSrv.hasRole('Admin');
  266. return (
  267. <Page navModel={nav}>
  268. <Page.Contents isLoading={loading}>
  269. {!loading && (
  270. <div className="sidebar-container">
  271. <div className="sidebar-content">
  272. {plugin.loadError && (
  273. <Alert
  274. severity={AppNotificationSeverity.Error}
  275. title="Error Loading Plugin"
  276. children={
  277. <>
  278. Check the server startup logs for more information. <br />
  279. If this plugin was loaded from git, make sure it was compiled.
  280. </>
  281. }
  282. />
  283. )}
  284. {this.renderBody()}
  285. </div>
  286. <aside className="page-sidebar">
  287. {plugin && (
  288. <section className="page-sidebar-section">
  289. {this.renderVersionInfo(plugin.meta)}
  290. {isAdmin && this.renderSidebarIncludes(plugin.meta.includes)}
  291. {this.renderSidebarDependencies(plugin.meta.dependencies)}
  292. {this.renderSidebarLinks(plugin.meta.info)}
  293. </section>
  294. )}
  295. </aside>
  296. </div>
  297. )}
  298. </Page.Contents>
  299. </Page>
  300. );
  301. }
  302. }
  303. function getPluginTabsNav(
  304. plugin: GrafanaPlugin,
  305. appSubUrl: string,
  306. path: string,
  307. query: UrlQueryMap,
  308. isAdmin: boolean
  309. ): { defaultPage: string; nav: NavModel } {
  310. const { meta } = plugin;
  311. let defaultPage: string;
  312. const pages: NavModelItem[] = [];
  313. if (true) {
  314. pages.push({
  315. text: 'Readme',
  316. icon: 'fa fa-fw fa-file-text-o',
  317. url: `${appSubUrl}${path}?page=${PAGE_ID_README}`,
  318. id: PAGE_ID_README,
  319. });
  320. }
  321. // We allow non admins to see plugins but only their readme. Config is hidden even though the API needs to be
  322. // public for plugins to work properly.
  323. if (isAdmin) {
  324. // Only show Config/Pages for app
  325. if (meta.type === PluginType.app) {
  326. // Legacy App Config
  327. if (plugin.angularConfigCtrl) {
  328. pages.push({
  329. text: 'Config',
  330. icon: 'gicon gicon-cog',
  331. url: `${appSubUrl}${path}?page=${PAGE_ID_CONFIG_CTRL}`,
  332. id: PAGE_ID_CONFIG_CTRL,
  333. });
  334. defaultPage = PAGE_ID_CONFIG_CTRL;
  335. }
  336. if (plugin.configPages) {
  337. for (const page of plugin.configPages) {
  338. pages.push({
  339. text: page.title,
  340. icon: page.icon,
  341. url: path + '?page=' + page.id,
  342. id: page.id,
  343. });
  344. if (!defaultPage) {
  345. defaultPage = page.id;
  346. }
  347. }
  348. }
  349. // Check for the dashboard pages
  350. if (find(meta.includes, { type: PluginIncludeType.dashboard })) {
  351. pages.push({
  352. text: 'Dashboards',
  353. icon: 'gicon gicon-dashboard',
  354. url: `${appSubUrl}${path}?page=${PAGE_ID_DASHBOARDS}`,
  355. id: PAGE_ID_DASHBOARDS,
  356. });
  357. }
  358. }
  359. }
  360. if (!defaultPage) {
  361. defaultPage = pages[0].id; // the first tab
  362. }
  363. const node = {
  364. text: meta.name,
  365. img: meta.info.logos.large,
  366. subTitle: meta.info.author.name,
  367. breadcrumbs: [{ title: 'Plugins', url: '/plugins' }],
  368. url: `${appSubUrl}${path}`,
  369. children: setActivePage(query.page as string, pages, defaultPage),
  370. };
  371. return {
  372. defaultPage,
  373. nav: {
  374. node: node,
  375. main: node,
  376. },
  377. };
  378. }
  379. function setActivePage(pageId: string, pages: NavModelItem[], defaultPageId: string): NavModelItem[] {
  380. let found = false;
  381. const selected = pageId || defaultPageId;
  382. const changed = pages.map(p => {
  383. const active = !found && selected === p.id;
  384. if (active) {
  385. found = true;
  386. }
  387. return { ...p, active };
  388. });
  389. if (!found) {
  390. changed[0].active = true;
  391. }
  392. return changed;
  393. }
  394. function getPluginIcon(type: string) {
  395. switch (type) {
  396. case 'datasource':
  397. return 'gicon gicon-datasources';
  398. case 'panel':
  399. return 'icon-gf icon-gf-panel';
  400. case 'app':
  401. return 'icon-gf icon-gf-apps';
  402. case 'page':
  403. return 'icon-gf icon-gf-endpoint-tiny';
  404. case 'dashboard':
  405. return 'gicon gicon-dashboard';
  406. default:
  407. return 'icon-gf icon-gf-apps';
  408. }
  409. }
  410. const mapStateToProps = (state: StoreState) => ({
  411. pluginId: state.location.routeParams.pluginId,
  412. query: state.location.query,
  413. path: state.location.path,
  414. });
  415. export default hot(module)(connect(mapStateToProps)(PluginPage));