PluginsCatalog: hiding version history tab and install controls for plugins not published to grafana-com (#41634)

* will hide the version tab for core plugins.

* will not try to fetch the version list if plugin is local.

* added the concept wheter or not a plugin is published or not.

* Update public/app/features/plugins/admin/pages/PluginDetails.test.tsx

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>

* Update public/app/features/plugins/admin/types.ts

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>

* removed unused api functions.

* fix(plugins/admin): fix a tiny linter issue

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
This commit is contained in:
Marcus Andersson 2021-11-15 16:58:15 +01:00 committed by GitHub
parent c780854a18
commit 487baf5a25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 115 additions and 60 deletions

View File

@ -17,6 +17,7 @@ export default {
isEnterprise: false,
isInstalled: false,
isDisabled: false,
isPublished: true,
name: 'Zabbix',
orgName: 'Alexander Zobnin',
popularity: 0.2093,

View File

@ -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<CatalogPlugin> {
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<CatalogPluginDetails> {
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<RemotePlugin[]> {
return remotePlugins.filter(isRemotePluginVisible);
}
async function getPlugin(slug: string): Promise<PluginDetails> {
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<PluginError[]> {
try {
return await getBackendSrv().get(`${API_ROOT}/errors`);
@ -69,7 +40,7 @@ export async function getPluginErrors(): Promise<PluginError[]> {
}
}
async function getRemotePlugin(id: string, isInstalled: boolean): Promise<RemotePlugin | undefined> {
async function getRemotePlugin(id: string): Promise<RemotePlugin | undefined> {
try {
return await getBackendSrv().get(`${GCOM_API_ROOT}/plugins/${id}`, {});
} catch (error) {
@ -79,8 +50,12 @@ async function getRemotePlugin(id: string, isInstalled: boolean): Promise<Remote
}
}
async function getPluginVersions(id: string): Promise<Version[]> {
async function getPluginVersions(id: string, isPublished: boolean): Promise<Version[]> {
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<LocalPlugin[]> {
return localPlugins.filter(isLocalPluginVisible);
}
async function getOrg(slug: string): Promise<Org> {
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,
};

View File

@ -63,6 +63,18 @@ export const InstallControls = ({ plugin, latestCompatibleVersion }: Props) => {
return <div className={styles.message}>{message}</div>;
}
if (!plugin.isPublished) {
return (
<div className={styles.message}>
<Icon name="exclamation-triangle" /> This plugin is not published to{' '}
<a href="https://www.grafana.com/plugins" target="__blank" rel="noreferrer">
grafana.com/plugins
</a>{' '}
and can&#39;t be managed via the catalog.
</div>
);
}
if (!isCompatible) {
return (
<div className={styles.message}>

View File

@ -52,6 +52,7 @@ describe('PluginListItem', () => {
isDev: false,
isEnterprise: false,
isDisabled: false,
isPublished: true,
};
/** As Grid */

View File

@ -28,6 +28,7 @@ describe('PluginListItemBadges', () => {
isDev: false,
isEnterprise: false,
isDisabled: false,
isPublished: true,
};
afterEach(() => {

View File

@ -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,

View File

@ -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<check if we would like to keep preferring the remote version>
name: remote?.name || local?.name || '',
// TODO<check if we would like to keep preferring the remote version>
@ -267,3 +270,7 @@ function isPluginVisible(id: string) {
return !pluginCatalogHiddenPlugins.includes(id);
}
export function isLocalCorePlugin(local?: LocalPlugin): boolean {
return Boolean(local?.signature === 'internal');
}

View File

@ -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,

View File

@ -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();
});
});

View File

@ -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);

View File

@ -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;