Navigation: Convert PluginDetails page to use new Page extensions (#58509)
* 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 <torkel@grafana.com>
This commit is contained in:
co-authored by
Torkel Ödegaard
parent
cced175ccf
commit
159607fe6f
@@ -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"]
|
||||
],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -111,7 +111,6 @@ export const PageHeader: FC<Props> = ({ navItem: model, renderTitle, actions, in
|
||||
: renderHeaderTitle(main.text, main.breadcrumbs ?? [], main.highlightText)}
|
||||
{info && <PageInfo info={info} />}
|
||||
{sub && <div className="page-header__sub-title">{sub}</div>}
|
||||
{main.headerExtra && <main.headerExtra />}
|
||||
{actions && <div className={styles.actions}>{actions}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,6 @@ export function PageHeader({ navItem, renderTitle, actions, info, subTitle }: Pr
|
||||
<div className={styles.actions}>{actions}</div>
|
||||
</div>
|
||||
{sub && <div className={styles.subTitle}>{sub}</div>}
|
||||
{navItem.headerExtra && <navItem.headerExtra />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
+2
-2
@@ -68,7 +68,7 @@ export function InstallControlsButton({ plugin, pluginStatus, latestCompatibleVe
|
||||
onConfirm={onUninstall}
|
||||
onDismiss={hideConfirmModal}
|
||||
/>
|
||||
<HorizontalGroup height="auto">
|
||||
<HorizontalGroup align="flex-start" width="auto" height="auto">
|
||||
<Button variant="destructive" disabled={isUninstalling} onClick={showConfirmModal}>
|
||||
{uninstallBtnText}
|
||||
</Button>
|
||||
@@ -79,7 +79,7 @@ export function InstallControlsButton({ plugin, pluginStatus, latestCompatibleVe
|
||||
|
||||
if (pluginStatus === PluginStatus.UPDATE) {
|
||||
return (
|
||||
<HorizontalGroup height="auto">
|
||||
<HorizontalGroup align="flex-start" width="auto" height="auto">
|
||||
<Button disabled={isInstalling} onClick={onUpdate}>
|
||||
{isInstalling ? 'Updating' : 'Update'}
|
||||
</Button>
|
||||
|
||||
+4
-27
@@ -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 <div className={styles.message}>Renderer plugins cannot be managed by the Plugin Catalog.</div>;
|
||||
@@ -96,10 +83,6 @@ export const InstallControls = ({ plugin, latestCompatibleVersion }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isExternallyManaged) {
|
||||
return <ExternallyManagedButton pluginId={plugin.id} pluginStatus={pluginStatus} />;
|
||||
}
|
||||
|
||||
if (!isRemotePluginsAvailable) {
|
||||
return (
|
||||
<div className={styles.message}>
|
||||
@@ -108,13 +91,7 @@ export const InstallControls = ({ plugin, latestCompatibleVersion }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InstallControlsButton
|
||||
plugin={plugin}
|
||||
pluginStatus={pluginStatus}
|
||||
latestCompatibleVersion={latestCompatibleVersion}
|
||||
/>
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
@@ -1 +1,2 @@
|
||||
export * from './InstallControls';
|
||||
export { InstallControlsWarning } from './InstallControlsWarning';
|
||||
export { InstallControlsButton } from './InstallControlsButton';
|
||||
|
||||
@@ -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 ? (
|
||||
<ExternallyManagedButton pluginId={plugin.id} pluginStatus={pluginStatus} />
|
||||
) : (
|
||||
<InstallControlsButton
|
||||
plugin={plugin}
|
||||
latestCompatibleVersion={latestCompatibleVersion}
|
||||
pluginStatus={pluginStatus}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<GetStartedWithPlugin plugin={plugin} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<div className={styles.headerContainer}>
|
||||
{plugin.description && <div className={styles.description}>{plugin.description}</div>}
|
||||
|
||||
<div className={styles.headerInformationRow}>
|
||||
{/* Version */}
|
||||
{Boolean(version) && <span>Version: {version}</span>}
|
||||
|
||||
{/* Org name */}
|
||||
<span>From: {plugin.orgName}</span>
|
||||
|
||||
{/* Links */}
|
||||
{plugin.details?.links.map((link: any) => (
|
||||
<a key={link.name} href={link.url} className="external-link">
|
||||
{link.name}
|
||||
</a>
|
||||
))}
|
||||
|
||||
{/* Downloads */}
|
||||
{plugin.downloads > 0 && (
|
||||
<span>
|
||||
<Icon name="cloud-download" />
|
||||
{` ${new Intl.NumberFormat().format(plugin.downloads)}`}{' '}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Signature information */}
|
||||
<PluginDetailsHeaderSignature plugin={plugin} />
|
||||
|
||||
{plugin.isDisabled && <PluginDisabledBadge error={plugin.error!} />}
|
||||
|
||||
<PluginDetailsHeaderDependencies plugin={plugin} latestCompatibleVersion={latestCompatibleVersion} />
|
||||
</div>
|
||||
|
||||
<HorizontalGroup height="auto">
|
||||
<InstallControls plugin={plugin} latestCompatibleVersion={latestCompatibleVersion} />
|
||||
<GetStartedWithPlugin plugin={plugin} />
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
@@ -31,11 +31,9 @@ export function PluginDetailsHeaderDependencies({
|
||||
|
||||
return (
|
||||
<Stack gap={1}>
|
||||
<div className={styles.dependencyTitle}>Dependencies:</div>
|
||||
|
||||
{/* Grafana dependency */}
|
||||
{Boolean(grafanaDependency) && (
|
||||
<div>
|
||||
<div className={styles.depBadge}>
|
||||
<Icon name="grafana" className={styles.icon} />
|
||||
Grafana {grafanaDependency}
|
||||
</div>
|
||||
@@ -46,7 +44,7 @@ export function PluginDetailsHeaderDependencies({
|
||||
<div>
|
||||
{pluginDependencies.map((p) => {
|
||||
return (
|
||||
<span key={p.name}>
|
||||
<span className={styles.depBadge} key={p.name}>
|
||||
<Icon name={PluginIconName[p.type]} className={styles.icon} />
|
||||
{p.name} {p.version}
|
||||
</span>
|
||||
@@ -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)};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,10 +31,12 @@ export function PluginSignatureDetailsBadge({ signatureType, signatureOrg = '' }
|
||||
return (
|
||||
<>
|
||||
<DetailsBadge>
|
||||
<strong className={styles.strong}>Level: </strong>
|
||||
<Icon size="xs" name={signatureIcon} />
|
||||
|
||||
{signatureTypeText}
|
||||
<div className={styles.detailsWrapper}>
|
||||
<strong className={styles.strong}>Level: </strong>
|
||||
<Icon size="xs" name={signatureIcon} />
|
||||
|
||||
{signatureTypeText}
|
||||
</div>
|
||||
</DetailsBadge>
|
||||
|
||||
<DetailsBadge>
|
||||
@@ -47,7 +49,7 @@ export function PluginSignatureDetailsBadge({ signatureType, signatureOrg = '' }
|
||||
export const DetailsBadge = ({ children }: React.PropsWithChildren<{}>) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return <Badge color="green" className={styles.badge} text={<>{children}</>} />;
|
||||
return <Badge color="green" className={styles.badge} text={children} />;
|
||||
};
|
||||
|
||||
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};
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.subtitle}>
|
||||
{plugin?.description && <div>{plugin?.description}</div>}
|
||||
{plugin?.details?.links && plugin.details.links.length > 0 && (
|
||||
<span>
|
||||
{plugin.details.links.map((link, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{index > 0 && ' | '}
|
||||
<a href={link.url} className="external-link">
|
||||
{link.name}
|
||||
</a>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
{hasInstallControlWarning(plugin, isRemotePluginsAvailable, latestCompatibleVersion) && (
|
||||
<InstallControlsWarning
|
||||
plugin={plugin}
|
||||
pluginStatus={pluginStatus}
|
||||
latestCompatibleVersion={latestCompatibleVersion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
subtitle: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(1)};
|
||||
`,
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ? <PluginDetailsHeader plugin={plugin} /> : null;
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -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: <PluginDetailsHeaderDependencies plugin={plugin} latestCompatibleVersion={latestCompatibleVersion} />,
|
||||
});
|
||||
}
|
||||
|
||||
if (plugin.isDisabled) {
|
||||
info.push({
|
||||
label: 'Status',
|
||||
value: <PluginDisabledBadge error={plugin.error!} />,
|
||||
});
|
||||
}
|
||||
|
||||
info.push({
|
||||
label: 'Signature',
|
||||
value: <PluginDetailsHeaderSignature plugin={plugin} />,
|
||||
});
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
subtitle: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(1)};
|
||||
`,
|
||||
};
|
||||
};
|
||||
@@ -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: <PluginActions plugin={plugin} />,
|
||||
info,
|
||||
subtitle: <PluginSubtitle plugin={plugin} />,
|
||||
};
|
||||
};
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 (
|
||||
<Page navId="plugins" pageNav={navModel}>
|
||||
<Page navId="plugins" pageNav={navModel} actions={actions} subTitle={subtitle} info={info}>
|
||||
<Page.Contents>
|
||||
<TabContent className={styles.tabContent}>
|
||||
<PluginDetailsSignature plugin={plugin} className={styles.alert} />
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user