From 749961112940370288de2579a5c40d6f3be90f49 Mon Sep 17 00:00:00 2001 From: Hugo Kiyodi Oshiro Date: Fri, 10 Jan 2025 16:02:09 +0100 Subject: [PATCH] Plugins: Disable version installation for specific plugin types (#98597) --- .../components/PluginDetailsBody.test.tsx | 102 ++++++++++++++++++ .../admin/components/PluginDetailsBody.tsx | 2 + .../admin/components/PluginListItemBadges.tsx | 4 +- .../admin/components/VersionList.test.tsx | 9 +- .../plugins/admin/components/VersionList.tsx | 16 ++- public/app/features/plugins/admin/helpers.ts | 60 ++++++----- .../app/features/plugins/admin/state/hooks.ts | 4 +- .../features/plugins/admin/state/selectors.ts | 4 +- 8 files changed, 165 insertions(+), 36 deletions(-) create mode 100644 public/app/features/plugins/admin/components/PluginDetailsBody.test.tsx diff --git a/public/app/features/plugins/admin/components/PluginDetailsBody.test.tsx b/public/app/features/plugins/admin/components/PluginDetailsBody.test.tsx new file mode 100644 index 00000000000..39770882e47 --- /dev/null +++ b/public/app/features/plugins/admin/components/PluginDetailsBody.test.tsx @@ -0,0 +1,102 @@ +import { act, render, screen } from '@testing-library/react'; +import { Provider } from 'react-redux'; + +import { PluginType } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { configureStore } from 'app/store/configureStore'; + +import { getCatalogPluginMock } from '../__mocks__'; +import { PluginTabIds } from '../types'; + +import { PluginDetailsBody } from './PluginDetailsBody'; + +function renderWithStore(component: JSX.Element) { + const store = configureStore(); + + return render({component}); +} + +describe('PluginDetailsBody', () => { + const tcs = [ + { + name: 'renderer type plugin', + plugin: { + type: PluginType.renderer, + }, + }, + { + name: 'secrets manager type plugin', + plugin: { + type: PluginType.secretsmanager, + }, + }, + { + name: 'enterprise plugin type without enterprise license', + plugin: { + isEnterprise: true, + }, + changeConfig: () => { + config.licenseInfo.enabledFeatures = { + 'enterprise-plugins': true, + }; + }, + }, + { + name: 'unpublished plugin', + plugin: { + isPublished: false, + }, + }, + { + name: 'core plugin', + plugin: { + isCore: true, + }, + }, + { + name: 'disabled plugin', + plugin: { + isDisabled: true, + }, + }, + { + name: 'provisioned plugin', + plugin: { + isProvisioned: true, + }, + }, + { + name: 'install controls disabled', + changeConfig: () => { + config.pluginAdminEnabled = false; + }, + }, + ]; + + tcs.forEach((tc) => { + it(`should render disable version installation for ${tc.name}`, async () => { + if (tc.changeConfig) { + tc.changeConfig(); + } + const plugin = getCatalogPluginMock({ ...tc.plugin }); + await act(async () => { + renderWithStore( + + ); + }); + + const installSpans = screen.getAllByText('Install'); + + installSpans.forEach((span) => { + const button = span.closest('button'); + expect(button).toHaveAttribute('aria-disabled', 'true'); + }); + }); + }); +}); diff --git a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx index 7554359b537..d45024f59b7 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx @@ -9,6 +9,7 @@ import { CellProps, Column, InteractiveTable, Stack, useStyles2 } from '@grafana import { Changelog } from '../components/Changelog'; import { PluginDetailsPanel } from '../components/PluginDetailsPanel'; import { VersionList } from '../components/VersionList'; +import { shouldDisablePluginInstall } from '../helpers'; import { usePluginConfig } from '../hooks/usePluginConfig'; import { CatalogPlugin, Permission, PluginTabIds } from '../types'; @@ -64,6 +65,7 @@ export function PluginDetailsBody({ plugin, queryParams, pageId, info, showDetai pluginId={plugin.id} versions={plugin.details?.versions} installedVersion={plugin.installedVersion} + disableInstallation={shouldDisablePluginInstall(plugin)} /> ); diff --git a/public/app/features/plugins/admin/components/PluginListItemBadges.tsx b/public/app/features/plugins/admin/components/PluginListItemBadges.tsx index e4f8bcaa8f5..6637315bb7a 100644 --- a/public/app/features/plugins/admin/components/PluginListItemBadges.tsx +++ b/public/app/features/plugins/admin/components/PluginListItemBadges.tsx @@ -1,6 +1,6 @@ import { PluginSignatureBadge, Stack } from '@grafana/ui'; -import { isPluginUpdateable } from '../helpers'; +import { isPluginUpdatable } from '../helpers'; import { CatalogPlugin } from '../types'; import { @@ -18,7 +18,7 @@ type PluginBadgeType = { export function PluginListItemBadges({ plugin }: PluginBadgeType) { // Currently renderer plugins are not supported by the catalog due to complications related to installation / update / uninstall. - const canUpdate = isPluginUpdateable(plugin); + const canUpdate = isPluginUpdatable(plugin); if (plugin.isEnterprise) { return ( diff --git a/public/app/features/plugins/admin/components/VersionList.test.tsx b/public/app/features/plugins/admin/components/VersionList.test.tsx index 465209b3e6a..f0a13ebfbda 100644 --- a/public/app/features/plugins/admin/components/VersionList.test.tsx +++ b/public/app/features/plugins/admin/components/VersionList.test.tsx @@ -22,7 +22,7 @@ describe('VersionList', () => { }, ]; - renderWithStore(); + renderWithStore(); const installElements = screen.getAllByText('Install'); expect(installElements).toHaveLength(versions.length); }); @@ -52,7 +52,12 @@ describe('VersionList', () => { const installedVersionIndex = 1; renderWithStore( - + ); expect(screen.getAllByText('Installed')).toHaveLength(1); expect(screen.getAllByText('Downgrade')).toHaveLength(1); diff --git a/public/app/features/plugins/admin/components/VersionList.tsx b/public/app/features/plugins/admin/components/VersionList.tsx index f910ea19ae0..aaf98fd183b 100644 --- a/public/app/features/plugins/admin/components/VersionList.tsx +++ b/public/app/features/plugins/admin/components/VersionList.tsx @@ -14,9 +14,10 @@ interface Props { pluginId: string; versions?: Version[]; installedVersion?: string; + disableInstallation: boolean; } -export const VersionList = ({ pluginId, versions = [], installedVersion }: Props) => { +export const VersionList = ({ pluginId, versions = [], installedVersion, disableInstallation }: Props) => { const styles = useStyles2(getStyles); const latestCompatibleVersion = getLatestCompatibleVersion(versions); @@ -58,6 +59,10 @@ export const VersionList = ({ pluginId, versions = [], installedVersion }: Props tooltip = 'This plugin version is not compatible with the current Grafana version'; } + if (disableInstallation) { + tooltip = `This plugin can't be managed through the Plugin Catalog`; + } + return ( {/* Version number */} @@ -77,7 +82,14 @@ export const VersionList = ({ pluginId, versions = [], installedVersion }: Props latestCompatibleVersion={latestCompatibleVersion?.version} installedVersion={installedVersion} onConfirmInstallation={onInstallClick} - disabled={isInstalledVersion || isInstalling || !canInstall || !version.isCompatible || !canInstall} + disabled={ + isInstalledVersion || + isInstalling || + !canInstall || + !version.isCompatible || + !canInstall || + disableInstallation + } tooltip={tooltip} /> diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 6310c2603dc..172a32e088f 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -431,41 +431,49 @@ export function filterByKeyword(plugins: CatalogPlugin[], query: string) { return idxs.map((id) => getId(dataArray[id])); } -export function isPluginUpdateable(plugin: CatalogPlugin) { +function isPluginModifiable(plugin: CatalogPlugin) { + if ( + plugin.isProvisioned || //provisioned plugins cannot be modified + plugin.isCore || //core plugins cannot be modified + plugin.type === PluginType.renderer || // currently renderer plugins are not supported by the catalog due to complications related to installation / update / uninstall + plugin.isPreinstalled.withVersion || // Preinstalled plugins (with specified version) cannot be modified + plugin.isManaged // Managed plugins cannot be modified + ) { + return false; + } + + return true; +} + +export function isPluginUpdatable(plugin: CatalogPlugin) { + if (!isPluginModifiable(plugin)) { + return false; + } + // If there is no update available, the plugin cannot be updated if (!plugin.hasUpdate) { return false; } - // Provisioned plugins cannot be updated - if (plugin.isProvisioned) { - return false; - } - - // Core plugins cannot be updated - if (plugin.isCore) { - return false; - } - - // Currently renderer plugins are not supported by the catalog due to complications related to installation / update / uninstall. - if (plugin.type === PluginType.renderer) { - return false; - } - - // Preinstalled plugins (with specified version) cannot be updated - if (plugin.isPreinstalled.withVersion) { - return false; - } - // If the plugin is currently being updated, it should not be updated if (plugin.isUpdatingFromInstance) { return false; } - // Managed plugins cannot be updated - if (plugin.isManaged) { - return false; - } - return true; } + +export function shouldDisablePluginInstall(plugin: CatalogPlugin) { + if ( + !isPluginModifiable(plugin) || + plugin.type === PluginType.secretsmanager || + (plugin.isEnterprise && !featureEnabled('enterprise.plugins')) || + !plugin.isPublished || + plugin.isDisabled || + !isInstallControlsEnabled() + ) { + return true; + } + + return false; +} diff --git a/public/app/features/plugins/admin/state/hooks.ts b/public/app/features/plugins/admin/state/hooks.ts index ddef973b456..da0ba859a3a 100644 --- a/public/app/features/plugins/admin/state/hooks.ts +++ b/public/app/features/plugins/admin/state/hooks.ts @@ -3,7 +3,7 @@ import { useEffect, useMemo } from 'react'; import { PluginError, PluginType } from '@grafana/data'; import { useDispatch, useSelector } from 'app/types'; -import { sortPlugins, Sorters, isPluginUpdateable } from '../helpers'; +import { sortPlugins, Sorters, isPluginUpdatable } from '../helpers'; import { CatalogPlugin } from '../types'; import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall, fetchAllLocal, unsetInstall } from './actions'; @@ -37,7 +37,7 @@ export const useGetAll = (filters: PluginFilters, sortBy: Sorters = Sorters.name export const useGetUpdatable = () => { const { isLoading } = useFetchStatus(); const { plugins: installed } = useGetAll({ isInstalled: true }); - const updatablePlugins = installed.filter(isPluginUpdateable); + const updatablePlugins = installed.filter(isPluginUpdatable); return { isLoading, updatablePlugins, diff --git a/public/app/features/plugins/admin/state/selectors.ts b/public/app/features/plugins/admin/state/selectors.ts index 128823e1c88..0416c1d4b7a 100644 --- a/public/app/features/plugins/admin/state/selectors.ts +++ b/public/app/features/plugins/admin/state/selectors.ts @@ -4,7 +4,7 @@ import { debounce } from 'lodash'; import { PluginError, PluginType, unEscapeStringFromRegex } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { filterByKeyword, isPluginUpdateable } from '../helpers'; +import { filterByKeyword, isPluginUpdatable } from '../helpers'; import { RequestStatus, PluginCatalogStoreState } from '../types'; import { pluginsAdapter } from './reducer'; @@ -69,7 +69,7 @@ export const selectPlugins = (filters: PluginFilters) => return false; } - if (filters.hasUpdate !== undefined && (plugin.hasUpdate !== filters.hasUpdate || !isPluginUpdateable(plugin))) { + if (filters.hasUpdate !== undefined && (plugin.hasUpdate !== filters.hasUpdate || !isPluginUpdatable(plugin))) { return false; }