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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 108 additions and 20 deletions

View File

@ -122,6 +122,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
pluginAdminEnabled = true;
pluginAdminExternalManageEnabled = false;
pluginCatalogHiddenPlugins: string[] = [];
pluginCatalogManagedPlugins: string[] = [];
pluginsCDNBaseURL = '';
expressionsEnabled = false;
customTheme?: undefined;

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"`

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,

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()

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,

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)

View File

@ -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{}
}

View File

@ -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(

View File

@ -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,
}
}

View File

@ -20,6 +20,7 @@ export default {
isDisabled: false,
isDeprecated: false,
isPublished: true,
isManaged: false,
name: 'Zabbix',
orgName: 'Alexander Zobnin',
popularity: 0.2093,

View File

@ -32,6 +32,7 @@ const plugin: CatalogPlugin = {
isDisabled: false,
isDeprecated: false,
isPublished: true,
isManaged: false,
};
describe('GetStartedWithDataSource', () => {

View File

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

View File

@ -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>

View File

@ -47,6 +47,7 @@ const getMockPlugin = (id: string): CatalogPlugin => {
isDisabled: false,
isDeprecated: false,
isPublished: true,
isManaged: false,
};
};

View File

@ -57,6 +57,7 @@ describe('PluginListItem', () => {
isDisabled: false,
isDeprecated: false,
isPublished: true,
isManaged: false,
};
/** As Grid */

View File

@ -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();

View File

@ -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>
);

View File

@ -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,

View File

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

View File

@ -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)) {

View File

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

View File

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