From ff56ea6ea6c0057d444be100bb14ec5a94387d05 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Tue, 20 Jul 2021 15:20:24 +0200 Subject: [PATCH] Plugins: Combine local and remote plugins into one structure (#36859) * adding some structure to combine the local and remote into one type. * feat(catalog): map local and remote responses to catalog plugin * feat(catalog): render CatalogPlugins in list * refactor(catalog): update usePluginsByFilter to work with new data structure * refactor(catalog): move helper functions into helpers file. delete redundent usePlugins hook * feat(catalog): create CatalogPluginDetails and pass to PluginDetails * feat(catalog): update types and components for plugin installation * chore(catalog): comment so not to forget to move code out of api layer * fix(catalog): make sure all filter shows gcom and installed * fix(catalog): fix up getCatalogPlugin logic for only locally available plugins * refactor(catalog): create getCatalogPluginDetails helper. Move usage to hook * revert(catalog): put back small logos in PluginList * revert(catalog): put back small logo for PluginDetails page * fix(catalog): prevent useDebounce from triggering when SearchField mounts * chore(catalog): add coment explaining reason for usedebouncewithoutfirstrender * refactor(catalog): replace reduce with filter to remove duplicate array of all plugins * refactor(catalog): update types for useDebounceWithoutFirstRender * chore(catalog): remove commented out import Co-authored-by: Jack Westbrook --- public/app/features/plugins/admin/api.ts | 14 +- .../admin/components/InstallControls.tsx | 73 +++------ .../plugins/admin/components/PluginList.tsx | 26 +--- .../plugins/admin/components/PluginLogo.tsx | 20 +-- .../plugins/admin/components/SearchField.tsx | 23 ++- public/app/features/plugins/admin/helpers.ts | 142 ++++++++++++++++++ .../plugins/admin/hooks/usePlugins.tsx | 112 +++++++------- .../plugins/admin/pages/Browse.test.tsx | 2 +- .../features/plugins/admin/pages/Browse.tsx | 32 ++-- .../plugins/admin/pages/PluginDetails.tsx | 133 ++++++++-------- public/app/features/plugins/admin/types.ts | 36 +++++ 11 files changed, 378 insertions(+), 235 deletions(-) diff --git a/public/app/features/plugins/admin/api.ts b/public/app/features/plugins/admin/api.ts index 0bd1ebec7ec..376c5e813f2 100644 --- a/public/app/features/plugins/admin/api.ts +++ b/public/app/features/plugins/admin/api.ts @@ -1,5 +1,4 @@ import { getBackendSrv } from '@grafana/runtime'; -import { PluginMeta } from '@grafana/data'; import { API_ROOT, GRAFANA_API_ROOT } from './constants'; import { Plugin, PluginDetails, Org, LocalPlugin } from './types'; @@ -43,7 +42,7 @@ async function getPluginVersions(id: string): Promise { } } -async function getInstalledPlugins(): Promise { +async function getInstalledPlugins(): Promise { const installed = await getBackendSrv().get(`${API_ROOT}`, { embedded: 0 }); return installed; } @@ -63,16 +62,6 @@ async function uninstallPlugin(id: string) { return await getBackendSrv().post(`${API_ROOT}/${id}/uninstall`); } -async function updatePlugin(pluginId: string, data: Partial) { - const response = await getBackendSrv().datasourceRequest({ - url: `/api/plugins/${pluginId}/settings`, - method: 'POST', - data, - }); - - return response?.data; -} - export const api = { getRemotePlugins, getPlugin, @@ -80,5 +69,4 @@ export const api = { getOrg, installPlugin, uninstallPlugin, - updatePlugin, }; diff --git a/public/app/features/plugins/admin/components/InstallControls.tsx b/public/app/features/plugins/admin/components/InstallControls.tsx index 62a3de8892e..13495f2c199 100644 --- a/public/app/features/plugins/admin/components/InstallControls.tsx +++ b/public/app/features/plugins/admin/components/InstallControls.tsx @@ -1,44 +1,42 @@ import React, { useState } from 'react'; import { css, cx } from '@emotion/css'; -import { gt, satisfies } from 'semver'; +import { satisfies } from 'semver'; import { config } from '@grafana/runtime'; import { Button, HorizontalGroup, Icon, LinkButton, useStyles2 } from '@grafana/ui'; import { AppEvents, GrafanaTheme2 } from '@grafana/data'; -import { LocalPlugin, Plugin } from '../types'; -import { api } from '../api'; - -// This isn't exported in the sdk yet -// @ts-ignore import appEvents from 'app/core/app_events'; +import { CatalogPluginDetails } from '../types'; +import { api } from '../api'; import { isGrafanaAdmin } from '../helpers'; interface Props { - localPlugin?: LocalPlugin; - remotePlugin: Plugin; + plugin: CatalogPluginDetails; } -export const InstallControls = ({ localPlugin, remotePlugin }: Props) => { +export const InstallControls = ({ plugin }: Props) => { const [loading, setLoading] = useState(false); - const [isInstalled, setIsInstalled] = useState(Boolean(localPlugin)); - const [shouldUpdate, setShouldUpdate] = useState( - remotePlugin?.version && localPlugin?.info.version && gt(remotePlugin?.version!, localPlugin?.info.version!) - ); + const [isInstalled, setIsInstalled] = useState(plugin.isInstalled || false); + const [shouldUpdate, setShouldUpdate] = useState(plugin.hasUpdate || false); const [hasInstalledPanel, setHasInstalledPanel] = useState(false); const isExternallyManaged = config.pluginAdminExternalManageEnabled; - const externalManageLink = getExternalManageLink(remotePlugin); + const externalManageLink = getExternalManageLink(plugin); const styles = useStyles2(getStyles); + if (!plugin) { + return null; + } + const onInstall = async () => { setLoading(true); try { - await api.installPlugin(remotePlugin.slug, remotePlugin.version); - appEvents.emit(AppEvents.alertSuccess, [`Installed ${remotePlugin?.name}`]); + await api.installPlugin(plugin.id, plugin.version); + appEvents.emit(AppEvents.alertSuccess, [`Installed ${plugin.name}`]); setLoading(false); setIsInstalled(true); - setHasInstalledPanel(remotePlugin.typeCode === 'panel'); + setHasInstalledPanel(plugin.type === 'panel'); } catch (error) { setLoading(false); } @@ -47,8 +45,8 @@ export const InstallControls = ({ localPlugin, remotePlugin }: Props) => { const onUninstall = async () => { setLoading(true); try { - await api.uninstallPlugin(remotePlugin.slug); - appEvents.emit(AppEvents.alertSuccess, [`Uninstalled ${remotePlugin?.name}`]); + await api.uninstallPlugin(plugin.id); + appEvents.emit(AppEvents.alertSuccess, [`Uninstalled ${plugin.name}`]); setLoading(false); setIsInstalled(false); } catch (error) { @@ -59,8 +57,8 @@ export const InstallControls = ({ localPlugin, remotePlugin }: Props) => { const onUpdate = async () => { setLoading(true); try { - await api.installPlugin(remotePlugin.slug, remotePlugin.version); - appEvents.emit(AppEvents.alertSuccess, [`Updated ${remotePlugin?.name}`]); + await api.installPlugin(plugin.id, plugin.version); + appEvents.emit(AppEvents.alertSuccess, [`Updated ${plugin.name}`]); setLoading(false); setShouldUpdate(false); } catch (error) { @@ -68,7 +66,7 @@ export const InstallControls = ({ localPlugin, remotePlugin }: Props) => { } }; - const grafanaDependency = remotePlugin?.json?.dependencies?.grafanaDependency; + const grafanaDependency = plugin.grafanaDependency; const unsupportedGrafanaVersion = grafanaDependency ? !satisfies(config.buildInfo.version, grafanaDependency, { // needed for when running against master @@ -76,9 +74,9 @@ export const InstallControls = ({ localPlugin, remotePlugin }: Props) => { }) : false; - const isDevelopmentBuild = Boolean(localPlugin?.dev); - const isEnterprise = remotePlugin?.status === 'enterprise'; - const isCore = remotePlugin?.internal || localPlugin?.signature === 'internal'; + const isDevelopmentBuild = Boolean(plugin.isDev); + const isEnterprise = plugin.isEnterprise; + const isCore = plugin.isCore; const hasPermission = isGrafanaAdmin(); if (isCore) { @@ -161,8 +159,8 @@ export const InstallControls = ({ localPlugin, remotePlugin }: Props) => { ); }; -function getExternalManageLink(plugin: Plugin): string { - return `https://grafana.com/grafana/plugins/${plugin.slug}`; +function getExternalManageLink(plugin: CatalogPluginDetails): string { + return `https://grafana.com/grafana/plugins/${plugin.id}`; } export const getStyles = (theme: GrafanaTheme2) => { @@ -173,26 +171,5 @@ export const getStyles = (theme: GrafanaTheme2) => { messageMargin: css` margin-left: ${theme.spacing()}; `, - readme: css` - margin: ${theme.spacing(3)} 0; - - & img { - max-width: 100%; - } - - h1, - h2, - h3 { - margin-top: ${theme.spacing(3)}; - margin-bottom: ${theme.spacing(2)}; - } - - li { - margin-left: ${theme.spacing(2)}; - & > p { - margin: ${theme.spacing()} 0; - } - } - `, }; }; diff --git a/public/app/features/plugins/admin/components/PluginList.tsx b/public/app/features/plugins/admin/components/PluginList.tsx index 236c9730d09..1c64a8b8f1c 100644 --- a/public/app/features/plugins/admin/components/PluginList.tsx +++ b/public/app/features/plugins/admin/components/PluginList.tsx @@ -4,13 +4,12 @@ import { css } from '@emotion/css'; import { Card } from '../components/Card'; import { Grid } from '../components/Grid'; -import { Plugin, LocalPlugin } from '../types'; +import { CatalogPlugin } from '../types'; import { GrafanaTheme2 } from '@grafana/data'; -import { isLocalPlugin } from '../guards'; import { PluginLogo } from './PluginLogo'; interface Props { - plugins: Array; + plugins: CatalogPlugin[]; } export const PluginList = ({ plugins }: Props) => { @@ -19,8 +18,7 @@ export const PluginList = ({ plugins }: Props) => { return ( {plugins.map((plugin) => { - const id = getPluginId(plugin); - const { name } = plugin; + const { name, id, orgName } = plugin; return ( { href={`/plugins/${id}`} image={ { text={ <>
{name}
-
{getOrgName(plugin)}
+
{orgName}
} /> @@ -47,20 +45,6 @@ export const PluginList = ({ plugins }: Props) => { ); }; -function getPluginId(plugin: Plugin | LocalPlugin): string { - if (isLocalPlugin(plugin)) { - return plugin.id; - } - return plugin.slug; -} - -function getOrgName(plugin: Plugin | LocalPlugin): string | undefined { - if (isLocalPlugin(plugin)) { - return plugin.info?.author?.name; - } - return plugin.orgName; -} - const getStyles = (theme: GrafanaTheme2) => ({ name: css` font-size: ${theme.typography.h4.fontSize}; diff --git a/public/app/features/plugins/admin/components/PluginLogo.tsx b/public/app/features/plugins/admin/components/PluginLogo.tsx index 25526db35e0..d111517e2eb 100644 --- a/public/app/features/plugins/admin/components/PluginLogo.tsx +++ b/public/app/features/plugins/admin/components/PluginLogo.tsx @@ -1,24 +1,10 @@ import React from 'react'; -import { isLocalPlugin } from '../guards'; -import { LocalPlugin, Plugin } from '../types'; type PluginLogoProps = { - plugin: Plugin | LocalPlugin | undefined; + src: string; className?: string; }; -export function PluginLogo({ plugin, className }: PluginLogoProps): React.ReactElement | null { - return ; -} - -function getImageSrc(plugin: Plugin | LocalPlugin | undefined): string { - if (!plugin) { - return 'https://grafana.com/api/plugins/404notfound/versions/none/logos/small'; - } - - if (isLocalPlugin(plugin)) { - return plugin?.info?.logos?.large; - } - - return `https://grafana.com/api/plugins/${plugin.slug}/versions/${plugin.version}/logos/small`; +export function PluginLogo({ src, className }: PluginLogoProps): React.ReactElement { + return ; } diff --git a/public/app/features/plugins/admin/components/SearchField.tsx b/public/app/features/plugins/admin/components/SearchField.tsx index c5fb4f61a98..8dbbaa3a379 100644 --- a/public/app/features/plugins/admin/components/SearchField.tsx +++ b/public/app/features/plugins/admin/components/SearchField.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { css } from '@emotion/css'; import { useStyles2 } from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data'; @@ -9,11 +9,30 @@ interface Props { onSearch: (value: string) => void; } +// useDebounce has a bug which causes it to fire on first render. This wrapper prevents that. +// https://github.com/streamich/react-use/issues/759 +const useDebounceWithoutFirstRender = (callBack: () => any, delay = 0, deps: React.DependencyList = []) => { + const isFirstRender = useRef(true); + const debounceDeps = [...deps, isFirstRender]; + + return useDebounce( + () => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + return callBack(); + }, + delay, + debounceDeps + ); +}; + export const SearchField = ({ value, onSearch }: Props) => { const [query, setQuery] = useState(value); const styles = useStyles2(getStyles); - useDebounce(() => onSearch(query ?? ''), 500, [query]); + useDebounceWithoutFirstRender(() => onSearch(query ?? ''), 500, [query]); return ( { + const fields: String[] = []; + + if (plugin.name) { + fields.push(plugin.name.toLowerCase()); + } + + if (plugin.orgName) { + fields.push(plugin.orgName.toLowerCase()); + } + + return fields.some((f) => f.includes(searchBy.toLowerCase())); + }); +} diff --git a/public/app/features/plugins/admin/hooks/usePlugins.tsx b/public/app/features/plugins/admin/hooks/usePlugins.tsx index 60cbb71751f..7da25701916 100644 --- a/public/app/features/plugins/admin/hooks/usePlugins.tsx +++ b/public/app/features/plugins/admin/hooks/usePlugins.tsx @@ -1,84 +1,86 @@ import { useMemo } from 'react'; import { useAsync } from 'react-use'; -import { Plugin, LocalPlugin } from '../types'; +import { CatalogPlugin, CatalogPluginDetails } from '../types'; import { api } from '../api'; +import { mapLocalToCatalog, mapRemoteToCatalog, getCatalogPluginDetails, applySearchFilter } from '../helpers'; -export const usePlugins = () => { - const result = useAsync(async () => { - const items = await api.getRemotePlugins(); - const filteredPlugins = items.filter((plugin) => { - const isNotRenderer = plugin.typeCode !== 'renderer'; - const isSigned = Boolean(plugin.versionSignatureType); +type CatalogPluginsState = { + loading: boolean; + error?: Error; + plugins: CatalogPlugin[]; +}; - return isNotRenderer && isSigned; - }); - - const installedPlugins = await api.getInstalledPlugins(); - - return { items: filteredPlugins, installedPlugins }; +export function usePlugins(): CatalogPluginsState { + const { loading, value, error } = useAsync(async () => { + const remote = await api.getRemotePlugins(); + const installed = await api.getInstalledPlugins(); + return { remote, installed }; }, []); - return result; -}; + const plugins = useMemo(() => { + const installed = value?.installed || []; + const remote = value?.remote || []; + const unique: Record = {}; + + for (const plugin of installed) { + unique[plugin.id] = mapLocalToCatalog(plugin); + } + + for (const plugin of remote) { + if (unique[plugin.slug]) { + continue; + } + + if (plugin.typeCode === 'renderer') { + continue; + } + + if (!Boolean(plugin.versionSignatureType)) { + continue; + } + + unique[plugin.slug] = mapRemoteToCatalog(plugin); + } + + return Object.values(unique); + }, [value?.installed, value?.remote]); + + return { + loading, + error, + plugins, + }; +} type FilteredPluginsState = { isLoading: boolean; - items: Array; + error?: Error; + plugins: CatalogPlugin[]; }; export const usePluginsByFilter = (searchBy: string, filterBy: string): FilteredPluginsState => { - const { loading, value } = usePlugins(); - const all = useMemo(() => { - const combined: Plugin[] = []; - Array.prototype.push.apply(combined, value?.items ?? []); - Array.prototype.push.apply(combined, value?.installedPlugins ?? []); + const { loading, error, plugins } = usePlugins(); - const bySlug = combined.reduce((unique: Record, plugin) => { - unique[plugin.slug] = plugin; - return unique; - }, {}); - - return Object.values(bySlug); - }, [value?.items, value?.installedPlugins]); + const installed = useMemo(() => plugins.filter((plugin) => plugin.isInstalled), [plugins]); if (filterBy === 'installed') { return { isLoading: loading, - items: applySearchFilter(searchBy, value?.installedPlugins ?? []), + error, + plugins: applySearchFilter(searchBy, installed), }; } return { isLoading: loading, - items: applySearchFilter(searchBy, all), + error, + plugins: applySearchFilter(searchBy, plugins), }; }; -function applySearchFilter(searchBy: string | undefined, plugins: Plugin[]): Plugin[] { - if (!searchBy) { - return plugins; - } - - return plugins.filter((plugin) => { - const fields: String[] = []; - - if (plugin.name) { - fields.push(plugin.name.toLowerCase()); - } - - if (plugin.orgName) { - fields.push(plugin.orgName.toLowerCase()); - } - - return fields.some((f) => f.includes(searchBy.toLowerCase())); - }); -} - type PluginState = { isLoading: boolean; - remote?: Plugin; - remoteVersions?: Array<{ version: string; createdAt: string }>; - local?: LocalPlugin; + plugin?: CatalogPluginDetails; }; export const usePlugin = (slug: string): PluginState => { @@ -86,8 +88,10 @@ export const usePlugin = (slug: string): PluginState => { return await api.getPlugin(slug); }, [slug]); + const plugin = getCatalogPluginDetails(value?.local, value?.remote, value?.remoteVersions); + return { isLoading: loading, - ...value, + plugin, }; }; diff --git a/public/app/features/plugins/admin/pages/Browse.test.tsx b/public/app/features/plugins/admin/pages/Browse.test.tsx index 4caa3c57ed1..d85502e1071 100644 --- a/public/app/features/plugins/admin/pages/Browse.test.tsx +++ b/public/app/features/plugins/admin/pages/Browse.test.tsx @@ -52,7 +52,7 @@ describe('Browse list of plugins', () => { } }); - it('should list all plugins by when filtering by all', async () => { + it('should list all plugins when filtering by all', async () => { const plugins = [...installed, ...remote]; const { getByText } = setup('/plugins?filterBy=all'); diff --git a/public/app/features/plugins/admin/pages/Browse.tsx b/public/app/features/plugins/admin/pages/Browse.tsx index 0ea822a104f..b76813e947f 100644 --- a/public/app/features/plugins/admin/pages/Browse.tsx +++ b/public/app/features/plugins/admin/pages/Browse.tsx @@ -9,7 +9,7 @@ import { PluginList } from '../components/PluginList'; import { SearchField } from '../components/SearchField'; import { HorizontalGroup } from '../components/HorizontalGroup'; import { useHistory } from '../hooks/useHistory'; -import { Plugin } from '../types'; +import { CatalogPlugin } from '../types'; import { Page as PluginPage } from '../components/Page'; import { Page } from 'app/core/components/Page/Page'; import { usePluginsByFilter } from '../hooks/usePlugins'; @@ -17,7 +17,7 @@ import { useSelector } from 'react-redux'; import { StoreState } from 'app/types/store'; import { getNavModel } from 'app/core/selectors/navModel'; -export default function Browse(): ReactElement { +export default function Browse(): ReactElement | null { const location = useLocation(); const query = locationSearchToObject(location.search); const navModel = useSelector((state: StoreState) => getNavModel(state.navIndex, 'plugins')); @@ -26,8 +26,8 @@ export default function Browse(): ReactElement { const filterBy = (query.filterBy as string) ?? 'installed'; const sortBy = (query.sortBy as string) ?? 'name'; - const plugins = usePluginsByFilter(q, filterBy); - const sortedPlugins = plugins.items.sort(sorters[sortBy]); + const { plugins, isLoading, error } = usePluginsByFilter(q, filterBy); + const sortedPlugins = plugins.sort(sorters[sortBy]); const history = useHistory(); const onSortByChange = (value: SelectableValue) => { @@ -42,6 +42,12 @@ export default function Browse(): ReactElement { history.push({ query: { filterBy: 'all', q } }); }; + // How should we handle errors? + if (error) { + console.error(error.message); + return null; + } + return ( @@ -49,7 +55,7 @@ export default function Browse(): ReactElement {
- {plugins.isLoading ? ( + {isLoading ? ( - {!plugins.isLoading && } + {!isLoading && } ); } -const sorters: { [name: string]: (a: Plugin, b: Plugin) => number } = { - name: (a: Plugin, b: Plugin) => a.name.localeCompare(b.name), - updated: (a: Plugin, b: Plugin) => dateTimeParse(b.updatedAt).valueOf() - dateTimeParse(a.updatedAt).valueOf(), - published: (a: Plugin, b: Plugin) => dateTimeParse(b.createdAt).valueOf() - dateTimeParse(a.createdAt).valueOf(), - downloads: (a: Plugin, b: Plugin) => b.downloads - a.downloads, - popularity: (a: Plugin, b: Plugin) => b.popularity - a.popularity, +const sorters: { [name: string]: (a: CatalogPlugin, b: CatalogPlugin) => number } = { + name: (a: CatalogPlugin, b: CatalogPlugin) => a.name.localeCompare(b.name), + updated: (a: CatalogPlugin, b: CatalogPlugin) => + dateTimeParse(b.updatedAt).valueOf() - dateTimeParse(a.updatedAt).valueOf(), + published: (a: CatalogPlugin, b: CatalogPlugin) => + dateTimeParse(b.publishedAt).valueOf() - dateTimeParse(a.publishedAt).valueOf(), + downloads: (a: CatalogPlugin, b: CatalogPlugin) => b.downloads - a.downloads, + popularity: (a: CatalogPlugin, b: CatalogPlugin) => b.popularity - a.popularity, }; diff --git a/public/app/features/plugins/admin/pages/PluginDetails.tsx b/public/app/features/plugins/admin/pages/PluginDetails.tsx index ebcb45f9b7d..eb6e9346dc2 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.tsx @@ -23,15 +23,9 @@ export default function PluginDetails({ match }: PluginDetailsProps): JSX.Elemen { label: 'Version history', active: false }, ]); - const { isLoading, local, remote, remoteVersions } = usePlugin(pluginId!); + const { isLoading, plugin } = usePlugin(pluginId!); const styles = useStyles2(getStyles); - const description = remote?.description ?? local?.info?.description; - const readme = remote?.readme; - const version = local?.info?.version || remote?.version; - const links = (local?.info?.links || remote?.json?.info?.links) ?? []; - const downloads = remote?.downloads; - if (isLoading) { return ( @@ -40,67 +34,75 @@ export default function PluginDetails({ match }: PluginDetailsProps): JSX.Elemen ); } - return ( - - -
- + if (plugin) { + return ( + + +
+ -
-

{remote?.name ?? local?.name}

-
- - {remote?.orgName ?? local?.info?.author?.name} - - {links.map((link: any) => ( - - {link.name} +
+

{plugin.name}

+
+ + {plugin.orgName} - ))} - {downloads && ( - - - {` ${new Intl.NumberFormat().format(downloads)}`}{' '} - - )} - {version && {version}} + {plugin.links.map((link: any) => ( + + {link.name} + + ))} + {plugin.downloads > 0 && ( + + + {` ${new Intl.NumberFormat().format(plugin.downloads)}`}{' '} + + )} + {plugin.version && {plugin.version}} +
+

{plugin.description}

+
-

{description}

- {remote && }
-
- - {tabs.map((tab, key) => ( - { - setTabs(tabs.map((tab, index) => ({ ...tab, active: index === key }))); - }} - /> - ))} - - - {tabs.find((_) => _.label === 'Overview')?.active && ( -
- )} - {tabs.find((_) => _.label === 'Version history')?.active && } - - - - ); + + {tabs.map((tab, key) => ( + { + setTabs(tabs.map((tab, index) => ({ ...tab, active: index === key }))); + }} + /> + ))} + + + {tabs.find((_) => _.label === 'Overview')?.active && ( +
+ )} + {tabs.find((_) => _.label === 'Version history')?.active && ( + + )} + + + + ); + } + + return null; } export const getStyles = (theme: GrafanaTheme2) => { @@ -137,9 +139,6 @@ export const getStyles = (theme: GrafanaTheme2) => { headerOrgName: css` font-size: ${theme.typography.h4.fontSize}; `, - message: css` - color: ${theme.colors.text.secondary}; - `, readme: css` padding: ${theme.spacing(3, 4)}; diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index b36c37033a6..dd703c9d49c 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -1,5 +1,41 @@ export type PluginTypeCode = 'app' | 'panel' | 'datasource'; +export interface CatalogPlugin { + description: string; + downloads: number; + hasUpdate: boolean; + id: string; + info: CatalogPluginInfo; + isDev: boolean; + isCore: boolean; + isEnterprise: boolean; + isInstalled: boolean; + name: string; + orgName: string; + popularity: number; + publishedAt: string; + type: string; + updatedAt: string; + version: string; +} + +export interface CatalogPluginDetails extends CatalogPlugin { + readme: string; + versions: Version[]; + links: Array<{ + name: string; + url: string; + }>; + grafanaDependency?: string; +} + +export interface CatalogPluginInfo { + logos: { + large: string; + small: string; + }; +} + export interface Plugin { name: string; description: string;