mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugin Admin App: make the catalog look like internal component (#34341)
* Allow Route component usage in app plugins * i tried * fix catalog app * fix catalog app * fix catalog app * cleanup imports * plugin catalog enabled to plugin admin * rename plugin catalog to plugin admin * expose catalog url * update text * import from react-router-dom * fix imports -- add logging * merge changes * avoid onNavUpdate * Fixed onNavChange issues * fix library imports * more links Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
95ee5f01b5
commit
a91edd7267
@ -880,9 +880,9 @@ enable_alpha = false
|
|||||||
app_tls_skip_verify_insecure = 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.
|
# 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 =
|
allow_loading_unsigned_plugins =
|
||||||
catalog_url = https://grafana.com/grafana/plugins/
|
# Enable or disable installing plugins directly from within Grafana.
|
||||||
# Enable or disable the Marketplace app which can be used to manage plugins from within Grafana.
|
plugin_admin_enabled = false
|
||||||
catalog_app_enabled = false
|
plugin_catalog_url = https://grafana.com/grafana/plugins/
|
||||||
|
|
||||||
#################################### Grafana Image Renderer Plugin ##########################
|
#################################### Grafana Image Renderer Plugin ##########################
|
||||||
[plugin.grafana-image-renderer]
|
[plugin.grafana-image-renderer]
|
||||||
|
@ -866,9 +866,9 @@
|
|||||||
;app_tls_skip_verify_insecure = 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.
|
# 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 =
|
;allow_loading_unsigned_plugins =
|
||||||
;catalog_url = https://grafana.com/grafana/plugins/
|
# Enable or disable installing plugins directly from within Grafana.
|
||||||
# Enable or disable the Marketplace app which can be used to manage plugins from within Grafana.
|
;plugin_admin_enabled = false
|
||||||
;catalog_app_enabled = false
|
;plugin_catalog_url = https://grafana.com/grafana/plugins/
|
||||||
|
|
||||||
#################################### Grafana Image Renderer Plugin ##########################
|
#################################### Grafana Image Renderer Plugin ##########################
|
||||||
[plugin.grafana-image-renderer]
|
[plugin.grafana-image-renderer]
|
||||||
|
@ -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.
|
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
|
### plugin_admin_enabled
|
||||||
|
|
||||||
Custom install/learn more URL for enterprise plugins. Defaults to https://grafana.com/grafana/plugins/.
|
|
||||||
|
|
||||||
### catalog_app_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" >}}).
|
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/.
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
## [plugin.grafana-image-renderer]
|
## [plugin.grafana-image-renderer]
|
||||||
|
@ -74,7 +74,8 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
|||||||
customEndpoint: '',
|
customEndpoint: '',
|
||||||
sampleRate: 1,
|
sampleRate: 1,
|
||||||
};
|
};
|
||||||
catalogUrl?: string;
|
pluginCatalogURL = 'https://grafana.com/grafana/plugins/';
|
||||||
|
pluginAdminEnabled = false;
|
||||||
expressionsEnabled = false;
|
expressionsEnabled = false;
|
||||||
customTheme?: any;
|
customTheme?: any;
|
||||||
awsAllowedAuthProviders: string[] = [];
|
awsAllowedAuthProviders: string[] = [];
|
||||||
|
@ -284,7 +284,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
apiRoute.Any("/plugins/:pluginId/resources/*", hs.CallResource)
|
apiRoute.Any("/plugins/:pluginId/resources/*", hs.CallResource)
|
||||||
apiRoute.Get("/plugins/errors", routing.Wrap(hs.GetPluginErrorsList))
|
apiRoute.Get("/plugins/errors", routing.Wrap(hs.GetPluginErrorsList))
|
||||||
|
|
||||||
if hs.Cfg.CatalogAppEnabled {
|
if hs.Cfg.PluginAdminEnabled {
|
||||||
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
|
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
|
||||||
pluginRoute.Post("/:pluginId/install", bind(dtos.InstallPluginCommand{}), routing.Wrap(hs.InstallPlugin))
|
pluginRoute.Post("/:pluginId/install", bind(dtos.InstallPluginCommand{}), routing.Wrap(hs.InstallPlugin))
|
||||||
pluginRoute.Post("/:pluginId/uninstall", routing.Wrap(hs.UninstallPlugin))
|
pluginRoute.Post("/:pluginId/uninstall", routing.Wrap(hs.UninstallPlugin))
|
||||||
|
@ -129,11 +129,15 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasPluginManagerApp := false
|
||||||
pluginsToPreload := []string{}
|
pluginsToPreload := []string{}
|
||||||
for _, app := range enabledPlugins.Apps {
|
for _, app := range enabledPlugins.Apps {
|
||||||
if app.Preload {
|
if app.Preload {
|
||||||
pluginsToPreload = append(pluginsToPreload, app.Module)
|
pluginsToPreload = append(pluginsToPreload, app.Module)
|
||||||
}
|
}
|
||||||
|
if app.Id == "grafana-plugin-admin-app" {
|
||||||
|
hasPluginManagerApp = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dataSources, err := hs.getFSDataSources(c, enabledPlugins)
|
dataSources, err := hs.getFSDataSources(c, enabledPlugins)
|
||||||
@ -242,7 +246,8 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
|||||||
"rendererAvailable": hs.RenderService.IsAvailable(),
|
"rendererAvailable": hs.RenderService.IsAvailable(),
|
||||||
"http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme,
|
"http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme,
|
||||||
"sentry": hs.Cfg.Sentry,
|
"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,
|
"expressionsEnabled": hs.Cfg.ExpressionsEnabled,
|
||||||
"awsAllowedAuthProviders": hs.Cfg.AWSAllowedAuthProviders,
|
"awsAllowedAuthProviders": hs.Cfg.AWSAllowedAuthProviders,
|
||||||
"awsAssumeRoleEnabled": hs.Cfg.AWSAssumeRoleEnabled,
|
"awsAssumeRoleEnabled": hs.Cfg.AWSAssumeRoleEnabled,
|
||||||
|
@ -84,11 +84,6 @@ func (app *AppPlugin) InitApp(panels map[string]*PanelPlugin, dataSources map[st
|
|||||||
cfg *setting.Cfg) []*PluginStaticRoute {
|
cfg *setting.Cfg) []*PluginStaticRoute {
|
||||||
staticRoutes := app.InitFrontendPlugin(cfg)
|
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
|
// check if we have child panels
|
||||||
for _, panel := range panels {
|
for _, panel := range panels {
|
||||||
if strings.HasPrefix(panel.PluginDir, app.PluginDir) {
|
if strings.HasPrefix(panel.PluginDir, app.PluginDir) {
|
||||||
|
@ -533,8 +533,8 @@ func verifyBundledPluginCatalogue(t *testing.T, pm *PluginManager) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
bundledPlugins := map[string]string{
|
bundledPlugins := map[string]string{
|
||||||
"input": "input-datasource",
|
"input": "input-datasource",
|
||||||
"grafana-plugin-catalog-app": "plugin-catalog-app",
|
"grafana-plugin-admin-app": "plugin-admin-app",
|
||||||
}
|
}
|
||||||
|
|
||||||
for pluginID, pluginDir := range bundledPlugins {
|
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.dataSources["input"])
|
||||||
assert.NotNil(t, pm.apps["grafana-plugin-catalog-app"])
|
assert.NotNil(t, pm.apps["grafana-plugin-admin-app"])
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeBackendPluginManager struct {
|
type fakeBackendPluginManager struct {
|
||||||
|
@ -257,8 +257,8 @@ type Cfg struct {
|
|||||||
PluginsAppsSkipVerifyTLS bool
|
PluginsAppsSkipVerifyTLS bool
|
||||||
PluginSettings PluginSettings
|
PluginSettings PluginSettings
|
||||||
PluginsAllowUnsigned []string
|
PluginsAllowUnsigned []string
|
||||||
CatalogURL string
|
PluginCatalogURL string
|
||||||
CatalogAppEnabled bool
|
PluginAdminEnabled bool
|
||||||
DisableSanitizeHtml bool
|
DisableSanitizeHtml bool
|
||||||
EnterpriseLicensePath string
|
EnterpriseLicensePath string
|
||||||
|
|
||||||
@ -892,8 +892,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
|||||||
plug = strings.TrimSpace(plug)
|
plug = strings.TrimSpace(plug)
|
||||||
cfg.PluginsAllowUnsigned = append(cfg.PluginsAllowUnsigned, plug)
|
cfg.PluginsAllowUnsigned = append(cfg.PluginsAllowUnsigned, plug)
|
||||||
}
|
}
|
||||||
cfg.CatalogURL = pluginsSection.Key("catalog_url").MustString("https://grafana.com/grafana/plugins/")
|
cfg.PluginCatalogURL = pluginsSection.Key("plugin_catalog_url").MustString("https://grafana.com/grafana/plugins/")
|
||||||
cfg.CatalogAppEnabled = pluginsSection.Key("catalog_app_enabled").MustBool(false)
|
cfg.PluginAdminEnabled = pluginsSection.Key("plugin_admin_enabled").MustBool(false)
|
||||||
|
|
||||||
// Read and populate feature toggles list
|
// Read and populate feature toggles list
|
||||||
featureTogglesSection := iniFile.Section("feature_toggles")
|
featureTogglesSection := iniFile.Section("feature_toggles")
|
||||||
|
@ -26,7 +26,7 @@ const (
|
|||||||
|
|
||||||
func TestPluginInstallAccess(t *testing.T) {
|
func TestPluginInstallAccess(t *testing.T) {
|
||||||
dir, cfgPath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
dir, cfgPath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||||
CatalogAppEnabled: true,
|
PluginAdminEnabled: true,
|
||||||
})
|
})
|
||||||
store := testinfra.SetUpDatabase(t, dir)
|
store := testinfra.SetUpDatabase(t, dir)
|
||||||
store.Bus = bus.GetBus() // in order to allow successful user auth
|
store.Bus = bus.GetBus() // in order to allow successful user auth
|
||||||
|
@ -233,10 +233,10 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) {
|
|||||||
_, err = anonSect.NewKey("enabled", "false")
|
_, err = anonSect.NewKey("enabled", "false")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
if o.CatalogAppEnabled {
|
if o.PluginAdminEnabled {
|
||||||
anonSect, err := cfg.NewSection("plugins")
|
anonSect, err := cfg.NewSection("plugins")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = anonSect.NewKey("catalog_app_enabled", "true")
|
_, err = anonSect.NewKey("plugin_admin_enabled", "true")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -257,5 +257,5 @@ type GrafanaOpts struct {
|
|||||||
AnonymousUserRole models.RoleType
|
AnonymousUserRole models.RoleType
|
||||||
EnableQuota bool
|
EnableQuota bool
|
||||||
DisableAnonymous bool
|
DisableAnonymous bool
|
||||||
CatalogAppEnabled bool
|
PluginAdminEnabled bool
|
||||||
}
|
}
|
||||||
|
3
plugins-bundled/internal/plugin-admin-app/CHANGELOG.md
Normal file
3
plugins-bundled/internal/plugin-admin-app/CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
Changes are included in the grafana core changelog
|
3
plugins-bundled/internal/plugin-admin-app/README.md
Normal file
3
plugins-bundled/internal/plugin-admin-app/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Grafana admin app
|
||||||
|
|
||||||
|
The grafana catalog is enabled or disabled by setting `plugin_admin_enabled` in the setup files.
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@grafana-plugins/catalog-app",
|
"name": "@grafana-plugins/admin-app",
|
||||||
"version": "8.1.0-pre",
|
"version": "8.1.0-pre",
|
||||||
"description": "Plugins catalog",
|
"description": "Plugins admin",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -18,9 +18,7 @@
|
|||||||
"@grafana/data": "8.1.0-pre",
|
"@grafana/data": "8.1.0-pre",
|
||||||
"@grafana/runtime": "8.1.0-pre",
|
"@grafana/runtime": "8.1.0-pre",
|
||||||
"@grafana/toolkit": "8.1.0-pre",
|
"@grafana/toolkit": "8.1.0-pre",
|
||||||
"@grafana/ui": "8.1.0-pre",
|
"@grafana/ui": "8.1.0-pre"
|
||||||
"@types/semver": "^7.3.4",
|
|
||||||
"semver": "^7.3.4"
|
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "12.16.2"
|
"node": "12.16.2"
|
58
plugins-bundled/internal/plugin-admin-app/src/RootPage.tsx
Normal file
58
plugins-bundled/internal/plugin-admin-app/src/RootPage.tsx
Normal file
@ -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 <NotEnabled {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={`${props.basename}`}
|
||||||
|
render={() => {
|
||||||
|
return <Browse {...props} />; // or discover?
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={`${props.basename}/browse`}
|
||||||
|
render={() => {
|
||||||
|
return <Browse {...props} />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={`${props.basename}/discover`}
|
||||||
|
render={() => {
|
||||||
|
return <Discover {...props} />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={`${props.basename}/plugin/:pluginId`}
|
||||||
|
render={() => {
|
||||||
|
return <PluginDetails {...props} />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={`${props.basename}/library`}
|
||||||
|
render={() => {
|
||||||
|
return <Library {...props} />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -35,7 +35,7 @@ export const Card = ({ href, text, image, layout = 'vertical' }: Props) => {
|
|||||||
|
|
||||||
const getCardStyles = (theme: GrafanaTheme2) => ({
|
const getCardStyles = (theme: GrafanaTheme2) => ({
|
||||||
root: css`
|
root: css`
|
||||||
background-color: ${theme.colors.background.primary};
|
background-color: ${theme.colors.background.secondary};
|
||||||
border-radius: ${theme.shape.borderRadius()};
|
border-radius: ${theme.shape.borderRadius()};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 100%;
|
height: 100%;
|
@ -17,10 +17,9 @@ import appEvents from 'grafana/app/core/app_events';
|
|||||||
interface Props {
|
interface Props {
|
||||||
localPlugin?: Metadata;
|
localPlugin?: Metadata;
|
||||||
remotePlugin: Plugin;
|
remotePlugin: Plugin;
|
||||||
slug: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InstallControls = ({ localPlugin, remotePlugin, slug }: Props) => {
|
export const InstallControls = ({ localPlugin, remotePlugin }: Props) => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isInstalled, setIsInstalled] = useState(Boolean(localPlugin));
|
const [isInstalled, setIsInstalled] = useState(Boolean(localPlugin));
|
||||||
const [shouldUpdate, setShouldUpdate] = useState(
|
const [shouldUpdate, setShouldUpdate] = useState(
|
||||||
@ -32,7 +31,7 @@ export const InstallControls = ({ localPlugin, remotePlugin, slug }: Props) => {
|
|||||||
const onInstall = async () => {
|
const onInstall = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await api.installPlugin(slug, remotePlugin.version);
|
await api.installPlugin(remotePlugin.slug, remotePlugin.version);
|
||||||
appEvents.emit(AppEvents.alertSuccess, [`Installed ${remotePlugin?.name}`]);
|
appEvents.emit(AppEvents.alertSuccess, [`Installed ${remotePlugin?.name}`]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setIsInstalled(true);
|
setIsInstalled(true);
|
||||||
@ -44,7 +43,7 @@ export const InstallControls = ({ localPlugin, remotePlugin, slug }: Props) => {
|
|||||||
const onUninstall = async () => {
|
const onUninstall = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await api.uninstallPlugin(slug);
|
await api.uninstallPlugin(remotePlugin.slug);
|
||||||
appEvents.emit(AppEvents.alertSuccess, [`Uninstalled ${remotePlugin?.name}`]);
|
appEvents.emit(AppEvents.alertSuccess, [`Uninstalled ${remotePlugin?.name}`]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setIsInstalled(false);
|
setIsInstalled(false);
|
||||||
@ -56,7 +55,7 @@ export const InstallControls = ({ localPlugin, remotePlugin, slug }: Props) => {
|
|||||||
const onUpdate = async () => {
|
const onUpdate = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await api.installPlugin(slug, remotePlugin.version);
|
await api.installPlugin(remotePlugin.slug, remotePlugin.version);
|
||||||
appEvents.emit(AppEvents.alertSuccess, [`Updated ${remotePlugin?.name}`]);
|
appEvents.emit(AppEvents.alertSuccess, [`Updated ${remotePlugin?.name}`]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setShouldUpdate(false);
|
setShouldUpdate(false);
|
@ -23,7 +23,7 @@ export const PluginList = ({ plugins }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={`${orgName}-${name}-${typeCode}`}
|
key={`${orgName}-${name}-${typeCode}`}
|
||||||
href={`${PLUGIN_ROOT}?tab=plugin&slug=${slug}`}
|
href={`${PLUGIN_ROOT}/plugin/${slug}`}
|
||||||
image={
|
image={
|
||||||
<img
|
<img
|
||||||
src={`https://grafana.com/api/plugins/${slug}/versions/${version}/logos/small`}
|
src={`https://grafana.com/api/plugins/${slug}/versions/${version}/logos/small`}
|
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginConfigPageProps, AppPluginMeta } from '@grafana/data';
|
||||||
|
import { LinkButton } from '@grafana/ui';
|
||||||
|
import { PLUGIN_ROOT } from '../constants';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
|
interface Props extends PluginConfigPageProps<AppPluginMeta> {}
|
||||||
|
|
||||||
|
export const Settings = ({ plugin }: Props) => {
|
||||||
|
if (!config.pluginAdminEnabled) {
|
||||||
|
return <div>Plugin admin is not enabled.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LinkButton href={PLUGIN_ROOT}>Manage plugins</LinkButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
export const API_ROOT = '/api/plugins';
|
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 PLUGIN_ROOT = '/a/' + PLUGIN_ID;
|
||||||
export const GRAFANA_API_ROOT = '/api/gnet';
|
export const GRAFANA_API_ROOT = '/api/gnet';
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
10
plugins-bundled/internal/plugin-admin-app/src/module.ts
Normal file
10
plugins-bundled/internal/plugin-admin-app/src/module.ts
Normal file
@ -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',
|
||||||
|
});
|
@ -1,7 +1,9 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { AppRootProps, SelectableValue, dateTimeParse } from '@grafana/data';
|
import { AppRootProps, SelectableValue, dateTimeParse } from '@grafana/data';
|
||||||
import { Field, LoadingPlaceholder, Select } from '@grafana/ui';
|
import { Field, LoadingPlaceholder, Select } from '@grafana/ui';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { locationSearchToObject } from '@grafana/runtime';
|
||||||
|
|
||||||
import { PluginList } from '../components/PluginList';
|
import { PluginList } from '../components/PluginList';
|
||||||
import { SearchField } from '../components/SearchField';
|
import { SearchField } from '../components/SearchField';
|
||||||
@ -10,13 +12,23 @@ import { usePlugins } from '../hooks/usePlugins';
|
|||||||
import { useHistory } from '../hooks/useHistory';
|
import { useHistory } from '../hooks/useHistory';
|
||||||
import { Plugin } from '../types';
|
import { Plugin } from '../types';
|
||||||
import { Page } from 'components/Page';
|
import { Page } from 'components/Page';
|
||||||
|
import { CatalogTab, getCatalogNavModel } from './nav';
|
||||||
|
|
||||||
export const Browse = ({ query }: AppRootProps) => {
|
export const Browse = ({ meta, onNavChanged, basename }: AppRootProps) => {
|
||||||
const { q, filterBy, sortBy } = query;
|
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 plugins = usePlugins();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onNavChanged(getCatalogNavModel(CatalogTab.Browse, basename));
|
||||||
|
}, [onNavChanged, basename]);
|
||||||
|
|
||||||
const onSortByChange = (value: SelectableValue<string>) => {
|
const onSortByChange = (value: SelectableValue<string>) => {
|
||||||
history.push({ query: { sortBy: value.value } });
|
history.push({ query: { sortBy: value.value } });
|
||||||
};
|
};
|
@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { cx, css } from '@emotion/css';
|
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 { useStyles2, Legend, LinkButton } from '@grafana/ui';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
|
|
||||||
import { PLUGIN_ROOT } from '../constants';
|
import { PLUGIN_ROOT } from '../constants';
|
||||||
import { Card } from '../components/Card';
|
import { Card } from '../components/Card';
|
||||||
@ -11,18 +12,19 @@ import { PluginList } from '../components/PluginList';
|
|||||||
import { SearchField } from '../components/SearchField';
|
import { SearchField } from '../components/SearchField';
|
||||||
import { PluginTypeIcon } from '../components/PluginTypeIcon';
|
import { PluginTypeIcon } from '../components/PluginTypeIcon';
|
||||||
import { usePlugins } from '../hooks/usePlugins';
|
import { usePlugins } from '../hooks/usePlugins';
|
||||||
import { useHistory } from '../hooks/useHistory';
|
|
||||||
import { Plugin } from '../types';
|
import { Plugin } from '../types';
|
||||||
import { Page } from 'components/Page';
|
import { Page } from 'components/Page';
|
||||||
import { Loader } from 'components/Loader';
|
import { Loader } from 'components/Loader';
|
||||||
|
|
||||||
export const Discover = () => {
|
export const Discover = ({ meta }: AppRootProps) => {
|
||||||
const { items, isLoading } = usePlugins();
|
const { items, isLoading } = usePlugins();
|
||||||
const history = useHistory();
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const onSearch = (q: string) => {
|
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);
|
const featuredPlugins = items.filter((_) => _.featured > 0);
|
||||||
@ -56,14 +58,14 @@ export const Discover = () => {
|
|||||||
{/* Most popular */}
|
{/* Most popular */}
|
||||||
<div className={styles.legendContainer}>
|
<div className={styles.legendContainer}>
|
||||||
<Legend className={styles.legend}>Most popular</Legend>
|
<Legend className={styles.legend}>Most popular</Legend>
|
||||||
<LinkButton href={`${PLUGIN_ROOT}?tab=browse&sortBy=popularity`}>See more</LinkButton>
|
<LinkButton href={`${PLUGIN_ROOT}/browse?sortBy=popularity`}>See more</LinkButton>
|
||||||
</div>
|
</div>
|
||||||
<PluginList plugins={mostPopular.slice(0, 5)} />
|
<PluginList plugins={mostPopular.slice(0, 5)} />
|
||||||
|
|
||||||
{/* Recently added */}
|
{/* Recently added */}
|
||||||
<div className={styles.legendContainer}>
|
<div className={styles.legendContainer}>
|
||||||
<Legend className={styles.legend}>Recently added</Legend>
|
<Legend className={styles.legend}>Recently added</Legend>
|
||||||
<LinkButton href={`${PLUGIN_ROOT}?tab=browse&sortBy=published'`}>See more</LinkButton>
|
<LinkButton href={`${PLUGIN_ROOT}/browse?sortBy=published'`}>See more</LinkButton>
|
||||||
</div>
|
</div>
|
||||||
<PluginList plugins={recentlyAdded.slice(0, 5)} />
|
<PluginList plugins={recentlyAdded.slice(0, 5)} />
|
||||||
|
|
||||||
@ -72,19 +74,19 @@ export const Discover = () => {
|
|||||||
<Grid>
|
<Grid>
|
||||||
<Card
|
<Card
|
||||||
layout="horizontal"
|
layout="horizontal"
|
||||||
href={`${PLUGIN_ROOT}?tab=browse&filterBy=panel`}
|
href={`${PLUGIN_ROOT}/browse?filterBy=panel`}
|
||||||
image={<PluginTypeIcon typeCode="panel" size={18} />}
|
image={<PluginTypeIcon typeCode="panel" size={18} />}
|
||||||
text={<span className={styles.typeLegend}> Panels</span>}
|
text={<span className={styles.typeLegend}> Panels</span>}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
layout="horizontal"
|
layout="horizontal"
|
||||||
href={`${PLUGIN_ROOT}?tab=browse&filterBy=datasource`}
|
href={`${PLUGIN_ROOT}/browse?filterBy=datasource`}
|
||||||
image={<PluginTypeIcon typeCode="datasource" size={18} />}
|
image={<PluginTypeIcon typeCode="datasource" size={18} />}
|
||||||
text={<span className={styles.typeLegend}> Data sources</span>}
|
text={<span className={styles.typeLegend}> Data sources</span>}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
layout="horizontal"
|
layout="horizontal"
|
||||||
href={`${PLUGIN_ROOT}?tab=browse&filterBy=app`}
|
href={`${PLUGIN_ROOT}/browse?filterBy=app`}
|
||||||
image={<PluginTypeIcon typeCode="app" size={18} />}
|
image={<PluginTypeIcon typeCode="app" size={18} />}
|
||||||
text={<span className={styles.typeLegend}> Apps</span>}
|
text={<span className={styles.typeLegend}> Apps</span>}
|
||||||
/>
|
/>
|
@ -1,17 +1,22 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { AppRootProps, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { PLUGIN_ROOT } from '../constants';
|
import { PLUGIN_ROOT } from '../constants';
|
||||||
import { PluginList } from '../components/PluginList';
|
import { PluginList } from '../components/PluginList';
|
||||||
import { usePlugins } from '../hooks/usePlugins';
|
import { usePlugins } from '../hooks/usePlugins';
|
||||||
import { Page } from 'components/Page';
|
import { Page } from 'components/Page';
|
||||||
import { Loader } from 'components/Loader';
|
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 { isLoading, items, installedPlugins } = usePlugins();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onNavChanged(getCatalogNavModel(CatalogTab.Browse, basename));
|
||||||
|
}, [onNavChanged, basename]);
|
||||||
|
|
||||||
const filteredPlugins = items.filter((plugin) => !!installedPlugins.find((_) => _.id === plugin.slug));
|
const filteredPlugins = items.filter((plugin) => !!installedPlugins.find((_) => _.id === plugin.slug));
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@ -26,7 +31,7 @@ export const Library = () => {
|
|||||||
) : (
|
) : (
|
||||||
<p>
|
<p>
|
||||||
You haven't installed any plugins. Browse the{' '}
|
You haven't installed any plugins. Browse the{' '}
|
||||||
<a className={styles.link} href={`${PLUGIN_ROOT}/?tab=browse&sortBy=popularity`}>
|
<a className={styles.link} href={`${PLUGIN_ROOT}/browse?sortBy=popularity`}>
|
||||||
catalog
|
catalog
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
for plugins to install.
|
for plugins to install.
|
@ -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 (
|
||||||
|
<Page>
|
||||||
|
To enabled installing plugins, set the{' '}
|
||||||
|
<a href="https://grafana.com/docs/grafana/latest/plugins/catalog">Plugin Catalog</a> instructions
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
@ -1,8 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
import { AppRootProps, GrafanaTheme2 } from '@grafana/data';
|
import { AppRootProps, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { useStyles2, TabsBar, TabContent, Tab, Icon } from '@grafana/ui';
|
import { useStyles2, TabsBar, TabContent, Tab, Icon } from '@grafana/ui';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { VersionList } from '../components/VersionList';
|
import { VersionList } from '../components/VersionList';
|
||||||
import { InstallControls } from '../components/InstallControls';
|
import { InstallControls } from '../components/InstallControls';
|
||||||
@ -11,13 +12,15 @@ import { usePlugin } from '../hooks/usePlugins';
|
|||||||
import { Page } from 'components/Page';
|
import { Page } from 'components/Page';
|
||||||
import { Loader } from 'components/Loader';
|
import { Loader } from 'components/Loader';
|
||||||
|
|
||||||
export const PluginDetails = ({ query }: AppRootProps) => {
|
export const PluginDetails = ({ onNavChanged }: AppRootProps) => {
|
||||||
const { slug } = query;
|
const { pluginId } = useParams<{ pluginId: string }>();
|
||||||
|
|
||||||
const [tabs, setTabs] = useState([
|
const [tabs, setTabs] = useState([
|
||||||
{ label: 'Overview', active: true },
|
{ label: 'Overview', active: true },
|
||||||
{ label: 'Version history', active: false },
|
{ label: 'Version history', active: false },
|
||||||
]);
|
]);
|
||||||
const { isLoading, local, remote, remoteVersions } = usePlugin(slug);
|
|
||||||
|
const { isLoading, local, remote, remoteVersions } = usePlugin(pluginId);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const description = remote?.description;
|
const description = remote?.description;
|
||||||
@ -26,6 +29,10 @@ export const PluginDetails = ({ query }: AppRootProps) => {
|
|||||||
const links = (local?.info?.links || remote?.json?.info?.links) ?? [];
|
const links = (local?.info?.links || remote?.json?.info?.links) ?? [];
|
||||||
const downloads = remote?.downloads;
|
const downloads = remote?.downloads;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onNavChanged(undefined as any);
|
||||||
|
}, [onNavChanged]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
@ -34,7 +41,7 @@ export const PluginDetails = ({ query }: AppRootProps) => {
|
|||||||
<Page>
|
<Page>
|
||||||
<div className={styles.headerContainer}>
|
<div className={styles.headerContainer}>
|
||||||
<img
|
<img
|
||||||
src={`${GRAFANA_API_ROOT}/plugins/${slug}/versions/${remote?.version}/logos/small`}
|
src={`${GRAFANA_API_ROOT}/plugins/${pluginId}/versions/${remote?.version}/logos/small`}
|
||||||
className={css`
|
className={css`
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -45,7 +52,7 @@ export const PluginDetails = ({ query }: AppRootProps) => {
|
|||||||
<div className={styles.headerWrapper}>
|
<div className={styles.headerWrapper}>
|
||||||
<h1>{remote?.name}</h1>
|
<h1>{remote?.name}</h1>
|
||||||
<div className={styles.headerLinks}>
|
<div className={styles.headerLinks}>
|
||||||
<a className={styles.headerOrgName} href={`${PLUGIN_ROOT}?tab=org&orgSlug=${remote?.orgSlug}`}>
|
<a className={styles.headerOrgName} href={`${PLUGIN_ROOT}`}>
|
||||||
{remote?.orgName}
|
{remote?.orgName}
|
||||||
</a>
|
</a>
|
||||||
{links.map((link: any) => (
|
{links.map((link: any) => (
|
||||||
@ -62,7 +69,7 @@ export const PluginDetails = ({ query }: AppRootProps) => {
|
|||||||
{version && <span>{version}</span>}
|
{version && <span>{version}</span>}
|
||||||
</div>
|
</div>
|
||||||
<p>{description}</p>
|
<p>{description}</p>
|
||||||
{remote && <InstallControls localPlugin={local} remotePlugin={remote} slug={slug} />}
|
{remote && <InstallControls localPlugin={local} remotePlugin={remote} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TabsBar>
|
<TabsBar>
|
@ -3,7 +3,6 @@ import { AppRootProps } from '@grafana/data';
|
|||||||
import { Discover } from './Discover';
|
import { Discover } from './Discover';
|
||||||
import { Browse } from './Browse';
|
import { Browse } from './Browse';
|
||||||
import { PluginDetails } from './PluginDetails';
|
import { PluginDetails } from './PluginDetails';
|
||||||
import { OrgDetails } from './OrgDetails';
|
|
||||||
import { Library } from './Library';
|
import { Library } from './Library';
|
||||||
|
|
||||||
export type PageDefinition = {
|
export type PageDefinition = {
|
||||||
@ -38,10 +37,4 @@ export const pages: PageDefinition[] = [
|
|||||||
id: 'plugin',
|
id: 'plugin',
|
||||||
text: 'Plugin',
|
text: 'Plugin',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
component: OrgDetails,
|
|
||||||
icon: 'file-alt',
|
|
||||||
id: 'org',
|
|
||||||
text: 'Organization',
|
|
||||||
},
|
|
||||||
];
|
];
|
67
plugins-bundled/internal/plugin-admin-app/src/pages/nav.ts
Normal file
67
plugins-bundled/internal/plugin-admin-app/src/pages/nav.ts
Normal file
@ -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;
|
||||||
|
}
|
29
plugins-bundled/internal/plugin-admin-app/src/plugin.json
Normal file
29
plugins-bundled/internal/plugin-admin-app/src/plugin.json
Normal file
@ -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": []
|
||||||
|
}
|
||||||
|
}
|
@ -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).
|
|
@ -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 <Page {...props} path={pathWithoutLeadingSlash} />;
|
|
||||||
});
|
|
@ -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<State>({
|
|
||||||
isLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchOrgData = async () => {
|
|
||||||
const org = await api.getOrg(slug);
|
|
||||||
setState({ org, isLoading: false });
|
|
||||||
};
|
|
||||||
fetchOrgData();
|
|
||||||
}, [slug]);
|
|
||||||
|
|
||||||
return state;
|
|
||||||
};
|
|
Binary file not shown.
Before Width: | Height: | Size: 529 KiB |
Binary file not shown.
Before Width: | Height: | Size: 396 KiB |
Binary file not shown.
Before Width: | Height: | Size: 509 KiB |
@ -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<AppRootProps>);
|
|
@ -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 <Loader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<img src={orgData.org?.avatarUrl} className={styles.img} />
|
|
||||||
<h1 className={styles.orgName}>{orgData.org?.name}</h1>
|
|
||||||
</div>
|
|
||||||
<PluginList plugins={plugins} />
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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)};
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
};
|
|
@ -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": []
|
|
||||||
}
|
|
||||||
}
|
|
@ -203,7 +203,7 @@ function getPhantomPlugin(options: GetPhantomPluginOptions): DataSourcePluginMet
|
|||||||
author: { name: 'Grafana Labs' },
|
author: { name: 'Grafana Labs' },
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
url: config.catalogUrl + options.id,
|
url: config.pluginCatalogURL + options.id,
|
||||||
name: 'Install now',
|
name: 'Install now',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -86,6 +86,7 @@ class AppRootPage extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onNavChanged = (nav: NavModel) => {
|
onNavChanged = (nav: NavModel) => {
|
||||||
|
console.log('NAV CHANGED!!!', nav);
|
||||||
this.setState({ nav });
|
this.setState({ nav });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import { setPluginsSearchQuery } from './state/reducers';
|
|||||||
import { useAsync } from 'react-use';
|
import { useAsync } from 'react-use';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { PluginsErrorsInfo } from './PluginsErrorsInfo';
|
import { PluginsErrorsInfo } from './PluginsErrorsInfo';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
const mapStateToProps = (state: StoreState) => ({
|
||||||
navModel: getNavModel(state.navIndex, 'plugins'),
|
navModel: getNavModel(state.navIndex, 'plugins'),
|
||||||
@ -40,11 +41,18 @@ export const PluginListPage: React.FC<Props> = ({
|
|||||||
loadPlugins();
|
loadPlugins();
|
||||||
}, [loadPlugins]);
|
}, [loadPlugins]);
|
||||||
|
|
||||||
|
let actionTarget: string | undefined = '_blank';
|
||||||
const linkButton = {
|
const linkButton = {
|
||||||
href: 'https://grafana.com/plugins?utm_source=grafana_plugin_list',
|
href: 'https://grafana.com/plugins?utm_source=grafana_plugin_list',
|
||||||
title: 'Find more plugins on Grafana.com',
|
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 (
|
return (
|
||||||
<Page navModel={navModel} aria-label={selectors.pages.PluginsList.page}>
|
<Page navModel={navModel} aria-label={selectors.pages.PluginsList.page}>
|
||||||
<Page.Contents isLoading={!hasFetched}>
|
<Page.Contents isLoading={!hasFetched}>
|
||||||
@ -54,7 +62,7 @@ export const PluginListPage: React.FC<Props> = ({
|
|||||||
setSearchQuery={(query) => setPluginsSearchQuery(query)}
|
setSearchQuery={(query) => setPluginsSearchQuery(query)}
|
||||||
linkButton={linkButton}
|
linkButton={linkButton}
|
||||||
placeholder="Search by name, author, description or type"
|
placeholder="Search by name, author, description or type"
|
||||||
target="_blank"
|
target={actionTarget}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PluginsErrorsInfo>
|
<PluginsErrorsInfo>
|
||||||
|
Loading…
Reference in New Issue
Block a user