Catalog: Add update text to PluginListCard (#39087)

* feat(catalog): add update info to PluginListCard

* refactor(catalog): use IconName enum and minor styling changes to PluginHeaderDependencies

* fix(catalog): add a semver range to grafanaVersion for dependency checks in InstallControls
This commit is contained in:
Jack Westbrook 2021-09-10 17:17:57 +02:00 committed by GitHub
parent cbf16bebc6
commit c9620b2202
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 50 additions and 31 deletions

View File

@ -21,9 +21,16 @@ export async function getPluginDetails(id: string): Promise<CatalogPluginDetails
const isInstalled = Boolean(local); const isInstalled = Boolean(local);
const [remote, versions] = await Promise.all([getRemotePlugin(id, isInstalled), getPluginVersions(id)]); const [remote, versions] = await Promise.all([getRemotePlugin(id, isInstalled), getPluginVersions(id)]);
const dependencies = remote?.json?.dependencies; const dependencies = remote?.json?.dependencies;
// Prepend semver range when we fallback to grafanaVersion (deprecated in favour of grafanaDependency)
// otherwise plugins cannot be installed.
const grafanaDependency = dependencies?.grafanaDependency
? dependencies?.grafanaDependency
: dependencies?.grafanaVersion
? `>=${dependencies?.grafanaVersion}`
: '';
return { return {
grafanaDependency: dependencies?.grafanaDependency || dependencies?.grafanaVersion || '', grafanaDependency,
pluginDependencies: dependencies?.plugins || [], pluginDependencies: dependencies?.plugins || [],
links: remote?.json?.info.links || local?.info.links || [], links: remote?.json?.info.links || local?.info.links || [],
readme: remote?.readme, readme: remote?.readme,

View File

@ -2,22 +2,13 @@ import React from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, Icon } from '@grafana/ui'; import { useStyles2, Icon } from '@grafana/ui';
import { CatalogPlugin } from '../types'; import { CatalogPlugin, IconName } from '../types';
type Props = { type Props = {
plugin: CatalogPlugin; plugin: CatalogPlugin;
className?: string; className?: string;
}; };
const PluginIconClassName: Record<string, string> = {
datasource: 'gicon gicon-datasources',
panel: 'icon-gf icon-gf-panel',
app: 'icon-gf icon-gf-apps',
page: 'icon-gf icon-gf-endpoint-tiny',
dashboard: 'gicon gicon-dashboard',
default: 'icon-gf icon-gf-apps',
};
export function PluginDetailsHeaderDependencies({ plugin, className }: Props): React.ReactElement | null { export function PluginDetailsHeaderDependencies({ plugin, className }: Props): React.ReactElement | null {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const pluginDependencies = plugin.details?.pluginDependencies; const pluginDependencies = plugin.details?.pluginDependencies;
@ -30,12 +21,12 @@ export function PluginDetailsHeaderDependencies({ plugin, className }: Props): R
return ( return (
<div className={className}> <div className={className}>
<div className={styles.textBold}>Dependencies:</div> <div className={styles.dependencyTitle}>Dependencies:</div>
{/* Grafana dependency */} {/* Grafana dependency */}
{Boolean(grafanaDependency) && ( {Boolean(grafanaDependency) && (
<div> <div>
<Icon name="grafana" /> <Icon name="grafana" className={styles.icon} />
Grafana {grafanaDependency} Grafana {grafanaDependency}
</div> </div>
)} )}
@ -46,7 +37,7 @@ export function PluginDetailsHeaderDependencies({ plugin, className }: Props): R
{pluginDependencies.map((p) => { {pluginDependencies.map((p) => {
return ( return (
<span key={p.name}> <span key={p.name}>
<i className={PluginIconClassName[p.type] || PluginIconClassName.default} /> <Icon name={IconName[p.type]} className={styles.icon} />
{p.name} {p.version} {p.name} {p.version}
</span> </span>
); );
@ -59,8 +50,18 @@ export function PluginDetailsHeaderDependencies({ plugin, className }: Props): R
export const getStyles = (theme: GrafanaTheme2) => { export const getStyles = (theme: GrafanaTheme2) => {
return { return {
textBold: css` dependencyTitle: css`
font-weight: ${theme.typography.fontWeightBold}; font-weight: ${theme.typography.fontWeightBold};
margin-right: ${theme.spacing(0.5)};
&::after {
content: '';
padding: 0;
}
`,
icon: css`
color: ${theme.colors.text.secondary};
margin-right: ${theme.spacing(0.5)};
`, `,
}; };
}; };

View File

@ -1,31 +1,23 @@
import React from 'react'; import React from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Icon, useStyles2, CardContainer, VerticalGroup } from '@grafana/ui'; import { Icon, useStyles2, CardContainer, HorizontalGroup, VerticalGroup, Tooltip } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { CatalogPlugin } from '../types'; import { CatalogPlugin, IconName } from '../types';
import { PluginLogo } from './PluginLogo'; import { PluginLogo } from './PluginLogo';
import { PluginListBadges } from './PluginListBadges'; import { PluginListBadges } from './PluginListBadges';
const LOGO_SIZE = '48px'; const LOGO_SIZE = '48px';
enum IconName {
app = 'apps',
datasource = 'database',
panel = 'credit-card',
renderer = 'pen',
}
type PluginListCardProps = { type PluginListCardProps = {
plugin: CatalogPlugin; plugin: CatalogPlugin;
pathName: string; pathName: string;
}; };
export function PluginListCard({ plugin, pathName }: PluginListCardProps) { export function PluginListCard({ plugin, pathName }: PluginListCardProps) {
const { name, id, orgName, type } = plugin;
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
return ( return (
<CardContainer href={`${pathName}/${id}`} className={styles.cardContainer}> <CardContainer href={`${pathName}/${plugin.id}`} className={styles.cardContainer}>
<VerticalGroup spacing="md"> <VerticalGroup spacing="md">
<div className={styles.headerWrap}> <div className={styles.headerWrap}>
<PluginLogo <PluginLogo
@ -34,15 +26,22 @@ export function PluginListCard({ plugin, pathName }: PluginListCardProps) {
className={styles.image} className={styles.image}
height={LOGO_SIZE} height={LOGO_SIZE}
/> />
<h3 className={styles.name}>{name}</h3> <h3 className={styles.name}>{plugin.name}</h3>
{type && ( {plugin.type && (
<div className={styles.icon}> <div className={styles.icon}>
<Icon name={IconName[type]} aria-label={`${type} plugin icon`} /> <Icon name={IconName[plugin.type]} aria-label={`${plugin.type} plugin icon`} />
</div> </div>
)} )}
</div> </div>
<p className={styles.orgName}>By {orgName}</p> <p className={styles.orgName}>By {plugin.orgName}</p>
<PluginListBadges plugin={plugin} /> <HorizontalGroup align="center">
<PluginListBadges plugin={plugin} />
{plugin.hasUpdate && !plugin.isCore ? (
<Tooltip content={plugin.version}>
<p className={styles.hasUpdate}>Update available!</p>
</Tooltip>
) : null}
</HorizontalGroup>
</VerticalGroup> </VerticalGroup>
</CardContainer> </CardContainer>
); );
@ -78,4 +77,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
color: ${theme.colors.text.secondary}; color: ${theme.colors.text.secondary};
margin-bottom: 0; margin-bottom: 0;
`, `,
hasUpdate: css`
color: ${theme.colors.text.secondary};
font-size: ${theme.typography.bodySmall.fontSize};
margin-bottom: 0;
`,
}); });

View File

@ -13,6 +13,13 @@ export enum PluginAdminRoutes {
DetailsAdmin = 'plugins-details-admin', DetailsAdmin = 'plugins-details-admin',
} }
export enum IconName {
app = 'apps',
datasource = 'database',
panel = 'credit-card',
renderer = 'pen',
}
export interface CatalogPlugin { export interface CatalogPlugin {
description: string; description: string;
downloads: number; downloads: number;