diff --git a/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts b/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts index b49aad19f33..112ebbc09cc 100644 --- a/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts +++ b/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts @@ -17,6 +17,7 @@ export default { isEnterprise: false, isInstalled: false, isDisabled: false, + isPublished: true, name: 'Zabbix', orgName: 'Alexander Zobnin', popularity: 0.2093, diff --git a/public/app/features/plugins/admin/api.ts b/public/app/features/plugins/admin/api.ts index 3232527eed9..4f7fd13140e 100644 --- a/public/app/features/plugins/admin/api.ts +++ b/public/app/features/plugins/admin/api.ts @@ -1,33 +1,20 @@ import { getBackendSrv } from '@grafana/runtime'; import { PluginError, renderMarkdown } from '@grafana/data'; import { API_ROOT, GCOM_API_ROOT } from './constants'; -import { mergeLocalAndRemote, isLocalPluginVisible, isRemotePluginVisible } from './helpers'; -import { - PluginDetails, - Org, - LocalPlugin, - RemotePlugin, - CatalogPlugin, - CatalogPluginDetails, - Version, - PluginVersion, -} from './types'; - -export async function getCatalogPlugin(id: string): Promise { - const { local, remote } = await getPlugin(id); - - return mergeLocalAndRemote(local, remote); -} +import { LocalPlugin, RemotePlugin, CatalogPluginDetails, Version, PluginVersion } from './types'; +import { isLocalPluginVisible, isRemotePluginVisible } from './helpers'; export async function getPluginDetails(id: string): Promise { - const localPlugins = await getLocalPlugins(); - const local = localPlugins.find((p) => p.id === id); - const isInstalled = Boolean(local); - const [remote, versions, localReadme] = await Promise.all([ - getRemotePlugin(id, isInstalled), - getPluginVersions(id), + const remote = await getRemotePlugin(id); + const isPublished = Boolean(remote); + + const [localPlugins, versions, localReadme] = await Promise.all([ + getLocalPlugins(), + getPluginVersions(id, isPublished), getLocalPluginReadme(id), ]); + + const local = localPlugins.find((p) => p.id === id); const dependencies = local?.dependencies || remote?.json?.dependencies; return { @@ -45,22 +32,6 @@ export async function getRemotePlugins(): Promise { return remotePlugins.filter(isRemotePluginVisible); } -async function getPlugin(slug: string): Promise { - const installed = await getLocalPlugins(); - - const localPlugin = installed?.find((plugin: LocalPlugin) => { - return plugin.id === slug; - }); - - const [remote, versions] = await Promise.all([getRemotePlugin(slug, Boolean(localPlugin)), getPluginVersions(slug)]); - - return { - remote: remote, - remoteVersions: versions, - local: localPlugin, - }; -} - export async function getPluginErrors(): Promise { try { return await getBackendSrv().get(`${API_ROOT}/errors`); @@ -69,7 +40,7 @@ export async function getPluginErrors(): Promise { } } -async function getRemotePlugin(id: string, isInstalled: boolean): Promise { +async function getRemotePlugin(id: string): Promise { try { return await getBackendSrv().get(`${GCOM_API_ROOT}/plugins/${id}`, {}); } catch (error) { @@ -79,8 +50,12 @@ async function getRemotePlugin(id: string, isInstalled: boolean): Promise { +async function getPluginVersions(id: string, isPublished: boolean): Promise { try { + if (!isPublished) { + return []; + } + const versions: { items: PluginVersion[] } = await getBackendSrv().get(`${GCOM_API_ROOT}/plugins/${id}/versions`); return (versions.items || []).map((v) => ({ @@ -114,11 +89,6 @@ export async function getLocalPlugins(): Promise { return localPlugins.filter(isLocalPluginVisible); } -async function getOrg(slug: string): Promise { - const org = await getBackendSrv().get(`${GCOM_API_ROOT}/orgs/${slug}`); - return { ...org, avatarUrl: `${GCOM_API_ROOT}/orgs/${slug}/avatar` }; -} - export async function installPlugin(id: string) { // This will install the latest compatible version based on the logic // on the backend. @@ -131,9 +101,7 @@ export async function uninstallPlugin(id: string) { export const api = { getRemotePlugins, - getPlugin, getInstalledPlugins: getLocalPlugins, - getOrg, installPlugin, uninstallPlugin, }; diff --git a/public/app/features/plugins/admin/components/InstallControls/index.tsx b/public/app/features/plugins/admin/components/InstallControls/index.tsx index 60a5960f955..faecc95e739 100644 --- a/public/app/features/plugins/admin/components/InstallControls/index.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/index.tsx @@ -63,6 +63,18 @@ export const InstallControls = ({ plugin, latestCompatibleVersion }: Props) => { return
{message}
; } + if (!plugin.isPublished) { + return ( +
+ This plugin is not published to{' '} + + grafana.com/plugins + {' '} + and can't be managed via the catalog. +
+ ); + } + if (!isCompatible) { return (
diff --git a/public/app/features/plugins/admin/components/PluginListItem.test.tsx b/public/app/features/plugins/admin/components/PluginListItem.test.tsx index 0606ff0b3f4..658cc047ade 100644 --- a/public/app/features/plugins/admin/components/PluginListItem.test.tsx +++ b/public/app/features/plugins/admin/components/PluginListItem.test.tsx @@ -52,6 +52,7 @@ describe('PluginListItem', () => { isDev: false, isEnterprise: false, isDisabled: false, + isPublished: true, }; /** As Grid */ diff --git a/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx b/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx index ea92a11242a..0c933680fc4 100644 --- a/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx +++ b/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx @@ -28,6 +28,7 @@ describe('PluginListItemBadges', () => { isDev: false, isEnterprise: false, isDisabled: false, + isPublished: true, }; afterEach(() => { diff --git a/public/app/features/plugins/admin/helpers.test.ts b/public/app/features/plugins/admin/helpers.test.ts index b3034f2d154..0c50d50dee5 100644 --- a/public/app/features/plugins/admin/helpers.test.ts +++ b/public/app/features/plugins/admin/helpers.test.ts @@ -99,6 +99,7 @@ describe('Plugins/Helpers', () => { isDisabled: false, isEnterprise: false, isInstalled: false, + isPublished: true, name: 'Zabbix', orgName: 'Alexander Zobnin', popularity: 0.2111, @@ -157,6 +158,7 @@ describe('Plugins/Helpers', () => { isDisabled: false, isEnterprise: false, isInstalled: true, + isPublished: false, name: 'Zabbix', orgName: 'Alexander Zobnin', popularity: 0, @@ -204,6 +206,7 @@ describe('Plugins/Helpers', () => { isDisabled: false, isEnterprise: false, isInstalled: true, + isPublished: true, name: 'Zabbix', orgName: 'Alexander Zobnin', popularity: 0.2111, diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 4f337eb2545..db942ab1b81 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -78,6 +78,7 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C signature: getPluginSignature({ remote: plugin, error }), updatedAt, hasUpdate: false, + isPublished: true, isInstalled: isDisabled, isDisabled: isDisabled, isCore: plugin.internal, @@ -93,9 +94,9 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat name, info: { description, version, logos, updated, author }, id, - signature, dev, type, + signature, signatureOrg, signatureType, hasUpdate, @@ -119,6 +120,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat isInstalled: true, isDisabled: !!error, isCore: signature === 'internal', + isPublished: false, isDev: Boolean(dev), isEnterprise: false, type, @@ -160,6 +162,7 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e isEnterprise: remote?.status === 'enterprise', isInstalled: Boolean(local) || isDisabled, isDisabled: isDisabled, + isPublished: true, // TODO name: remote?.name || local?.name || '', // TODO @@ -267,3 +270,7 @@ function isPluginVisible(id: string) { return !pluginCatalogHiddenPlugins.includes(id); } + +export function isLocalCorePlugin(local?: LocalPlugin): boolean { + return Boolean(local?.signature === 'internal'); +} diff --git a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx index 925ef3ae3af..ffcb3059789 100644 --- a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import { PluginIncludeType, PluginType } from '@grafana/data'; -import { CatalogPlugin, PluginDetailsTab, PluginTabIds } from '../types'; +import { CatalogPlugin, PluginDetailsTab, PluginTabIds, PluginTabLabels } from '../types'; import { usePluginConfig } from '../hooks/usePluginConfig'; import { isOrgAdmin } from '../permissions'; @@ -13,11 +13,21 @@ type ReturnType = { export const usePluginDetailsTabs = (plugin?: CatalogPlugin, defaultTabs: PluginDetailsTab[] = []): ReturnType => { const { loading, error, value: pluginConfig } = usePluginConfig(plugin); + const isPublished = Boolean(plugin?.isPublished); const { pathname } = useLocation(); const tabs = useMemo(() => { const canConfigurePlugins = isOrgAdmin(); const tabs: PluginDetailsTab[] = [...defaultTabs]; + if (isPublished) { + tabs.push({ + label: PluginTabLabels.VERSIONS, + icon: 'history', + id: PluginTabIds.VERSIONS, + href: `${pathname}?page=${PluginTabIds.VERSIONS}`, + }); + } + // Not extending the tabs with the config pages if the plugin is not installed if (!pluginConfig) { return tabs; @@ -57,7 +67,7 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, defaultTabs: Plugin } return tabs; - }, [pluginConfig, defaultTabs, pathname]); + }, [pluginConfig, defaultTabs, pathname, isPublished]); return { error, diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx index 306af38271d..3bb2567816c 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx @@ -194,7 +194,7 @@ describe('Plugin details page', () => { await waitFor(() => expect(queryByText('Invalid signature')).toBeInTheDocument()); }); - it('should display version history in case it is available', async () => { + it('should display version history if the plugin is published', async () => { const versions = [ { version: '1.2.0', @@ -215,6 +215,7 @@ describe('Plugin details page', () => { grafanaDependency: '>=7.0.0', }, ]; + const { queryByText, getByRole } = renderPluginDetails( { id, @@ -489,6 +490,61 @@ describe('Plugin details page', () => { await waitFor(() => queryByText('Uninstall')); expect(queryByText(`Create a ${name} data source`)).toBeNull(); }); + + it('should not display versions tab for plugins not published to gcom', async () => { + const { queryByText } = renderPluginDetails({ + name: 'Akumuli', + isInstalled: true, + type: PluginType.app, + isPublished: false, + }); + + await waitFor(() => expect(queryByText(PluginTabLabels.OVERVIEW)).toBeInTheDocument()); + + expect(queryByText(PluginTabLabels.VERSIONS)).toBeNull(); + }); + + it('should not display update for plugins not published to gcom', async () => { + const { queryByText, queryByRole } = renderPluginDetails({ + name: 'Akumuli', + isInstalled: true, + hasUpdate: true, + type: PluginType.app, + isPublished: false, + }); + + await waitFor(() => expect(queryByText(PluginTabLabels.OVERVIEW)).toBeInTheDocument()); + + expect(queryByRole('button', { name: /update/i })).not.toBeInTheDocument(); + }); + + it('should not display install for plugins not published to gcom', async () => { + const { queryByText, queryByRole } = renderPluginDetails({ + name: 'Akumuli', + isInstalled: false, + hasUpdate: false, + type: PluginType.app, + isPublished: false, + }); + + await waitFor(() => expect(queryByText(PluginTabLabels.OVERVIEW)).toBeInTheDocument()); + + expect(queryByRole('button', { name: /^install/i })).not.toBeInTheDocument(); + }); + + it('should not display uninstall for plugins not published to gcom', async () => { + const { queryByText, queryByRole } = renderPluginDetails({ + name: 'Akumuli', + isInstalled: true, + hasUpdate: false, + type: PluginType.app, + isPublished: false, + }); + + await waitFor(() => expect(queryByText(PluginTabLabels.OVERVIEW)).toBeInTheDocument()); + + expect(queryByRole('button', { name: /uninstall/i })).not.toBeInTheDocument(); + }); }); describe('viewed as user without grafana admin permissions', () => { @@ -505,7 +561,7 @@ describe('Plugin details page', () => { await waitFor(() => expect(queryByText(PluginTabLabels.OVERVIEW)).toBeInTheDocument()); - expect(queryByRole('button', { name: /install/i })).not.toBeInTheDocument(); + expect(queryByRole('button', { name: /^install/i })).not.toBeInTheDocument(); }); it('should not display an uninstall button for an already installed plugin', async () => { @@ -531,7 +587,7 @@ describe('Plugin details page', () => { await waitFor(() => expect(queryByText(PluginTabLabels.OVERVIEW)).toBeInTheDocument()); - expect(queryByRole('button', { name: /install/i })).not.toBeInTheDocument(); + expect(queryByRole('button', { name: /^install/i })).not.toBeInTheDocument(); }); }); diff --git a/public/app/features/plugins/admin/pages/PluginDetails.tsx b/public/app/features/plugins/admin/pages/PluginDetails.tsx index ae4979570ed..e8bd18aee35 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.tsx @@ -34,12 +34,6 @@ export default function PluginDetails({ match, queryParams }: Props): JSX.Elemen id: PluginTabIds.OVERVIEW, href: `${url}?page=${PluginTabIds.OVERVIEW}`, }, - { - label: PluginTabLabels.VERSIONS, - icon: 'history', - id: PluginTabIds.VERSIONS, - href: `${url}?page=${PluginTabIds.VERSIONS}`, - }, ]; const plugin = useGetSingle(pluginId); // fetches the localplugin settings const { tabs } = usePluginDetailsTabs(plugin, defaultTabs); diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index 9a2cace5735..a20e212b427 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -43,6 +43,8 @@ export interface CatalogPlugin { isEnterprise: boolean; isInstalled: boolean; isDisabled: boolean; + // `isPublished` is TRUE if the plugin is published to grafana.com + isPublished: boolean; name: string; orgName: string; signature: PluginSignatureStatus;