mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Update Catalog Card UI (#37350)
* feat(catalog): lazy load and add alt text to plugin logos * refactor(catalog): use plugin types, make sure data is available for new ui * test(catalog): fix up tests after types refactor * feat(catalog): introduce Tile and PluginBadge components for ui updates * refactor(catalog): update PluginList to use new components, lazy load images, add creditcard icon * test(catalog): update Browse.test types * fix(catalog): if local and remote make sure to get the correct local plugin from array * refactor(catalog): prefer grafana/ui components over custom Tile component * chore(catalog): delete redundant components * feat(catalog): introduce ascending descending name sort for Browse * refactor(catalog): prefer theme spacing over hardcoded values * refactor(catalog): update Local and Remote plugin types to match api responses * fix(catalog): prefer local.hasUpdate and local.signature so updateable plugin signature is valid * test(catalog): update test plugin mocks * test(catalog): add tests for sorting and categorise * test(catalog): add tests for plugin cards, remove grid component * test(catalog): add tests for PluginBadges component * refactor(catalog): change enterprise learn more link to open plugin page on website
This commit is contained in:
parent
e8e1a0b50b
commit
cc7c54be0e
@ -33,6 +33,7 @@ export enum PluginSignatureType {
|
||||
commercial = 'commercial',
|
||||
community = 'community',
|
||||
private = 'private',
|
||||
core = 'core',
|
||||
}
|
||||
|
||||
/** Describes error code returned from Grafana plugins API call */
|
||||
|
@ -47,6 +47,7 @@ export const getAvailableIcons = () =>
|
||||
'comments-alt',
|
||||
'compass',
|
||||
'copy',
|
||||
'credit-card',
|
||||
'cube',
|
||||
'database',
|
||||
'document-info',
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { API_ROOT, GRAFANA_API_ROOT } from './constants';
|
||||
import { Plugin, PluginDetails, Org, LocalPlugin } from './types';
|
||||
import { PluginDetails, Org, LocalPlugin, RemotePlugin } from './types';
|
||||
|
||||
async function getRemotePlugins(): Promise<Plugin[]> {
|
||||
async function getRemotePlugins(): Promise<RemotePlugin[]> {
|
||||
const res = await getBackendSrv().get(`${GRAFANA_API_ROOT}/plugins`);
|
||||
return res.items;
|
||||
}
|
||||
@ -23,7 +23,7 @@ async function getPlugin(slug: string): Promise<PluginDetails> {
|
||||
};
|
||||
}
|
||||
|
||||
async function getRemotePlugin(slug: string, local: LocalPlugin | undefined): Promise<Plugin | undefined> {
|
||||
async function getRemotePlugin(slug: string, local: LocalPlugin | undefined): Promise<RemotePlugin | undefined> {
|
||||
try {
|
||||
return await getBackendSrv().get(`${GRAFANA_API_ROOT}/plugins/${slug}`);
|
||||
} catch (error) {
|
||||
|
@ -1,68 +0,0 @@
|
||||
import React from 'react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
text: React.ReactNode;
|
||||
image: React.ReactNode;
|
||||
layout?: 'horizontal' | 'vertical';
|
||||
}
|
||||
|
||||
export const Card = ({ href, text, image, layout = 'vertical' }: Props) => {
|
||||
const styles = useStyles2(getCardStyles);
|
||||
|
||||
return (
|
||||
<a href={href} className={styles.root}>
|
||||
<div
|
||||
className={cx(styles.container, {
|
||||
[styles.containerHorizontal]: layout === 'horizontal',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={cx(styles.imgContainer, {
|
||||
[styles.imgContainerHorizontal]: layout === 'horizontal',
|
||||
})}
|
||||
>
|
||||
{image}
|
||||
</div>
|
||||
{text}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const getCardStyles = (theme: GrafanaTheme2) => ({
|
||||
root: css`
|
||||
background-color: ${theme.colors.background.secondary};
|
||||
border-radius: ${theme.shape.borderRadius()};
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
padding: ${theme.spacing(2)};
|
||||
|
||||
&:hover {
|
||||
background-color: ${theme.colors.action.hover};
|
||||
}
|
||||
`,
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
height: 100%;
|
||||
`,
|
||||
containerHorizontal: css`
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
`,
|
||||
imgContainer: css`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
padding: ${theme.spacing()} 0;
|
||||
`,
|
||||
imgContainerHorizontal: css`
|
||||
flex-grow: 0;
|
||||
`,
|
||||
});
|
@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Grid = ({ children }: Props) => {
|
||||
const theme = useTheme2();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(175px, 1fr));
|
||||
grid-gap: ${theme.spacing.gridSize}px;
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { PluginSignatureStatus } from '@grafana/data';
|
||||
import { PluginBadges } from './PluginBadges';
|
||||
import { CatalogPlugin } from '../types';
|
||||
|
||||
const runtimeMock = jest.requireMock('@grafana/runtime');
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
config: {
|
||||
licenseInfo: {
|
||||
hasValidLicense: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('PluginBadges', () => {
|
||||
const plugin: CatalogPlugin = {
|
||||
description: 'The test plugin',
|
||||
downloads: 5,
|
||||
id: 'test-plugin',
|
||||
info: {
|
||||
logos: {
|
||||
small: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/small',
|
||||
large: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/large',
|
||||
},
|
||||
},
|
||||
name: 'Testing Plugin',
|
||||
orgName: 'Test',
|
||||
popularity: 0,
|
||||
signature: PluginSignatureStatus.valid,
|
||||
publishedAt: '2020-09-01',
|
||||
updatedAt: '2021-06-28',
|
||||
version: '1.0.0',
|
||||
hasUpdate: false,
|
||||
isInstalled: false,
|
||||
isCore: false,
|
||||
isDev: false,
|
||||
isEnterprise: false,
|
||||
};
|
||||
|
||||
it('renders a plugin signature badge', () => {
|
||||
render(<PluginBadges plugin={plugin} />);
|
||||
|
||||
expect(screen.getByText(/signed/i)).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders an installed badge', () => {
|
||||
render(<PluginBadges plugin={{ ...plugin, isInstalled: true }} />);
|
||||
|
||||
expect(screen.getByText(/signed/i)).toBeVisible();
|
||||
expect(screen.getByText(/installed/i)).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders an enterprise badge (when a license is valid)', () => {
|
||||
runtimeMock.config.licenseInfo.hasValidLicense = true;
|
||||
render(<PluginBadges plugin={{ ...plugin, isEnterprise: true }} />);
|
||||
expect(screen.getByText(/enterprise/i)).toBeVisible();
|
||||
expect(screen.queryByRole('button', { name: /learn more/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an enterprise badge with icon and link (when a license is invalid)', () => {
|
||||
runtimeMock.config.licenseInfo.hasValidLicense = false;
|
||||
render(<PluginBadges plugin={{ ...plugin, isEnterprise: true }} />);
|
||||
expect(screen.getByText(/enterprise/i)).toBeVisible();
|
||||
expect(screen.getByLabelText(/lock icon/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /learn more/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Badge, Button, HorizontalGroup, PluginSignatureBadge, useStyles2 } from '@grafana/ui';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { CatalogPlugin } from '../types';
|
||||
|
||||
type PluginBadgeType = {
|
||||
plugin: CatalogPlugin;
|
||||
};
|
||||
|
||||
export function PluginBadges({ plugin }: PluginBadgeType) {
|
||||
if (plugin.isEnterprise) {
|
||||
return <EnterpriseBadge id={plugin.id} />;
|
||||
}
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<PluginSignatureBadge status={plugin.signature} />
|
||||
{plugin.isInstalled && <InstalledBadge />}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function EnterpriseBadge({ id }: { id: string }) {
|
||||
const customBadgeStyles = useStyles2(getBadgeColor);
|
||||
const onClick = (ev: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
ev.preventDefault();
|
||||
window.open(
|
||||
`https://grafana.com/grafana/plugins/${id}?utm_source=grafana_catalog_learn_more`,
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
);
|
||||
};
|
||||
|
||||
if (config.licenseInfo?.hasValidLicense) {
|
||||
return <Badge text="Enterprise" color="blue" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<Badge icon="lock" aria-label="lock icon" text="Enterprise" color="blue" className={customBadgeStyles} />
|
||||
<Button size="sm" fill="text" icon="external-link-alt" onClick={onClick}>
|
||||
Learn more
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function InstalledBadge() {
|
||||
const customBadgeStyles = useStyles2(getBadgeColor);
|
||||
return <Badge text="Installed" color="orange" className={customBadgeStyles} />;
|
||||
}
|
||||
|
||||
const getBadgeColor = (theme: GrafanaTheme2) => css`
|
||||
background: ${theme.colors.background.primary};
|
||||
border-color: ${theme.colors.border.strong};
|
||||
color: ${theme.colors.text.secondary};
|
||||
`;
|
@ -1,13 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Card } from '../components/Card';
|
||||
import { Grid } from '../components/Grid';
|
||||
|
||||
import { CatalogPlugin } from '../types';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { PluginLogo } from './PluginLogo';
|
||||
import { CatalogPlugin } from '../types';
|
||||
import { PluginListCard } from './PluginListCard';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
interface Props {
|
||||
plugins: CatalogPlugin[];
|
||||
@ -18,50 +15,16 @@ export const PluginList = ({ plugins }: Props) => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
{plugins.map((plugin) => {
|
||||
const { name, id, orgName } = plugin;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={`${id}`}
|
||||
href={`${location.pathname}/${id}`}
|
||||
image={
|
||||
<PluginLogo
|
||||
src={plugin.info.logos.small}
|
||||
className={css`
|
||||
max-height: 64px;
|
||||
`}
|
||||
/>
|
||||
}
|
||||
text={
|
||||
<>
|
||||
<div className={styles.name}>{name}</div>
|
||||
<div className={styles.orgName}>{orgName}</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
<div className={styles} data-testid="plugin-list">
|
||||
{plugins.map((plugin) => (
|
||||
<PluginListCard key={plugin.id} plugin={plugin} pathName={location.pathname} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
name: css`
|
||||
font-size: ${theme.typography.h4.fontSize};
|
||||
color: ${theme.colors.text};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
`,
|
||||
orgName: css`
|
||||
font-size: ${theme.typography.body.fontSize};
|
||||
color: ${theme.colors.text.secondary};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
`,
|
||||
});
|
||||
const getStyles = (theme: GrafanaTheme2) => css`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(288px, 1fr));
|
||||
grid-gap: ${theme.spacing(3)};
|
||||
`;
|
||||
|
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { PluginSignatureStatus, PluginType } from '@grafana/data';
|
||||
import { PluginListCard } from './PluginListCard';
|
||||
import { CatalogPlugin } from '../types';
|
||||
|
||||
describe('PluginCard', () => {
|
||||
const plugin: CatalogPlugin = {
|
||||
description: 'The test plugin',
|
||||
downloads: 5,
|
||||
id: 'test-plugin',
|
||||
info: {
|
||||
logos: {
|
||||
small: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/small',
|
||||
large: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/large',
|
||||
},
|
||||
},
|
||||
name: 'Testing Plugin',
|
||||
orgName: 'Test',
|
||||
popularity: 0,
|
||||
signature: PluginSignatureStatus.valid,
|
||||
publishedAt: '2020-09-01',
|
||||
updatedAt: '2021-06-28',
|
||||
version: '1.0.0',
|
||||
hasUpdate: false,
|
||||
isInstalled: false,
|
||||
isCore: false,
|
||||
isDev: false,
|
||||
isEnterprise: false,
|
||||
};
|
||||
|
||||
it('renders a card with link, image, name, orgName and badges', () => {
|
||||
render(<PluginListCard plugin={plugin} pathName="/plugins" />);
|
||||
|
||||
expect(screen.getByRole('link')).toHaveAttribute('href', '/plugins/test-plugin');
|
||||
|
||||
const logo = screen.getByRole('img');
|
||||
expect(logo).toHaveAttribute('src', plugin.info.logos.small);
|
||||
expect(logo).toHaveAttribute('alt', `${plugin.name} logo`);
|
||||
|
||||
expect(screen.getByRole('heading', { name: /testing plugin/i })).toBeVisible();
|
||||
expect(screen.getByText(`By ${plugin.orgName}`)).toBeVisible();
|
||||
expect(screen.getByText(/signed/i)).toBeVisible();
|
||||
expect(screen.queryByLabelText(/icon/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a datasource plugin with correct icon', () => {
|
||||
const datasourcePlugin = { ...plugin, type: PluginType.datasource };
|
||||
render(<PluginListCard plugin={datasourcePlugin} pathName="" />);
|
||||
|
||||
expect(screen.getByLabelText(/datasource plugin icon/i)).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders a panel plugin with correct icon', () => {
|
||||
const panelPlugin = { ...plugin, type: PluginType.panel };
|
||||
render(<PluginListCard plugin={panelPlugin} pathName="" />);
|
||||
|
||||
expect(screen.getByLabelText(/panel plugin icon/i)).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders an app plugin with correct icon', () => {
|
||||
const appPlugin = { ...plugin, type: PluginType.app };
|
||||
render(<PluginListCard plugin={appPlugin} pathName="" />);
|
||||
|
||||
expect(screen.getByLabelText(/app plugin icon/i)).toBeVisible();
|
||||
});
|
||||
});
|
@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Icon, useStyles2, CardContainer, VerticalGroup } from '@grafana/ui';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { CatalogPlugin } from '../types';
|
||||
import { PluginLogo } from './PluginLogo';
|
||||
import { PluginBadges } from './PluginBadges';
|
||||
|
||||
const LOGO_SIZE = '48px';
|
||||
|
||||
enum IconName {
|
||||
app = 'apps',
|
||||
datasource = 'database',
|
||||
panel = 'credit-card',
|
||||
renderer = 'pen',
|
||||
}
|
||||
|
||||
type PluginListCardProps = {
|
||||
plugin: CatalogPlugin;
|
||||
pathName: string;
|
||||
};
|
||||
|
||||
export function PluginListCard({ plugin, pathName }: PluginListCardProps) {
|
||||
const { name, id, orgName, type } = plugin;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<CardContainer href={`${pathName}/${id}`} className={styles.cardContainer}>
|
||||
<VerticalGroup spacing="md">
|
||||
<div className={styles.headerWrap}>
|
||||
<PluginLogo
|
||||
src={plugin.info.logos.small}
|
||||
alt={`${plugin.name} logo`}
|
||||
className={styles.image}
|
||||
height={LOGO_SIZE}
|
||||
/>
|
||||
<h3 className={styles.name}>{name}</h3>
|
||||
{type && (
|
||||
<div className={styles.icon}>
|
||||
<Icon name={IconName[type]} aria-label={`${type} plugin icon`} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className={styles.orgName}>By {orgName}</p>
|
||||
<PluginBadges plugin={plugin} />
|
||||
</VerticalGroup>
|
||||
</CardContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
cardContainer: css`
|
||||
margin-bottom: 0;
|
||||
padding: ${theme.spacing()};
|
||||
`,
|
||||
headerWrap: css`
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-columns: ${LOGO_SIZE} 1fr ${theme.spacing(3)};
|
||||
grid-gap: ${theme.spacing(2)};
|
||||
width: 100%;
|
||||
`,
|
||||
name: css`
|
||||
color: ${theme.colors.text.primary};
|
||||
flex-grow: 1;
|
||||
font-size: ${theme.typography.h4.fontSize};
|
||||
margin-bottom: 0;
|
||||
`,
|
||||
image: css`
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
`,
|
||||
icon: css`
|
||||
align-self: flex-start;
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
orgName: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
margin-bottom: 0;
|
||||
`,
|
||||
});
|
@ -1,10 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
type PluginLogoProps = {
|
||||
src: string;
|
||||
alt: string;
|
||||
className?: string;
|
||||
src: string;
|
||||
height?: string | number;
|
||||
};
|
||||
|
||||
export function PluginLogo({ src, className }: PluginLogoProps): React.ReactElement {
|
||||
return <img src={src} className={className} />;
|
||||
export function PluginLogo({ alt, className, src, height }: PluginLogoProps): React.ReactElement {
|
||||
// @ts-ignore - react doesn't know about loading attr.
|
||||
return <img src={src} className={className} alt={alt} loading="lazy" height={height} />;
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import { gt } from 'semver';
|
||||
import { PluginSignatureStatus } from '@grafana/data';
|
||||
import { CatalogPlugin, CatalogPluginDetails, LocalPlugin, RemotePlugin, Version, PluginFilter } from './types';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { CatalogPlugin, CatalogPluginDetails, LocalPlugin, Plugin, Version, PluginFilter } from './types';
|
||||
|
||||
export function isGrafanaAdmin(): boolean {
|
||||
return config.bootData.user.isGrafanaAdmin;
|
||||
@ -11,7 +12,7 @@ export function isOrgAdmin() {
|
||||
return contextSrv.hasRole('Admin');
|
||||
}
|
||||
|
||||
export function mapRemoteToCatalog(plugin: Plugin): CatalogPlugin {
|
||||
export function mapRemoteToCatalog(plugin: RemotePlugin): CatalogPlugin {
|
||||
const {
|
||||
name,
|
||||
slug: id,
|
||||
@ -24,7 +25,11 @@ export function mapRemoteToCatalog(plugin: Plugin): CatalogPlugin {
|
||||
updatedAt,
|
||||
createdAt: publishedAt,
|
||||
status,
|
||||
versionSignatureType,
|
||||
signatureType,
|
||||
} = plugin;
|
||||
|
||||
const hasSignature = signatureType !== '' || versionSignatureType !== '';
|
||||
const catalogPlugin = {
|
||||
description,
|
||||
downloads,
|
||||
@ -39,6 +44,7 @@ export function mapRemoteToCatalog(plugin: Plugin): CatalogPlugin {
|
||||
orgName,
|
||||
popularity,
|
||||
publishedAt,
|
||||
signature: hasSignature ? PluginSignatureStatus.valid : PluginSignatureStatus.missing,
|
||||
updatedAt,
|
||||
version,
|
||||
hasUpdate: false,
|
||||
@ -69,6 +75,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin): CatalogPlugin {
|
||||
orgName: author.name,
|
||||
popularity: 0,
|
||||
publishedAt: '',
|
||||
signature,
|
||||
updatedAt: updated,
|
||||
version,
|
||||
hasUpdate: false,
|
||||
@ -80,15 +87,12 @@ export function mapLocalToCatalog(plugin: LocalPlugin): CatalogPlugin {
|
||||
};
|
||||
}
|
||||
|
||||
export function getCatalogPluginDetails(
|
||||
local: LocalPlugin | undefined,
|
||||
remote: Plugin | undefined,
|
||||
pluginVersions: Version[] | undefined
|
||||
): CatalogPluginDetails {
|
||||
export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin): CatalogPlugin {
|
||||
const version = remote?.version || local?.info.version || '';
|
||||
const hasUpdate = Boolean(remote?.version && local?.info.version && gt(remote?.version, local?.info.version));
|
||||
const hasUpdate =
|
||||
local?.hasUpdate || Boolean(remote?.version && local?.info.version && gt(remote?.version, local?.info.version));
|
||||
const id = remote?.slug || local?.id || '';
|
||||
|
||||
const hasRemoteSignature = remote?.signatureType !== '' || remote?.versionSignatureType !== '';
|
||||
let logos = {
|
||||
small: 'https://grafana.com/api/plugins/404notfound/versions/none/logos/small',
|
||||
large: 'https://grafana.com/api/plugins/404notfound/versions/none/logos/large',
|
||||
@ -103,32 +107,43 @@ export function getCatalogPluginDetails(
|
||||
logos = local.info.logos;
|
||||
}
|
||||
|
||||
const plugin = {
|
||||
return {
|
||||
description: remote?.description || local?.info.description || '',
|
||||
downloads: remote?.downloads || 0,
|
||||
grafanaDependency: remote?.json?.dependencies?.grafanaDependency || '',
|
||||
hasUpdate,
|
||||
id,
|
||||
info: {
|
||||
logos,
|
||||
},
|
||||
isCore: Boolean(remote?.internal || local?.signature === 'internal'),
|
||||
isCore: Boolean(remote?.internal || local?.signature === PluginSignatureStatus.internal),
|
||||
isDev: Boolean(local?.dev),
|
||||
isEnterprise: remote?.status === 'enterprise' || false,
|
||||
isEnterprise: remote?.status === 'enterprise',
|
||||
isInstalled: Boolean(local),
|
||||
links: remote?.json?.info.links || local?.info.links || [],
|
||||
name: remote?.name || local?.name || '',
|
||||
orgName: remote?.orgName || local?.info.author.name || '',
|
||||
popularity: remote?.popularity || 0,
|
||||
publishedAt: remote?.createdAt || '',
|
||||
readme: remote?.readme || 'No plugin help or readme markdown file was found',
|
||||
type: remote?.typeCode || local?.type || '',
|
||||
type: remote?.typeCode || local?.type,
|
||||
signature: local?.signature || hasRemoteSignature ? PluginSignatureStatus.valid : PluginSignatureStatus.missing,
|
||||
updatedAt: remote?.updatedAt || local?.info.updated || '',
|
||||
version,
|
||||
versions: pluginVersions || [],
|
||||
};
|
||||
}
|
||||
|
||||
return plugin;
|
||||
export function getCatalogPluginDetails(
|
||||
local: LocalPlugin | undefined,
|
||||
remote: RemotePlugin | undefined,
|
||||
pluginVersions: Version[] = []
|
||||
): CatalogPluginDetails {
|
||||
const plugin = mapToCatalogPlugin(local, remote);
|
||||
|
||||
return {
|
||||
...plugin,
|
||||
grafanaDependency: remote?.json?.dependencies?.grafanaDependency || '',
|
||||
links: remote?.json?.info.links || local?.info.links || [],
|
||||
readme: remote?.readme || 'No plugin help or readme markdown file was found',
|
||||
versions: pluginVersions,
|
||||
};
|
||||
}
|
||||
|
||||
export const isInstalled: PluginFilter = (plugin, query) =>
|
||||
|
@ -2,7 +2,14 @@ import { useMemo } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { CatalogPlugin, CatalogPluginsState, PluginsByFilterType, FilteredPluginsState } from '../types';
|
||||
import { api } from '../api';
|
||||
import { mapLocalToCatalog, mapRemoteToCatalog, isInstalled, isType, matchesKeyword } from '../helpers';
|
||||
import {
|
||||
mapLocalToCatalog,
|
||||
mapRemoteToCatalog,
|
||||
mapToCatalogPlugin,
|
||||
isInstalled,
|
||||
isType,
|
||||
matchesKeyword,
|
||||
} from '../helpers';
|
||||
|
||||
export function usePlugins(): CatalogPluginsState {
|
||||
const { loading, value, error } = useAsync(async () => {
|
||||
@ -21,10 +28,6 @@ export function usePlugins(): CatalogPluginsState {
|
||||
}
|
||||
|
||||
for (const plugin of remote) {
|
||||
if (unique[plugin.slug]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (plugin.typeCode === 'renderer') {
|
||||
continue;
|
||||
}
|
||||
@ -33,9 +36,15 @@ export function usePlugins(): CatalogPluginsState {
|
||||
continue;
|
||||
}
|
||||
|
||||
unique[plugin.slug] = mapRemoteToCatalog(plugin);
|
||||
if (unique[plugin.slug]) {
|
||||
unique[plugin.slug] = mapToCatalogPlugin(
|
||||
installed.find((installedPlugin) => installedPlugin.id === plugin.slug),
|
||||
plugin
|
||||
);
|
||||
} else {
|
||||
unique[plugin.slug] = mapRemoteToCatalog(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(unique);
|
||||
}, [value?.installed, value?.remote]);
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { render, RenderResult, waitFor } from '@testing-library/react';
|
||||
import { render, RenderResult, waitFor, within } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { PluginSignatureStatus, PluginSignatureType, PluginType } from '@grafana/data';
|
||||
import BrowsePage from './Browse';
|
||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { LocalPlugin, Plugin, PluginAdminRoutes } from '../types';
|
||||
import { LocalPlugin, RemotePlugin, PluginAdminRoutes } from '../types';
|
||||
import { API_ROOT, GRAFANA_API_ROOT } from '../constants';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
@ -42,135 +43,221 @@ function setup(path = '/plugins'): RenderResult {
|
||||
}
|
||||
|
||||
describe('Browse list of plugins', () => {
|
||||
it('should list installed plugins by default', async () => {
|
||||
const { queryByText } = setup('/plugins');
|
||||
describe('when filtering', () => {
|
||||
it('should list installed plugins by default', async () => {
|
||||
const { queryByText } = setup('/plugins');
|
||||
|
||||
await waitFor(() => queryByText('Installed'));
|
||||
await waitFor(() => queryByText('Installed'));
|
||||
|
||||
for (const plugin of installed) {
|
||||
expect(queryByText(plugin.name)).toBeInTheDocument();
|
||||
}
|
||||
for (const plugin of installed) {
|
||||
expect(queryByText(plugin.name)).toBeInTheDocument();
|
||||
}
|
||||
|
||||
for (const plugin of remote) {
|
||||
expect(queryByText(plugin.name)).toBeNull();
|
||||
}
|
||||
for (const plugin of remote) {
|
||||
expect(queryByText(plugin.name)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('should list all plugins (except core plugins) when filtering by all', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=all&filterByType=all');
|
||||
|
||||
await waitFor(() => expect(queryByText('Diagram')).toBeInTheDocument());
|
||||
for (const plugin of remote) {
|
||||
expect(queryByText(plugin.name)).toBeInTheDocument();
|
||||
}
|
||||
|
||||
expect(queryByText('Alert Manager')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should list installed plugins (including core plugins) when filtering by installed', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=installed');
|
||||
|
||||
await waitFor(() => queryByText('Installed'));
|
||||
|
||||
for (const plugin of installed) {
|
||||
expect(queryByText(plugin.name)).toBeInTheDocument();
|
||||
}
|
||||
|
||||
for (const plugin of remote) {
|
||||
expect(queryByText(plugin.name)).not.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('should list enterprise plugins', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=all&q=wavefront');
|
||||
|
||||
await waitFor(() => expect(queryByText('Wavefront')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should list only datasource plugins when filtering by datasource', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=all&filterByType=datasource');
|
||||
|
||||
await waitFor(() => expect(queryByText('Wavefront')).toBeInTheDocument());
|
||||
|
||||
expect(queryByText('Alert Manager')).not.toBeInTheDocument();
|
||||
expect(queryByText('Diagram')).not.toBeInTheDocument();
|
||||
expect(queryByText('Zabbix')).not.toBeInTheDocument();
|
||||
expect(queryByText('ACE.SVG')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should list only panel plugins when filtering by panel', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=all&filterByType=panel');
|
||||
|
||||
await waitFor(() => expect(queryByText('Diagram')).toBeInTheDocument());
|
||||
expect(queryByText('ACE.SVG')).toBeInTheDocument();
|
||||
|
||||
expect(queryByText('Wavefront')).not.toBeInTheDocument();
|
||||
expect(queryByText('Alert Manager')).not.toBeInTheDocument();
|
||||
expect(queryByText('Zabbix')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should list only app plugins when filtering by app', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=all&filterByType=app');
|
||||
|
||||
await waitFor(() => expect(queryByText('Zabbix')).toBeInTheDocument());
|
||||
|
||||
expect(queryByText('Wavefront')).not.toBeInTheDocument();
|
||||
expect(queryByText('Alert Manager')).not.toBeInTheDocument();
|
||||
expect(queryByText('Diagram')).not.toBeInTheDocument();
|
||||
expect(queryByText('ACE.SVG')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('when searching', () => {
|
||||
it('should only list plugins matching search', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=all&q=zabbix');
|
||||
|
||||
await waitFor(() => expect(queryByText('Zabbix')).toBeInTheDocument());
|
||||
|
||||
expect(queryByText('Wavefront')).not.toBeInTheDocument();
|
||||
expect(queryByText('Alert Manager')).not.toBeInTheDocument();
|
||||
expect(queryByText('Diagram')).not.toBeInTheDocument();
|
||||
expect(queryByText('Redis Application')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should list all plugins (except core plugins) when filtering by all', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=all?filterByType=all');
|
||||
describe('when sorting', () => {
|
||||
it('should sort plugins by name in ascending alphabetical order', async () => {
|
||||
const { findByTestId } = setup('/plugins?filterBy=all');
|
||||
|
||||
await waitFor(() => expect(queryByText('Diagram')).toBeInTheDocument());
|
||||
for (const plugin of remote) {
|
||||
expect(queryByText(plugin.name)).toBeInTheDocument();
|
||||
}
|
||||
const pluginList = await findByTestId('plugin-list');
|
||||
const pluginHeadings = within(pluginList).queryAllByRole('heading');
|
||||
|
||||
expect(queryByText('Alert Manager')).not.toBeInTheDocument();
|
||||
});
|
||||
expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
|
||||
'ACE.SVG',
|
||||
'Diagram',
|
||||
'Redis Application',
|
||||
'Wavefront',
|
||||
'Zabbix',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should list installed plugins (including core plugins) when filtering by installed', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=installed');
|
||||
it('should sort plugins by name in descending alphabetical order', async () => {
|
||||
const { findByTestId } = setup('/plugins?filterBy=all&sortBy=nameDesc');
|
||||
|
||||
await waitFor(() => queryByText('Installed'));
|
||||
const pluginList = await findByTestId('plugin-list');
|
||||
const pluginHeadings = within(pluginList).queryAllByRole('heading');
|
||||
|
||||
for (const plugin of installed) {
|
||||
expect(queryByText(plugin.name)).toBeInTheDocument();
|
||||
}
|
||||
expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
|
||||
'Zabbix',
|
||||
'Wavefront',
|
||||
'Redis Application',
|
||||
'Diagram',
|
||||
'ACE.SVG',
|
||||
]);
|
||||
});
|
||||
|
||||
for (const plugin of remote) {
|
||||
expect(queryByText(plugin.name)).not.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
it('should sort plugins by date in ascending updated order', async () => {
|
||||
const { findByTestId } = setup('/plugins?filterBy=all&sortBy=updated');
|
||||
|
||||
it('should list enterprise plugins', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=all&q=wavefront');
|
||||
const pluginList = await findByTestId('plugin-list');
|
||||
const pluginHeadings = within(pluginList).queryAllByRole('heading');
|
||||
|
||||
await waitFor(() => expect(queryByText('Wavefront')).toBeInTheDocument());
|
||||
});
|
||||
expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
|
||||
'Diagram',
|
||||
'Wavefront',
|
||||
'Redis Application',
|
||||
'ACE.SVG',
|
||||
'Zabbix',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should list only datasource plugins when filtering by datasource', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=all&filterByType=datasource');
|
||||
it('should sort plugins by date in ascending published order', async () => {
|
||||
const { findByTestId } = setup('/plugins?filterBy=all&sortBy=published');
|
||||
|
||||
await waitFor(() => expect(queryByText('Wavefront')).toBeInTheDocument());
|
||||
const pluginList = await findByTestId('plugin-list');
|
||||
const pluginHeadings = within(pluginList).queryAllByRole('heading');
|
||||
|
||||
expect(queryByText('Alert Manager')).not.toBeInTheDocument();
|
||||
expect(queryByText('Diagram')).not.toBeInTheDocument();
|
||||
expect(queryByText('Zabbix')).not.toBeInTheDocument();
|
||||
});
|
||||
expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
|
||||
'Diagram',
|
||||
'Redis Application',
|
||||
'ACE.SVG',
|
||||
'Wavefront',
|
||||
'Zabbix',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should list only panel plugins when filtering by panel', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=all&filterByType=panel');
|
||||
it('should sort plugins by number of downloads in ascending order', async () => {
|
||||
const { findByTestId } = setup('/plugins?filterBy=all&sortBy=downloads');
|
||||
|
||||
await waitFor(() => expect(queryByText('Diagram')).toBeInTheDocument());
|
||||
const pluginList = await findByTestId('plugin-list');
|
||||
const pluginHeadings = within(pluginList).queryAllByRole('heading');
|
||||
|
||||
expect(queryByText('Wavefront')).not.toBeInTheDocument();
|
||||
expect(queryByText('Alert Manager')).not.toBeInTheDocument();
|
||||
expect(queryByText('Zabbix')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should list only app plugins when filtering by app', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=all&filterByType=app');
|
||||
|
||||
await waitFor(() => expect(queryByText('Zabbix')).toBeInTheDocument());
|
||||
|
||||
expect(queryByText('Wavefront')).not.toBeInTheDocument();
|
||||
expect(queryByText('Alert Manager')).not.toBeInTheDocument();
|
||||
expect(queryByText('Diagram')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should only list plugins matching search', async () => {
|
||||
const { queryByText } = setup('/plugins?filterBy=all&q=zabbix');
|
||||
|
||||
await waitFor(() => expect(queryByText('Zabbix')).toBeInTheDocument());
|
||||
|
||||
expect(queryByText('Wavefront')).not.toBeInTheDocument();
|
||||
expect(queryByText('Alert Manager')).not.toBeInTheDocument();
|
||||
expect(queryByText('Diagram')).not.toBeInTheDocument();
|
||||
expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
|
||||
'Zabbix',
|
||||
'ACE.SVG',
|
||||
'Wavefront',
|
||||
'Diagram',
|
||||
'Redis Application',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const installed: LocalPlugin[] = [
|
||||
{
|
||||
category: '',
|
||||
defaultNavUrl: '/plugins/alertmanager/',
|
||||
name: 'Alert Manager',
|
||||
type: PluginType.datasource,
|
||||
id: 'alertmanager',
|
||||
enabled: true,
|
||||
pinned: false,
|
||||
info: {
|
||||
author: {
|
||||
name: 'Prometheus alertmanager',
|
||||
url: 'https://grafana.com',
|
||||
},
|
||||
build: {},
|
||||
description: '',
|
||||
links: [],
|
||||
links: [
|
||||
{
|
||||
name: 'Learn more',
|
||||
url: 'https://prometheus.io/docs/alerting/latest/alertmanager/',
|
||||
},
|
||||
],
|
||||
logos: {
|
||||
small: '',
|
||||
large: '',
|
||||
small: 'public/app/plugins/datasource/alertmanager/img/logo.svg',
|
||||
large: 'public/app/plugins/datasource/alertmanager/img/logo.svg',
|
||||
},
|
||||
updated: '',
|
||||
build: {},
|
||||
screenshots: null,
|
||||
version: '',
|
||||
updated: '',
|
||||
},
|
||||
enabled: true,
|
||||
hasUpdate: false,
|
||||
id: 'alertmanager',
|
||||
latestVersion: '',
|
||||
name: 'Alert Manager',
|
||||
pinned: false,
|
||||
signature: 'internal',
|
||||
signatureOrg: '',
|
||||
signatureType: '',
|
||||
hasUpdate: false,
|
||||
defaultNavUrl: '/plugins/alertmanager/',
|
||||
category: '',
|
||||
state: 'alpha',
|
||||
type: 'datasource',
|
||||
dev: false,
|
||||
signature: PluginSignatureStatus.internal,
|
||||
signatureType: '',
|
||||
signatureOrg: '',
|
||||
},
|
||||
{
|
||||
name: 'Diagram',
|
||||
type: 'panel',
|
||||
type: PluginType.panel,
|
||||
id: 'jdbranham-diagram-panel',
|
||||
enabled: true,
|
||||
pinned: false,
|
||||
info: {
|
||||
author: {
|
||||
name: 'Jeremy Branham',
|
||||
url: 'https://savantly.net',
|
||||
},
|
||||
author: { name: 'Jeremy Branham', url: 'https://savantly.net' },
|
||||
description: 'Display diagrams and charts with colored metric indicators',
|
||||
links: [
|
||||
{
|
||||
@ -187,61 +274,153 @@ const installed: LocalPlugin[] = [
|
||||
large: 'public/plugins/jdbranham-diagram-panel/img/logo.svg',
|
||||
},
|
||||
build: {},
|
||||
version: '1.7.1',
|
||||
updated: '2021-05-26',
|
||||
screenshots: [],
|
||||
version: '1.7.3',
|
||||
updated: '2021-07-20',
|
||||
},
|
||||
latestVersion: '1.7.3',
|
||||
hasUpdate: true,
|
||||
defaultNavUrl: '/plugins/jdbranham-diagram-panel/',
|
||||
category: '',
|
||||
state: '',
|
||||
signature: 'unsigned',
|
||||
signature: PluginSignatureStatus.missing,
|
||||
signatureType: '',
|
||||
signatureOrg: '',
|
||||
dev: false,
|
||||
},
|
||||
{
|
||||
name: 'Redis Application',
|
||||
type: PluginType.app,
|
||||
id: 'redis-app',
|
||||
enabled: false,
|
||||
pinned: false,
|
||||
info: {
|
||||
author: {
|
||||
name: 'RedisGrafana',
|
||||
url: 'https://redisgrafana.github.io',
|
||||
},
|
||||
description: 'Provides Application pages and custom panels for Redis Data Source.',
|
||||
links: [
|
||||
{ name: 'Website', url: 'https://redisgrafana.github.io' },
|
||||
{
|
||||
name: 'License',
|
||||
url: 'https://github.com/RedisGrafana/grafana-redis-app/blob/master/LICENSE',
|
||||
},
|
||||
],
|
||||
logos: {
|
||||
small: 'public/plugins/redis-app/img/logo.svg',
|
||||
large: 'public/plugins/redis-app/img/logo.svg',
|
||||
},
|
||||
build: {},
|
||||
screenshots: [],
|
||||
version: '2.0.1',
|
||||
updated: '2021-07-07',
|
||||
},
|
||||
latestVersion: '2.0.1',
|
||||
hasUpdate: false,
|
||||
defaultNavUrl: '/plugins/redis-app/',
|
||||
category: '',
|
||||
state: '',
|
||||
signature: PluginSignatureStatus.valid,
|
||||
signatureType: PluginSignatureType.commercial,
|
||||
signatureOrg: 'RedisGrafana',
|
||||
},
|
||||
];
|
||||
const remote: Plugin[] = [
|
||||
|
||||
const remote: RemotePlugin[] = [
|
||||
{
|
||||
createdAt: '2016-04-06T20:23:41.000Z',
|
||||
description: 'Zabbix plugin for Grafana',
|
||||
downloads: 33645089,
|
||||
featured: 180,
|
||||
internal: false,
|
||||
links: [],
|
||||
status: 'active',
|
||||
id: 74,
|
||||
typeId: 1,
|
||||
typeName: 'Application',
|
||||
typeCode: PluginType.app,
|
||||
slug: 'alexanderzobnin-zabbix-app',
|
||||
name: 'Zabbix',
|
||||
description: 'Zabbix plugin for Grafana',
|
||||
version: '4.1.5',
|
||||
versionStatus: 'active',
|
||||
versionSignatureType: PluginSignatureType.community,
|
||||
versionSignedByOrg: 'alexanderzobnin',
|
||||
versionSignedByOrgName: 'Alexander Zobnin',
|
||||
userId: 0,
|
||||
orgId: 13056,
|
||||
orgName: 'Alexander Zobnin',
|
||||
orgSlug: 'alexanderzobnin',
|
||||
packages: {},
|
||||
popularity: 0.2111,
|
||||
signatureType: 'community',
|
||||
slug: 'alexanderzobnin-zabbix-app',
|
||||
status: 'active',
|
||||
typeCode: 'app',
|
||||
orgUrl: 'https://github.com/alexanderzobnin',
|
||||
url: 'https://github.com/alexanderzobnin/grafana-zabbix',
|
||||
createdAt: '2016-04-06T20:23:41.000Z',
|
||||
updatedAt: '2021-05-18T14:53:01.000Z',
|
||||
version: '4.1.5',
|
||||
versionSignatureType: 'community',
|
||||
readme: '',
|
||||
downloads: 34387994,
|
||||
verified: false,
|
||||
featured: 180,
|
||||
internal: false,
|
||||
downloadSlug: 'alexanderzobnin-zabbix-app',
|
||||
popularity: 0.2019,
|
||||
signatureType: PluginSignatureType.community,
|
||||
packages: {},
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
createdAt: '2020-09-01T13:02:57.000Z',
|
||||
description: 'Wavefront Datasource',
|
||||
downloads: 7283,
|
||||
featured: 0,
|
||||
internal: false,
|
||||
links: [],
|
||||
status: 'enterprise',
|
||||
id: 658,
|
||||
typeId: 2,
|
||||
typeName: 'Data Source',
|
||||
typeCode: PluginType.datasource,
|
||||
slug: 'grafana-wavefront-datasource',
|
||||
name: 'Wavefront',
|
||||
description: 'Wavefront Datasource',
|
||||
version: '1.0.8',
|
||||
versionStatus: 'active',
|
||||
versionSignatureType: PluginSignatureType.grafana,
|
||||
versionSignedByOrg: 'grafana',
|
||||
versionSignedByOrgName: 'Grafana Labs',
|
||||
userId: 0,
|
||||
orgId: 5000,
|
||||
orgName: 'Grafana Labs',
|
||||
orgSlug: 'grafana',
|
||||
orgUrl: 'https://grafana.org',
|
||||
url: 'https://github.com/grafana/wavefront-datasource/',
|
||||
createdAt: '2020-09-01T13:02:57.000Z',
|
||||
updatedAt: '2021-07-12T18:41:03.000Z',
|
||||
downloads: 7818,
|
||||
verified: false,
|
||||
featured: 0,
|
||||
internal: false,
|
||||
downloadSlug: 'grafana-wavefront-datasource',
|
||||
popularity: 0.0107,
|
||||
signatureType: PluginSignatureType.grafana,
|
||||
packages: {},
|
||||
popularity: 0.0133,
|
||||
signatureType: 'grafana',
|
||||
slug: 'grafana-wavefront-datasource',
|
||||
status: 'enterprise',
|
||||
typeCode: 'datasource',
|
||||
updatedAt: '2021-06-23T12:45:13.000Z',
|
||||
version: '1.0.7',
|
||||
versionSignatureType: 'grafana',
|
||||
readme: '',
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
status: 'active',
|
||||
id: 659,
|
||||
typeId: 3,
|
||||
typeName: 'Panel',
|
||||
typeCode: PluginType.panel,
|
||||
slug: 'aceiot-svg-panel',
|
||||
name: 'ACE.SVG',
|
||||
description: 'SVG Visualization Panel',
|
||||
version: '0.0.10',
|
||||
versionStatus: 'active',
|
||||
versionSignatureType: PluginSignatureType.community,
|
||||
versionSignedByOrg: 'aceiot',
|
||||
versionSignedByOrgName: 'Andrew Rodgers',
|
||||
userId: 0,
|
||||
orgId: 409764,
|
||||
orgName: 'Andrew Rodgers',
|
||||
orgSlug: 'aceiot',
|
||||
orgUrl: '',
|
||||
url: 'https://github.com/ACE-IoT-Solutions/ace-svg-react',
|
||||
createdAt: '2020-09-01T14:46:44.000Z',
|
||||
updatedAt: '2021-06-28T14:01:36.000Z',
|
||||
downloads: 101569,
|
||||
verified: false,
|
||||
featured: 0,
|
||||
internal: false,
|
||||
downloadSlug: 'aceiot-svg-panel',
|
||||
popularity: 0.0134,
|
||||
signatureType: PluginSignatureType.community,
|
||||
packages: {},
|
||||
links: [],
|
||||
},
|
||||
];
|
||||
|
@ -27,7 +27,7 @@ export default function Browse({ route }: GrafanaRouteComponentProps): ReactElem
|
||||
const q = (query.q as string) ?? '';
|
||||
const filterBy = (query.filterBy as string) ?? 'installed';
|
||||
const filterByType = (query.filterByType as string) ?? 'all';
|
||||
const sortBy = (query.sortBy as string) ?? 'name';
|
||||
const sortBy = (query.sortBy as string) ?? 'nameAsc';
|
||||
|
||||
const { plugins, isLoading, error } = usePluginsByFilter({ searchBy: q, filterBy, filterByType });
|
||||
const sortedPlugins = plugins.sort(sorters[sortBy]);
|
||||
@ -90,7 +90,8 @@ export default function Browse({ route }: GrafanaRouteComponentProps): ReactElem
|
||||
value={sortBy}
|
||||
onChange={onSortByChange}
|
||||
options={[
|
||||
{ value: 'name', label: 'Sort by name (A-Z)' },
|
||||
{ value: 'nameAsc', label: 'Sort by name (A-Z)' },
|
||||
{ value: 'nameDesc', label: 'Sort by name (Z-A)' },
|
||||
{ value: 'updated', label: 'Sort by updated date' },
|
||||
{ value: 'published', label: 'Sort by published date' },
|
||||
{ value: 'downloads', label: 'Sort by downloads' },
|
||||
@ -139,7 +140,8 @@ const getNavModelId = (routeName?: string) => {
|
||||
};
|
||||
|
||||
const sorters: { [name: string]: (a: CatalogPlugin, b: CatalogPlugin) => number } = {
|
||||
name: (a: CatalogPlugin, b: CatalogPlugin) => a.name.localeCompare(b.name),
|
||||
nameAsc: (a: CatalogPlugin, b: CatalogPlugin) => a.name.localeCompare(b.name),
|
||||
nameDesc: (a: CatalogPlugin, b: CatalogPlugin) => b.name.localeCompare(a.name),
|
||||
updated: (a: CatalogPlugin, b: CatalogPlugin) =>
|
||||
dateTimeParse(b.updatedAt).valueOf() - dateTimeParse(a.updatedAt).valueOf(),
|
||||
published: (a: CatalogPlugin, b: CatalogPlugin) =>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render, RenderResult, waitFor } from '@testing-library/react';
|
||||
import { PluginSignatureStatus, PluginSignatureType, PluginType } from '@grafana/data';
|
||||
import PluginDetailsPage from './PluginDetails';
|
||||
import { API_ROOT, GRAFANA_API_ROOT } from '../constants';
|
||||
import { LocalPlugin, Plugin } from '../types';
|
||||
import { LocalPlugin, RemotePlugin } from '../types';
|
||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||
|
||||
jest.mock('@grafana/runtime', () => {
|
||||
@ -74,26 +75,38 @@ describe('Plugin details page', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function remotePlugin(plugin: Partial<Plugin> = {}): Plugin {
|
||||
function remotePlugin(plugin: Partial<RemotePlugin> = {}): RemotePlugin {
|
||||
return {
|
||||
createdAt: '2016-04-06T20:23:41.000Z',
|
||||
description: 'Zabbix plugin for Grafana',
|
||||
downloads: 33645089,
|
||||
featured: 180,
|
||||
id: 74,
|
||||
typeId: 1,
|
||||
typeName: 'Application',
|
||||
internal: false,
|
||||
links: [],
|
||||
name: 'Zabbix',
|
||||
orgId: 13056,
|
||||
orgName: 'Alexander Zobnin',
|
||||
orgSlug: 'alexanderzobnin',
|
||||
orgUrl: 'https://github.com/alexanderzobnin',
|
||||
url: 'https://github.com/alexanderzobnin/grafana-zabbix',
|
||||
verified: false,
|
||||
downloadSlug: 'alexanderzobnin-zabbix-app',
|
||||
packages: {},
|
||||
popularity: 0.2111,
|
||||
signatureType: 'community',
|
||||
signatureType: PluginSignatureType.community,
|
||||
slug: 'alexanderzobnin-zabbix-app',
|
||||
status: 'active',
|
||||
typeCode: 'app',
|
||||
typeCode: PluginType.app,
|
||||
updatedAt: '2021-05-18T14:53:01.000Z',
|
||||
version: '4.1.5',
|
||||
versionSignatureType: 'community',
|
||||
versionStatus: 'active',
|
||||
versionSignatureType: PluginSignatureType.community,
|
||||
versionSignedByOrg: 'alexanderzobnin',
|
||||
versionSignedByOrgName: 'Alexander Zobnin',
|
||||
userId: 0,
|
||||
readme: '',
|
||||
json: {
|
||||
dependencies: {
|
||||
@ -133,12 +146,11 @@ function localPlugin(plugin: Partial<LocalPlugin> = {}): LocalPlugin {
|
||||
latestVersion: '',
|
||||
name: 'Alert Manager',
|
||||
pinned: false,
|
||||
signature: 'internal',
|
||||
signature: PluginSignatureStatus.internal,
|
||||
signatureOrg: '',
|
||||
signatureType: '',
|
||||
state: 'alpha',
|
||||
type: 'datasource',
|
||||
dev: false,
|
||||
type: PluginType.datasource,
|
||||
...plugin,
|
||||
};
|
||||
}
|
||||
@ -147,7 +159,6 @@ function corePlugin(plugin: Partial<LocalPlugin> = {}): LocalPlugin {
|
||||
return {
|
||||
category: 'sql',
|
||||
defaultNavUrl: '/plugins/postgres/',
|
||||
dev: false,
|
||||
enabled: true,
|
||||
hasUpdate: false,
|
||||
id: 'core',
|
||||
@ -166,11 +177,11 @@ function corePlugin(plugin: Partial<LocalPlugin> = {}): LocalPlugin {
|
||||
latestVersion: '',
|
||||
name: 'PostgreSQL',
|
||||
pinned: false,
|
||||
signature: 'internal',
|
||||
signature: PluginSignatureStatus.internal,
|
||||
signatureOrg: '',
|
||||
signatureType: '',
|
||||
state: '',
|
||||
type: 'datasource',
|
||||
type: PluginType.datasource,
|
||||
...plugin,
|
||||
};
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ export default function PluginDetails({ match }: PluginDetailsProps): JSX.Elemen
|
||||
<PluginPage>
|
||||
<div className={styles.headerContainer}>
|
||||
<PluginLogo
|
||||
alt={`${plugin.name} logo`}
|
||||
src={plugin.info.logos.small}
|
||||
className={css`
|
||||
object-fit: contain;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { GrafanaPlugin, PluginMeta } from '@grafana/data';
|
||||
import { GrafanaPlugin, PluginMeta, PluginType, PluginSignatureStatus, PluginSignatureType } from '@grafana/data';
|
||||
export type PluginTypeCode = 'app' | 'panel' | 'datasource';
|
||||
|
||||
export enum PluginAdminRoutes {
|
||||
@ -22,9 +22,10 @@ export interface CatalogPlugin {
|
||||
isInstalled: boolean;
|
||||
name: string;
|
||||
orgName: string;
|
||||
signature: PluginSignatureStatus;
|
||||
popularity: number;
|
||||
publishedAt: string;
|
||||
type: string;
|
||||
type?: PluginType;
|
||||
updatedAt: string;
|
||||
version: string;
|
||||
}
|
||||
@ -46,34 +47,14 @@ export interface CatalogPluginInfo {
|
||||
};
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
name: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
orgName: string;
|
||||
orgSlug: string;
|
||||
signatureType: string;
|
||||
version: string;
|
||||
status: string;
|
||||
popularity: number;
|
||||
downloads: number;
|
||||
updatedAt: string;
|
||||
export type RemotePlugin = {
|
||||
createdAt: string;
|
||||
typeCode: string;
|
||||
description: string;
|
||||
downloads: number;
|
||||
downloadSlug: string;
|
||||
featured: number;
|
||||
readme: string;
|
||||
id: number;
|
||||
internal: boolean;
|
||||
versionSignatureType: string;
|
||||
packages: {
|
||||
[arch: string]: {
|
||||
packageName: string;
|
||||
downloadUrl: string;
|
||||
};
|
||||
};
|
||||
links: Array<{
|
||||
rel: string;
|
||||
href: string;
|
||||
}>;
|
||||
json?: {
|
||||
dependencies: {
|
||||
grafanaDependency: string;
|
||||
@ -86,50 +67,89 @@ export interface Plugin {
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}
|
||||
links: Array<{ rel: string; href: string }>;
|
||||
name: string;
|
||||
orgId: number;
|
||||
orgName: string;
|
||||
orgSlug: string;
|
||||
orgUrl: string;
|
||||
packages: {
|
||||
[arch: string]: {
|
||||
packageName: string;
|
||||
downloadUrl: string;
|
||||
};
|
||||
};
|
||||
popularity: number;
|
||||
readme?: string;
|
||||
signatureType: PluginSignatureType | '';
|
||||
slug: string;
|
||||
status: string;
|
||||
typeCode: PluginType;
|
||||
typeId: number;
|
||||
typeName: string;
|
||||
updatedAt: string;
|
||||
url: string;
|
||||
userId: number;
|
||||
verified: boolean;
|
||||
version: string;
|
||||
versionSignatureType: PluginSignatureType | '';
|
||||
versionSignedByOrg: string;
|
||||
versionSignedByOrgName: string;
|
||||
versionStatus: string;
|
||||
};
|
||||
|
||||
export type LocalPlugin = {
|
||||
category: string;
|
||||
defaultNavUrl: string;
|
||||
dev?: boolean;
|
||||
enabled: boolean;
|
||||
hasUpdate: boolean;
|
||||
id: string;
|
||||
info: {
|
||||
author: {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
build: {};
|
||||
author: Rel;
|
||||
description: string;
|
||||
links: Array<{
|
||||
name: string;
|
||||
url: string;
|
||||
}>;
|
||||
links?: Rel[];
|
||||
logos: {
|
||||
large: string;
|
||||
small: string;
|
||||
large: string;
|
||||
};
|
||||
updated: string;
|
||||
build: Build;
|
||||
screenshots?: Array<{
|
||||
path: string;
|
||||
name: string;
|
||||
}> | null;
|
||||
version: string;
|
||||
updated: string;
|
||||
};
|
||||
latestVersion: string;
|
||||
name: string;
|
||||
pinned: boolean;
|
||||
signature: string;
|
||||
signature: PluginSignatureStatus;
|
||||
signatureOrg: string;
|
||||
signatureType: string;
|
||||
state: string;
|
||||
type: string;
|
||||
dev: boolean | undefined;
|
||||
type: PluginType;
|
||||
};
|
||||
|
||||
interface Rel {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface Build {
|
||||
time?: number;
|
||||
repo?: string;
|
||||
branch?: string;
|
||||
hash?: string;
|
||||
}
|
||||
|
||||
export interface Version {
|
||||
version: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface PluginDetails {
|
||||
remote?: Plugin;
|
||||
remote?: RemotePlugin;
|
||||
remoteVersions?: Version[];
|
||||
local?: LocalPlugin;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user