From 159607fe6f5f2db997b2d42124a6464a73898434 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 9 Nov 2022 14:44:38 +0000 Subject: [PATCH] Navigation: Convert `PluginDetails` page to use new `Page` extensions (#58509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added labels * App page fixes * Switch to switch * wip * Updates * I am stuck * Minor tweak * This props interface could work * removed change * use new page extensions in plugin details page * add link separator, fix action button spacing * some renaming * Move PageInfo into it's own folder + add tests * add support for new props in old page header * remove PluginDetailsHeader as it's no longer used * Fix unit tests * fix some badge alignments * center align actions * badge alignment + only show downloads for community/commercial plugins * better link alignment * conditionally render description * move install control warnings to below subtitle + refactor Co-authored-by: Torkel Ödegaard --- .betterer.results | 3 - packages/grafana-data/src/types/navModel.ts | 2 - .../core/components/PageHeader/PageHeader.tsx | 1 - .../core/components/PageNew/PageHeader.tsx | 2 +- .../InstallControls/InstallControlsButton.tsx | 4 +- ...ontrols.tsx => InstallControlsWarning.tsx} | 31 +---- .../components/InstallControls/index.tsx | 3 +- .../admin/components/PluginActions.tsx | 50 +++++++ .../admin/components/PluginDetailsHeader.tsx | 125 ------------------ .../PluginDetailsHeaderDependencies.tsx | 10 +- .../PluginDetailsHeaderSignature.tsx | 2 + .../PluginSignatureDetailsBadge.tsx | 18 ++- .../admin/components/PluginSubtitle.tsx | 63 +++++++++ public/app/features/plugins/admin/helpers.ts | 25 +++- .../admin/hooks/usePluginDetailsTabs.tsx | 6 +- .../plugins/admin/hooks/usePluginInfo.tsx | 85 ++++++++++++ .../admin/hooks/usePluginPageExtensions.tsx | 24 ++++ .../admin/pages/PluginDetails.test.tsx | 8 +- .../plugins/admin/pages/PluginDetails.tsx | 9 +- 19 files changed, 287 insertions(+), 184 deletions(-) rename public/app/features/plugins/admin/components/InstallControls/{InstallControls.tsx => InstallControlsWarning.tsx} (77%) create mode 100644 public/app/features/plugins/admin/components/PluginActions.tsx delete mode 100644 public/app/features/plugins/admin/components/PluginDetailsHeader.tsx create mode 100644 public/app/features/plugins/admin/components/PluginSubtitle.tsx create mode 100644 public/app/features/plugins/admin/hooks/usePluginInfo.tsx create mode 100644 public/app/features/plugins/admin/hooks/usePluginPageExtensions.tsx diff --git a/.betterer.results b/.betterer.results index f2403794aff..1d9a09770e0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4389,9 +4389,6 @@ exports[`better eslint`] = { "public/app/features/plugins/admin/components/PluginDetailsBody.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/plugins/admin/components/PluginDetailsHeader.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/plugins/admin/components/SearchField.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/packages/grafana-data/src/types/navModel.ts b/packages/grafana-data/src/types/navModel.ts index dce924e8d9b..33905084342 100644 --- a/packages/grafana-data/src/types/navModel.ts +++ b/packages/grafana-data/src/types/navModel.ts @@ -42,8 +42,6 @@ export interface NavModelItem extends NavLinkDTO { highlightId?: string; tabSuffix?: ComponentType<{ className?: string }>; hideFromBreadcrumbs?: boolean; - /** To render custom things between title and child tabs */ - headerExtra?: ComponentType; } export enum NavSection { diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index 9309c5561de..4fd478791c2 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -111,7 +111,6 @@ export const PageHeader: FC = ({ navItem: model, renderTitle, actions, in : renderHeaderTitle(main.text, main.breadcrumbs ?? [], main.highlightText)} {info && } {sub &&
{sub}
} - {main.headerExtra && } {actions &&
{actions}
} diff --git a/public/app/core/components/PageNew/PageHeader.tsx b/public/app/core/components/PageNew/PageHeader.tsx index 0534a7416a2..b0d3161bdbd 100644 --- a/public/app/core/components/PageNew/PageHeader.tsx +++ b/public/app/core/components/PageNew/PageHeader.tsx @@ -36,7 +36,6 @@ export function PageHeader({ navItem, renderTitle, actions, info, subTitle }: Pr
{actions}
{sub &&
{sub}
} - {navItem.headerExtra && } ); } @@ -44,6 +43,7 @@ export function PageHeader({ navItem, renderTitle, actions, info, subTitle }: Pr const getStyles = (theme: GrafanaTheme2) => { return { topRow: css({ + alignItems: 'center', display: 'flex', flexDirection: 'row', flexWrap: 'wrap', diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx index 25044906169..b9ecdaf1de6 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx @@ -68,7 +68,7 @@ export function InstallControlsButton({ plugin, pluginStatus, latestCompatibleVe onConfirm={onUninstall} onDismiss={hideConfirmModal} /> - + @@ -79,7 +79,7 @@ export function InstallControlsButton({ plugin, pluginStatus, latestCompatibleVe if (pluginStatus === PluginStatus.UPDATE) { return ( - + diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControls.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx similarity index 77% rename from public/app/features/plugins/admin/components/InstallControls/InstallControls.tsx rename to public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx index 8d66efa360a..c2c3643d301 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControls.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx @@ -7,36 +7,23 @@ import { HorizontalGroup, Icon, LinkButton, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { AccessControlAction } from 'app/types'; -import { getExternalManageLink, isInstallControlsEnabled } from '../../helpers'; +import { getExternalManageLink } from '../../helpers'; import { isGrafanaAdmin } from '../../permissions'; import { useIsRemotePluginsAvailable } from '../../state/hooks'; import { CatalogPlugin, PluginStatus, Version } from '../../types'; -import { ExternallyManagedButton } from './ExternallyManagedButton'; -import { InstallControlsButton } from './InstallControlsButton'; - interface Props { plugin: CatalogPlugin; + pluginStatus: PluginStatus; latestCompatibleVersion?: Version; } -export const InstallControls = ({ plugin, latestCompatibleVersion }: Props) => { +export const InstallControlsWarning = ({ plugin, pluginStatus, latestCompatibleVersion }: Props) => { const styles = useStyles2(getStyles); const isExternallyManaged = config.pluginAdminExternalManageEnabled; const hasPermission = contextSrv.hasAccess(AccessControlAction.PluginsInstall, isGrafanaAdmin()); const isRemotePluginsAvailable = useIsRemotePluginsAvailable(); const isCompatible = Boolean(latestCompatibleVersion); - const isInstallControlsDisabled = plugin.isCore || plugin.isDisabled || !isInstallControlsEnabled(); - - const pluginStatus = plugin.isInstalled - ? plugin.hasUpdate - ? PluginStatus.UPDATE - : PluginStatus.UNINSTALL - : PluginStatus.INSTALL; - - if (isInstallControlsDisabled) { - return null; - } if (plugin.type === PluginType.renderer) { return
Renderer plugins cannot be managed by the Plugin Catalog.
; @@ -96,10 +83,6 @@ export const InstallControls = ({ plugin, latestCompatibleVersion }: Props) => { ); } - if (isExternallyManaged) { - return ; - } - if (!isRemotePluginsAvailable) { return (
@@ -108,13 +91,7 @@ export const InstallControls = ({ plugin, latestCompatibleVersion }: Props) => { ); } - return ( - - ); + return null; }; export const getStyles = (theme: GrafanaTheme2) => { diff --git a/public/app/features/plugins/admin/components/InstallControls/index.tsx b/public/app/features/plugins/admin/components/InstallControls/index.tsx index 6d6ba2dc7c9..b471668b365 100644 --- a/public/app/features/plugins/admin/components/InstallControls/index.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/index.tsx @@ -1 +1,2 @@ -export * from './InstallControls'; +export { InstallControlsWarning } from './InstallControlsWarning'; +export { InstallControlsButton } from './InstallControlsButton'; diff --git a/public/app/features/plugins/admin/components/PluginActions.tsx b/public/app/features/plugins/admin/components/PluginActions.tsx new file mode 100644 index 00000000000..c7b9663a81b --- /dev/null +++ b/public/app/features/plugins/admin/components/PluginActions.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +import { config } from '@grafana/runtime'; + +import { GetStartedWithPlugin } from '../components/GetStartedWithPlugin'; +import { InstallControlsButton } from '../components/InstallControls'; +import { ExternallyManagedButton } from '../components/InstallControls/ExternallyManagedButton'; +import { getLatestCompatibleVersion, hasInstallControlWarning, isInstallControlsEnabled } from '../helpers'; +import { useIsRemotePluginsAvailable } from '../state/hooks'; +import { CatalogPlugin, PluginStatus } from '../types'; + +interface Props { + plugin?: CatalogPlugin; +} + +export const PluginActions = ({ plugin }: Props) => { + const isRemotePluginsAvailable = useIsRemotePluginsAvailable(); + const latestCompatibleVersion = getLatestCompatibleVersion(plugin?.details?.versions); + + if (!plugin || hasInstallControlWarning(plugin, isRemotePluginsAvailable, latestCompatibleVersion)) { + return null; + } + + const isExternallyManaged = config.pluginAdminExternalManageEnabled; + const pluginStatus = plugin.isInstalled + ? plugin.hasUpdate + ? PluginStatus.UPDATE + : PluginStatus.UNINSTALL + : PluginStatus.INSTALL; + const isInstallControlsDisabled = plugin.isCore || plugin.isDisabled || !isInstallControlsEnabled(); + + return ( + <> + {!isInstallControlsDisabled && ( + <> + {isExternallyManaged ? ( + + ) : ( + + )} + + )} + + + ); +}; diff --git a/public/app/features/plugins/admin/components/PluginDetailsHeader.tsx b/public/app/features/plugins/admin/components/PluginDetailsHeader.tsx deleted file mode 100644 index 87d89760cff..00000000000 --- a/public/app/features/plugins/admin/components/PluginDetailsHeader.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { css } from '@emotion/css'; -import React from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2, Icon, HorizontalGroup } from '@grafana/ui'; - -import { getLatestCompatibleVersion } from '../helpers'; -import { CatalogPlugin } from '../types'; - -import { PluginDisabledBadge } from './Badges'; -import { GetStartedWithPlugin } from './GetStartedWithPlugin'; -import { InstallControls } from './InstallControls'; -import { PluginDetailsHeaderDependencies } from './PluginDetailsHeaderDependencies'; -import { PluginDetailsHeaderSignature } from './PluginDetailsHeaderSignature'; - -type Props = { - plugin: CatalogPlugin; -}; - -export function PluginDetailsHeader({ plugin }: Props): React.ReactElement { - const styles = useStyles2(getStyles); - const latestCompatibleVersion = getLatestCompatibleVersion(plugin.details?.versions); - const version = plugin.installedVersion || latestCompatibleVersion?.version; - - return ( -
- {plugin.description &&
{plugin.description}
} - -
- {/* Version */} - {Boolean(version) && Version: {version}} - - {/* Org name */} - From: {plugin.orgName} - - {/* Links */} - {plugin.details?.links.map((link: any) => ( - - {link.name} - - ))} - - {/* Downloads */} - {plugin.downloads > 0 && ( - - - {` ${new Intl.NumberFormat().format(plugin.downloads)}`}{' '} - - )} - - {/* Signature information */} - - - {plugin.isDisabled && } - - -
- - - - - -
- ); -} - -export const getStyles = (theme: GrafanaTheme2) => { - return { - headerContainer: css` - display: flex; - flex-direction: column; - margin-bottom: ${theme.spacing(1)}; - `, - description: css` - margin-bottom: ${theme.spacing(1)}; - `, - breadcrumb: css` - font-size: ${theme.typography.h2.fontSize}; - li { - display: inline; - list-style: none; - &::after { - content: '/'; - padding: 0 0.25ch; - } - &:last-child::after { - content: ''; - } - } - `, - headerInformationRow: css` - display: flex; - align-items: center; - margin-bottom: ${theme.spacing(1)}; - flex-flow: wrap; - - & > * { - &::after { - content: '|'; - padding: 0 ${theme.spacing()}; - } - &:last-child::after { - content: ''; - padding-right: 0; - } - } - - a { - &:hover { - text-decoration: underline; - } - } - `, - headerOrgName: css` - font-size: ${theme.typography.h4.fontSize}; - `, - signature: css` - margin: ${theme.spacing(3)}; - margin-bottom: 0; - `, - textUnderline: css` - text-decoration: underline; - `, - }; -}; diff --git a/public/app/features/plugins/admin/components/PluginDetailsHeaderDependencies.tsx b/public/app/features/plugins/admin/components/PluginDetailsHeaderDependencies.tsx index be9d69ab168..570fe47a553 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsHeaderDependencies.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsHeaderDependencies.tsx @@ -31,11 +31,9 @@ export function PluginDetailsHeaderDependencies({ return ( -
Dependencies:
- {/* Grafana dependency */} {Boolean(grafanaDependency) && ( -
+
Grafana {grafanaDependency}
@@ -46,7 +44,7 @@ export function PluginDetailsHeaderDependencies({
{pluginDependencies.map((p) => { return ( - + {p.name} {p.version} @@ -68,6 +66,10 @@ export const getStyles = (theme: GrafanaTheme2) => { padding: 0; } `, + depBadge: css({ + display: 'flex', + alignItems: 'flex-start', + }), icon: css` color: ${theme.colors.text.secondary}; margin-right: ${theme.spacing(0.5)}; diff --git a/public/app/features/plugins/admin/components/PluginDetailsHeaderSignature.tsx b/public/app/features/plugins/admin/components/PluginDetailsHeaderSignature.tsx index f44452ed7d9..689cea2b513 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsHeaderSignature.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsHeaderSignature.tsx @@ -39,6 +39,8 @@ export const getStyles = (theme: GrafanaTheme2) => { return { container: css` display: flex; + flex-wrap: wrap; + gap: ${theme.spacing(0.5)}; `, link: css` display: inline-flex; diff --git a/public/app/features/plugins/admin/components/PluginSignatureDetailsBadge.tsx b/public/app/features/plugins/admin/components/PluginSignatureDetailsBadge.tsx index 4ac1f84b56b..4dce5af0536 100644 --- a/public/app/features/plugins/admin/components/PluginSignatureDetailsBadge.tsx +++ b/public/app/features/plugins/admin/components/PluginSignatureDetailsBadge.tsx @@ -31,10 +31,12 @@ export function PluginSignatureDetailsBadge({ signatureType, signatureOrg = '' } return ( <> - Level:  - -   - {signatureTypeText} +
+ Level:  + +   + {signatureTypeText} +
@@ -47,7 +49,7 @@ export function PluginSignatureDetailsBadge({ signatureType, signatureOrg = '' } export const DetailsBadge = ({ children }: React.PropsWithChildren<{}>) => { const styles = useStyles2(getStyles); - return {children}} />; + return ; }; const getStyles = (theme: GrafanaTheme2) => ({ @@ -55,7 +57,11 @@ const getStyles = (theme: GrafanaTheme2) => ({ background-color: ${theme.colors.background.canvas}; border-color: ${theme.colors.border.strong}; color: ${theme.colors.text.secondary}; - margin-left: ${theme.spacing()}; + white-space: nowrap; + `, + detailsWrapper: css` + align-items: center; + display: flex; `, strong: css` color: ${theme.colors.text.primary}; diff --git a/public/app/features/plugins/admin/components/PluginSubtitle.tsx b/public/app/features/plugins/admin/components/PluginSubtitle.tsx new file mode 100644 index 00000000000..192d450a9fd --- /dev/null +++ b/public/app/features/plugins/admin/components/PluginSubtitle.tsx @@ -0,0 +1,63 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; + +import { InstallControlsWarning } from '../components/InstallControls'; +import { getLatestCompatibleVersion, hasInstallControlWarning } from '../helpers'; +import { useIsRemotePluginsAvailable } from '../state/hooks'; +import { CatalogPlugin, PluginStatus } from '../types'; + +interface Props { + plugin?: CatalogPlugin; +} + +export const PluginSubtitle = ({ plugin }: Props) => { + const isRemotePluginsAvailable = useIsRemotePluginsAvailable(); + const styles = useStyles2(getStyles); + if (!plugin) { + return null; + } + const latestCompatibleVersion = getLatestCompatibleVersion(plugin.details?.versions); + const pluginStatus = plugin.isInstalled + ? plugin.hasUpdate + ? PluginStatus.UPDATE + : PluginStatus.UNINSTALL + : PluginStatus.INSTALL; + + return ( +
+ {plugin?.description &&
{plugin?.description}
} + {plugin?.details?.links && plugin.details.links.length > 0 && ( + + {plugin.details.links.map((link, index) => ( + + {index > 0 && ' | '} + + {link.name} + + + ))} + + )} + {hasInstallControlWarning(plugin, isRemotePluginsAvailable, latestCompatibleVersion) && ( + + )} +
+ ); +}; + +export const getStyles = (theme: GrafanaTheme2) => { + return { + subtitle: css` + display: flex; + flex-direction: column; + gap: ${theme.spacing(1)}; + `, + }; +}; diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 433c8b8da78..6036a1c4b1a 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -1,8 +1,11 @@ import { PluginSignatureStatus, dateTimeParse, PluginError, PluginType, PluginErrorCode } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { config, featureEnabled } from '@grafana/runtime'; import { Settings } from 'app/core/config'; +import { contextSrv } from 'app/core/core'; import { getBackendSrv } from 'app/core/services/backend_srv'; +import { AccessControlAction } from 'app/types'; +import { isGrafanaAdmin } from './permissions'; import { CatalogPlugin, LocalPlugin, RemotePlugin, Version } from './types'; export function mergeLocalsAndRemotes( @@ -269,6 +272,26 @@ export function getLatestCompatibleVersion(versions: Version[] | undefined): Ver export const isInstallControlsEnabled = () => config.pluginAdminEnabled; +export const hasInstallControlWarning = ( + plugin: CatalogPlugin, + isRemotePluginsAvailable: boolean, + latestCompatibleVersion?: Version +) => { + const isExternallyManaged = config.pluginAdminExternalManageEnabled; + const hasPermission = contextSrv.hasAccess(AccessControlAction.PluginsInstall, isGrafanaAdmin()); + const isCompatible = Boolean(latestCompatibleVersion); + return ( + plugin.type === PluginType.renderer || + plugin.type === PluginType.secretsmanager || + (plugin.isEnterprise && !featureEnabled('enterprise.plugins')) || + plugin.isDev || + (!hasPermission && !isExternallyManaged) || + !plugin.isPublished || + !isCompatible || + !isRemotePluginsAvailable + ); +}; + export const isLocalPluginVisible = (p: LocalPlugin) => isPluginVisible(p.id); export const isRemotePluginVisible = (p: RemotePlugin) => isPluginVisible(p.slug); diff --git a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx index 4e015d721b0..42c2b9962b4 100644 --- a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import { useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import { GrafanaPlugin, NavModelItem, PluginIncludeType, PluginType } from '@grafana/data'; @@ -6,7 +6,6 @@ import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { AccessControlAction } from 'app/types'; -import { PluginDetailsHeader } from '../components/PluginDetailsHeader'; import { usePluginConfig } from '../hooks/usePluginConfig'; import { isOrgAdmin } from '../permissions'; import { CatalogPlugin, PluginTabIds, PluginTabLabels } from '../types'; @@ -110,9 +109,6 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, pageId?: PluginTabI }, ...navModelChildren, ], - headerExtra: () => { - return plugin ? : null; - }, }; return { diff --git a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx new file mode 100644 index 00000000000..8874d62d0b3 --- /dev/null +++ b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx @@ -0,0 +1,85 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2, PluginSignatureType } from '@grafana/data'; + +import { PageInfoItem } from '../../../../core/components/Page/types'; +import { PluginDisabledBadge } from '../components/Badges'; +import { PluginDetailsHeaderDependencies } from '../components/PluginDetailsHeaderDependencies'; +import { PluginDetailsHeaderSignature } from '../components/PluginDetailsHeaderSignature'; +import { getLatestCompatibleVersion } from '../helpers'; +import { CatalogPlugin } from '../types'; + +export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => { + const info: PageInfoItem[] = []; + + if (!plugin) { + return info; + } + + // Populate info + const latestCompatibleVersion = getLatestCompatibleVersion(plugin.details?.versions); + const version = plugin.installedVersion || latestCompatibleVersion?.version; + + if (Boolean(version)) { + info.push({ + label: 'Version', + value: version, + }); + } + + if (Boolean(plugin.orgName)) { + info.push({ + label: 'From', + value: plugin.orgName, + }); + } + + const showDownloads = + !plugin.signatureType || + plugin.signatureType === PluginSignatureType.community || + plugin.signatureType === PluginSignatureType.commercial; + if (showDownloads && Boolean(plugin.downloads > 0)) { + info.push({ + label: 'Downloads', + value: new Intl.NumberFormat().format(plugin.downloads), + }); + } + + const pluginDependencies = plugin.details?.pluginDependencies; + const grafanaDependency = plugin.isInstalled + ? plugin.details?.grafanaDependency + : latestCompatibleVersion?.grafanaDependency || plugin.details?.grafanaDependency; + const hasNoDependencyInfo = !grafanaDependency && (!pluginDependencies || !pluginDependencies.length); + + if (!hasNoDependencyInfo) { + info.push({ + label: 'Dependencies', + value: , + }); + } + + if (plugin.isDisabled) { + info.push({ + label: 'Status', + value: , + }); + } + + info.push({ + label: 'Signature', + value: , + }); + + return info; +}; + +export const getStyles = (theme: GrafanaTheme2) => { + return { + subtitle: css` + display: flex; + flex-direction: column; + gap: ${theme.spacing(1)}; + `, + }; +}; diff --git a/public/app/features/plugins/admin/hooks/usePluginPageExtensions.tsx b/public/app/features/plugins/admin/hooks/usePluginPageExtensions.tsx new file mode 100644 index 00000000000..e02d50805ff --- /dev/null +++ b/public/app/features/plugins/admin/hooks/usePluginPageExtensions.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { PageInfoItem } from '../../../../core/components/Page/types'; +import { PluginActions } from '../components/PluginActions'; +import { PluginSubtitle } from '../components/PluginSubtitle'; +import { CatalogPlugin } from '../types'; + +import { usePluginInfo } from './usePluginInfo'; + +type ReturnType = { + actions: React.ReactNode; + info: PageInfoItem[]; + subtitle: React.ReactNode; +}; + +export const usePluginPageExtensions = (plugin?: CatalogPlugin): ReturnType => { + const info = usePluginInfo(plugin); + + return { + actions: , + info, + subtitle: , + }; +}; diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx index 7d571015c02..f1b21bb99ac 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx @@ -214,7 +214,7 @@ describe('Plugin details page', () => { const installedVersion = '1.3.443'; const { queryByText } = renderPluginDetails({ id, installedVersion }); - expect(await queryByText(`Version: ${installedVersion}`)).toBeInTheDocument(); + expect(await queryByText(`${installedVersion}`)).toBeInTheDocument(); }); it('should display the latest compatible version in the header if a plugin is not installed', async () => { @@ -230,7 +230,7 @@ describe('Plugin details page', () => { }; const { findByText, queryByText } = renderPluginDetails({ id, details }); - expect(await findByText('Version: 1.1.1')).toBeInTheDocument(); + expect(await findByText('1.1.1')).toBeInTheDocument(); expect(queryByText(/>=8.0.0/i)).toBeInTheDocument(); }); @@ -430,9 +430,7 @@ describe('Plugin details page', () => { }); // Wait for the dependencies part to be loaded - expect(await queryByText(/dependencies:/i)).toBeInTheDocument(); - - expect(queryByText('Grafana >=8.0.0')).toBeInTheDocument(); + expect(await queryByText('Grafana >=8.0.0')).toBeInTheDocument(); }); it('should show a confirm modal when trying to uninstall a plugin', async () => { diff --git a/public/app/features/plugins/admin/pages/PluginDetails.tsx b/public/app/features/plugins/admin/pages/PluginDetails.tsx index 219ece99780..7b30a496878 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.tsx @@ -13,6 +13,7 @@ import { PluginDetailsBody } from '../components/PluginDetailsBody'; import { PluginDetailsDisabledError } from '../components/PluginDetailsDisabledError'; import { PluginDetailsSignature } from '../components/PluginDetailsSignature'; import { usePluginDetailsTabs } from '../hooks/usePluginDetailsTabs'; +import { usePluginPageExtensions } from '../hooks/usePluginPageExtensions'; import { useGetSingle, useFetchStatus, useFetchDetailsStatus } from '../state/hooks'; import { PluginTabIds } from '../types'; @@ -27,6 +28,7 @@ export default function PluginDetails({ match, queryParams }: Props): JSX.Elemen const plugin = useGetSingle(pluginId); // fetches the localplugin settings const { navModel, activePageId } = usePluginDetailsTabs(plugin, queryParams.page as PluginTabIds); + const { actions, info, subtitle } = usePluginPageExtensions(plugin); const { isLoading: isFetchLoading } = useFetchStatus(); const { isLoading: isFetchDetailsLoading } = useFetchDetailsStatus(); const styles = useStyles2(getStyles); @@ -51,7 +53,7 @@ export default function PluginDetails({ match, queryParams }: Props): JSX.Elemen } return ( - + @@ -68,6 +70,11 @@ export const getStyles = (theme: GrafanaTheme2) => { alert: css` margin-bottom: ${theme.spacing(2)}; `, + subtitle: css` + display: flex; + flex-direction: column; + gap: ${theme.spacing(1)}; + `, // Needed due to block formatting context tabContent: css` overflow: auto;