Plugins Catalog: fix overflowing text in plugin cards (#39862)

* refactor(Plugins/Admin): add a "badge" for displaying available updates

* refactor(Plugins/Admin): rename component

* refactor(Plugins/Admin): use the PluginListItemBadges component
This commit is contained in:
Levente Balogh 2021-09-30 17:23:40 +02:00 committed by GitHub
parent fffbdf4c82
commit 3ad5ee87a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 71 additions and 31 deletions

View File

@ -0,0 +1,33 @@
import React from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Tooltip, useStyles2 } from '@grafana/ui';
import { CatalogPlugin } from '../../types';
type Props = {
plugin: CatalogPlugin;
};
export function PluginUpdateAvailableBadge({ plugin }: Props): React.ReactElement | null {
const styles = useStyles2(getStyles);
if (plugin.hasUpdate && !plugin.isCore) {
return (
<Tooltip content={plugin.version}>
<p className={styles.hasUpdate}>Update available!</p>
</Tooltip>
);
}
return null;
}
export const getStyles = (theme: GrafanaTheme2) => {
return {
hasUpdate: css`
color: ${theme.colors.text.secondary};
font-size: ${theme.typography.bodySmall.fontSize};
margin-bottom: 0;
`,
};
};

View File

@ -1,3 +1,4 @@
export { PluginDisabledBadge } from './PluginDisabledBadge'; export { PluginDisabledBadge } from './PluginDisabledBadge';
export { PluginInstalledBadge } from './PluginInstallBadge'; export { PluginInstalledBadge } from './PluginInstallBadge';
export { PluginEnterpriseBadge } from './PluginEnterpriseBadge'; export { PluginEnterpriseBadge } from './PluginEnterpriseBadge';
export { PluginUpdateAvailableBadge } from './PluginUpdateAvailableBadge';

View File

@ -1,11 +1,11 @@
import React from 'react'; import React from 'react';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { PluginErrorCode, PluginSignatureStatus } from '@grafana/data'; import { PluginErrorCode, PluginSignatureStatus } from '@grafana/data';
import { PluginListBadges } from './PluginListBadges'; import { PluginListItemBadges } from './PluginListItemBadges';
import { CatalogPlugin } from '../types'; import { CatalogPlugin } from '../types';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
describe('PluginBadges', () => { describe('PluginListItemBadges', () => {
const plugin: CatalogPlugin = { const plugin: CatalogPlugin = {
description: 'The test plugin', description: 'The test plugin',
downloads: 5, downloads: 5,
@ -36,13 +36,13 @@ describe('PluginBadges', () => {
}); });
it('renders a plugin signature badge', () => { it('renders a plugin signature badge', () => {
render(<PluginListBadges plugin={plugin} />); render(<PluginListItemBadges plugin={plugin} />);
expect(screen.getByText(/signed/i)).toBeVisible(); expect(screen.getByText(/signed/i)).toBeVisible();
}); });
it('renders an installed badge', () => { it('renders an installed badge', () => {
render(<PluginListBadges plugin={{ ...plugin, isInstalled: true }} />); render(<PluginListItemBadges plugin={{ ...plugin, isInstalled: true }} />);
expect(screen.getByText(/signed/i)).toBeVisible(); expect(screen.getByText(/signed/i)).toBeVisible();
expect(screen.getByText(/installed/i)).toBeVisible(); expect(screen.getByText(/installed/i)).toBeVisible();
@ -50,21 +50,21 @@ describe('PluginBadges', () => {
it('renders an enterprise badge (when a license is valid)', () => { it('renders an enterprise badge (when a license is valid)', () => {
config.licenseInfo.hasValidLicense = true; config.licenseInfo.hasValidLicense = true;
render(<PluginListBadges plugin={{ ...plugin, isEnterprise: true }} />); render(<PluginListItemBadges plugin={{ ...plugin, isEnterprise: true }} />);
expect(screen.getByText(/enterprise/i)).toBeVisible(); expect(screen.getByText(/enterprise/i)).toBeVisible();
expect(screen.queryByRole('button', { name: /learn more/i })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /learn more/i })).not.toBeInTheDocument();
}); });
it('renders an enterprise badge with icon and link (when a license is invalid)', () => { it('renders an enterprise badge with icon and link (when a license is invalid)', () => {
config.licenseInfo.hasValidLicense = false; config.licenseInfo.hasValidLicense = false;
render(<PluginListBadges plugin={{ ...plugin, isEnterprise: true }} />); render(<PluginListItemBadges plugin={{ ...plugin, isEnterprise: true }} />);
expect(screen.getByText(/enterprise/i)).toBeVisible(); expect(screen.getByText(/enterprise/i)).toBeVisible();
expect(screen.getByLabelText(/lock icon/i)).toBeInTheDocument(); expect(screen.getByLabelText(/lock icon/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /learn more/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /learn more/i })).toBeInTheDocument();
}); });
it('renders a error badge (when plugin has an error', () => { it('renders a error badge (when plugin has an error', () => {
render(<PluginListBadges plugin={{ ...plugin, isDisabled: true, error: PluginErrorCode.modifiedSignature }} />); render(<PluginListItemBadges plugin={{ ...plugin, isDisabled: true, error: PluginErrorCode.modifiedSignature }} />);
expect(screen.getByText(/disabled/i)).toBeVisible(); expect(screen.getByText(/disabled/i)).toBeVisible();
}); });
}); });

View File

@ -1,27 +1,29 @@
import React from 'react'; import React from 'react';
import { HorizontalGroup, PluginSignatureBadge } from '@grafana/ui'; import { HorizontalGroup, PluginSignatureBadge } from '@grafana/ui';
import { CatalogPlugin } from '../types'; import { CatalogPlugin } from '../types';
import { PluginEnterpriseBadge, PluginDisabledBadge, PluginInstalledBadge } from './Badges'; import { PluginEnterpriseBadge, PluginDisabledBadge, PluginInstalledBadge, PluginUpdateAvailableBadge } from './Badges';
type PluginBadgeType = { type PluginBadgeType = {
plugin: CatalogPlugin; plugin: CatalogPlugin;
}; };
export function PluginListBadges({ plugin }: PluginBadgeType) { export function PluginListItemBadges({ plugin }: PluginBadgeType) {
if (plugin.isEnterprise) { if (plugin.isEnterprise) {
return ( return (
<HorizontalGroup> <HorizontalGroup height="auto" wrap>
<PluginEnterpriseBadge plugin={plugin} /> <PluginEnterpriseBadge plugin={plugin} />
{plugin.isDisabled && <PluginDisabledBadge error={plugin.error} />} {plugin.isDisabled && <PluginDisabledBadge error={plugin.error} />}
<PluginUpdateAvailableBadge plugin={plugin} />
</HorizontalGroup> </HorizontalGroup>
); );
} }
return ( return (
<HorizontalGroup> <HorizontalGroup height="auto" wrap>
<PluginSignatureBadge status={plugin.signature} /> <PluginSignatureBadge status={plugin.signature} />
{plugin.isDisabled && <PluginDisabledBadge error={plugin.error} />} {plugin.isDisabled && <PluginDisabledBadge error={plugin.error} />}
{plugin.isInstalled && <PluginInstalledBadge />} {plugin.isInstalled && <PluginInstalledBadge />}
<PluginUpdateAvailableBadge plugin={plugin} />
</HorizontalGroup> </HorizontalGroup>
); );
} }

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { Icon, useStyles2, HorizontalGroup, Tooltip, CardContainer, VerticalGroup } from '@grafana/ui'; import { Icon, useStyles2, CardContainer, VerticalGroup } from '@grafana/ui';
import { CatalogPlugin, PluginIconName, PluginListDisplayMode, PluginTabIds } from '../types'; import { CatalogPlugin, PluginIconName, PluginListDisplayMode, PluginTabIds } from '../types';
import { PluginLogo } from './PluginLogo'; import { PluginLogo } from './PluginLogo';
import { PluginListBadges } from './PluginListBadges'; import { PluginListItemBadges } from './PluginListItemBadges';
import { getStyles, LOGO_SIZE } from './PluginListItem'; import { getStyles, LOGO_SIZE } from './PluginListItem';
type Props = { type Props = {
@ -17,28 +17,30 @@ export function PluginListItemCard({ plugin, pathName }: Props) {
<CardContainer href={`${pathName}/${plugin.id}?page=${PluginTabIds.OVERVIEW}`} className={styles.cardContainer}> <CardContainer href={`${pathName}/${plugin.id}?page=${PluginTabIds.OVERVIEW}`} className={styles.cardContainer}>
<VerticalGroup spacing="md"> <VerticalGroup spacing="md">
<div className={styles.headerWrap}> <div className={styles.headerWrap}>
{/* Logo */}
<PluginLogo <PluginLogo
src={plugin.info.logos.small} src={plugin.info.logos.small}
alt={`${plugin.name} logo`} alt={`${plugin.name} logo`}
className={styles.image} className={styles.image}
height={LOGO_SIZE} height={LOGO_SIZE}
/> />
{/* Name */}
<h2 className={styles.name}>{plugin.name}</h2> <h2 className={styles.name}>{plugin.name}</h2>
{/* Type Icon */}
{plugin.type && ( {plugin.type && (
<div className={styles.icon} data-testid={`${plugin.type} plugin icon`}> <div className={styles.icon} data-testid={`${plugin.type} plugin icon`}>
<Icon name={PluginIconName[plugin.type]} /> <Icon name={PluginIconName[plugin.type]} />
</div> </div>
)} )}
</div> </div>
{/* Org */}
<p className={styles.orgName}>By {plugin.orgName}</p> <p className={styles.orgName}>By {plugin.orgName}</p>
<HorizontalGroup align="center">
<PluginListBadges plugin={plugin} /> {/* Badges */}
{plugin.hasUpdate && !plugin.isCore ? ( <PluginListItemBadges plugin={plugin} />
<Tooltip content={plugin.version}>
<p className={styles.hasUpdate}>Update available!</p>
</Tooltip>
) : null}
</HorizontalGroup>
</VerticalGroup> </VerticalGroup>
</CardContainer> </CardContainer>
); );

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { Icon, useStyles2, HorizontalGroup, Tooltip, CardContainer, VerticalGroup } from '@grafana/ui'; import { Icon, useStyles2, CardContainer, VerticalGroup } from '@grafana/ui';
import { CatalogPlugin, PluginIconName, PluginListDisplayMode, PluginTabIds } from '../types'; import { CatalogPlugin, PluginIconName, PluginListDisplayMode, PluginTabIds } from '../types';
import { PluginLogo } from './PluginLogo'; import { PluginLogo } from './PluginLogo';
import { PluginListBadges } from './PluginListBadges'; import { PluginListItemBadges } from './PluginListItemBadges';
import { getStyles, LOGO_SIZE } from './PluginListItem'; import { getStyles, LOGO_SIZE } from './PluginListItem';
type Props = { type Props = {
@ -17,24 +17,26 @@ export function PluginListItemRow({ plugin, pathName }: Props) {
<CardContainer href={`${pathName}/${plugin.id}?page=${PluginTabIds.OVERVIEW}`} className={styles.cardContainer}> <CardContainer href={`${pathName}/${plugin.id}?page=${PluginTabIds.OVERVIEW}`} className={styles.cardContainer}>
<VerticalGroup spacing="md"> <VerticalGroup spacing="md">
<div className={styles.headerWrap}> <div className={styles.headerWrap}>
{/* Logo */}
<PluginLogo <PluginLogo
src={plugin.info.logos.small} src={plugin.info.logos.small}
alt={`${plugin.name} logo`} alt={`${plugin.name} logo`}
className={styles.image} className={styles.image}
height={LOGO_SIZE} height={LOGO_SIZE}
/> />
<div> <div>
{/* Name */}
<h3 className={styles.name}>{plugin.name}</h3> <h3 className={styles.name}>{plugin.name}</h3>
{/* Org */}
<p className={styles.orgName}>By {plugin.orgName}</p> <p className={styles.orgName}>By {plugin.orgName}</p>
<HorizontalGroup height="auto">
<PluginListBadges plugin={plugin} /> {/* Badges */}
{plugin.hasUpdate && !plugin.isCore && ( <PluginListItemBadges plugin={plugin} />
<Tooltip content={plugin.version}>
<p className={styles.hasUpdate}>Update available!</p>
</Tooltip>
)}
</HorizontalGroup>
</div> </div>
{/* Type Icon */}
{plugin.type && ( {plugin.type && (
<div className={styles.icon}> <div className={styles.icon}>
<Icon name={PluginIconName[plugin.type]} aria-label={`${plugin.type} plugin icon`} /> <Icon name={PluginIconName[plugin.type]} aria-label={`${plugin.type} plugin icon`} />