Plugins: Disable version installation for specific plugin types (#98597)

This commit is contained in:
Hugo Kiyodi Oshiro
2025-01-10 16:02:09 +01:00
committed by GitHub
parent 67ddadbab9
commit 7499611129
8 changed files with 165 additions and 36 deletions
@@ -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(<Provider store={store}>{component}</Provider>);
}
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(
<PluginDetailsBody
plugin={plugin}
info={[]}
queryParams={{}}
pageId={PluginTabIds.VERSIONS}
showDetails={false}
/>
);
});
const installSpans = screen.getAllByText('Install');
installSpans.forEach((span) => {
const button = span.closest('button');
expect(button).toHaveAttribute('aria-disabled', 'true');
});
});
});
});
@@ -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)}
/>
</div>
);
@@ -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 (
<Stack height="auto" wrap="wrap">
@@ -22,7 +22,7 @@ describe('VersionList', () => {
},
];
renderWithStore(<VersionList pluginId={''} versions={versions} />);
renderWithStore(<VersionList pluginId={''} versions={versions} disableInstallation={false} />);
const installElements = screen.getAllByText('Install');
expect(installElements).toHaveLength(versions.length);
});
@@ -52,7 +52,12 @@ describe('VersionList', () => {
const installedVersionIndex = 1;
renderWithStore(
<VersionList pluginId={''} versions={versions} installedVersion={versions[installedVersionIndex].version} />
<VersionList
pluginId={''}
versions={versions}
installedVersion={versions[installedVersionIndex].version}
disableInstallation={false}
/>
);
expect(screen.getAllByText('Installed')).toHaveLength(1);
expect(screen.getAllByText('Downgrade')).toHaveLength(1);
@@ -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 (
<tr key={version.version}>
{/* 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}
/>
</td>
+34 -26
View File
@@ -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;
}
@@ -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,
@@ -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;
}