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 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 |
@ -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