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