Panel/PluginList: Migrate to React (#31738)

* Panel/PluginList: Migrate to React
This commit is contained in:
kay delaney 2021-03-09 12:22:59 +00:00 committed by GitHub
parent 63746d027b
commit 3d459b556a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 214 additions and 111 deletions

View File

@ -0,0 +1,151 @@
import React from 'react';
import { useAsync } from 'react-use';
import { css, cx } from 'emotion';
import { GrafanaTheme, PanelProps, PluginMeta, PluginType } from '@grafana/data';
import { CustomScrollbar, ModalsController, stylesFactory, Tooltip, useStyles } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { UpdatePluginModal } from './components/UpdatePluginModal';
export function PluginList(props: PanelProps) {
const pluginState = useAsync(async () => {
const plugins: PluginMeta[] = await getBackendSrv().get('api/plugins', { embedded: 0, core: 0 });
return [
{ header: 'Installed Apps', list: plugins.filter((p) => p.type === PluginType.app), type: PluginType.app },
{ header: 'Installed Panels', list: plugins.filter((p) => p.type === PluginType.panel), type: PluginType.panel },
{
header: 'Installed Datasources',
list: plugins.filter((p) => p.type === PluginType.datasource),
type: PluginType.datasource,
},
];
}, []);
const styles = useStyles(getStyles);
const isAdmin = contextSrv.user.isGrafanaAdmin;
if (pluginState.loading || pluginState.value === undefined) {
return null;
}
return (
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
<div className={styles.pluginList}>
{pluginState.value.map((category) => (
<div className={styles.section} key={`category-${category.type}`}>
<h6 className={styles.sectionHeader}>{category.header}</h6>
{category.list.map((plugin) => (
<a className={styles.item} href={plugin.defaultNavUrl} key={`plugin-${plugin.id}`}>
<img src={plugin.info.logos.small} className={styles.image} width="17" height="17" alt="" />
<span className={styles.title}>{plugin.name}</span>
<span className={styles.version}>v{plugin.info.version}</span>
{isAdmin &&
(plugin.hasUpdate ? (
<ModalsController>
{({ showModal, hideModal }) => (
<Tooltip content={`New version: ${plugin.latestVersion}`} placement="top">
<span
className={cx(styles.message, styles.messageUpdate)}
onClick={(e) => {
e.preventDefault();
showModal(UpdatePluginModal, {
pluginID: plugin.id,
pluginName: plugin.name,
onDismiss: hideModal,
isOpen: true,
});
}}
>
Update available!
</span>
</Tooltip>
)}
</ModalsController>
) : plugin.enabled ? (
<span className={cx(styles.message, styles.messageNoUpdate)}>Up to date</span>
) : (
<span className={cx(styles.message, styles.messageEnable)}>Enable now</span>
))}
</a>
))}
{category.list.length === 0 && (
<a className={styles.item} href="https://grafana.com/plugins">
<span className={styles.noneInstalled}>
None installed. <em className={styles.emphasis}>Browse Grafana.com</em>
</span>
</a>
)}
</div>
))}
</div>
</CustomScrollbar>
);
}
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
pluginList: css`
display: flex;
flex-direction: column;
`,
section: css`
display: flex;
flex-direction: column;
&:not(:last-of-type) {
margin-bottom: 16px;
}
`,
sectionHeader: css`
color: ${theme.colors.textWeak};
margin-bottom: ${theme.spacing.d};
`,
image: css`
width: 17px;
margin-right: ${theme.spacing.xxs};
`,
title: css`
margin-right: calc(${theme.spacing.d} / 3);
`,
version: css`
font-size: ${theme.typography.size.sm};
color: ${theme.colors.textWeak};
`,
item: css`
display: flex;
justify-content: flex-start;
align-items: center;
cursor: pointer;
margin: ${theme.spacing.xxs};
padding: ${theme.spacing.sm};
background: ${theme.colors.dashboardBg};
border-radius: ${theme.border.radius.md};
`,
message: css`
margin-left: auto;
font-size: ${theme.typography.size.sm};
`,
messageEnable: css`
color: ${theme.colors.linkExternal};
&:hover {
border-bottom: ${theme.border.width.sm} solid ${theme.colors.linkExternal};
}
`,
messageUpdate: css`
&:hover {
border-bottom: ${theme.border.width.sm} solid ${theme.colors.text};
}
`,
messageNoUpdate: css`
color: ${theme.colors.textWeak};
`,
noneInstalled: css`
color: ${theme.colors.textWeak};
font-size: ${theme.typography.size.sm};
`,
emphasis: css`
font-weight: ${theme.typography.weight.semibold};
font-style: normal;
color: ${theme.colors.textWeak};
`,
}));

View File

@ -0,0 +1,59 @@
import React from 'react';
import { Modal, stylesFactory, useStyles } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { css } from 'emotion';
interface Props {
pluginName: string;
pluginID: string;
onConfirm?: () => void;
onDismiss?: () => void;
}
export function UpdatePluginModal({ pluginName, pluginID, onDismiss }: Props) {
const styles = useStyles(getStyles);
return (
<Modal title="Update Plugin" icon="cloud-download" onDismiss={onDismiss} isOpen>
<div className={styles.container}>
<p>Type the following on the command line to update {pluginName}.</p>
<pre>
<code>grafana-cli plugins update {pluginID}</code>
</pre>
<span className={styles.small}>
Check out {pluginName} on <a href={`https://grafana.com/plugins/${pluginID}`}>Grafana.com</a> for README and
changelog. If you do not have access to the command line, ask your Grafana administator.
</span>
</div>
<p className={styles.updateAllTip}>
<img className={styles.inlineLogo} src="public/img/grafana_icon.svg" />
<strong>Pro tip</strong>: To update all plugins at once, type{' '}
<code className={styles.codeSmall}>grafana-cli plugins update-all</code> on the command line.
</p>
</Modal>
);
}
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
small: css`
font-size: ${theme.typography.size.sm};
font-weight: ${theme.typography.weight.regular};
`,
codeSmall: css`
font-size: ${theme.typography.size.xs};
padding: ${theme.spacing.xxs};
margin: 0 ${theme.spacing.xxs};
`,
container: css`
margin-bottom: calc(${theme.spacing.d} * 2.5);
`,
updateAllTip: css`
color: ${theme.colors.textWeak};
font-size: ${theme.typography.size.sm};
`,
inlineLogo: css`
vertical-align: sub;
margin-right: calc(${theme.spacing.d} / 3);
width: ${theme.spacing.md};
`,
}));

View File

@ -1,32 +0,0 @@
<div class="pluginlist">
<div class="pluginlist-section" ng-repeat="category in ctrl.viewModel">
<h6 class="pluginlist-section-header">
{{category.header}}
</h6>
<div class="pluginlist-item" ng-repeat="plugin in category.list">
<a class="pluginlist-link pluginlist-link-{{plugin.state}} pointer" href="{{plugin.defaultNavUrl}}">
<span>
<img ng-src="{{plugin.info.logos.small}}" class="pluginlist-image">
<span class="pluginlist-title">{{plugin.name}}</span>
<span class="pluginlist-version">v{{plugin.info.version}}</span>
</span>
<span ng-if="ctrl.isAdmin">
<span class="pluginlist-message pluginlist-message--update" ng-show="plugin.hasUpdate" ng-click="ctrl.updateAvailable(plugin, $event)" bs-tooltip="'New version: ' + plugin.latestVersion">
Update available!
</span>
<span class="pluginlist-message pluginlist-message--enable" ng-show="!plugin.enabled && !plugin.hasUpdate">
Enable now
</span>
<span class="pluginlist-message pluginlist-message--no-update" ng-show="plugin.enabled && !plugin.hasUpdate">
Up to date
</span>
</span>
</a>
</div>
<div class="pluginlist-item" ng-show="category.list.length === 0">
<a class="pluginlist-link pluginlist-link-{{plugin.state}}" href="https://grafana.com/plugins">
<span class="pluginlist-none-installed">None installed. <span class="pluginlist-emphasis">Browse Grafana.com</span></span>
</a>
</div>
</div>
</div>

View File

@ -1,79 +0,0 @@
import _ from 'lodash';
import { PanelCtrl } from '../../../features/panel/panel_ctrl';
import { auto, IScope } from 'angular';
import { ContextSrv } from '../../../core/services/context_srv';
import { CoreEvents } from 'app/types';
import { getBackendSrv } from '@grafana/runtime';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
class PluginListCtrl extends PanelCtrl {
static templateUrl = 'module.html';
static scrollable = true;
pluginList: any[];
viewModel: any;
isAdmin: boolean;
// Set and populate defaults
panelDefaults = {};
/** @ngInject */
constructor($scope: IScope, $injector: auto.IInjectorService, contextSrv: ContextSrv) {
super($scope, $injector);
_.defaults(this.panel, this.panelDefaults);
this.isAdmin = contextSrv.hasRole('Admin');
this.pluginList = [];
this.viewModel = [
{ header: 'Installed Apps', list: [], type: 'app' },
{ header: 'Installed Panels', list: [], type: 'panel' },
{ header: 'Installed Datasources', list: [], type: 'datasource' },
];
this.update();
}
gotoPlugin(plugin: { id: any }, evt: any) {
if (evt) {
evt.stopPropagation();
}
this.$location.url(`plugins/${plugin.id}/edit`);
}
updateAvailable(plugin: any, $event: any) {
$event.stopPropagation();
$event.preventDefault();
const modalScope = this.$scope.$new(true);
modalScope.plugin = plugin;
this.publishAppEvent(CoreEvents.showModal, {
src: 'public/app/features/plugins/partials/update_instructions.html',
scope: modalScope,
});
}
update() {
promiseToDigest(this.$scope)(
getBackendSrv()
.get('api/plugins', { embedded: 0, core: 0 })
.then((plugins) => {
this.pluginList = plugins;
this.viewModel[0].list = _.filter(plugins, { type: 'app' });
this.viewModel[1].list = _.filter(plugins, { type: 'panel' });
this.viewModel[2].list = _.filter(plugins, { type: 'datasource' });
for (const plugin of this.pluginList) {
if (plugin.hasUpdate) {
plugin.state = 'has-update';
} else if (!plugin.enabled) {
plugin.state = 'not-enabled';
}
}
})
);
}
}
export { PluginListCtrl, PluginListCtrl as PanelCtrl };

View File

@ -0,0 +1,4 @@
import { PanelPlugin } from '@grafana/data';
import { PluginList } from './PluginList';
export const plugin = new PanelPlugin(PluginList);