grafana/public/app/features/plugins/PluginPage.tsx
Dominik Prokop a55a272276
Routing NG: Replace Angular routing with react-router (#31463)
* Add router packages

* Get react app root work instead of Angular one

* Logger util

* Patch Angular routing ($routeProvider, $routeParamsProvider)

* Use react-router-dom history instead of separate dependency

* Add test routes

* Sidemenu - use Link instead of anchors

* Patch Angular $location service (stub)

* WIP: geting rid of $location provider from TimeSrv

* Intercept anchor clicks to use history under the hood

* Sync Redux location slice with history state

* Make login/logout work

* Debug routes for testing

* Make force login work

* Make sure query param change does not recreate page components

* Hide side menu in specified locations

* Make the dashboar route query parameters work, make panel edit menu work

* Enable more routes

* Fix side menu

* Handle view modes

* Disable playlist routes

* Make SafeDynamicImport work again

* Bring back router-debug

* Separate redux location sync from route rendering

* Refactor updateLocation to thunk and move force refresh(login) to it

* Fixing init dashboard issue

* Support switching between dashboards without an unmount of DashboardPage

* More fixes for init dashboard and panel edit

* More type fixes

* Moving angular location wrapper out of main LocationService, and fixing typescript issues

* Fixed last typescript errors

* LocationService: Move to runtime and  remove getLocationService and export singleston const instead (#31523)

* Moving location service implementation to runtime and removing get function and making it a package const singleton

* Added test that used locationService directly

* removed unused import

* AngularApp: Moving angular dependencies and the app boot out of the main app into it's own file  (#31525)

* Fixes angular panels by calling the monkey patch

* Moving angular stuff to to it's own files

* udpated

* Fixing clicking on divs and spans inside anchor

* Moving app notifications out of angular app and removing angular directive wrapper

* Moving search from angular to react and removing angular search wrapper

* Clean up, tried to remove the redux location wrapper but requires a big update for DashboardPage, so adding it back

* Moving AppWrapper to root to limit circular dependencies (app/core -> app/routing and back)

* Open and close search now works

* Hide sidemenu when in kiosk mode

* Restoring some keybindings like ESC key

* Removed kiosk events and simplified it, just handled through updating URL

* Fixing typescript errors

* Simplified GrafanaRouteComponentProps and renamed to ContainerProps

* renamed back

* Changed AlertRuleList to use GrafanaRouteComponentProps and location.search passed to it

* Removing the reloadOnSearch property, this is not needed now for react as react by default does not unmount components when only url match or query parmas change

* SafeDynamicImport causing unmount un every search update, not sure how to fix yet

* Fix signature for SafeDynamicImport so we do not create new route components on every route render

* Removing the redux location wrapper as it was causing errors, and making dashboard page work with RouteProps (location, match) etc

* Updating DashboardPage and SoloPanelPage to use match params and history location

* Fixed DashboardPage tests

* Fixing solo route tests

* LocationService: Rename getCurrentLocation to just getLocation

* do not intercept link clicks with target blank or self

* Experimental useUrlParams hook

* Update DataSourceSettingsPage to use router match params

* fix links with urls that have no starting / to work like before

* Fix forceLogin

* Add queryParams to GrafanaRouteComponentProps

* PanelEditor get rid of updateLocation and location state

* Improve grafana route query params typing

* Add getSearchObject to LocationService

* Use DashboardPAge queryParams instead of location.search parsing

* Fix DashboardPage typing

* Fix some tests weirdness

* Bring back KeyboardSrv

* Fixes typescript issues

* Team pages now use router match params

* Get rid of  from GrafanaRouteComponent props

* Removed unnessary calls to getSearchObject when calling locationService.partial

* Updated DashboardPage tests after queryParams was added

* Fixing dashboard settings back

* GrafanaRoute: Adding tests and remove use of global locationService

* Fixing tests and typescript errors

* Bring back kiosk modes and add tests

* Fix TimeSrv tests

* Fix typecheck errors

* Fixing tests

* Updated SideMenu test to react-testing and wrapped component in Router, and fixed issue importing createMemoryHistory

* Get rid of routeChange event from TimeSrv from

* Fixed TopSectionItem test

* Trying to make basename work but failing

* Update TopSectionItem snapshot

* Fix TopSectionItem snapshot test

* Fix API keys creation

* Remove Angular dependencies from KeybindingSrv (#31617)

* Remove Angular dependency from KeybindingsSrv

* Fix tests and typecheck issues

* basename is starting to work

* Make dashboard save work

* KeybindingSrv: Remove as angular service and no usage angular scope

* So long bridge_srv, we won't miss you

* Update snapshots

* Dashboard: Refactoring ChangeTracker to use History api and no angular (#31653)

* Dashboard: Refactoring ChangeTracker to use History api and no angular

* Updated

* Removed logging

* fixed unit tests

* updated snapshots

* Mechanism for force reloading routes (#31683)

* e2e: Fixes various things in e2e scenarios after router migration (#31685)

* Explore: Update reading query params from router props and updating location via locationService (ReactRouter)  (#31688)

* RoutingNG: Initial explore redux location to router location migration

* Updated explore Wrapper tests

* Fixing more tests

* remove loggin

* rename back to make naming consistent

* Fixing return to dashboard button

* fixing navigation to explore from dashboard

* updated routeProps

* Updated tests

* Make DashboardListPage work

* Fixing navigation after add new data source, and fixes explore e2e

* Fixing solo panel page

* PluginsPage now works

* RoutingNG: When parsing and rendering url search/query params preseve old logic of handling booleans and arrays (#31725)

* RoutingNG: When parsing and rendering url search/query params preserve old logic of handling booleans and arrays

* Fixed test

* Make snapshots list work

* fixed alert notification channel edit page

* Simplify LocationService, did not need special handling for login or forceLogin as target _self on link already handles that

* fixed UserAdminPage

* fixed edit orgs page

* Fixing LdapPage

* fixing dashboard import

* Fixed new folder page

* Fixed data source dashboards page

* fixing Folder permissions and folder settings page

* fixing snapshot list page nav model

* remove unused file

* Added placeholder page for playlist

* Moved browser compatability to index-template

* Restored 404/default page

* Fixed reset password page

* Fixed SignUpInvited page

* Fixing CreateTeam, Create user page, add panel widget

* Restore browwser file to make tests happy

* Fixed unit tests

* Removed unused import

* Replacing usage of updateLocation

* Fixed test

* Updating search filters to use history / location service for filters

* remove unused file

* AppRootPage fixed

* Fixing test and search issue

* Changes to support enterprise extensions

* remove console.log

* Removing more use of redux location

* Fixed signup page

* removed unused old angular controllers

* Fixing bugs

* one final bugfix

* Removed location from redux state

* Fixing ts issues and tests

* Fixing test issue

* fixing tests

* Fixing tests

* removed unused stuff

* Fixed search test

* Adding some doc comments

* Routing NG: Angular location provider patch (#31773)

* Patch Angulars $location provider

* Update public/app/angular/bridgeReactAngularRouting.ts

* Remove only test

* Update tests, disable loggers in test env

* Routing NG: remove $location provider usage (#31816)

* Remove dashboard_loaders

* Remove $location from Analytics service, track page views form GrafanaRoute

* Remove NotificationsEditCtrl

* Remove Angular dependencies from uploadDashboardDirective

* Update public/app/features/dashboard/containers/DashboardPage.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Update public/app/features/dashboard/containers/DashboardPage.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Remove unused test helpers (#31831)

* Playlist react (#31829)

* playlist list in react

* Playlist start

* Things started to work

* Updated

* Handle empty list

* Fix ts

* Fixes and kiosk mode stuff

* Removed unused events

* fixing ts issue

* Another ts issue

* Fixing tests

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>

* fixed test

* Update public/app/AppWrapper.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Update public/app/AppWrapper.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Remove Angular dependency from DashboardLoaderSrv (#31863)

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
2021-03-10 18:03:36 +01:00

496 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Libraries
import React, { PureComponent } from 'react';
import find from 'lodash/find';
// Types
import {
AppPlugin,
GrafanaPlugin,
NavModel,
NavModelItem,
PluginDependencies,
PluginInclude,
PluginIncludeType,
PluginMeta,
PluginMetaInfo,
PluginSignatureStatus,
PluginType,
UrlQueryMap,
} from '@grafana/data';
import { AppNotificationSeverity } from 'app/types';
import { Alert, InfoBox, Tooltip } 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';
import { config } from 'app/core/config';
import { contextSrv } from '../../core/services/context_srv';
import { css } from 'emotion';
import { PluginSignatureBadge } from './PluginSignatureBadge';
import { selectors } from '@grafana/e2e-selectors';
import { ShowModalEvent } from 'app/types/events';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
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<Props, State> {
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 <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={queryParams} />;
}
}
}
// 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.publish(
new ShowModalEvent({
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(' ', '-');
const url = item.path ?? `plugins/${pluginId}/page/${page}`;
return (
<a href={url}>
<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" alt="Grafana logo" />
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="noreferrer noopener">
{link.name}
</a>
</li>
);
})}
</ul>
</section>
);
}
renderPluginNotice() {
const { plugin } = this.state;
if (!plugin) {
return null;
}
if (plugin.meta.signature === PluginSignatureStatus.internal) {
return null;
}
return (
<InfoBox
aria-label={selectors.pages.PluginPage.signatureInfo}
severity={plugin.meta.signature !== PluginSignatureStatus.valid ? 'warning' : 'info'}
urlTitle="Read more about plugins signing"
url="https://grafana.com/docs/grafana/latest/plugins/plugin-signatures/"
>
<PluginSignatureBadge
status={plugin.meta.signature}
className={css`
margin-top: 0;
`}
/>
<br />
<br />
<p>
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.
{plugin.meta.signature !== PluginSignatureStatus.valid &&
'Grafana Labs cant guarantee the integrity of this unsigned plugin. Ask the plugin author to request it to be signed.'}
</p>
</InfoBox>
);
}
render() {
const { loading, nav, plugin } = this.state;
const isAdmin = contextSrv.hasRole('Admin');
return (
<Page navModel={nav} aria-label={selectors.pages.PluginPage.page}>
<Page.Contents isLoading={loading}>
{plugin && (
<div className="sidebar-container">
<div className="sidebar-content">
{plugin.loadError && (
<Alert severity={AppNotificationSeverity.Error} title="Error Loading Plugin">
<>
Check the server startup logs for more information. <br />
If this plugin was loaded from git, make sure it was compiled.
</>
</Alert>
)}
{this.renderPluginNotice()}
{this.renderBody()}
</div>
<aside className="page-sidebar">
<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 | 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<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;
});
});
}
if (info.type === PluginType.renderer) {
return Promise.resolve({ meta: info } as GrafanaPlugin);
}
return Promise.reject('Unknown Plugin type: ' + info.type);
});
}
export default PluginPage;