diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts
index 8a55a128763..45f92e05a43 100644
--- a/packages/grafana-runtime/src/config.ts
+++ b/packages/grafana-runtime/src/config.ts
@@ -122,6 +122,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
pluginAdminEnabled = true;
pluginAdminExternalManageEnabled = false;
pluginCatalogHiddenPlugins: string[] = [];
+ pluginCatalogManagedPlugins: string[] = [];
pluginsCDNBaseURL = '';
expressionsEnabled = false;
customTheme?: undefined;
diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go
index 60a4964b56e..a07a5e4358c 100644
--- a/pkg/api/dtos/frontend_settings.go
+++ b/pkg/api/dtos/frontend_settings.go
@@ -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"`
diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go
index 750ec9a9f11..90b3e5317e1 100644
--- a/pkg/api/frontendsettings.go
+++ b/pkg/api/frontendsettings.go
@@ -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,
diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go
index da298c3f0f4..6f3f9ad5c68 100644
--- a/pkg/api/frontendsettings_test.go
+++ b/pkg/api/frontendsettings_test.go
@@ -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()
diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go
index 9f03b7f833e..554d51b9099 100644
--- a/pkg/api/http_server.go
+++ b/pkg/api/http_server.go
@@ -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,
diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go
index 3258000d063..b29b0fdfa00 100644
--- a/pkg/api/plugins_test.go
+++ b/pkg/api/plugins_test.go
@@ -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)
diff --git a/pkg/services/pluginsintegration/managedplugins/managed.go b/pkg/services/pluginsintegration/managedplugins/managed.go
new file mode 100644
index 00000000000..1e3c4a6f369
--- /dev/null
+++ b/pkg/services/pluginsintegration/managedplugins/managed.go
@@ -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{}
+}
diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go
index 6963438dd8a..64262eecede 100644
--- a/pkg/services/pluginsintegration/pluginsintegration.go
+++ b/pkg/services/pluginsintegration/pluginsintegration.go
@@ -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(
diff --git a/pkg/services/pluginsintegration/pluginstore/plugins.go b/pkg/services/pluginsintegration/pluginstore/plugins.go
index 273e20ef3e9..194ba92ade4 100644
--- a/pkg/services/pluginsintegration/pluginstore/plugins.go
+++ b/pkg/services/pluginsintegration/pluginstore/plugins.go
@@ -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,
}
}
diff --git a/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts b/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts
index e4c0bf11c3d..4372f87602f 100644
--- a/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts
+++ b/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts
@@ -20,6 +20,7 @@ export default {
isDisabled: false,
isDeprecated: false,
isPublished: true,
+ isManaged: 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 40d747aefea..0a67af40b99 100644
--- a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx
+++ b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx
@@ -32,6 +32,7 @@ const plugin: CatalogPlugin = {
isDisabled: false,
isDeprecated: false,
isPublished: true,
+ isManaged: 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 3894a2c05b8..258f5895d9d 100644
--- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx
+++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx
@@ -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(
+
+
+
+ );
+ 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 671a5b2e8ab..d7babbf3013 100644
--- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx
+++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx
@@ -152,9 +152,11 @@ export function InstallControlsButton({
return (
-
+ {!plugin.isManaged && (
+
+ )}
diff --git a/public/app/features/plugins/admin/components/PluginList.test.tsx b/public/app/features/plugins/admin/components/PluginList.test.tsx
index 3d8c67c9765..e3fd9c61eed 100644
--- a/public/app/features/plugins/admin/components/PluginList.test.tsx
+++ b/public/app/features/plugins/admin/components/PluginList.test.tsx
@@ -47,6 +47,7 @@ const getMockPlugin = (id: string): CatalogPlugin => {
isDisabled: false,
isDeprecated: false,
isPublished: true,
+ isManaged: false,
};
};
diff --git a/public/app/features/plugins/admin/components/PluginListItem.test.tsx b/public/app/features/plugins/admin/components/PluginListItem.test.tsx
index 8aa31776608..9a609f4be84 100644
--- a/public/app/features/plugins/admin/components/PluginListItem.test.tsx
+++ b/public/app/features/plugins/admin/components/PluginListItem.test.tsx
@@ -57,6 +57,7 @@ describe('PluginListItem', () => {
isDisabled: false,
isDeprecated: false,
isPublished: true,
+ isManaged: 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 2864fe38af1..251ab8df195 100644
--- a/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx
+++ b/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx
@@ -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(
+
+ );
+ 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 eb381aec070..bc533a791a2 100644
--- a/public/app/features/plugins/admin/components/PluginListItemBadges.tsx
+++ b/public/app/features/plugins/admin/components/PluginListItemBadges.tsx
@@ -24,7 +24,7 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) {
{plugin.isDisabled && }
- {hasUpdate && }
+ {hasUpdate && !plugin.isManaged && }
{plugin.angularDetected && }
);
@@ -36,7 +36,7 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) {
{plugin.isDisabled && }
{plugin.isDeprecated && }
{plugin.isInstalled && }
- {hasUpdate && }
+ {hasUpdate && !plugin.isManaged && }
{plugin.angularDetected && }
);
diff --git a/public/app/features/plugins/admin/helpers.test.ts b/public/app/features/plugins/admin/helpers.test.ts
index ff4d8eca75e..dfadd646071 100644
--- a/public/app/features/plugins/admin/helpers.test.ts
+++ b/public/app/features/plugins/admin/helpers.test.ts
@@ -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,
diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts
index 2e9d9982e40..88416895c87 100644
--- a/public/app/features/plugins/admin/helpers.ts
+++ b/public/app/features/plugins/admin/helpers.ts
@@ -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
name: remote?.name || local?.name || '',
// TODO
@@ -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;
}
diff --git a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx
index eb9c70b3051..e50b393e022 100644
--- a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx
+++ b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx
@@ -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)) {
diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
index 6a656c03064..e13a855336b 100644
--- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
+++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
@@ -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 };
diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts
index 734bc504953..aa6b41c7737 100644
--- a/public/app/features/plugins/admin/types.ts
+++ b/public/app/features/plugins/admin/types.ts
@@ -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;