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
|
||||
# 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]
|
||||
|
@ -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]
|
||||
|
@ -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/.
|
||||
|
||||
<hr>
|
||||
|
||||
## [plugin.grafana-image-renderer]
|
||||
|
@ -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[] = [];
|
||||
|
@ -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))
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -534,7 +534,7 @@ func verifyBundledPluginCatalogue(t *testing.T, pm *PluginManager) {
|
||||
|
||||
bundledPlugins := map[string]string{
|
||||
"input": "input-datasource",
|
||||
"grafana-plugin-catalog-app": "plugin-catalog-app",
|
||||
"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 {
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
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",
|
||||
"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"
|
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) => ({
|
||||
root: css`
|
||||
background-color: ${theme.colors.background.primary};
|
||||
background-color: ${theme.colors.background.secondary};
|
||||
border-radius: ${theme.shape.borderRadius()};
|
||||
cursor: pointer;
|
||||
height: 100%;
|
@ -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);
|
@ -23,7 +23,7 @@ export const PluginList = ({ plugins }: Props) => {
|
||||
return (
|
||||
<Card
|
||||
key={`${orgName}-${name}-${typeCode}`}
|
||||
href={`${PLUGIN_ROOT}?tab=plugin&slug=${slug}`}
|
||||
href={`${PLUGIN_ROOT}/plugin/${slug}`}
|
||||
image={
|
||||
<img
|
||||
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 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';
|
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 { 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<string>) => {
|
||||
history.push({ query: { sortBy: value.value } });
|
||||
};
|
@ -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 */}
|
||||
<div className={styles.legendContainer}>
|
||||
<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>
|
||||
<PluginList plugins={mostPopular.slice(0, 5)} />
|
||||
|
||||
{/* Recently added */}
|
||||
<div className={styles.legendContainer}>
|
||||
<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>
|
||||
<PluginList plugins={recentlyAdded.slice(0, 5)} />
|
||||
|
||||
@ -72,19 +74,19 @@ export const Discover = () => {
|
||||
<Grid>
|
||||
<Card
|
||||
layout="horizontal"
|
||||
href={`${PLUGIN_ROOT}?tab=browse&filterBy=panel`}
|
||||
href={`${PLUGIN_ROOT}/browse?filterBy=panel`}
|
||||
image={<PluginTypeIcon typeCode="panel" size={18} />}
|
||||
text={<span className={styles.typeLegend}> Panels</span>}
|
||||
/>
|
||||
<Card
|
||||
layout="horizontal"
|
||||
href={`${PLUGIN_ROOT}?tab=browse&filterBy=datasource`}
|
||||
href={`${PLUGIN_ROOT}/browse?filterBy=datasource`}
|
||||
image={<PluginTypeIcon typeCode="datasource" size={18} />}
|
||||
text={<span className={styles.typeLegend}> Data sources</span>}
|
||||
/>
|
||||
<Card
|
||||
layout="horizontal"
|
||||
href={`${PLUGIN_ROOT}?tab=browse&filterBy=app`}
|
||||
href={`${PLUGIN_ROOT}/browse?filterBy=app`}
|
||||
image={<PluginTypeIcon typeCode="app" size={18} />}
|
||||
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 { 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 = () => {
|
||||
) : (
|
||||
<p>
|
||||
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
|
||||
</a>{' '}
|
||||
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 { 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 <Loader />;
|
||||
}
|
||||
@ -34,7 +41,7 @@ export const PluginDetails = ({ query }: AppRootProps) => {
|
||||
<Page>
|
||||
<div className={styles.headerContainer}>
|
||||
<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`
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
@ -45,7 +52,7 @@ export const PluginDetails = ({ query }: AppRootProps) => {
|
||||
<div className={styles.headerWrapper}>
|
||||
<h1>{remote?.name}</h1>
|
||||
<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}
|
||||
</a>
|
||||
{links.map((link: any) => (
|
||||
@ -62,7 +69,7 @@ export const PluginDetails = ({ query }: AppRootProps) => {
|
||||
{version && <span>{version}</span>}
|
||||
</div>
|
||||
<p>{description}</p>
|
||||
{remote && <InstallControls localPlugin={local} remotePlugin={remote} slug={slug} />}
|
||||
{remote && <InstallControls localPlugin={local} remotePlugin={remote} />}
|
||||
</div>
|
||||
</div>
|
||||
<TabsBar>
|
@ -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',
|
||||
},
|
||||
];
|
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' },
|
||||
links: [
|
||||
{
|
||||
url: config.catalogUrl + options.id,
|
||||
url: config.pluginCatalogURL + options.id,
|
||||
name: 'Install now',
|
||||
},
|
||||
],
|
||||
|
@ -86,6 +86,7 @@ class AppRootPage extends Component<Props, State> {
|
||||
}
|
||||
|
||||
onNavChanged = (nav: NavModel) => {
|
||||
console.log('NAV CHANGED!!!', nav);
|
||||
this.setState({ nav });
|
||||
};
|
||||
|
||||
|
@ -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<Props> = ({
|
||||
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 (
|
||||
<Page navModel={navModel} aria-label={selectors.pages.PluginsList.page}>
|
||||
<Page.Contents isLoading={!hasFetched}>
|
||||
@ -54,7 +62,7 @@ export const PluginListPage: React.FC<Props> = ({
|
||||
setSearchQuery={(query) => setPluginsSearchQuery(query)}
|
||||
linkButton={linkButton}
|
||||
placeholder="Search by name, author, description or type"
|
||||
target="_blank"
|
||||
target={actionTarget}
|
||||
/>
|
||||
|
||||
<PluginsErrorsInfo>
|
||||
|
Loading…
Reference in New Issue
Block a user