Plugins: Hide version information when plugin is managed (#88065)

* first pass

* fixup

* fix linter issues

* fix API test

* update naming

* rework

* update var name

* empty check

* prettier

* fix test

* fix lint
This commit is contained in:
Will Browne
2024-07-29 11:18:43 +01:00
committed by GitHub
parent e2ee7f06eb
commit 1b3fa8c47f
22 changed files with 108 additions and 20 deletions
+1
View File
@@ -122,6 +122,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
pluginAdminEnabled = true;
pluginAdminExternalManageEnabled = false;
pluginCatalogHiddenPlugins: string[] = [];
pluginCatalogManagedPlugins: string[] = [];
pluginsCDNBaseURL = '';
expressionsEnabled = false;
customTheme?: undefined;
+1
View File
@@ -227,6 +227,7 @@ type FrontendSettingsDTO struct {
PluginAdminEnabled bool `json:"pluginAdminEnabled"`
PluginAdminExternalManageEnabled bool `json:"pluginAdminExternalManageEnabled"`
PluginCatalogHiddenPlugins []string `json:"pluginCatalogHiddenPlugins"`
PluginCatalogManagedPlugins []string `json:"pluginCatalogManagedPlugins"`
ExpressionsEnabled bool `json:"expressionsEnabled"`
AwsAllowedAuthProviders []string `json:"awsAllowedAuthProviders"`
AwsAssumeRoleEnabled bool `json:"awsAssumeRoleEnabled"`
+1
View File
@@ -266,6 +266,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
PluginAdminEnabled: hs.Cfg.PluginAdminEnabled,
PluginAdminExternalManageEnabled: hs.Cfg.PluginAdminEnabled && hs.Cfg.PluginAdminExternalManageEnabled,
PluginCatalogHiddenPlugins: hs.Cfg.PluginCatalogHiddenPlugins,
PluginCatalogManagedPlugins: hs.managedPluginsService.ManagedPlugins(c.Req.Context()),
ExpressionsEnabled: hs.Cfg.ExpressionsEnabled,
AwsAllowedAuthProviders: hs.Cfg.AWSAllowedAuthProviders,
AwsAssumeRoleEnabled: hs.Cfg.AWSAssumeRoleEnabled,
+4 -2
View File
@@ -23,6 +23,7 @@ import (
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/services/rendering"
@@ -77,8 +78,9 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate,
PluginSettings: cfg.PluginSettings,
}),
namespacer: request.GetNamespaceMapper(cfg),
SocialService: socialimpl.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeCacheStorage(), nil, &ssosettingstests.MockService{}),
namespacer: request.GetNamespaceMapper(cfg),
SocialService: socialimpl.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeCacheStorage(), nil, &ssosettingstests.MockService{}),
managedPluginsService: managedplugins.NewNoop(),
}
m := web.New()
+7 -5
View File
@@ -25,10 +25,6 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/grafana/grafana/pkg/services/anonymous"
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/api/avatar"
"github.com/grafana/grafana/pkg/api/routing"
httpstatic "github.com/grafana/grafana/pkg/api/static"
@@ -48,7 +44,10 @@ import (
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/anonymous"
"github.com/grafana/grafana/pkg/services/apikey"
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/cleanup"
@@ -78,6 +77,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/playlist"
"github.com/grafana/grafana/pkg/services/plugindashboards"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
@@ -195,6 +195,7 @@ type HTTPServer struct {
apiKeyService apikey.Service
kvStore kvstore.KVStore
pluginsCDNService *pluginscdn.Service
managedPluginsService managedplugins.Manager
userService user.Service
tempUserService tempUser.Service
@@ -254,7 +255,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service,
folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
starService star.Service, csrfService csrf.Service,
starService star.Service, csrfService csrf.Service, managedPlugins managedplugins.Manager,
playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore,
secretsMigrator secrets.Migrator, secretsPluginManager plugins.SecretsPluginManager, secretsService secrets.Service,
secretsPluginMigrator spm.SecretMigrationProvider, secretsStore secretsKV.SecretsKVStore,
@@ -359,6 +360,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
statsService: statsService,
authnService: authnService,
pluginsCDNService: pluginsCDNService,
managedPluginsService: managedPlugins,
starApi: starApi,
promRegister: promRegister,
promGatherer: promGatherer,
+4 -1
View File
@@ -12,12 +12,12 @@ import (
"strings"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/log/logtest"
@@ -39,6 +39,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
@@ -99,6 +100,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
ID: pluginID,
},
})
hs.managedPluginsService = managedplugins.NewNoop()
expectedIdentity := &authn.Identity{
OrgID: tc.permissionOrg,
@@ -641,6 +643,7 @@ func Test_PluginsList_AccessControl(t *testing.T) {
hs.PluginSettings = &pluginSettings
hs.pluginStore = pluginstore.New(pluginRegistry, &fakes.FakeLoader{})
hs.pluginFileStore = filestore.ProvideService(pluginRegistry)
hs.managedPluginsService = managedplugins.NewNoop()
var err error
hs.pluginsUpdateChecker, err = updatechecker.ProvidePluginsService(hs.Cfg, nil, tracing.InitializeTracerForTest())
require.NoError(t, err)
@@ -0,0 +1,19 @@
package managedplugins
import "context"
type Manager interface {
ManagedPlugins(ctx context.Context) []string
}
var _ Manager = (*Noop)(nil)
type Noop struct{}
func NewNoop() *Noop {
return &Noop{}
}
func (s *Noop) ManagedPlugins(_ context.Context) []string {
return []string{}
}
@@ -41,6 +41,7 @@ import (
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
"github.com/grafana/grafana/pkg/services/pluginsintegration/licensing"
"github.com/grafana/grafana/pkg/services/pluginsintegration/loader"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
@@ -132,10 +133,12 @@ var WireExtensionSet = wire.NewSet(
wire.Bind(new(plugins.BackendFactoryProvider), new(*provider.Service)),
signature.ProvideOSSAuthorizer,
wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)),
wire.Bind(new(finder.Finder), new(*finder.Local)),
finder.ProvideLocalFinder,
wire.Bind(new(finder.Finder), new(*finder.Local)),
ProvideClientDecorator,
wire.Bind(new(plugins.Client), new(*client.Decorator)),
managedplugins.NewNoop,
wire.Bind(new(managedplugins.Manager), new(*managedplugins.Noop)),
)
func ProvideClientDecorator(
@@ -74,7 +74,6 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin {
Module: p.Module,
BaseURL: p.BaseURL,
ExternalService: p.ExternalService,
Angular: p.Angular,
Angular: p.Angular,
}
}
@@ -20,6 +20,7 @@ export default {
isDisabled: false,
isDeprecated: false,
isPublished: true,
isManaged: false,
name: 'Zabbix',
orgName: 'Alexander Zobnin',
popularity: 0.2093,
@@ -32,6 +32,7 @@ const plugin: CatalogPlugin = {
isDisabled: false,
isDeprecated: false,
isPublished: true,
isManaged: false,
};
describe('GetStartedWithDataSource', () => {
@@ -32,6 +32,7 @@ const plugin: CatalogPlugin = {
isDisabled: false,
isDeprecated: false,
isPublished: true,
isManaged: false,
};
function setup(opts: { angularSupportEnabled: boolean; angularDetected: boolean }) {
@@ -242,4 +243,15 @@ describe('InstallControlsButton', () => {
expect(button).toBeEnabled();
});
});
describe('update button', () => {
it('should be hidden when plugin is managed', () => {
render(
<TestProvider>
<InstallControlsButton plugin={{ ...plugin, isManaged: true }} pluginStatus={PluginStatus.UPDATE} />
</TestProvider>
);
expect(screen.queryByText('Update')).not.toBeInTheDocument();
});
});
});
@@ -152,9 +152,11 @@ export function InstallControlsButton({
return (
<Stack alignItems="flex-start" width="auto" height="auto">
<Button disabled={disableUpdate} onClick={onUpdate}>
{isInstalling ? 'Updating' : 'Update'}
</Button>
{!plugin.isManaged && (
<Button disabled={disableUpdate} onClick={onUpdate}>
{isInstalling ? 'Updating' : 'Update'}
</Button>
)}
<Button variant="destructive" disabled={isUninstalling} onClick={onUninstall}>
{uninstallBtnText}
</Button>
@@ -47,6 +47,7 @@ const getMockPlugin = (id: string): CatalogPlugin => {
isDisabled: false,
isDeprecated: false,
isPublished: true,
isManaged: false,
};
};
@@ -57,6 +57,7 @@ describe('PluginListItem', () => {
isDisabled: false,
isDeprecated: false,
isPublished: true,
isManaged: false,
};
/** As Grid */
@@ -33,6 +33,7 @@ describe('PluginListItemBadges', () => {
isDisabled: false,
isDeprecated: false,
isPublished: true,
isManaged: false,
};
afterEach(() => {
@@ -76,6 +77,13 @@ describe('PluginListItemBadges', () => {
expect(screen.getByText(/update available/i)).toBeVisible();
});
it('does not render an upgrade badge (when plugin has an available update and is managed)', () => {
render(
<PluginListItemBadges plugin={{ ...plugin, hasUpdate: true, installedVersion: '0.0.9', isManaged: true }} />
);
expect(screen.queryByText(/update available/i)).toBeNull();
});
it('renders an angular badge (when plugin is angular)', () => {
render(<PluginListItemBadges plugin={{ ...plugin, angularDetected: true }} />);
expect(screen.getByText(/angular/i)).toBeVisible();
@@ -24,7 +24,7 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) {
<Stack height="auto" wrap="wrap">
<PluginEnterpriseBadge plugin={plugin} />
{plugin.isDisabled && <PluginDisabledBadge error={plugin.error} />}
{hasUpdate && <PluginUpdateAvailableBadge plugin={plugin} />}
{hasUpdate && !plugin.isManaged && <PluginUpdateAvailableBadge plugin={plugin} />}
{plugin.angularDetected && <PluginAngularBadge />}
</Stack>
);
@@ -36,7 +36,7 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) {
{plugin.isDisabled && <PluginDisabledBadge error={plugin.error} />}
{plugin.isDeprecated && <PluginDeprecatedBadge />}
{plugin.isInstalled && <PluginInstalledBadge />}
{hasUpdate && <PluginUpdateAvailableBadge plugin={plugin} />}
{hasUpdate && !plugin.isManaged && <PluginUpdateAvailableBadge plugin={plugin} />}
{plugin.angularDetected && <PluginAngularBadge />}
</Stack>
);
@@ -203,6 +203,7 @@ describe('Plugins/Helpers', () => {
isInstalled: false,
isDeprecated: false,
isPublished: true,
isManaged: false,
name: 'Zabbix',
orgName: 'Alexander Zobnin',
popularity: 0.2111,
@@ -280,6 +281,7 @@ describe('Plugins/Helpers', () => {
isInstalled: true,
isPublished: false,
isDeprecated: false,
isManaged: false,
name: 'Zabbix',
orgName: 'Alexander Zobnin',
popularity: 0,
@@ -332,6 +334,7 @@ describe('Plugins/Helpers', () => {
isInstalled: true,
isPublished: true,
isDeprecated: false,
isManaged: false,
name: 'Zabbix',
orgName: 'Alexander Zobnin',
popularity: 0.2111,
@@ -142,6 +142,7 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C
isPublished: true,
isInstalled: isDisabled,
isDisabled: isDisabled,
isManaged: isManagedPlugin(id),
isDeprecated: status === RemotePluginStatus.Deprecated,
isCore: plugin.internal,
isDev: false,
@@ -191,6 +192,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat
isDeprecated: false,
isDev: Boolean(dev),
isEnterprise: false,
isManaged: isManagedPlugin(id),
type,
error: error?.errorCode,
accessControl: accessControl,
@@ -238,6 +240,7 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e
isDisabled: isDisabled,
isDeprecated: remote?.status === RemotePluginStatus.Deprecated,
isPublished: true,
isManaged: isManagedPlugin(id),
// TODO<check if we would like to keep preferring the remote version>
name: remote?.name || local?.name || '',
// TODO<check if we would like to keep preferring the remote version>
@@ -373,6 +376,12 @@ function isNotHiddenByConfig(id: string) {
return !pluginCatalogHiddenPlugins.includes(id);
}
export function isManagedPlugin(id: string) {
const { pluginCatalogManagedPlugins }: { pluginCatalogManagedPlugins: string[] } = config;
return pluginCatalogManagedPlugins?.includes(id);
}
function isDisabledSecretsPlugin(type?: PluginType): boolean {
return type === PluginType.secretsmanager && !config.secretsManagerPluginEnabled;
}
@@ -25,10 +25,17 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => {
}
if (Boolean(version)) {
info.push({
label: 'Version',
value: version,
});
if (plugin.isManaged) {
info.push({
label: 'Version',
value: 'Managed by Grafana',
});
} else {
info.push({
label: 'Version',
value: version,
});
}
}
if (Boolean(plugin.orgName)) {
@@ -315,6 +315,17 @@ describe('Plugin details page', () => {
expect(queryByRole('button', { name: /^install/i })).not.toBeInTheDocument();
});
it('should not display an update button for a plugin that is managed', async () => {
const { queryByRole } = renderPluginDetails({ id, isInstalled: true, hasUpdate: true, isManaged: true });
// Does not display an "update" button
expect(await queryByRole('button', { name: /update/i })).not.toBeInTheDocument();
expect(queryByRole('button', { name: /uninstall/i })).toBeInTheDocument();
// Does not display "install" button
expect(queryByRole('button', { name: /^install/i })).not.toBeInTheDocument();
});
it('should display an install button for enterprise plugins if license is valid', async () => {
config.licenseInfo.enabledFeatures = { 'enterprise.plugins': true };
@@ -44,6 +44,7 @@ export interface CatalogPlugin extends WithAccessControlMetadata {
isInstalled: boolean;
isDisabled: boolean;
isDeprecated: boolean;
isManaged: boolean; // Indicates that the plugin version is managed by Grafana
// `isPublished` is TRUE if the plugin is published to grafana.com
isPublished: boolean;
name: string;