diff --git a/pkg/api/index.go b/pkg/api/index.go index 1889aa61c04..3f9a42eafd6 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -376,14 +376,6 @@ func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink { }) } - if c.IsGrafanaAdmin { - if hs.Cfg.PluginAdminEnabled { - adminNavLinks = append(adminNavLinks, &dtos.NavLink{ - Text: "Plugin catalog", Id: "plugin-catalog", Url: hs.Cfg.AppSubURL + "/a/grafana-plugin-admin-app", Icon: "plug", - }) - } - } - if hs.Cfg.LDAPEnabled && hasAccess(ac.ReqGrafanaAdmin, ac.ActionLDAPStatusRead) { adminNavLinks = append(adminNavLinks, &dtos.NavLink{ Text: "LDAP", Id: "ldap", Url: hs.Cfg.AppSubURL + "/admin/ldap", Icon: "book", diff --git a/pkg/plugins/manager/manager_test.go b/pkg/plugins/manager/manager_test.go index 7a070727278..f76adabe430 100644 --- a/pkg/plugins/manager/manager_test.go +++ b/pkg/plugins/manager/manager_test.go @@ -40,7 +40,7 @@ func TestPluginManager_Init(t *testing.T) { assert.Empty(t, pm.scanningErrors) verifyCorePluginCatalogue(t, pm) - verifyBundledPluginCatalogue(t, pm) + verifyBundledPlugins(t, pm) }) t.Run("Base case with single external plugin", func(t *testing.T) { @@ -607,12 +607,11 @@ func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) { } } -func verifyBundledPluginCatalogue(t *testing.T, pm *PluginManager) { +func verifyBundledPlugins(t *testing.T, pm *PluginManager) { t.Helper() bundledPlugins := map[string]string{ - "input": "input-datasource", - "grafana-plugin-admin-app": "plugin-admin-app", + "input": "input-datasource", } for pluginID, pluginDir := range bundledPlugins { @@ -625,7 +624,6 @@ func verifyBundledPluginCatalogue(t *testing.T, pm *PluginManager) { } assert.NotNil(t, pm.dataSources["input"]) - assert.NotNil(t, pm.apps["grafana-plugin-admin-app"]) } type fakeBackendPluginManager struct { diff --git a/plugins-bundled/internal/plugin-admin-app/CHANGELOG.md b/plugins-bundled/internal/plugin-admin-app/CHANGELOG.md deleted file mode 100644 index 752430173e8..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -# Change Log - -Changes are included in the grafana core changelog \ No newline at end of file diff --git a/plugins-bundled/internal/plugin-admin-app/README.md b/plugins-bundled/internal/plugin-admin-app/README.md deleted file mode 100644 index 95a0dd7e7d0..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Grafana admin app - -The grafana catalog is enabled or disabled by setting `plugin_admin_enabled` in the setup files. diff --git a/plugins-bundled/internal/plugin-admin-app/jest.config.js b/plugins-bundled/internal/plugin-admin-app/jest.config.js deleted file mode 100644 index bcf17c9087d..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/jest.config.js +++ /dev/null @@ -1,8 +0,0 @@ -// This file is needed because it is used by vscode and other tools that -// call `jest` directly. However, unless you are doing anything special -// do not edit this file - -const standard = require('@grafana/toolkit/src/config/jest.plugin.config'); - -// This process will use the same config that `yarn test` is using -module.exports = standard.jestConfig(); diff --git a/plugins-bundled/internal/plugin-admin-app/package.json b/plugins-bundled/internal/plugin-admin-app/package.json deleted file mode 100644 index 9c67bca64a1..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@grafana-plugins/admin-app", - "version": "8.1.0-pre", - "description": "Plugins admin", - "private": true, - "repository": { - "type": "git", - "url": "http://github.com/grafana/grafana.git" - }, - "scripts": { - "build": "grafana-toolkit plugin:build", - "test": "grafana-toolkit plugin:test", - "dev": "grafana-toolkit plugin:dev", - "watch": "grafana-toolkit plugin:dev --watch" - }, - "author": "Grafana Labs", - "devDependencies": { - "@grafana/data": "8.1.0-pre", - "@grafana/runtime": "8.1.0-pre", - "@grafana/toolkit": "8.1.0-pre", - "@grafana/ui": "8.1.0-pre" - }, - "volta": { - "extends": "../../../package.json" - } -} diff --git a/plugins-bundled/internal/plugin-admin-app/src/RootPage.tsx b/plugins-bundled/internal/plugin-admin-app/src/RootPage.tsx deleted file mode 100644 index ee6f0610fe8..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/src/RootPage.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { AppRootProps } from '@grafana/data'; -import React from 'react'; -import { Discover } from 'pages/Discover'; -import { Browse } from 'pages/Browse'; -import { PluginDetails } from 'pages/PluginDetails'; -import { Library } from 'pages/Library'; -import { Route } from 'react-router-dom'; -import { config } from '@grafana/runtime'; -import { NotEnabled } from 'pages/NotEnabed'; - -export const CatalogRootPage = React.memo(function CatalogRootPage(props: AppRootProps) { - if (!config.pluginAdminEnabled) { - return ; - } - - return ( - <> - { - return ; // or discover? - }} - /> - - { - return ; - }} - /> - - { - return ; - }} - /> - - { - return ; - }} - /> - - { - return ; - }} - /> - - ); -}); diff --git a/plugins-bundled/internal/plugin-admin-app/src/config/Settings.tsx b/plugins-bundled/internal/plugin-admin-app/src/config/Settings.tsx deleted file mode 100644 index 993e0a0cc80..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/src/config/Settings.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { PluginConfigPageProps, AppPluginMeta } from '@grafana/data'; -import { LinkButton } from '@grafana/ui'; -import { PLUGIN_ROOT } from '../constants'; -import { config } from '@grafana/runtime'; - -interface Props extends PluginConfigPageProps {} - -export const Settings = ({ plugin }: Props) => { - if (!config.pluginAdminEnabled) { - return
Plugin admin is not enabled.
; - } - - return ( - <> - Manage plugins - - ); -}; diff --git a/plugins-bundled/internal/plugin-admin-app/src/constants.ts b/plugins-bundled/internal/plugin-admin-app/src/constants.ts deleted file mode 100644 index b87478b27a6..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/src/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const API_ROOT = '/api/plugins'; -export const PLUGIN_ID = 'grafana-plugin-admin-app'; -export const PLUGIN_ROOT = '/a/' + PLUGIN_ID; -export const GRAFANA_API_ROOT = '/api/gnet'; diff --git a/plugins-bundled/internal/plugin-admin-app/src/hooks/usePlugins.tsx b/plugins-bundled/internal/plugin-admin-app/src/hooks/usePlugins.tsx deleted file mode 100644 index 13577eef9b9..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/src/hooks/usePlugins.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { Plugin, Metadata } from '../types'; -import { api } from '../api'; - -type PluginsState = { - isLoading: boolean; - items: Plugin[]; - installedPlugins: any[]; -}; - -export const usePlugins = () => { - const [state, setState] = useState({ isLoading: true, items: [], installedPlugins: [] }); - - useEffect(() => { - const fetchPluginData = async () => { - const items = await api.getRemotePlugins(); - const filteredPlugins = items.filter((plugin) => { - const isNotRenderer = plugin.typeCode !== 'renderer'; - const isSigned = Boolean(plugin.versionSignatureType); - const isNotEnterprise = plugin.status !== 'enterprise'; - - return isNotRenderer && isSigned && isNotEnterprise; - }); - - const installedPlugins = await api.getInstalledPlugins(); - - setState((state) => ({ ...state, items: filteredPlugins, installedPlugins, isLoading: false })); - }; - - fetchPluginData(); - }, []); - - return state; -}; - -type PluginState = { - isLoading: boolean; - remote?: Plugin; - remoteVersions?: Array<{ version: string; createdAt: string }>; - local?: Metadata; -}; - -export const usePlugin = (slug: string): PluginState => { - const [state, setState] = useState({ - isLoading: true, - }); - - useEffect(() => { - const fetchPluginData = async () => { - const plugin = await api.getPlugin(slug); - setState({ ...plugin, isLoading: false }); - }; - fetchPluginData(); - }, [slug]); - - return state; -}; diff --git a/plugins-bundled/internal/plugin-admin-app/src/module.ts b/plugins-bundled/internal/plugin-admin-app/src/module.ts deleted file mode 100644 index f7281707768..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/src/module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AppPlugin } from '@grafana/data'; -import { Settings } from './config/Settings'; -import { CatalogRootPage } from './RootPage'; - -export const plugin = new AppPlugin().setRootPage(CatalogRootPage as any).addConfigPage({ - title: 'Settings', - icon: 'info-circle', - body: Settings as any, - id: 'settings', -}); diff --git a/plugins-bundled/internal/plugin-admin-app/src/pages/Browse.tsx b/plugins-bundled/internal/plugin-admin-app/src/pages/Browse.tsx deleted file mode 100644 index df0628aa3be..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/src/pages/Browse.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useEffect } from 'react'; -import { css } from '@emotion/css'; -import { AppRootProps, SelectableValue, dateTimeParse } from '@grafana/data'; -import { Field, LoadingPlaceholder, Select } from '@grafana/ui'; -import { useLocation } from 'react-router-dom'; -import { locationSearchToObject } from '@grafana/runtime'; - -import { PluginList } from '../components/PluginList'; -import { SearchField } from '../components/SearchField'; -import { HorizontalGroup } from '../components/HorizontalGroup'; -import { usePlugins } from '../hooks/usePlugins'; -import { useHistory } from '../hooks/useHistory'; -import { Plugin } from '../types'; -import { Page } from 'components/Page'; -import { CatalogTab, getCatalogNavModel } from './nav'; - -export const Browse = ({ meta, onNavChanged, basename }: AppRootProps) => { - const location = useLocation(); - const query = locationSearchToObject(location.search); - - const q = query.q as string; - const filterBy = query.filterBy as string; - const sortBy = query.sortBy as string; - - const plugins = usePlugins(); - const history = useHistory(); - - useEffect(() => { - onNavChanged(getCatalogNavModel(CatalogTab.Browse, basename)); - }, [onNavChanged, basename]); - - const onSortByChange = (value: SelectableValue) => { - history.push({ query: { sortBy: value.value } }); - }; - - const onFilterByChange = (value: SelectableValue) => { - history.push({ query: { filterBy: value.value } }); - }; - - const onSearch = (q: any) => { - history.push({ query: { filterBy: null, q } }); - }; - - const filteredPlugins = plugins.items - // Filter by plugin type - .filter((_) => !filterBy || _.typeCode === filterBy || filterBy === 'all') - // Naïve search by checking if any of the properties contains the query string - .filter((plugin) => { - const fields = [plugin.name.toLowerCase(), plugin.orgName.toLowerCase()]; - return !q || fields.some((f) => f.includes(q.toLowerCase())); - }); - - filteredPlugins.sort(sorters[sortBy || 'name']); - - return ( - - - -
- {plugins.isLoading ? ( - - ) : ( - `${filteredPlugins.length} ${filteredPlugins.length > 1 ? 'results' : 'result'}` - )} -
- - - -
- - {!plugins.isLoading && } -
- ); -}; - -const sorters: { [name: string]: (a: Plugin, b: Plugin) => number } = { - name: (a: Plugin, b: Plugin) => a.name.localeCompare(b.name), - updated: (a: Plugin, b: Plugin) => dateTimeParse(b.updatedAt).valueOf() - dateTimeParse(a.updatedAt).valueOf(), - published: (a: Plugin, b: Plugin) => dateTimeParse(b.createdAt).valueOf() - dateTimeParse(a.createdAt).valueOf(), - downloads: (a: Plugin, b: Plugin) => b.downloads - a.downloads, - popularity: (a: Plugin, b: Plugin) => b.popularity - a.popularity, -}; diff --git a/plugins-bundled/internal/plugin-admin-app/src/pages/Discover.tsx b/plugins-bundled/internal/plugin-admin-app/src/pages/Discover.tsx deleted file mode 100644 index 5a716f348d1..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/src/pages/Discover.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import { cx, css } from '@emotion/css'; - -import { dateTimeParse, AppRootProps, GrafanaTheme2 } from '@grafana/data'; -import { useStyles2, Legend, LinkButton } from '@grafana/ui'; -import { locationService } from '@grafana/runtime'; - -import { PLUGIN_ROOT } from '../constants'; -import { Card } from '../components/Card'; -import { Grid } from '../components/Grid'; -import { PluginList } from '../components/PluginList'; -import { SearchField } from '../components/SearchField'; -import { PluginTypeIcon } from '../components/PluginTypeIcon'; -import { usePlugins } from '../hooks/usePlugins'; -import { Plugin } from '../types'; -import { Page } from 'components/Page'; -import { Loader } from 'components/Loader'; - -export const Discover = ({ meta }: AppRootProps) => { - const { items, isLoading } = usePlugins(); - const styles = useStyles2(getStyles); - - const onSearch = (q: string) => { - locationService.push({ - pathname: `${PLUGIN_ROOT}/browse`, - search: `?q=${q}`, - }); - }; - - const featuredPlugins = items.filter((_) => _.featured > 0); - featuredPlugins.sort((a: Plugin, b: Plugin) => { - return b.featured - a.featured; - }); - - const recentlyAdded = items.filter((_) => true); - recentlyAdded.sort((a: Plugin, b: Plugin) => { - const at = dateTimeParse(a.createdAt); - const bt = dateTimeParse(b.createdAt); - return bt.valueOf() - at.valueOf(); - }); - - const mostPopular = items.filter((_) => true); - mostPopular.sort((a: Plugin, b: Plugin) => { - return b.popularity - a.popularity; - }); - - if (isLoading) { - return ; - } - - return ( - - - {/* Featured */} - Featured - - - {/* Most popular */} -
- Most popular - See more -
- - - {/* Recently added */} -
- Recently added - See more -
- - - {/* Browse by type */} - Browse by type - - } - text={ Panels} - /> - } - text={ Data sources} - /> - } - text={ Apps} - /> - -
- ); -}; - -const getStyles = (theme: GrafanaTheme2) => { - return { - legend: css` - margin-top: ${theme.spacing(4)}; - `, - legendContainer: css` - align-items: baseline; - display: flex; - justify-content: space-between; - `, - typeLegend: css` - align-items: center; - display: flex; - font-size: ${theme.typography.h4.fontSize}; - `, - }; -}; diff --git a/plugins-bundled/internal/plugin-admin-app/src/pages/Library.tsx b/plugins-bundled/internal/plugin-admin-app/src/pages/Library.tsx deleted file mode 100644 index 99cf61935df..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/src/pages/Library.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useEffect } from 'react'; -import { css } from '@emotion/css'; -import { AppRootProps, GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; -import { PLUGIN_ROOT } from '../constants'; -import { PluginList } from '../components/PluginList'; -import { usePlugins } from '../hooks/usePlugins'; -import { Page } from 'components/Page'; -import { Loader } from 'components/Loader'; -import { CatalogTab, getCatalogNavModel } from './nav'; - -export const Library = ({ meta, onNavChanged, basename }: AppRootProps) => { - const { isLoading, items, installedPlugins } = usePlugins(); - const styles = useStyles2(getStyles); - - useEffect(() => { - onNavChanged(getCatalogNavModel(CatalogTab.Library, basename)); - }, [onNavChanged, basename]); - - const filteredPlugins = items.filter((plugin) => !!installedPlugins.find((_) => _.id === plugin.slug)); - - if (isLoading) { - return ; - } - - return ( - -

Library

- {filteredPlugins.length > 0 ? ( - - ) : ( -

- You haven't installed any plugins. Browse the{' '} - - catalog - {' '} - for plugins to install. -

- )} -
- ); -}; - -const getStyles = (theme: GrafanaTheme2) => { - return { - header: css` - margin-bottom: ${theme.spacing(3)}; - margin-top: ${theme.spacing(3)}; - `, - link: css` - text-decoration: underline; - `, - }; -}; diff --git a/plugins-bundled/internal/plugin-admin-app/src/pages/NotEnabed.tsx b/plugins-bundled/internal/plugin-admin-app/src/pages/NotEnabed.tsx deleted file mode 100644 index 2386307fde5..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/src/pages/NotEnabed.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { Page } from 'components/Page'; -import { AppRootProps, NavModelItem } from '@grafana/data'; -import { css } from '@emotion/css'; - -export const NotEnabled = ({ onNavChanged }: AppRootProps) => { - const node: NavModelItem = { - id: 'not-found', - text: 'The plugin catalog is not enabled', - icon: 'exclamation-triangle', - url: 'not-found', - }; - onNavChanged({ - node: node, - main: node, - }); - - return ( - - To enable installing plugins, please refer to the{' '} - - Plugin Catalog - {' '} - instructions - - ); -}; diff --git a/plugins-bundled/internal/plugin-admin-app/src/pages/PluginDetails.tsx b/plugins-bundled/internal/plugin-admin-app/src/pages/PluginDetails.tsx deleted file mode 100644 index 6e5e656606f..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/src/pages/PluginDetails.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { css } from '@emotion/css'; - -import { AppRootProps, GrafanaTheme2 } from '@grafana/data'; -import { useStyles2, TabsBar, TabContent, Tab, Icon } from '@grafana/ui'; -import { useParams } from 'react-router-dom'; - -import { VersionList } from '../components/VersionList'; -import { InstallControls } from '../components/InstallControls'; -import { PLUGIN_ROOT, GRAFANA_API_ROOT } from '../constants'; -import { usePlugin } from '../hooks/usePlugins'; -import { Page } from 'components/Page'; -import { Loader } from 'components/Loader'; - -export const PluginDetails = ({ onNavChanged }: AppRootProps) => { - const { pluginId } = useParams<{ pluginId: string }>(); - - const [tabs, setTabs] = useState([ - { label: 'Overview', active: true }, - { label: 'Version history', active: false }, - ]); - - const { isLoading, local, remote, remoteVersions } = usePlugin(pluginId); - const styles = useStyles2(getStyles); - - const description = remote?.description; - const readme = remote?.readme; - const version = local?.info?.version || remote?.version; - const links = (local?.info?.links || remote?.json?.info?.links) ?? []; - const downloads = remote?.downloads; - - useEffect(() => { - onNavChanged(undefined as any); - }, [onNavChanged]); - - if (isLoading) { - return ; - } - - return ( - -
- -
-

{remote?.name}

-
- - {remote?.orgName} - - {links.map((link: any) => ( - - {link.name} - - ))} - {downloads && ( - - - {` ${new Intl.NumberFormat().format(downloads)}`}{' '} - - )} - {version && {version}} -
-

{description}

- {remote && } -
-
- - {tabs.map((tab, key) => ( - { - setTabs(tabs.map((tab, index) => ({ ...tab, active: index === key }))); - }} - /> - ))} - - - {tabs.find((_) => _.label === 'Overview')?.active && ( -
- )} - {tabs.find((_) => _.label === 'Version history')?.active && } - - - ); -}; - -export const getStyles = (theme: GrafanaTheme2) => { - return { - headerContainer: css` - display: flex; - margin-bottom: 24px; - margin-top: 24px; - min-height: 120px; - `, - headerWrapper: css` - margin-left: ${theme.spacing(3)}; - `, - headerLinks: css` - display: flex; - align-items: center; - margin-top: ${theme.spacing()}; - margin-bottom: ${theme.spacing(3)}; - - & > * { - &::after { - content: '|'; - padding: 0 ${theme.spacing()}; - } - } - & > *:last-child { - &::after { - content: ''; - padding-right: 0; - } - } - font-size: ${theme.typography.h4.fontSize}; - `, - headerOrgName: css` - font-size: ${theme.typography.h4.fontSize}; - `, - message: css` - color: ${theme.colors.text.secondary}; - `, - readme: css` - padding: ${theme.spacing(3, 4)}; - - & img { - max-width: 100%; - } - - h1, - h2, - h3 { - margin-top: ${theme.spacing(3)}; - margin-bottom: ${theme.spacing(2)}; - } - - *:first-child { - margin-top: 0; - } - - li { - margin-left: ${theme.spacing(2)}; - & > p { - margin: ${theme.spacing()} 0; - } - } - `, - }; -}; diff --git a/plugins-bundled/internal/plugin-admin-app/src/pages/index.ts b/plugins-bundled/internal/plugin-admin-app/src/pages/index.ts deleted file mode 100644 index 9206f6d6a07..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/src/pages/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { AppRootProps } from '@grafana/data'; -import { Discover } from './Discover'; -import { Browse } from './Browse'; -import { PluginDetails } from './PluginDetails'; -import { Library } from './Library'; - -export type PageDefinition = { - component: React.FC; - icon: string; - id: string; - text: string; -}; - -export const pages: PageDefinition[] = [ - { - component: Discover, - icon: 'file-alt', - id: 'discover', - text: 'Discover', - }, - { - component: Browse, - icon: 'file-alt', - id: 'browse', - text: 'Browse', - }, - { - component: Library, - icon: 'file-alt', - id: 'library', - text: 'Library', - }, - { - component: PluginDetails, - icon: 'file-alt', - id: 'plugin', - text: 'Plugin', - }, -]; diff --git a/plugins-bundled/internal/plugin-admin-app/src/plugin.json b/plugins-bundled/internal/plugin-admin-app/src/plugin.json deleted file mode 100644 index 2c275a22101..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/src/plugin.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "https://github.com/grafana/grafana/raw/master/docs/sources/developers/plugins/plugin.schema.json", - "type": "app", - "name": "Plugin Admin", - "id": "grafana-plugin-admin-app", - "backend": false, - "autoEnabled": true, - "pinned": false, - "info": { - "author": { - "name": "Grafana Labs", - "url": "https://grafana.com" - }, - "keywords": ["plugins"], - "logos": { - "small": "img/logo.svg", - "large": "img/logo.svg" - }, - "links": [], - "screenshots": [], - "version": "%VERSION%", - "updated": "%TODAY%" - }, - "dependencies": { - "grafanaDependency": ">=8.0.0", - "grafanaVersion": "8.0.x", - "plugins": [] - } -} diff --git a/plugins-bundled/internal/plugin-admin-app/tsconfig.json b/plugins-bundled/internal/plugin-admin-app/tsconfig.json deleted file mode 100644 index cd034409c3c..00000000000 --- a/plugins-bundled/internal/plugin-admin-app/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../../packages/grafana-toolkit/src/config/tsconfig.plugin.json", - "include": ["src", "types"], - "compilerOptions": { - "rootDir": "./src", - "baseUrl": "./src", - "typeRoots": ["./node_modules/@types", "../../../node_modules/@types"] - } -} diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index 7d843545e72..1daf1c74ecb 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -12,7 +12,6 @@ import { setPluginsSearchQuery } from './state/reducers'; import { useAsync } from 'react-use'; import { selectors } from '@grafana/e2e-selectors'; import { PluginsErrorsInfo } from './PluginsErrorsInfo'; -import { config } from '@grafana/runtime'; const mapStateToProps = (state: StoreState) => ({ navModel: getNavModel(state.navIndex, 'plugins'), @@ -47,12 +46,6 @@ export const PluginListPage: React.FC = ({ title: 'Find more plugins on Grafana.com', }; - if (config.pluginAdminEnabled) { - linkButton.href = '/a/grafana-plugin-admin-app/'; - linkButton.title = 'Install & manage plugins'; - actionTarget = undefined; - } - return ( diff --git a/plugins-bundled/internal/plugin-admin-app/src/api.ts b/public/app/features/plugins/admin/api.ts similarity index 62% rename from plugins-bundled/internal/plugin-admin-app/src/api.ts rename to public/app/features/plugins/admin/api.ts index 48d8420397c..b6c0030fc6a 100644 --- a/plugins-bundled/internal/plugin-admin-app/src/api.ts +++ b/public/app/features/plugins/admin/api.ts @@ -1,7 +1,7 @@ import { getBackendSrv } from '@grafana/runtime'; import { PluginMeta } from '@grafana/data'; import { API_ROOT, GRAFANA_API_ROOT } from './constants'; -import { Plugin, PluginDetails, Org } from './types'; +import { Plugin, PluginDetails, Org, LocalPlugin } from './types'; async function getRemotePlugins(): Promise { const res = await getBackendSrv().get(`${GRAFANA_API_ROOT}/plugins`); @@ -9,29 +9,42 @@ async function getRemotePlugins(): Promise { } async function getPlugin(slug: string): Promise { - const res = await getBackendSrv().get(`${GRAFANA_API_ROOT}/plugins/${slug}`); - - const versions = await getPluginVersions(slug); const installed = await getInstalledPlugins(); - const plugin = installed?.find((_: any) => { - return _.id === slug; + const localPlugin = installed?.find((plugin: LocalPlugin) => { + return plugin.id === slug; }); + const [remote, versions] = await Promise.all([getRemotePlugin(slug, localPlugin), getPluginVersions(slug)]); + return { - remote: res, + remote: remote, remoteVersions: versions, - local: plugin, + local: localPlugin, }; } +async function getRemotePlugin(slug: string, local: LocalPlugin | undefined): Promise { + try { + return await getBackendSrv().get(`${GRAFANA_API_ROOT}/plugins/${slug}`); + } catch (error) { + // this might be a plugin that doesn't exist on gcom. + error.isHandled = !!local; + return; + } +} + async function getPluginVersions(id: string): Promise { - const versions = await getBackendSrv().get(`${GRAFANA_API_ROOT}/plugins/${id}/versions`); - return versions.items; + try { + const versions = await getBackendSrv().get(`${GRAFANA_API_ROOT}/plugins/${id}/versions`); + return versions.items; + } catch (error) { + return []; + } } async function getInstalledPlugins(): Promise { - const installed = await getBackendSrv().get(`${API_ROOT}?core=0`); + const installed = await getBackendSrv().get(`${API_ROOT}`); return installed; } diff --git a/plugins-bundled/internal/plugin-admin-app/src/components/Card.tsx b/public/app/features/plugins/admin/components/Card.tsx similarity index 100% rename from plugins-bundled/internal/plugin-admin-app/src/components/Card.tsx rename to public/app/features/plugins/admin/components/Card.tsx diff --git a/plugins-bundled/internal/plugin-admin-app/src/components/Grid.tsx b/public/app/features/plugins/admin/components/Grid.tsx similarity index 100% rename from plugins-bundled/internal/plugin-admin-app/src/components/Grid.tsx rename to public/app/features/plugins/admin/components/Grid.tsx diff --git a/plugins-bundled/internal/plugin-admin-app/src/components/HorizontalGroup.tsx b/public/app/features/plugins/admin/components/HorizontalGroup.tsx similarity index 100% rename from plugins-bundled/internal/plugin-admin-app/src/components/HorizontalGroup.tsx rename to public/app/features/plugins/admin/components/HorizontalGroup.tsx diff --git a/plugins-bundled/internal/plugin-admin-app/src/components/InstallControls.tsx b/public/app/features/plugins/admin/components/InstallControls.tsx similarity index 97% rename from plugins-bundled/internal/plugin-admin-app/src/components/InstallControls.tsx rename to public/app/features/plugins/admin/components/InstallControls.tsx index c20d4d83839..3fa120d6692 100644 --- a/plugins-bundled/internal/plugin-admin-app/src/components/InstallControls.tsx +++ b/public/app/features/plugins/admin/components/InstallControls.tsx @@ -6,16 +6,16 @@ import { config } from '@grafana/runtime'; import { Button, HorizontalGroup, Icon, LinkButton, useStyles2 } from '@grafana/ui'; import { AppEvents, GrafanaTheme2 } from '@grafana/data'; -import { Metadata, Plugin } from '../types'; +import { LocalPlugin, Plugin } from '../types'; import { api } from '../api'; // This isn't exported in the sdk yet // @ts-ignore -import appEvents from 'grafana/app/core/app_events'; +import appEvents from 'app/core/app_events'; import { isGrafanaAdmin } from '../helpers'; interface Props { - localPlugin?: Metadata; + localPlugin?: LocalPlugin; remotePlugin: Plugin; } diff --git a/plugins-bundled/internal/plugin-admin-app/src/components/Loader.tsx b/public/app/features/plugins/admin/components/Loader.tsx similarity index 100% rename from plugins-bundled/internal/plugin-admin-app/src/components/Loader.tsx rename to public/app/features/plugins/admin/components/Loader.tsx diff --git a/plugins-bundled/internal/plugin-admin-app/src/components/Page.tsx b/public/app/features/plugins/admin/components/Page.tsx similarity index 100% rename from plugins-bundled/internal/plugin-admin-app/src/components/Page.tsx rename to public/app/features/plugins/admin/components/Page.tsx diff --git a/plugins-bundled/internal/plugin-admin-app/src/components/PluginList.tsx b/public/app/features/plugins/admin/components/PluginList.tsx similarity index 78% rename from plugins-bundled/internal/plugin-admin-app/src/components/PluginList.tsx rename to public/app/features/plugins/admin/components/PluginList.tsx index 4dcbcec1192..c4abe9d5836 100644 --- a/plugins-bundled/internal/plugin-admin-app/src/components/PluginList.tsx +++ b/public/app/features/plugins/admin/components/PluginList.tsx @@ -4,9 +4,10 @@ import { css } from '@emotion/css'; import { Card } from '../components/Card'; import { Grid } from '../components/Grid'; -import { PLUGIN_ROOT } from '../constants'; import { Plugin } from '../types'; import { GrafanaTheme2 } from '@grafana/data'; +import { isLocalPlugin } from '../guards'; +import { PluginLogo } from './PluginLogo'; interface Props { plugins: Plugin[]; @@ -18,15 +19,14 @@ export const PluginList = ({ plugins }: Props) => { return ( {plugins.map((plugin) => { - const { name, slug, version, orgName, typeCode } = plugin; - + const { name, orgName, typeCode } = plugin; return ( { ); }; +function getPluginId(plugin: Plugin): string { + if (isLocalPlugin(plugin)) { + return plugin.id; + } + return plugin.slug; +} + const getStyles = (theme: GrafanaTheme2) => ({ name: css` font-size: ${theme.typography.h4.fontSize}; diff --git a/public/app/features/plugins/admin/components/PluginLogo.tsx b/public/app/features/plugins/admin/components/PluginLogo.tsx new file mode 100644 index 00000000000..25526db35e0 --- /dev/null +++ b/public/app/features/plugins/admin/components/PluginLogo.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { isLocalPlugin } from '../guards'; +import { LocalPlugin, Plugin } from '../types'; + +type PluginLogoProps = { + plugin: Plugin | LocalPlugin | undefined; + className?: string; +}; + +export function PluginLogo({ plugin, className }: PluginLogoProps): React.ReactElement | null { + return ; +} + +function getImageSrc(plugin: Plugin | LocalPlugin | undefined): string { + if (!plugin) { + return 'https://grafana.com/api/plugins/404notfound/versions/none/logos/small'; + } + + if (isLocalPlugin(plugin)) { + return plugin?.info?.logos?.large; + } + + return `https://grafana.com/api/plugins/${plugin.slug}/versions/${plugin.version}/logos/small`; +} diff --git a/plugins-bundled/internal/plugin-admin-app/src/components/PluginTypeIcon.tsx b/public/app/features/plugins/admin/components/PluginTypeIcon.tsx similarity index 99% rename from plugins-bundled/internal/plugin-admin-app/src/components/PluginTypeIcon.tsx rename to public/app/features/plugins/admin/components/PluginTypeIcon.tsx index f4453132daf..bf4d39e1bee 100644 --- a/plugins-bundled/internal/plugin-admin-app/src/components/PluginTypeIcon.tsx +++ b/public/app/features/plugins/admin/components/PluginTypeIcon.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { css } from '@emotion/css'; -import { PluginTypeCode } from 'types'; +import { PluginTypeCode } from '../types'; interface PluginTypeIconProps { typeCode: PluginTypeCode; diff --git a/plugins-bundled/internal/plugin-admin-app/src/components/SearchField.tsx b/public/app/features/plugins/admin/components/SearchField.tsx similarity index 100% rename from plugins-bundled/internal/plugin-admin-app/src/components/SearchField.tsx rename to public/app/features/plugins/admin/components/SearchField.tsx diff --git a/plugins-bundled/internal/plugin-admin-app/src/components/VersionList.tsx b/public/app/features/plugins/admin/components/VersionList.tsx similarity index 90% rename from plugins-bundled/internal/plugin-admin-app/src/components/VersionList.tsx rename to public/app/features/plugins/admin/components/VersionList.tsx index a801d4efeb4..d20d2c42dd9 100644 --- a/plugins-bundled/internal/plugin-admin-app/src/components/VersionList.tsx +++ b/public/app/features/plugins/admin/components/VersionList.tsx @@ -12,6 +12,10 @@ interface Props { export const VersionList = ({ versions }: Props) => { const styles = useStyles2(getStyles); + if (versions.length === 0) { + return
No version history was found.
; + } + return (
diff --git a/public/app/features/plugins/admin/constants.ts b/public/app/features/plugins/admin/constants.ts new file mode 100644 index 00000000000..8a37d9b97f0 --- /dev/null +++ b/public/app/features/plugins/admin/constants.ts @@ -0,0 +1,2 @@ +export const API_ROOT = '/api/plugins'; +export const GRAFANA_API_ROOT = '/api/gnet'; diff --git a/public/app/features/plugins/admin/guards.ts b/public/app/features/plugins/admin/guards.ts new file mode 100644 index 00000000000..29a22132891 --- /dev/null +++ b/public/app/features/plugins/admin/guards.ts @@ -0,0 +1,6 @@ +import { LocalPlugin } from './types'; + +export function isLocalPlugin(plugin: any): plugin is LocalPlugin { + // super naive way of figuring out if this is a local plugin + return 'category' in plugin; +} diff --git a/plugins-bundled/internal/plugin-admin-app/src/helpers.ts b/public/app/features/plugins/admin/helpers.ts similarity index 100% rename from plugins-bundled/internal/plugin-admin-app/src/helpers.ts rename to public/app/features/plugins/admin/helpers.ts diff --git a/plugins-bundled/internal/plugin-admin-app/src/hooks/useHistory.tsx b/public/app/features/plugins/admin/hooks/useHistory.tsx similarity index 100% rename from plugins-bundled/internal/plugin-admin-app/src/hooks/useHistory.tsx rename to public/app/features/plugins/admin/hooks/useHistory.tsx diff --git a/public/app/features/plugins/admin/hooks/usePlugins.tsx b/public/app/features/plugins/admin/hooks/usePlugins.tsx new file mode 100644 index 00000000000..5458f20e1c8 --- /dev/null +++ b/public/app/features/plugins/admin/hooks/usePlugins.tsx @@ -0,0 +1,111 @@ +import { useEffect, useMemo, useState } from 'react'; + +import { Plugin, LocalPlugin } from '../types'; +import { api } from '../api'; + +type PluginsState = { + isLoading: boolean; + items: Plugin[]; + installedPlugins: any[]; +}; + +export const usePlugins = () => { + const [state, setState] = useState({ isLoading: true, items: [], installedPlugins: [] }); + + useEffect(() => { + const fetchPluginData = async () => { + const items = await api.getRemotePlugins(); + const filteredPlugins = items.filter((plugin) => { + const isNotRenderer = plugin.typeCode !== 'renderer'; + const isSigned = Boolean(plugin.versionSignatureType); + const isNotEnterprise = plugin.status !== 'enterprise'; + + return isNotRenderer && isSigned && isNotEnterprise; + }); + + const installedPlugins = await api.getInstalledPlugins(); + + setState((state) => ({ ...state, items: filteredPlugins, installedPlugins, isLoading: false })); + }; + + fetchPluginData(); + }, []); + + return state; +}; + +type FilteredPluginsState = { + isLoading: boolean; + items: Plugin[]; +}; + +export const usePluginsByFilter = (searchBy: string, filterBy: string): FilteredPluginsState => { + const plugins = usePlugins(); + const all = useMemo(() => { + const combined: Plugin[] = []; + Array.prototype.push.apply(combined, plugins.items); + Array.prototype.push.apply(combined, plugins.installedPlugins); + + const bySlug = combined.reduce((unique: Record, plugin) => { + unique[plugin.slug] = plugin; + return unique; + }, {}); + + return Object.values(bySlug); + }, [plugins.items, plugins.installedPlugins]); + + if (filterBy === 'installed') { + return { + isLoading: plugins.isLoading, + items: applySearchFilter(searchBy, plugins.installedPlugins ?? []), + }; + } + + return { + isLoading: plugins.isLoading, + items: applySearchFilter(searchBy, all), + }; +}; + +function applySearchFilter(searchBy: string | undefined, plugins: Plugin[]): Plugin[] { + if (!searchBy) { + return plugins; + } + + return plugins.filter((plugin) => { + const fields: String[] = []; + + if (plugin.name) { + fields.push(plugin.name.toLowerCase()); + } + + if (plugin.orgName) { + fields.push(plugin.orgName.toLowerCase()); + } + + return fields.some((f) => f.includes(searchBy.toLowerCase())); + }); +} + +type PluginState = { + isLoading: boolean; + remote?: Plugin; + remoteVersions?: Array<{ version: string; createdAt: string }>; + local?: LocalPlugin; +}; + +export const usePlugin = (slug: string): PluginState => { + const [state, setState] = useState({ + isLoading: true, + }); + + useEffect(() => { + const fetchPluginData = async () => { + const plugin = await api.getPlugin(slug); + setState({ ...plugin, isLoading: false }); + }; + fetchPluginData(); + }, [slug]); + + return state; +}; diff --git a/plugins-bundled/internal/plugin-admin-app/src/img/logo.svg b/public/app/features/plugins/admin/img/logo.svg similarity index 100% rename from plugins-bundled/internal/plugin-admin-app/src/img/logo.svg rename to public/app/features/plugins/admin/img/logo.svg diff --git a/public/app/features/plugins/admin/pages/Browse.tsx b/public/app/features/plugins/admin/pages/Browse.tsx new file mode 100644 index 00000000000..546d663387f --- /dev/null +++ b/public/app/features/plugins/admin/pages/Browse.tsx @@ -0,0 +1,103 @@ +import React, { ReactElement } from 'react'; +import { css } from '@emotion/css'; +import { SelectableValue, dateTimeParse } from '@grafana/data'; +import { Field, LoadingPlaceholder, Select } from '@grafana/ui'; +import { useLocation } from 'react-router-dom'; +import { locationSearchToObject } from '@grafana/runtime'; + +import { PluginList } from '../components/PluginList'; +import { SearchField } from '../components/SearchField'; +import { HorizontalGroup } from '../components/HorizontalGroup'; +import { useHistory } from '../hooks/useHistory'; +import { Plugin } from '../types'; +import { Page as PluginPage } from '../components/Page'; +import { Page } from 'app/core/components/Page/Page'; +import { usePluginsByFilter } from '../hooks/usePlugins'; +import { useSelector } from 'react-redux'; +import { StoreState } from 'app/types/store'; +import { getNavModel } from 'app/core/selectors/navModel'; + +export default function Browse(): ReactElement { + const location = useLocation(); + const query = locationSearchToObject(location.search); + const navModel = useSelector((state: StoreState) => getNavModel(state.navIndex, 'plugins')); + + const q = query.q as string; + const filterBy = (query.filterBy as string) ?? 'installed'; + const sortBy = (query.sortBy as string) ?? 'name'; + + const plugins = usePluginsByFilter(q, filterBy); + const sortedPlugins = plugins.items.sort(sorters[sortBy]); + const history = useHistory(); + + const onSortByChange = (value: SelectableValue) => { + history.push({ query: { sortBy: value.value } }); + }; + + const onFilterByChange = (value: SelectableValue) => { + history.push({ query: { filterBy: value.value } }); + }; + + const onSearch = (q: any) => { + history.push({ query: { filterBy: null, q } }); + }; + + return ( + + + + + +
+ {plugins.isLoading ? ( + + ) : ( + `${sortedPlugins.length} ${sortedPlugins.length > 1 ? 'results' : 'result'}` + )} +
+ + + +
+ + {!plugins.isLoading && } +
+
+
+ ); +} + +const sorters: { [name: string]: (a: Plugin, b: Plugin) => number } = { + name: (a: Plugin, b: Plugin) => a.name.localeCompare(b.name), + updated: (a: Plugin, b: Plugin) => dateTimeParse(b.updatedAt).valueOf() - dateTimeParse(a.updatedAt).valueOf(), + published: (a: Plugin, b: Plugin) => dateTimeParse(b.createdAt).valueOf() - dateTimeParse(a.createdAt).valueOf(), + downloads: (a: Plugin, b: Plugin) => b.downloads - a.downloads, + popularity: (a: Plugin, b: Plugin) => b.popularity - a.popularity, +}; diff --git a/public/app/features/plugins/admin/pages/Discover.tsx b/public/app/features/plugins/admin/pages/Discover.tsx new file mode 100644 index 00000000000..dc987ef8ae3 --- /dev/null +++ b/public/app/features/plugins/admin/pages/Discover.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { cx, css } from '@emotion/css'; + +import { dateTimeParse, GrafanaTheme2 } from '@grafana/data'; +import { useStyles2, Legend, LinkButton } from '@grafana/ui'; +import { locationService } from '@grafana/runtime'; + +import { Card } from '../components/Card'; +import { Grid } from '../components/Grid'; +import { PluginList } from '../components/PluginList'; +import { SearchField } from '../components/SearchField'; +import { PluginTypeIcon } from '../components/PluginTypeIcon'; +import { usePlugins } from '../hooks/usePlugins'; +import { Plugin } from '../types'; +import { Page as PluginPage } from '../components/Page'; +import { Loader } from '../components/Loader'; +import { Page } from 'app/core/components/Page/Page'; + +export default function Discover(): JSX.Element | null { + const { items, isLoading } = usePlugins(); + const styles = useStyles2(getStyles); + + const onSearch = (q: string) => { + locationService.push({ + pathname: '/plugins/browse', + search: `?q=${q}`, + }); + }; + + const featuredPlugins = items.filter((_) => _.featured > 0); + featuredPlugins.sort((a: Plugin, b: Plugin) => { + return b.featured - a.featured; + }); + + const recentlyAdded = items.filter((_) => true); + recentlyAdded.sort((a: Plugin, b: Plugin) => { + const at = dateTimeParse(a.createdAt); + const bt = dateTimeParse(b.createdAt); + return bt.valueOf() - at.valueOf(); + }); + + const mostPopular = items.filter((_) => true); + mostPopular.sort((a: Plugin, b: Plugin) => { + return b.popularity - a.popularity; + }); + + if (isLoading) { + return ( + + + + + + ); + } + + return ( + + + + + {/* Featured */} + Featured + + + {/* Most popular */} +
+ Most popular + See more +
+ + + {/* Recently added */} +
+ Recently added + See more +
+ + + {/* Browse by type */} + Browse by type + + } + text={ Panels} + /> + } + text={ Data sources} + /> + } + text={ Apps} + /> + +
+
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + legend: css` + margin-top: ${theme.spacing(4)}; + `, + legendContainer: css` + align-items: baseline; + display: flex; + justify-content: space-between; + `, + typeLegend: css` + align-items: center; + display: flex; + font-size: ${theme.typography.h4.fontSize}; + `, + }; +}; diff --git a/public/app/features/plugins/admin/pages/Library.tsx b/public/app/features/plugins/admin/pages/Library.tsx new file mode 100644 index 00000000000..755b6ee2d21 --- /dev/null +++ b/public/app/features/plugins/admin/pages/Library.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; +import { PluginList } from '../components/PluginList'; +import { usePlugins } from '../hooks/usePlugins'; +import { Page as PluginPage } from '../components/Page'; +import { Loader } from '../components/Loader'; +import { CatalogTab, getCatalogNavModel } from './nav'; +import { Page } from 'app/core/components/Page/Page'; + +export default function Library(): JSX.Element | null { + const { isLoading, items, installedPlugins } = usePlugins(); + const styles = useStyles2(getStyles); + + const filteredPlugins = items.filter((plugin) => !!installedPlugins.find((_) => _.id === plugin.slug)); + + if (isLoading) { + return ( + + + + + + ); + } + + return ( + + + +

Library

+ {filteredPlugins.length > 0 ? ( + + ) : ( +

+ You haven't installed any plugins. Browse the{' '} + + catalog + {' '} + for plugins to install. +

+ )} +
+
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + header: css` + margin-bottom: ${theme.spacing(3)}; + margin-top: ${theme.spacing(3)}; + `, + link: css` + text-decoration: underline; + `, + }; +}; diff --git a/public/app/features/plugins/admin/pages/NotEnabed.tsx b/public/app/features/plugins/admin/pages/NotEnabed.tsx new file mode 100644 index 00000000000..e0442d31c39 --- /dev/null +++ b/public/app/features/plugins/admin/pages/NotEnabed.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { css } from '@emotion/css'; +import { NavModel, NavModelItem } from '@grafana/data'; +import { Page as PluginPage } from '../components/Page'; +import { Page } from 'app/core/components/Page/Page'; + +const node: NavModelItem = { + id: 'not-found', + text: 'The plugin catalog is not enabled', + icon: 'exclamation-triangle', + url: 'not-found', +}; + +const navModel: NavModel = { node, main: node }; + +export default function NotEnabled(): JSX.Element | null { + return ( + + + + To enable installing plugins via catalog, please refer to the{' '} + + Plugin Catalog + {' '} + instructions + + + + ); +} diff --git a/public/app/features/plugins/admin/pages/PluginDetails.tsx b/public/app/features/plugins/admin/pages/PluginDetails.tsx new file mode 100644 index 00000000000..a3d83b03bff --- /dev/null +++ b/public/app/features/plugins/admin/pages/PluginDetails.tsx @@ -0,0 +1,167 @@ +import React, { useState } from 'react'; +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2, TabsBar, TabContent, Tab, Icon } from '@grafana/ui'; +import { useParams } from 'react-router-dom'; + +import { VersionList } from '../components/VersionList'; +import { InstallControls } from '../components/InstallControls'; +import { usePlugin } from '../hooks/usePlugins'; +import { Page as PluginPage } from '../components/Page'; +import { Loader } from '../components/Loader'; +import { Page } from 'app/core/components/Page/Page'; +import { PluginLogo } from '../components/PluginLogo'; + +export default function PluginDetails(): JSX.Element | null { + const { pluginId } = useParams<{ pluginId: string }>(); + + const [tabs, setTabs] = useState([ + { label: 'Overview', active: true }, + { label: 'Version history', active: false }, + ]); + + const { isLoading, local, remote, remoteVersions } = usePlugin(pluginId); + const styles = useStyles2(getStyles); + + const description = remote?.description ?? local?.info?.description; + const readme = remote?.readme; + const version = local?.info?.version || remote?.version; + const links = (local?.info?.links || remote?.json?.info?.links) ?? []; + const downloads = remote?.downloads; + + if (isLoading) { + return ( + + + + ); + } + + return ( + + +
+ + +
+

{remote?.name ?? local?.name}

+
+ + {remote?.orgName ?? local?.info?.author?.name} + + {links.map((link: any) => ( + + {link.name} + + ))} + {downloads && ( + + + {` ${new Intl.NumberFormat().format(downloads)}`}{' '} + + )} + {version && {version}} +
+

{description}

+ {remote && } +
+
+ + {tabs.map((tab, key) => ( + { + setTabs(tabs.map((tab, index) => ({ ...tab, active: index === key }))); + }} + /> + ))} + + + {tabs.find((_) => _.label === 'Overview')?.active && ( +
+ )} + {tabs.find((_) => _.label === 'Version history')?.active && } + + + + ); +} + +export const getStyles = (theme: GrafanaTheme2) => { + return { + headerContainer: css` + display: flex; + margin-bottom: 24px; + margin-top: 24px; + min-height: 120px; + `, + headerWrapper: css` + margin-left: ${theme.spacing(3)}; + `, + headerLinks: css` + display: flex; + align-items: center; + margin-top: ${theme.spacing()}; + margin-bottom: ${theme.spacing(3)}; + + & > * { + &::after { + content: '|'; + padding: 0 ${theme.spacing()}; + } + } + & > *:last-child { + &::after { + content: ''; + padding-right: 0; + } + } + font-size: ${theme.typography.h4.fontSize}; + `, + headerOrgName: css` + font-size: ${theme.typography.h4.fontSize}; + `, + message: css` + color: ${theme.colors.text.secondary}; + `, + readme: css` + padding: ${theme.spacing(3, 4)}; + + & img { + max-width: 100%; + } + + h1, + h2, + h3 { + margin-top: ${theme.spacing(3)}; + margin-bottom: ${theme.spacing(2)}; + } + + *:first-child { + margin-top: 0; + } + + li { + margin-left: ${theme.spacing(2)}; + & > p { + margin: ${theme.spacing()} 0; + } + } + `, + }; +}; diff --git a/plugins-bundled/internal/plugin-admin-app/src/pages/nav.ts b/public/app/features/plugins/admin/pages/nav.ts similarity index 86% rename from plugins-bundled/internal/plugin-admin-app/src/pages/nav.ts rename to public/app/features/plugins/admin/pages/nav.ts index f8c6485c4d9..9f6e96ebb28 100644 --- a/plugins-bundled/internal/plugin-admin-app/src/pages/nav.ts +++ b/public/app/features/plugins/admin/pages/nav.ts @@ -16,17 +16,10 @@ export function getCatalogNavModel(tab: CatalogTab, baseURL: string): NavModel { pages.push({ text: 'Browse', icon: 'icon-gf icon-gf-apps', - url: `${baseURL}`, + url: `${baseURL}${CatalogTab.Browse}`, id: CatalogTab.Browse, }); - // pages.push({ - // text: 'Discover', - // icon: 'file-alt', - // url: `${baseURL}${CatalogTab.Discover}`, - // id: CatalogTab.Discover, - // }); - pages.push({ text: 'Library', icon: 'icon-gf icon-gf-apps', diff --git a/plugins-bundled/internal/plugin-admin-app/src/types.ts b/public/app/features/plugins/admin/types.ts similarity index 69% rename from plugins-bundled/internal/plugin-admin-app/src/types.ts rename to public/app/features/plugins/admin/types.ts index 11e6ab0958a..7b3f105b0d1 100644 --- a/plugins-bundled/internal/plugin-admin-app/src/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -42,15 +42,39 @@ export interface Plugin { }; } -export type Metadata = { +export type LocalPlugin = { + category: string; + defaultNavUrl: string; + enabled: boolean; + hasUpdate: boolean; + id: string; info: { - version: string; + author: { + name: string; + url: string; + }; + build: {}; + description: string; links: Array<{ name: string; url: string; }>; + logos: { + large: string; + small: string; + }; + updated: string; + version: string; }; - dev: boolean; + latestVersion: string; + name: string; + pinned: boolean; + signature: string; + signatureOrg: string; + signatureType: string; + state: string; + type: string; + dev: boolean | undefined; }; export interface Version { @@ -61,7 +85,7 @@ export interface Version { export interface PluginDetails { remote?: Plugin; remoteVersions?: Version[]; - local?: Metadata; + local?: LocalPlugin; } export interface Org { diff --git a/public/app/features/plugins/routes.ts b/public/app/features/plugins/routes.ts new file mode 100644 index 00000000000..02352b28cb1 --- /dev/null +++ b/public/app/features/plugins/routes.ts @@ -0,0 +1,39 @@ +import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; +import { config } from 'app/core/config'; +import { RouteDescriptor } from 'app/core/navigation/types'; + +export function getPluginsAdminRoutes(cfg = config): RouteDescriptor[] { + if (!cfg.pluginAdminEnabled) { + return [ + { + path: '/plugins', + component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './PluginListPage')), + }, + { + path: '/plugins/browse', + component: SafeDynamicImport( + () => import(/* webpackChunkName: "PluginAdminNotEnabled" */ './admin/pages/NotEnabed') + ), + }, + { + path: '/plugins/:pluginId/', + component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginPage" */ './PluginPage')), + }, + ]; + } + + return [ + { + path: '/plugins', + component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './admin/pages/Browse')), + }, + { + path: '/plugins/browse', + component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './admin/pages/Browse')), + }, + { + path: '/plugins/:pluginId/', + component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginPage" */ './admin/pages/PluginDetails')), + }, + ]; +} diff --git a/public/app/plugins/panel/pluginlist/PluginList.tsx b/public/app/plugins/panel/pluginlist/PluginList.tsx index 1692ef90a5e..c3af90ac740 100644 --- a/public/app/plugins/panel/pluginlist/PluginList.tsx +++ b/public/app/plugins/panel/pluginlist/PluginList.tsx @@ -3,7 +3,6 @@ import { useAsync } from 'react-use'; import { css, cx } from '@emotion/css'; import { GrafanaTheme, PanelProps, PluginMeta, PluginType } from '@grafana/data'; import { CustomScrollbar, ModalsController, stylesFactory, Tooltip, useStyles } from '@grafana/ui'; -import { config, locationService } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { UpdatePluginModal } from './components/UpdatePluginModal'; @@ -49,16 +48,13 @@ export function PluginList(props: PanelProps) { className={cx(styles.message, styles.messageUpdate)} onClick={(e) => { e.preventDefault(); - if (config.pluginAdminEnabled) { - locationService.push(`/a/grafana-plugin-admin-app/plugin/${plugin.id}`); - } else { - showModal(UpdatePluginModal, { - pluginID: plugin.id, - pluginName: plugin.name, - onDismiss: hideModal, - isOpen: true, - }); - } + + showModal(UpdatePluginModal, { + pluginID: plugin.id, + pluginName: plugin.name, + onDismiss: hideModal, + isOpen: true, + }); }} > Update available! diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 3db699be012..cad127c3b0f 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -8,6 +8,7 @@ import { SafeDynamicImport } from '../core/components/DynamicImports/SafeDynamic import { RouteDescriptor } from '../core/navigation/types'; import { Redirect } from 'react-router-dom'; import ErrorPage from 'app/core/components/ErrorPage/ErrorPage'; +import { getPluginsAdminRoutes } from 'app/features/plugins/routes'; import { contextSrv } from 'app/core/services/context_srv'; export const extraRoutes: RouteDescriptor[] = []; @@ -322,16 +323,6 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "SnapshotListPage" */ 'app/features/manage-dashboards/SnapshotListPage') ), }, - { - path: '/plugins', - component: SafeDynamicImport( - () => import(/* webpackChunkName: "PluginListPage" */ 'app/features/plugins/PluginListPage') - ), - }, - { - path: '/plugins/:pluginId/', - component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginPage" */ '../features/plugins/PluginPage')), - }, // TODO[Router] // { // path: '/plugins/:pluginId/page/:slug', @@ -517,12 +508,12 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "LibraryPanelsPage"*/ 'app/features/library-panels/LibraryPanelsPage') ), }, + ...getPluginsAdminRoutes(), ...extraRoutes, { path: '/*', component: ErrorPage, }, - // TODO[Router] // ...playlistRoutes, ];