mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
c780854a18
commit
487baf5a25
@ -17,6 +17,7 @@ export default {
|
||||
isEnterprise: false,
|
||||
isInstalled: false,
|
||||
isDisabled: false,
|
||||
isPublished: true,
|
||||
name: 'Zabbix',
|
||||
orgName: 'Alexander Zobnin',
|
||||
popularity: 0.2093,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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't be managed via the catalog.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isCompatible) {
|
||||
return (
|
||||
<div className={styles.message}>
|
||||
|
@ -52,6 +52,7 @@ describe('PluginListItem', () => {
|
||||
isDev: false,
|
||||
isEnterprise: false,
|
||||
isDisabled: false,
|
||||
isPublished: true,
|
||||
};
|
||||
|
||||
/** As Grid */
|
||||
|
@ -28,6 +28,7 @@ describe('PluginListItemBadges', () => {
|
||||
isDev: false,
|
||||
isEnterprise: false,
|
||||
isDisabled: false,
|
||||
isPublished: true,
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user