diff --git a/conf/defaults.ini b/conf/defaults.ini index 8b8511aec78..c64eb7e9e43 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -880,9 +880,9 @@ enable_alpha = false app_tls_skip_verify_insecure = false # Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature. allow_loading_unsigned_plugins = -catalog_url = https://grafana.com/grafana/plugins/ -# Enable or disable the Marketplace app which can be used to manage plugins from within Grafana. -catalog_app_enabled = false +# Enable or disable installing plugins directly from within Grafana. +plugin_admin_enabled = false +plugin_catalog_url = https://grafana.com/grafana/plugins/ #################################### Grafana Image Renderer Plugin ########################## [plugin.grafana-image-renderer] diff --git a/conf/sample.ini b/conf/sample.ini index 18e792a39b9..ed2e289432c 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -866,9 +866,9 @@ ;app_tls_skip_verify_insecure = false # Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature. ;allow_loading_unsigned_plugins = -;catalog_url = https://grafana.com/grafana/plugins/ -# Enable or disable the Marketplace app which can be used to manage plugins from within Grafana. -;catalog_app_enabled = false +# Enable or disable installing plugins directly from within Grafana. +;plugin_admin_enabled = false +;plugin_catalog_url = https://grafana.com/grafana/plugins/ #################################### Grafana Image Renderer Plugin ########################## [plugin.grafana-image-renderer] diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index f1db5410468..ce38c87aaba 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -1471,17 +1471,18 @@ Set to `true` if you want to test alpha plugins that are not yet ready for gener Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature. -### catalog_url - -Custom install/learn more URL for enterprise plugins. Defaults to https://grafana.com/grafana/plugins/. - -### catalog_app_enabled +### plugin_admin_enabled -Available to Grafana administrators only, the plugin catalog app is set to `false` by default. Set it to `true` to enable the app. +Available to Grafana administrators only, the plugin admin app is set to `false` by default. Set it to `true` to enable the app. For more information, refer to [Plugin catalog]({{< relref "../plugins/catalog.md" >}}). + +### plugin_catalog_url + +Custom install/learn more URL for enterprise plugins. Defaults to https://grafana.com/grafana/plugins/. +
## [plugin.grafana-image-renderer] diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index d95d7547f51..d1306c1fc35 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -74,7 +74,8 @@ export class GrafanaBootConfig implements GrafanaConfig { customEndpoint: '', sampleRate: 1, }; - catalogUrl?: string; + pluginCatalogURL = 'https://grafana.com/grafana/plugins/'; + pluginAdminEnabled = false; expressionsEnabled = false; customTheme?: any; awsAllowedAuthProviders: string[] = []; diff --git a/pkg/api/api.go b/pkg/api/api.go index 4416c097015..289f662b2f0 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -284,7 +284,7 @@ func (hs *HTTPServer) registerRoutes() { apiRoute.Any("/plugins/:pluginId/resources/*", hs.CallResource) apiRoute.Get("/plugins/errors", routing.Wrap(hs.GetPluginErrorsList)) - if hs.Cfg.CatalogAppEnabled { + if hs.Cfg.PluginAdminEnabled { apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) { pluginRoute.Post("/:pluginId/install", bind(dtos.InstallPluginCommand{}), routing.Wrap(hs.InstallPlugin)) pluginRoute.Post("/:pluginId/uninstall", routing.Wrap(hs.UninstallPlugin)) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 1f89920e535..37c663bf7ee 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -129,11 +129,15 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i return nil, err } + hasPluginManagerApp := false pluginsToPreload := []string{} for _, app := range enabledPlugins.Apps { if app.Preload { pluginsToPreload = append(pluginsToPreload, app.Module) } + if app.Id == "grafana-plugin-admin-app" { + hasPluginManagerApp = true + } } dataSources, err := hs.getFSDataSources(c, enabledPlugins) @@ -242,7 +246,8 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "rendererAvailable": hs.RenderService.IsAvailable(), "http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme, "sentry": hs.Cfg.Sentry, - "catalogUrl": hs.Cfg.CatalogURL, + "pluginCatalogURL": hs.Cfg.PluginCatalogURL, + "pluginAdminEnabled": c.HasUserRole(models.ROLE_ADMIN) && hs.Cfg.PluginAdminEnabled && hasPluginManagerApp, "expressionsEnabled": hs.Cfg.ExpressionsEnabled, "awsAllowedAuthProviders": hs.Cfg.AWSAllowedAuthProviders, "awsAssumeRoleEnabled": hs.Cfg.AWSAssumeRoleEnabled, diff --git a/pkg/plugins/app_plugin.go b/pkg/plugins/app_plugin.go index c1f10201415..84b6d6b8111 100644 --- a/pkg/plugins/app_plugin.go +++ b/pkg/plugins/app_plugin.go @@ -84,11 +84,6 @@ func (app *AppPlugin) InitApp(panels map[string]*PanelPlugin, dataSources map[st cfg *setting.Cfg) []*PluginStaticRoute { staticRoutes := app.InitFrontendPlugin(cfg) - // force enable bundled catalog app - if app.Id == "grafana-plugin-catalog-app" && cfg.CatalogAppEnabled { - app.AutoEnabled = true - } - // check if we have child panels for _, panel := range panels { if strings.HasPrefix(panel.PluginDir, app.PluginDir) { diff --git a/pkg/plugins/manager/manager_test.go b/pkg/plugins/manager/manager_test.go index d9fe062776b..a3038d696d9 100644 --- a/pkg/plugins/manager/manager_test.go +++ b/pkg/plugins/manager/manager_test.go @@ -533,8 +533,8 @@ func verifyBundledPluginCatalogue(t *testing.T, pm *PluginManager) { t.Helper() bundledPlugins := map[string]string{ - "input": "input-datasource", - "grafana-plugin-catalog-app": "plugin-catalog-app", + "input": "input-datasource", + "grafana-plugin-admin-app": "plugin-admin-app", } for pluginID, pluginDir := range bundledPlugins { @@ -547,7 +547,7 @@ func verifyBundledPluginCatalogue(t *testing.T, pm *PluginManager) { } assert.NotNil(t, pm.dataSources["input"]) - assert.NotNil(t, pm.apps["grafana-plugin-catalog-app"]) + assert.NotNil(t, pm.apps["grafana-plugin-admin-app"]) } type fakeBackendPluginManager struct { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 65df953c046..a2ecbb42f40 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -257,8 +257,8 @@ type Cfg struct { PluginsAppsSkipVerifyTLS bool PluginSettings PluginSettings PluginsAllowUnsigned []string - CatalogURL string - CatalogAppEnabled bool + PluginCatalogURL string + PluginAdminEnabled bool DisableSanitizeHtml bool EnterpriseLicensePath string @@ -892,8 +892,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { plug = strings.TrimSpace(plug) cfg.PluginsAllowUnsigned = append(cfg.PluginsAllowUnsigned, plug) } - cfg.CatalogURL = pluginsSection.Key("catalog_url").MustString("https://grafana.com/grafana/plugins/") - cfg.CatalogAppEnabled = pluginsSection.Key("catalog_app_enabled").MustBool(false) + cfg.PluginCatalogURL = pluginsSection.Key("plugin_catalog_url").MustString("https://grafana.com/grafana/plugins/") + cfg.PluginAdminEnabled = pluginsSection.Key("plugin_admin_enabled").MustBool(false) // Read and populate feature toggles list featureTogglesSection := iniFile.Section("feature_toggles") diff --git a/pkg/tests/api/plugins/api_install_test.go b/pkg/tests/api/plugins/api_install_test.go index aa6e3d549ec..532f4028a7e 100644 --- a/pkg/tests/api/plugins/api_install_test.go +++ b/pkg/tests/api/plugins/api_install_test.go @@ -26,7 +26,7 @@ const ( func TestPluginInstallAccess(t *testing.T) { dir, cfgPath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - CatalogAppEnabled: true, + PluginAdminEnabled: true, }) store := testinfra.SetUpDatabase(t, dir) store.Bus = bus.GetBus() // in order to allow successful user auth diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index 67acbddf0cc..86eceb4959c 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -233,10 +233,10 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) { _, err = anonSect.NewKey("enabled", "false") require.NoError(t, err) } - if o.CatalogAppEnabled { + if o.PluginAdminEnabled { anonSect, err := cfg.NewSection("plugins") require.NoError(t, err) - _, err = anonSect.NewKey("catalog_app_enabled", "true") + _, err = anonSect.NewKey("plugin_admin_enabled", "true") require.NoError(t, err) } } @@ -257,5 +257,5 @@ type GrafanaOpts struct { AnonymousUserRole models.RoleType EnableQuota bool DisableAnonymous bool - CatalogAppEnabled bool + PluginAdminEnabled bool } diff --git a/plugins-bundled/internal/plugin-admin-app/CHANGELOG.md b/plugins-bundled/internal/plugin-admin-app/CHANGELOG.md new file mode 100644 index 00000000000..752430173e8 --- /dev/null +++ b/plugins-bundled/internal/plugin-admin-app/CHANGELOG.md @@ -0,0 +1,3 @@ +# Change Log + +Changes are included in the grafana core changelog \ No newline at end of file diff --git a/plugins-bundled/internal/plugin-admin-app/README.md b/plugins-bundled/internal/plugin-admin-app/README.md new file mode 100644 index 00000000000..95a0dd7e7d0 --- /dev/null +++ b/plugins-bundled/internal/plugin-admin-app/README.md @@ -0,0 +1,3 @@ +# Grafana admin app + +The grafana catalog is enabled or disabled by setting `plugin_admin_enabled` in the setup files. diff --git a/plugins-bundled/internal/plugin-catalog-app/jest.config.js b/plugins-bundled/internal/plugin-admin-app/jest.config.js similarity index 100% rename from plugins-bundled/internal/plugin-catalog-app/jest.config.js rename to plugins-bundled/internal/plugin-admin-app/jest.config.js diff --git a/plugins-bundled/internal/plugin-catalog-app/package.json b/plugins-bundled/internal/plugin-admin-app/package.json similarity index 76% rename from plugins-bundled/internal/plugin-catalog-app/package.json rename to plugins-bundled/internal/plugin-admin-app/package.json index 5ec1d8f1096..dda136d891e 100644 --- a/plugins-bundled/internal/plugin-catalog-app/package.json +++ b/plugins-bundled/internal/plugin-admin-app/package.json @@ -1,7 +1,7 @@ { - "name": "@grafana-plugins/catalog-app", + "name": "@grafana-plugins/admin-app", "version": "8.1.0-pre", - "description": "Plugins catalog", + "description": "Plugins admin", "private": true, "repository": { "type": "git", @@ -18,9 +18,7 @@ "@grafana/data": "8.1.0-pre", "@grafana/runtime": "8.1.0-pre", "@grafana/toolkit": "8.1.0-pre", - "@grafana/ui": "8.1.0-pre", - "@types/semver": "^7.3.4", - "semver": "^7.3.4" + "@grafana/ui": "8.1.0-pre" }, "volta": { "node": "12.16.2" diff --git a/plugins-bundled/internal/plugin-admin-app/src/RootPage.tsx b/plugins-bundled/internal/plugin-admin-app/src/RootPage.tsx new file mode 100644 index 00000000000..ee6f0610fe8 --- /dev/null +++ b/plugins-bundled/internal/plugin-admin-app/src/RootPage.tsx @@ -0,0 +1,58 @@ +import { AppRootProps } from '@grafana/data'; +import React from 'react'; +import { Discover } from 'pages/Discover'; +import { Browse } from 'pages/Browse'; +import { PluginDetails } from 'pages/PluginDetails'; +import { Library } from 'pages/Library'; +import { Route } from 'react-router-dom'; +import { config } from '@grafana/runtime'; +import { NotEnabled } from 'pages/NotEnabed'; + +export const CatalogRootPage = React.memo(function CatalogRootPage(props: AppRootProps) { + if (!config.pluginAdminEnabled) { + return ; + } + + return ( + <> + { + return ; // or discover? + }} + /> + + { + return ; + }} + /> + + { + return ; + }} + /> + + { + return ; + }} + /> + + { + return ; + }} + /> + + ); +}); diff --git a/plugins-bundled/internal/plugin-catalog-app/src/api.ts b/plugins-bundled/internal/plugin-admin-app/src/api.ts similarity index 100% rename from plugins-bundled/internal/plugin-catalog-app/src/api.ts rename to plugins-bundled/internal/plugin-admin-app/src/api.ts diff --git a/plugins-bundled/internal/plugin-catalog-app/src/components/Card.tsx b/plugins-bundled/internal/plugin-admin-app/src/components/Card.tsx similarity index 96% rename from plugins-bundled/internal/plugin-catalog-app/src/components/Card.tsx rename to plugins-bundled/internal/plugin-admin-app/src/components/Card.tsx index 39b1e03dfb1..a41367ef728 100644 --- a/plugins-bundled/internal/plugin-catalog-app/src/components/Card.tsx +++ b/plugins-bundled/internal/plugin-admin-app/src/components/Card.tsx @@ -35,7 +35,7 @@ export const Card = ({ href, text, image, layout = 'vertical' }: Props) => { const getCardStyles = (theme: GrafanaTheme2) => ({ root: css` - background-color: ${theme.colors.background.primary}; + background-color: ${theme.colors.background.secondary}; border-radius: ${theme.shape.borderRadius()}; cursor: pointer; height: 100%; diff --git a/plugins-bundled/internal/plugin-catalog-app/src/components/Grid.tsx b/plugins-bundled/internal/plugin-admin-app/src/components/Grid.tsx similarity index 100% rename from plugins-bundled/internal/plugin-catalog-app/src/components/Grid.tsx rename to plugins-bundled/internal/plugin-admin-app/src/components/Grid.tsx diff --git a/plugins-bundled/internal/plugin-catalog-app/src/components/HorizontalGroup.tsx b/plugins-bundled/internal/plugin-admin-app/src/components/HorizontalGroup.tsx similarity index 100% rename from plugins-bundled/internal/plugin-catalog-app/src/components/HorizontalGroup.tsx rename to plugins-bundled/internal/plugin-admin-app/src/components/HorizontalGroup.tsx diff --git a/plugins-bundled/internal/plugin-catalog-app/src/components/InstallControls.tsx b/plugins-bundled/internal/plugin-admin-app/src/components/InstallControls.tsx similarity index 93% rename from plugins-bundled/internal/plugin-catalog-app/src/components/InstallControls.tsx rename to plugins-bundled/internal/plugin-admin-app/src/components/InstallControls.tsx index a6f728d6d7d..4a31ab4fa83 100644 --- a/plugins-bundled/internal/plugin-catalog-app/src/components/InstallControls.tsx +++ b/plugins-bundled/internal/plugin-admin-app/src/components/InstallControls.tsx @@ -17,10 +17,9 @@ import appEvents from 'grafana/app/core/app_events'; interface Props { localPlugin?: Metadata; remotePlugin: Plugin; - slug: string; } -export const InstallControls = ({ localPlugin, remotePlugin, slug }: Props) => { +export const InstallControls = ({ localPlugin, remotePlugin }: Props) => { const [loading, setLoading] = useState(false); const [isInstalled, setIsInstalled] = useState(Boolean(localPlugin)); const [shouldUpdate, setShouldUpdate] = useState( @@ -32,7 +31,7 @@ export const InstallControls = ({ localPlugin, remotePlugin, slug }: Props) => { const onInstall = async () => { setLoading(true); try { - await api.installPlugin(slug, remotePlugin.version); + await api.installPlugin(remotePlugin.slug, remotePlugin.version); appEvents.emit(AppEvents.alertSuccess, [`Installed ${remotePlugin?.name}`]); setLoading(false); setIsInstalled(true); @@ -44,7 +43,7 @@ export const InstallControls = ({ localPlugin, remotePlugin, slug }: Props) => { const onUninstall = async () => { setLoading(true); try { - await api.uninstallPlugin(slug); + await api.uninstallPlugin(remotePlugin.slug); appEvents.emit(AppEvents.alertSuccess, [`Uninstalled ${remotePlugin?.name}`]); setLoading(false); setIsInstalled(false); @@ -56,7 +55,7 @@ export const InstallControls = ({ localPlugin, remotePlugin, slug }: Props) => { const onUpdate = async () => { setLoading(true); try { - await api.installPlugin(slug, remotePlugin.version); + await api.installPlugin(remotePlugin.slug, remotePlugin.version); appEvents.emit(AppEvents.alertSuccess, [`Updated ${remotePlugin?.name}`]); setLoading(false); setShouldUpdate(false); diff --git a/plugins-bundled/internal/plugin-catalog-app/src/components/Loader.tsx b/plugins-bundled/internal/plugin-admin-app/src/components/Loader.tsx similarity index 100% rename from plugins-bundled/internal/plugin-catalog-app/src/components/Loader.tsx rename to plugins-bundled/internal/plugin-admin-app/src/components/Loader.tsx diff --git a/plugins-bundled/internal/plugin-catalog-app/src/components/Page.tsx b/plugins-bundled/internal/plugin-admin-app/src/components/Page.tsx similarity index 100% rename from plugins-bundled/internal/plugin-catalog-app/src/components/Page.tsx rename to plugins-bundled/internal/plugin-admin-app/src/components/Page.tsx diff --git a/plugins-bundled/internal/plugin-catalog-app/src/components/PluginList.tsx b/plugins-bundled/internal/plugin-admin-app/src/components/PluginList.tsx similarity index 96% rename from plugins-bundled/internal/plugin-catalog-app/src/components/PluginList.tsx rename to plugins-bundled/internal/plugin-admin-app/src/components/PluginList.tsx index bd2f20fb9b0..4dcbcec1192 100644 --- a/plugins-bundled/internal/plugin-catalog-app/src/components/PluginList.tsx +++ b/plugins-bundled/internal/plugin-admin-app/src/components/PluginList.tsx @@ -23,7 +23,7 @@ export const PluginList = ({ plugins }: Props) => { return ( {} + +export const Settings = ({ plugin }: Props) => { + if (!config.pluginAdminEnabled) { + return
Plugin admin is not enabled.
; + } + + return ( + <> + Manage plugins + + ); +}; diff --git a/plugins-bundled/internal/plugin-catalog-app/src/constants.ts b/plugins-bundled/internal/plugin-admin-app/src/constants.ts similarity index 70% rename from plugins-bundled/internal/plugin-catalog-app/src/constants.ts rename to plugins-bundled/internal/plugin-admin-app/src/constants.ts index 9be16c85f64..b87478b27a6 100644 --- a/plugins-bundled/internal/plugin-catalog-app/src/constants.ts +++ b/plugins-bundled/internal/plugin-admin-app/src/constants.ts @@ -1,4 +1,4 @@ export const API_ROOT = '/api/plugins'; -export const PLUGIN_ID = 'grafana-plugin-catalog-app'; +export const PLUGIN_ID = 'grafana-plugin-admin-app'; export const PLUGIN_ROOT = '/a/' + PLUGIN_ID; export const GRAFANA_API_ROOT = '/api/gnet'; diff --git a/plugins-bundled/internal/plugin-catalog-app/src/helpers.ts b/plugins-bundled/internal/plugin-admin-app/src/helpers.ts similarity index 100% rename from plugins-bundled/internal/plugin-catalog-app/src/helpers.ts rename to plugins-bundled/internal/plugin-admin-app/src/helpers.ts diff --git a/plugins-bundled/internal/plugin-catalog-app/src/hooks/useHistory.tsx b/plugins-bundled/internal/plugin-admin-app/src/hooks/useHistory.tsx similarity index 100% rename from plugins-bundled/internal/plugin-catalog-app/src/hooks/useHistory.tsx rename to plugins-bundled/internal/plugin-admin-app/src/hooks/useHistory.tsx diff --git a/plugins-bundled/internal/plugin-catalog-app/src/hooks/usePlugins.tsx b/plugins-bundled/internal/plugin-admin-app/src/hooks/usePlugins.tsx similarity index 100% rename from plugins-bundled/internal/plugin-catalog-app/src/hooks/usePlugins.tsx rename to plugins-bundled/internal/plugin-admin-app/src/hooks/usePlugins.tsx diff --git a/plugins-bundled/internal/plugin-catalog-app/src/img/logo.svg b/plugins-bundled/internal/plugin-admin-app/src/img/logo.svg similarity index 100% rename from plugins-bundled/internal/plugin-catalog-app/src/img/logo.svg rename to plugins-bundled/internal/plugin-admin-app/src/img/logo.svg diff --git a/plugins-bundled/internal/plugin-admin-app/src/module.ts b/plugins-bundled/internal/plugin-admin-app/src/module.ts new file mode 100644 index 00000000000..f7281707768 --- /dev/null +++ b/plugins-bundled/internal/plugin-admin-app/src/module.ts @@ -0,0 +1,10 @@ +import { AppPlugin } from '@grafana/data'; +import { Settings } from './config/Settings'; +import { CatalogRootPage } from './RootPage'; + +export const plugin = new AppPlugin().setRootPage(CatalogRootPage as any).addConfigPage({ + title: 'Settings', + icon: 'info-circle', + body: Settings as any, + id: 'settings', +}); diff --git a/plugins-bundled/internal/plugin-catalog-app/src/pages/Browse.tsx b/plugins-bundled/internal/plugin-admin-app/src/pages/Browse.tsx similarity index 84% rename from plugins-bundled/internal/plugin-catalog-app/src/pages/Browse.tsx rename to plugins-bundled/internal/plugin-admin-app/src/pages/Browse.tsx index 4257e671681..df0628aa3be 100644 --- a/plugins-bundled/internal/plugin-catalog-app/src/pages/Browse.tsx +++ b/plugins-bundled/internal/plugin-admin-app/src/pages/Browse.tsx @@ -1,7 +1,9 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { css } from '@emotion/css'; import { AppRootProps, SelectableValue, dateTimeParse } from '@grafana/data'; import { Field, LoadingPlaceholder, Select } from '@grafana/ui'; +import { useLocation } from 'react-router-dom'; +import { locationSearchToObject } from '@grafana/runtime'; import { PluginList } from '../components/PluginList'; import { SearchField } from '../components/SearchField'; @@ -10,13 +12,23 @@ import { usePlugins } from '../hooks/usePlugins'; import { useHistory } from '../hooks/useHistory'; import { Plugin } from '../types'; import { Page } from 'components/Page'; +import { CatalogTab, getCatalogNavModel } from './nav'; -export const Browse = ({ query }: AppRootProps) => { - const { q, filterBy, sortBy } = query; +export const Browse = ({ meta, onNavChanged, basename }: AppRootProps) => { + const location = useLocation(); + const query = locationSearchToObject(location.search); + + const q = query.q as string; + const filterBy = query.filterBy as string; + const sortBy = query.sortBy as string; const plugins = usePlugins(); const history = useHistory(); + useEffect(() => { + onNavChanged(getCatalogNavModel(CatalogTab.Browse, basename)); + }, [onNavChanged, basename]); + const onSortByChange = (value: SelectableValue) => { history.push({ query: { sortBy: value.value } }); }; diff --git a/plugins-bundled/internal/plugin-catalog-app/src/pages/Discover.tsx b/plugins-bundled/internal/plugin-admin-app/src/pages/Discover.tsx similarity index 82% rename from plugins-bundled/internal/plugin-catalog-app/src/pages/Discover.tsx rename to plugins-bundled/internal/plugin-admin-app/src/pages/Discover.tsx index b5952306204..5a716f348d1 100644 --- a/plugins-bundled/internal/plugin-catalog-app/src/pages/Discover.tsx +++ b/plugins-bundled/internal/plugin-admin-app/src/pages/Discover.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { cx, css } from '@emotion/css'; -import { dateTimeParse, GrafanaTheme2 } from '@grafana/data'; +import { dateTimeParse, AppRootProps, GrafanaTheme2 } from '@grafana/data'; import { useStyles2, Legend, LinkButton } from '@grafana/ui'; +import { locationService } from '@grafana/runtime'; import { PLUGIN_ROOT } from '../constants'; import { Card } from '../components/Card'; @@ -11,18 +12,19 @@ import { PluginList } from '../components/PluginList'; import { SearchField } from '../components/SearchField'; import { PluginTypeIcon } from '../components/PluginTypeIcon'; import { usePlugins } from '../hooks/usePlugins'; -import { useHistory } from '../hooks/useHistory'; import { Plugin } from '../types'; import { Page } from 'components/Page'; import { Loader } from 'components/Loader'; -export const Discover = () => { +export const Discover = ({ meta }: AppRootProps) => { const { items, isLoading } = usePlugins(); - const history = useHistory(); const styles = useStyles2(getStyles); const onSearch = (q: string) => { - history.push({ query: { q, tab: 'browse' } }); + locationService.push({ + pathname: `${PLUGIN_ROOT}/browse`, + search: `?q=${q}`, + }); }; const featuredPlugins = items.filter((_) => _.featured > 0); @@ -56,14 +58,14 @@ export const Discover = () => { {/* Most popular */}
Most popular - See more + See more
{/* Recently added */}
Recently added - See more + See more
@@ -72,19 +74,19 @@ export const Discover = () => { } text={ Panels} /> } text={ Data sources} /> } text={ Apps} /> diff --git a/plugins-bundled/internal/plugin-catalog-app/src/pages/Library.tsx b/plugins-bundled/internal/plugin-admin-app/src/pages/Library.tsx similarity index 72% rename from plugins-bundled/internal/plugin-catalog-app/src/pages/Library.tsx rename to plugins-bundled/internal/plugin-admin-app/src/pages/Library.tsx index 3ed89817bb3..620dc14d286 100644 --- a/plugins-bundled/internal/plugin-catalog-app/src/pages/Library.tsx +++ b/plugins-bundled/internal/plugin-admin-app/src/pages/Library.tsx @@ -1,17 +1,22 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { css } from '@emotion/css'; -import { GrafanaTheme2 } from '@grafana/data'; +import { AppRootProps, GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; import { PLUGIN_ROOT } from '../constants'; import { PluginList } from '../components/PluginList'; import { usePlugins } from '../hooks/usePlugins'; import { Page } from 'components/Page'; import { Loader } from 'components/Loader'; +import { CatalogTab, getCatalogNavModel } from './nav'; -export const Library = () => { +export const Library = ({ meta, onNavChanged, basename }: AppRootProps) => { const { isLoading, items, installedPlugins } = usePlugins(); const styles = useStyles2(getStyles); + useEffect(() => { + onNavChanged(getCatalogNavModel(CatalogTab.Browse, basename)); + }, [onNavChanged, basename]); + const filteredPlugins = items.filter((plugin) => !!installedPlugins.find((_) => _.id === plugin.slug)); if (isLoading) { @@ -26,7 +31,7 @@ export const Library = () => { ) : (

You haven't installed any plugins. Browse the{' '} - + catalog {' '} for plugins to install. diff --git a/plugins-bundled/internal/plugin-admin-app/src/pages/NotEnabed.tsx b/plugins-bundled/internal/plugin-admin-app/src/pages/NotEnabed.tsx new file mode 100644 index 00000000000..9ebc9e3137e --- /dev/null +++ b/plugins-bundled/internal/plugin-admin-app/src/pages/NotEnabed.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Page } from 'components/Page'; +import { AppRootProps, NavModelItem } from '@grafana/data'; + +export const NotEnabled = ({ onNavChanged }: AppRootProps) => { + const node: NavModelItem = { + id: 'not-found', + text: 'The plugin catalog is not enabled', + icon: 'exclamation-triangle', + url: 'not-found', + }; + onNavChanged({ + node: node, + main: node, + }); + + return ( + + To enabled installing plugins, set the{' '} + Plugin Catalog instructions + + ); +}; diff --git a/plugins-bundled/internal/plugin-catalog-app/src/pages/PluginDetails.tsx b/plugins-bundled/internal/plugin-admin-app/src/pages/PluginDetails.tsx similarity index 90% rename from plugins-bundled/internal/plugin-catalog-app/src/pages/PluginDetails.tsx rename to plugins-bundled/internal/plugin-admin-app/src/pages/PluginDetails.tsx index dfe5af03057..6e5e656606f 100644 --- a/plugins-bundled/internal/plugin-catalog-app/src/pages/PluginDetails.tsx +++ b/plugins-bundled/internal/plugin-admin-app/src/pages/PluginDetails.tsx @@ -1,8 +1,9 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { css } from '@emotion/css'; import { AppRootProps, GrafanaTheme2 } from '@grafana/data'; import { useStyles2, TabsBar, TabContent, Tab, Icon } from '@grafana/ui'; +import { useParams } from 'react-router-dom'; import { VersionList } from '../components/VersionList'; import { InstallControls } from '../components/InstallControls'; @@ -11,13 +12,15 @@ import { usePlugin } from '../hooks/usePlugins'; import { Page } from 'components/Page'; import { Loader } from 'components/Loader'; -export const PluginDetails = ({ query }: AppRootProps) => { - const { slug } = query; +export const PluginDetails = ({ onNavChanged }: AppRootProps) => { + const { pluginId } = useParams<{ pluginId: string }>(); + const [tabs, setTabs] = useState([ { label: 'Overview', active: true }, { label: 'Version history', active: false }, ]); - const { isLoading, local, remote, remoteVersions } = usePlugin(slug); + + const { isLoading, local, remote, remoteVersions } = usePlugin(pluginId); const styles = useStyles2(getStyles); const description = remote?.description; @@ -26,6 +29,10 @@ export const PluginDetails = ({ query }: AppRootProps) => { const links = (local?.info?.links || remote?.json?.info?.links) ?? []; const downloads = remote?.downloads; + useEffect(() => { + onNavChanged(undefined as any); + }, [onNavChanged]); + if (isLoading) { return ; } @@ -34,7 +41,7 @@ export const PluginDetails = ({ query }: AppRootProps) => {

{

{remote?.name}

- + {remote?.orgName} {links.map((link: any) => ( @@ -62,7 +69,7 @@ export const PluginDetails = ({ query }: AppRootProps) => { {version && {version}}

{description}

- {remote && } + {remote && }
diff --git a/plugins-bundled/internal/plugin-catalog-app/src/pages/index.ts b/plugins-bundled/internal/plugin-admin-app/src/pages/index.ts similarity index 84% rename from plugins-bundled/internal/plugin-catalog-app/src/pages/index.ts rename to plugins-bundled/internal/plugin-admin-app/src/pages/index.ts index 294726e7bd7..9206f6d6a07 100644 --- a/plugins-bundled/internal/plugin-catalog-app/src/pages/index.ts +++ b/plugins-bundled/internal/plugin-admin-app/src/pages/index.ts @@ -3,7 +3,6 @@ import { AppRootProps } from '@grafana/data'; import { Discover } from './Discover'; import { Browse } from './Browse'; import { PluginDetails } from './PluginDetails'; -import { OrgDetails } from './OrgDetails'; import { Library } from './Library'; export type PageDefinition = { @@ -38,10 +37,4 @@ export const pages: PageDefinition[] = [ id: 'plugin', text: 'Plugin', }, - { - component: OrgDetails, - icon: 'file-alt', - id: 'org', - text: 'Organization', - }, ]; diff --git a/plugins-bundled/internal/plugin-admin-app/src/pages/nav.ts b/plugins-bundled/internal/plugin-admin-app/src/pages/nav.ts new file mode 100644 index 00000000000..f8c6485c4d9 --- /dev/null +++ b/plugins-bundled/internal/plugin-admin-app/src/pages/nav.ts @@ -0,0 +1,67 @@ +import { NavModel, NavModelItem } from '@grafana/data'; + +export enum CatalogTab { + Browse = 'browse', + Discover = 'discover', + Library = 'library', +} + +export function getCatalogNavModel(tab: CatalogTab, baseURL: string): NavModel { + const pages: NavModelItem[] = []; + + if (!baseURL.endsWith('/')) { + baseURL += '/'; + } + + pages.push({ + text: 'Browse', + icon: 'icon-gf icon-gf-apps', + url: `${baseURL}`, + id: CatalogTab.Browse, + }); + + // pages.push({ + // text: 'Discover', + // icon: 'file-alt', + // url: `${baseURL}${CatalogTab.Discover}`, + // id: CatalogTab.Discover, + // }); + + pages.push({ + text: 'Library', + icon: 'icon-gf icon-gf-apps', + url: `${baseURL}${CatalogTab.Library}`, + id: CatalogTab.Library, + }); + + const node: NavModelItem = { + text: 'Catalog', + icon: 'cog', + subTitle: 'Manage plugin installations', + breadcrumbs: [{ title: 'Plugins', url: 'plugins' }], + children: setActivePage(tab, pages, CatalogTab.Browse), + }; + + return { + node: node, + main: node, + }; +} + +function setActivePage(pageId: CatalogTab, pages: NavModelItem[], defaultPageId: CatalogTab): NavModelItem[] { + let found = false; + const selected = pageId || defaultPageId; + const changed = pages.map((p) => { + const active = !found && selected === p.id; + if (active) { + found = true; + } + return { ...p, active }; + }); + + if (!found) { + changed[0].active = true; + } + + return changed; +} diff --git a/plugins-bundled/internal/plugin-admin-app/src/plugin.json b/plugins-bundled/internal/plugin-admin-app/src/plugin.json new file mode 100644 index 00000000000..2c275a22101 --- /dev/null +++ b/plugins-bundled/internal/plugin-admin-app/src/plugin.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://github.com/grafana/grafana/raw/master/docs/sources/developers/plugins/plugin.schema.json", + "type": "app", + "name": "Plugin Admin", + "id": "grafana-plugin-admin-app", + "backend": false, + "autoEnabled": true, + "pinned": false, + "info": { + "author": { + "name": "Grafana Labs", + "url": "https://grafana.com" + }, + "keywords": ["plugins"], + "logos": { + "small": "img/logo.svg", + "large": "img/logo.svg" + }, + "links": [], + "screenshots": [], + "version": "%VERSION%", + "updated": "%TODAY%" + }, + "dependencies": { + "grafanaDependency": ">=8.0.0", + "grafanaVersion": "8.0.x", + "plugins": [] + } +} diff --git a/plugins-bundled/internal/plugin-catalog-app/src/types.ts b/plugins-bundled/internal/plugin-admin-app/src/types.ts similarity index 100% rename from plugins-bundled/internal/plugin-catalog-app/src/types.ts rename to plugins-bundled/internal/plugin-admin-app/src/types.ts diff --git a/plugins-bundled/internal/plugin-catalog-app/tsconfig.json b/plugins-bundled/internal/plugin-admin-app/tsconfig.json similarity index 100% rename from plugins-bundled/internal/plugin-catalog-app/tsconfig.json rename to plugins-bundled/internal/plugin-admin-app/tsconfig.json diff --git a/plugins-bundled/internal/plugin-catalog-app/README.md b/plugins-bundled/internal/plugin-catalog-app/README.md deleted file mode 100644 index b8a4e1f5540..00000000000 --- a/plugins-bundled/internal/plugin-catalog-app/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Grafana plugin catalog - -Allow Admin users to browse and manage plugins from within Grafana. - -This plugin is **included** with Grafana however it is only accessible if [enabled in Grafana settings](https://grafana.com/docs/grafana/next/administration/configuration/#catalog_app_enabled). diff --git a/plugins-bundled/internal/plugin-catalog-app/src/RootPage.tsx b/plugins-bundled/internal/plugin-catalog-app/src/RootPage.tsx deleted file mode 100644 index c54561ad9d0..00000000000 --- a/plugins-bundled/internal/plugin-catalog-app/src/RootPage.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { AppRootProps } from '@grafana/data'; -import { pages } from 'pages'; -import React from 'react'; - -export const MarketplaceRootPage = React.memo(function MarketplaceRootPage(props: AppRootProps) { - const { - path, - query: { tab }, - } = props; - // Required to support grafana instances that use a custom `root_url`. - const pathWithoutLeadingSlash = path.replace(/^\//, ''); - - const Page = pages.find(({ id }) => id === tab)?.component || pages[0].component; - return ; -}); diff --git a/plugins-bundled/internal/plugin-catalog-app/src/hooks/useOrg.tsx b/plugins-bundled/internal/plugin-catalog-app/src/hooks/useOrg.tsx deleted file mode 100644 index 7f4fcac5c82..00000000000 --- a/plugins-bundled/internal/plugin-catalog-app/src/hooks/useOrg.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Org } from '../types'; -import { api } from '../api'; - -interface State { - isLoading: boolean; - org?: Org; -} - -export const useOrg = (slug: string): State => { - const [state, setState] = useState({ - isLoading: true, - }); - - useEffect(() => { - const fetchOrgData = async () => { - const org = await api.getOrg(slug); - setState({ org, isLoading: false }); - }; - fetchOrgData(); - }, [slug]); - - return state; -}; diff --git a/plugins-bundled/internal/plugin-catalog-app/src/img/browse.png b/plugins-bundled/internal/plugin-catalog-app/src/img/browse.png deleted file mode 100644 index 82ed80bb12c..00000000000 Binary files a/plugins-bundled/internal/plugin-catalog-app/src/img/browse.png and /dev/null differ diff --git a/plugins-bundled/internal/plugin-catalog-app/src/img/details.png b/plugins-bundled/internal/plugin-catalog-app/src/img/details.png deleted file mode 100644 index 15831b8a9f2..00000000000 Binary files a/plugins-bundled/internal/plugin-catalog-app/src/img/details.png and /dev/null differ diff --git a/plugins-bundled/internal/plugin-catalog-app/src/img/discover.png b/plugins-bundled/internal/plugin-catalog-app/src/img/discover.png deleted file mode 100644 index 183a8e92f75..00000000000 Binary files a/plugins-bundled/internal/plugin-catalog-app/src/img/discover.png and /dev/null differ diff --git a/plugins-bundled/internal/plugin-catalog-app/src/module.ts b/plugins-bundled/internal/plugin-catalog-app/src/module.ts deleted file mode 100644 index 37605d5484b..00000000000 --- a/plugins-bundled/internal/plugin-catalog-app/src/module.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ComponentClass } from 'react'; - -import { AppPlugin, AppRootProps } from '@grafana/data'; -import { MarketplaceRootPage } from './RootPage'; - -export const plugin = new AppPlugin().setRootPage((MarketplaceRootPage as unknown) as ComponentClass); diff --git a/plugins-bundled/internal/plugin-catalog-app/src/pages/OrgDetails.tsx b/plugins-bundled/internal/plugin-catalog-app/src/pages/OrgDetails.tsx deleted file mode 100644 index 648f8a3bea2..00000000000 --- a/plugins-bundled/internal/plugin-catalog-app/src/pages/OrgDetails.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import { css } from '@emotion/css'; - -import { AppRootProps, GrafanaTheme2 } from '@grafana/data'; - -import { PluginList } from '../components/PluginList'; -import { usePlugins } from '../hooks/usePlugins'; -import { useOrg } from '../hooks/useOrg'; - -import { useStyles2 } from '@grafana/ui'; -import { Page } from 'components/Page'; -import { Loader } from 'components/Loader'; - -export const OrgDetails = ({ query }: AppRootProps) => { - const { orgSlug } = query; - - const orgData = useOrg(orgSlug); - const { isLoading, items } = usePlugins(); - const styles = useStyles2(getStyles); - - const plugins = items.filter((plugin) => plugin.orgSlug === orgSlug); - - if (isLoading) { - return ; - } - - return ( - -
- -

{orgData.org?.name}

-
- -
- ); -}; - -const getStyles = (theme: GrafanaTheme2) => { - return { - header: css` - align-items: center; - display: flex; - margin-bottom: ${theme.spacing(3)}; - margin-top: ${theme.spacing(3)}; - `, - img: css` - height: 64px; - max-width: 64px; - object-fit: cover; - width: 100%; - `, - orgName: css` - margin-left: ${theme.spacing(3)}; - `, - }; -}; diff --git a/plugins-bundled/internal/plugin-catalog-app/src/plugin.json b/plugins-bundled/internal/plugin-catalog-app/src/plugin.json deleted file mode 100644 index 625651b4db7..00000000000 --- a/plugins-bundled/internal/plugin-catalog-app/src/plugin.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "$schema": "https://github.com/grafana/grafana/raw/master/docs/sources/developers/plugins/plugin.schema.json", - "type": "app", - "name": "Plugin Catalog", - "id": "grafana-plugin-catalog-app", - "backend": false, - "info": { - "author": { - "name": "Grafana Labs", - "url": "https://grafana.com" - }, - "keywords": ["plugins"], - "logos": { - "small": "img/logo.svg", - "large": "img/logo.svg" - }, - "links": [], - "screenshots": [ - { - "name": "Discover", - "path": "img/discover.png" - }, - { - "name": "Browse", - "path": "img/browse.png" - }, - { - "name": "Install", - "path": "img/details.png" - } - ], - "version": "%VERSION%", - "updated": "%TODAY%" - }, - "includes": [ - { - "type": "page", - "name": "Discover", - "path": "/a/grafana-plugin-catalog-app?tab=discover", - "role": "Admin", - "addToNav": true, - "defaultNav": true - }, - { - "type": "page", - "name": "Browse", - "path": "/a/grafana-plugin-catalog-app/?tab=browse", - "role": "Admin", - "addToNav": true - }, - { - "type": "page", - "name": "Library", - "path": "/a/grafana-plugin-catalog-app/?tab=library", - "role": "Admin", - "addToNav": true - } - ], - "dependencies": { - "grafanaDependency": ">=8.0.0", - "grafanaVersion": "8.0.x", - "plugins": [] - } -} diff --git a/public/app/features/datasources/state/buildCategories.ts b/public/app/features/datasources/state/buildCategories.ts index 484cdfe1385..9cea0ee5914 100644 --- a/public/app/features/datasources/state/buildCategories.ts +++ b/public/app/features/datasources/state/buildCategories.ts @@ -203,7 +203,7 @@ function getPhantomPlugin(options: GetPhantomPluginOptions): DataSourcePluginMet author: { name: 'Grafana Labs' }, links: [ { - url: config.catalogUrl + options.id, + url: config.pluginCatalogURL + options.id, name: 'Install now', }, ], diff --git a/public/app/features/plugins/AppRootPage.tsx b/public/app/features/plugins/AppRootPage.tsx index a4243995557..9c65fca1bcf 100644 --- a/public/app/features/plugins/AppRootPage.tsx +++ b/public/app/features/plugins/AppRootPage.tsx @@ -86,6 +86,7 @@ class AppRootPage extends Component { } onNavChanged = (nav: NavModel) => { + console.log('NAV CHANGED!!!', nav); this.setState({ nav }); }; diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index b0e6ab2b3c4..5f0f123abe0 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -12,6 +12,7 @@ import { setPluginsSearchQuery } from './state/reducers'; import { useAsync } from 'react-use'; import { selectors } from '@grafana/e2e-selectors'; import { PluginsErrorsInfo } from './PluginsErrorsInfo'; +import { config } from '@grafana/runtime'; const mapStateToProps = (state: StoreState) => ({ navModel: getNavModel(state.navIndex, 'plugins'), @@ -40,11 +41,18 @@ export const PluginListPage: React.FC = ({ loadPlugins(); }, [loadPlugins]); + let actionTarget: string | undefined = '_blank'; const linkButton = { href: 'https://grafana.com/plugins?utm_source=grafana_plugin_list', title: 'Find more plugins on Grafana.com', }; + if (config.pluginAdminEnabled) { + linkButton.href = '/a/grafana-plugin-admin-app/'; + linkButton.title = 'Install & manage plugins'; + actionTarget = undefined; + } + return ( @@ -54,7 +62,7 @@ export const PluginListPage: React.FC = ({ setSearchQuery={(query) => setPluginsSearchQuery(query)} linkButton={linkButton} placeholder="Search by name, author, description or type" - target="_blank" + target={actionTarget} />