diff --git a/public/app/plugins/panel/pluginlist/PluginList.tsx b/public/app/plugins/panel/pluginlist/PluginList.tsx
new file mode 100644
index 00000000000..44726ac04f1
--- /dev/null
+++ b/public/app/plugins/panel/pluginlist/PluginList.tsx
@@ -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 (
+
+
+ {pluginState.value.map((category) => (
+
+ ))}
+
+
+ );
+}
+
+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};
+ `,
+}));
diff --git a/public/app/plugins/panel/pluginlist/components/UpdatePluginModal.tsx b/public/app/plugins/panel/pluginlist/components/UpdatePluginModal.tsx
new file mode 100644
index 00000000000..ab523789b6e
--- /dev/null
+++ b/public/app/plugins/panel/pluginlist/components/UpdatePluginModal.tsx
@@ -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 (
+
+
+
Type the following on the command line to update {pluginName}.
+
+ grafana-cli plugins update {pluginID}
+
+
+ Check out {pluginName} on Grafana.com for README and
+ changelog. If you do not have access to the command line, ask your Grafana administator.
+
+
+
+
+ Pro tip: To update all plugins at once, type{' '}
+ grafana-cli plugins update-all on the command line.
+
+
+ );
+}
+
+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};
+ `,
+}));
diff --git a/public/app/plugins/panel/pluginlist/module.html b/public/app/plugins/panel/pluginlist/module.html
deleted file mode 100644
index 1ab69bbc62a..00000000000
--- a/public/app/plugins/panel/pluginlist/module.html
+++ /dev/null
@@ -1,32 +0,0 @@
-
diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts
deleted file mode 100644
index 76f9f2a0d98..00000000000
--- a/public/app/plugins/panel/pluginlist/module.ts
+++ /dev/null
@@ -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 };
diff --git a/public/app/plugins/panel/pluginlist/module.tsx b/public/app/plugins/panel/pluginlist/module.tsx
new file mode 100644
index 00000000000..86f97e04b5d
--- /dev/null
+++ b/public/app/plugins/panel/pluginlist/module.tsx
@@ -0,0 +1,4 @@
+import { PanelPlugin } from '@grafana/data';
+import { PluginList } from './PluginList';
+
+export const plugin = new PanelPlugin(PluginList);