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 && ( )} - 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;