mirror of https://github.com/grafana/grafana
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>pull/34484/head
parent
95ee5f01b5
commit
a91edd7267
@ -0,0 +1,3 @@ |
||||
# Change Log |
||||
|
||||
Changes are included in the grafana core changelog |
@ -0,0 +1,3 @@ |
||||
# Grafana admin app |
||||
|
||||
The grafana catalog is enabled or disabled by setting `plugin_admin_enabled` in the setup files. |
@ -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} />; |
||||
}} |
||||
/> |
||||
</> |
||||
); |
||||
}); |
@ -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 |
@ -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', |
||||
}); |
@ -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> |
||||
); |
||||
}; |
@ -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; |
||||
} |
@ -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; |
||||
}; |
Before Width: | Height: | Size: 529 KiB |
Before Width: | Height: | Size: 396 KiB |
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": [] |
||||
} |
||||
} |
Loading…
Reference in new issue