diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts
index b2bc145eacf..ad37ef63f28 100644
--- a/packages/grafana-runtime/src/config.ts
+++ b/packages/grafana-runtime/src/config.ts
@@ -42,6 +42,11 @@ export type AppPluginConfig = {
angular: AngularMeta;
};
+export type PreinstalledPlugin = {
+ id: string;
+ version: string;
+};
+
export class GrafanaBootConfig implements GrafanaConfig {
publicDashboardAccessToken?: string;
publicDashboardsEnabled = true;
@@ -124,6 +129,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
pluginAdminExternalManageEnabled = false;
pluginCatalogHiddenPlugins: string[] = [];
pluginCatalogManagedPlugins: string[] = [];
+ pluginCatalogPreinstalledPlugins: PreinstalledPlugin[] = [];
pluginsCDNBaseURL = '';
expressionsEnabled = false;
customTheme?: undefined;
diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go
index 92ce60699b6..c93e00cea0f 100644
--- a/pkg/api/dtos/frontend_settings.go
+++ b/pkg/api/dtos/frontend_settings.go
@@ -228,6 +228,7 @@ type FrontendSettingsDTO struct {
PluginAdminExternalManageEnabled bool `json:"pluginAdminExternalManageEnabled"`
PluginCatalogHiddenPlugins []string `json:"pluginCatalogHiddenPlugins"`
PluginCatalogManagedPlugins []string `json:"pluginCatalogManagedPlugins"`
+ PluginCatalogPreinstalledPlugins []setting.InstallPlugin `json:"pluginCatalogPreinstalledPlugins"`
ExpressionsEnabled bool `json:"expressionsEnabled"`
AwsAllowedAuthProviders []string `json:"awsAllowedAuthProviders"`
AwsAssumeRoleEnabled bool `json:"awsAssumeRoleEnabled"`
diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go
index b57bb201022..c8e1170454c 100644
--- a/pkg/api/frontendsettings.go
+++ b/pkg/api/frontendsettings.go
@@ -274,6 +274,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
PluginAdminExternalManageEnabled: hs.Cfg.PluginAdminEnabled && hs.Cfg.PluginAdminExternalManageEnabled,
PluginCatalogHiddenPlugins: hs.Cfg.PluginCatalogHiddenPlugins,
PluginCatalogManagedPlugins: hs.managedPluginsService.ManagedPlugins(c.Req.Context()),
+ PluginCatalogPreinstalledPlugins: hs.Cfg.InstallPlugins,
ExpressionsEnabled: hs.Cfg.ExpressionsEnabled,
AwsAllowedAuthProviders: hs.Cfg.AWSAllowedAuthProviders,
AwsAssumeRoleEnabled: hs.Cfg.AWSAssumeRoleEnabled,
diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go
index 639fab90aee..1426857f091 100644
--- a/pkg/setting/setting.go
+++ b/pkg/setting/setting.go
@@ -524,8 +524,8 @@ type Cfg struct {
}
type InstallPlugin struct {
- ID string
- Version string
+ ID string `json:"id"`
+ Version string `json:"version"`
}
// AddChangePasswordLink returns if login form is disabled or not since
diff --git a/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts b/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts
index 4372f87602f..0a587e25c92 100644
--- a/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts
+++ b/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts
@@ -21,6 +21,7 @@ export default {
isDeprecated: false,
isPublished: true,
isManaged: false,
+ isPreinstalled: { found: false, withVersion: false },
name: 'Zabbix',
orgName: 'Alexander Zobnin',
popularity: 0.2093,
diff --git a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx
index 0a67af40b99..941fbb1eee7 100644
--- a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx
+++ b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx
@@ -33,6 +33,7 @@ const plugin: CatalogPlugin = {
isDeprecated: false,
isPublished: true,
isManaged: false,
+ isPreinstalled: { found: false, withVersion: false },
};
describe('GetStartedWithDataSource', () => {
diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx
index 258f5895d9d..68090c5ba54 100644
--- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx
+++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx
@@ -33,6 +33,7 @@ const plugin: CatalogPlugin = {
isDeprecated: false,
isPublished: true,
isManaged: false,
+ isPreinstalled: { found: false, withVersion: false },
};
function setup(opts: { angularSupportEnabled: boolean; angularDetected: boolean }) {
@@ -253,5 +254,17 @@ describe('InstallControlsButton', () => {
);
expect(screen.queryByText('Update')).not.toBeInTheDocument();
});
+
+ it('should be hidden when plugin is preinstalled with a specific version', () => {
+ render(
+
+
+
+ );
+ expect(screen.queryByText('Update')).not.toBeInTheDocument();
+ });
});
});
diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx
index d7babbf3013..4791edbe6f9 100644
--- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx
+++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx
@@ -113,12 +113,17 @@ export function InstallControlsButton({
}
};
- if (pluginStatus === PluginStatus.UNINSTALL) {
- const disableUninstall =
- config.pluginAdminExternalManageEnabled && configCore.featureToggles.managedPluginsInstall
- ? plugin.isUninstallingFromInstance
- : isUninstalling;
+ let disableUninstall =
+ config.pluginAdminExternalManageEnabled && configCore.featureToggles.managedPluginsInstall
+ ? plugin.isUninstallingFromInstance
+ : isUninstalling;
+ let uninstallTitle = '';
+ if (plugin.isPreinstalled.found) {
+ disableUninstall = true;
+ uninstallTitle = 'Preinstalled plugin. Remove from Grafana config before uninstalling.';
+ }
+ if (pluginStatus === PluginStatus.UNINSTALL) {
return (
<>
-
@@ -152,12 +157,12 @@ export function InstallControlsButton({
return (
- {!plugin.isManaged && (
+ {!plugin.isManaged && !plugin.isPreinstalled.withVersion && (
{isInstalling ? 'Updating' : 'Update'}
)}
-
+
{uninstallBtnText}
diff --git a/public/app/features/plugins/admin/components/PluginListItem.test.tsx b/public/app/features/plugins/admin/components/PluginListItem.test.tsx
index 0339c154d74..1f7a5bf3bb8 100644
--- a/public/app/features/plugins/admin/components/PluginListItem.test.tsx
+++ b/public/app/features/plugins/admin/components/PluginListItem.test.tsx
@@ -58,6 +58,7 @@ describe('PluginListItem', () => {
isDeprecated: false,
isPublished: true,
isManaged: false,
+ isPreinstalled: { found: false, withVersion: false },
};
/** As Grid */
diff --git a/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx b/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx
index 251ab8df195..b6afda55066 100644
--- a/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx
+++ b/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx
@@ -34,6 +34,7 @@ describe('PluginListItemBadges', () => {
isDeprecated: false,
isPublished: true,
isManaged: false,
+ isPreinstalled: { found: false, withVersion: false },
};
afterEach(() => {
@@ -84,6 +85,20 @@ describe('PluginListItemBadges', () => {
expect(screen.queryByText(/update available/i)).toBeNull();
});
+ it('does not render an upgrade badge (when plugin is preinstalled with a version)', () => {
+ render(
+
+ );
+ expect(screen.queryByText(/update available/i)).toBeNull();
+ });
+
it('renders an angular badge (when plugin is angular)', () => {
render();
expect(screen.getByText(/angular/i)).toBeVisible();
diff --git a/public/app/features/plugins/admin/components/PluginListItemBadges.tsx b/public/app/features/plugins/admin/components/PluginListItemBadges.tsx
index bc533a791a2..909f380db92 100644
--- a/public/app/features/plugins/admin/components/PluginListItemBadges.tsx
+++ b/public/app/features/plugins/admin/components/PluginListItemBadges.tsx
@@ -24,7 +24,9 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) {
{plugin.isDisabled && }
- {hasUpdate && !plugin.isManaged && }
+ {hasUpdate && !plugin.isManaged && !plugin.isPreinstalled.withVersion && (
+
+ )}
{plugin.angularDetected && }
);
@@ -36,7 +38,9 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) {
{plugin.isDisabled && }
{plugin.isDeprecated && }
{plugin.isInstalled && }
- {hasUpdate && !plugin.isManaged && }
+ {hasUpdate && !plugin.isManaged && !plugin.isPreinstalled.withVersion && (
+
+ )}
{plugin.angularDetected && }
);
diff --git a/public/app/features/plugins/admin/helpers.test.ts b/public/app/features/plugins/admin/helpers.test.ts
index dfadd646071..b2a80ec3ee0 100644
--- a/public/app/features/plugins/admin/helpers.test.ts
+++ b/public/app/features/plugins/admin/helpers.test.ts
@@ -204,6 +204,7 @@ describe('Plugins/Helpers', () => {
isDeprecated: false,
isPublished: true,
isManaged: false,
+ isPreinstalled: { found: false, withVersion: false },
name: 'Zabbix',
orgName: 'Alexander Zobnin',
popularity: 0.2111,
@@ -282,6 +283,7 @@ describe('Plugins/Helpers', () => {
isPublished: false,
isDeprecated: false,
isManaged: false,
+ isPreinstalled: { found: false, withVersion: false },
name: 'Zabbix',
orgName: 'Alexander Zobnin',
popularity: 0,
@@ -335,6 +337,7 @@ describe('Plugins/Helpers', () => {
isPublished: true,
isDeprecated: false,
isManaged: false,
+ isPreinstalled: { found: false, withVersion: false },
name: 'Zabbix',
orgName: 'Alexander Zobnin',
popularity: 0.2111,
diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts
index 88416895c87..86d441763d6 100644
--- a/public/app/features/plugins/admin/helpers.ts
+++ b/public/app/features/plugins/admin/helpers.ts
@@ -143,6 +143,7 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C
isInstalled: isDisabled,
isDisabled: isDisabled,
isManaged: isManagedPlugin(id),
+ isPreinstalled: isPreinstalledPlugin(id),
isDeprecated: status === RemotePluginStatus.Deprecated,
isCore: plugin.internal,
isDev: false,
@@ -193,6 +194,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat
isDev: Boolean(dev),
isEnterprise: false,
isManaged: isManagedPlugin(id),
+ isPreinstalled: isPreinstalledPlugin(id),
type,
error: error?.errorCode,
accessControl: accessControl,
@@ -241,6 +243,7 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e
isDeprecated: remote?.status === RemotePluginStatus.Deprecated,
isPublished: true,
isManaged: isManagedPlugin(id),
+ isPreinstalled: isPreinstalledPlugin(id),
// TODO
name: remote?.name || local?.name || '',
// TODO
@@ -382,6 +385,13 @@ export function isManagedPlugin(id: string) {
return pluginCatalogManagedPlugins?.includes(id);
}
+export function isPreinstalledPlugin(id: string): { found: boolean; withVersion: boolean } {
+ const { pluginCatalogPreinstalledPlugins } = config;
+
+ const plugin = pluginCatalogPreinstalledPlugins?.find((p) => p.id === id);
+ return { found: !!plugin?.id, withVersion: !!plugin?.version };
+}
+
function isDisabledSecretsPlugin(type?: PluginType): boolean {
return type === PluginType.secretsmanager && !config.secretsManagerPluginEnabled;
}
diff --git a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx
index 54ccebbb21a..0b4fca761a5 100644
--- a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx
+++ b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx
@@ -25,7 +25,7 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => {
version = latestCompatibleVersion?.version;
}
- if (Boolean(version)) {
+ if (version) {
if (plugin.isManaged) {
info.push({
label: t('plugins.details.labels.version', 'Version'),
@@ -34,7 +34,7 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => {
} else {
info.push({
label: t('plugins.details.labels.version', 'Version'),
- value: version,
+ value: `${version}${plugin.isPreinstalled.withVersion ? ' (preinstalled)' : ''}`,
});
}
}
diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
index 931d20cb16a..41303424649 100644
--- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
+++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
@@ -326,6 +326,24 @@ describe('Plugin details page', () => {
expect(queryByRole('button', { name: /^install/i })).not.toBeInTheDocument();
});
+ it('should not display an update button for a plugin that is pre installed', async () => {
+ const { queryByRole, getByText } = renderPluginDetails({
+ id,
+ isInstalled: true,
+ hasUpdate: true,
+ isPreinstalled: { found: true, withVersion: true },
+ });
+
+ // Does not display an "update" button
+ expect(await queryByRole('button', { name: /update/i })).not.toBeInTheDocument();
+
+ // Does not display "install" button
+ expect(queryByRole('button', { name: /^install/i })).not.toBeInTheDocument();
+
+ // Display an uninstall button but disabled
+ expect(getByText(/Uninstall/i).closest('button')).toBeDisabled();
+ });
+
it('should display an install button for enterprise plugins if license is valid', async () => {
config.licenseInfo.enabledFeatures = { 'enterprise.plugins': true };
diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts
index b8f8e3b1669..93517af5261 100644
--- a/public/app/features/plugins/admin/types.ts
+++ b/public/app/features/plugins/admin/types.ts
@@ -40,6 +40,7 @@ export interface CatalogPlugin extends WithAccessControlMetadata {
isDisabled: boolean;
isDeprecated: boolean;
isManaged: boolean; // Indicates that the plugin version is managed by Grafana
+ isPreinstalled: { found: boolean; withVersion: boolean }; // Indicates that the plugin is pre-installed
// `isPublished` is TRUE if the plugin is published to grafana.com
isPublished: boolean;
name: string;