Plugins: add level and signature badges to plugin details page (#33553)

* feat(grafana-ui): badge can accept react node for text, add shield-exclamation to icons

* feat(plugins): add PluginSignatureType type

* feat(pluginpage): introduce PluginSignatureDetailsBadge. Fix sidebar icon margin

* feat(pluginlistpage): update filterinput placeholder, introduce filter by plugin type
This commit is contained in:
Jack Westbrook 2021-04-30 11:00:41 +02:00 committed by GitHub
parent ec3d8b590a
commit 8f62e42554
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 113 additions and 12 deletions

View File

@ -27,6 +27,14 @@ export enum PluginSignatureStatus {
missing = 'missing', // missing signature file
}
/** Describes level of {@link https://grafana.com/docs/grafana/latest/plugins/plugin-signatures/#plugin-signature-levels/ | plugin signature level} */
export enum PluginSignatureType {
grafana = 'grafana',
commercial = 'commercial',
community = 'community',
private = 'private',
}
/** Describes error code returned from Grafana plugins API call */
export enum PluginErrorCode {
missingSignature = 'signatureMissing',
@ -65,6 +73,8 @@ export interface PluginMeta<T extends KeyValue = {}> {
latestVersion?: string;
pinned?: boolean;
signature?: PluginSignatureStatus;
signatureType?: PluginSignatureType;
signatureOrg?: string;
live?: boolean;
}

View File

@ -12,7 +12,7 @@ import { HorizontalGroup } from '../Layout/Layout';
export type BadgeColor = 'blue' | 'red' | 'green' | 'orange' | 'purple';
export interface BadgeProps extends HTMLAttributes<HTMLDivElement> {
text: string;
text: React.ReactNode;
color: BadgeColor;
icon?: IconName;
tooltip?: string;

View File

@ -120,6 +120,7 @@ export type IconName =
| 'search'
| 'share-alt'
| 'shield'
| 'shield-exclamation'
| 'sign-in-alt'
| 'signal'
| 'signin'
@ -255,6 +256,7 @@ export const getAvailableIcons = (): IconName[] => [
'search',
'share-alt',
'shield',
'shield-exclamation',
'sign-in-alt',
'signal',
'signin',

View File

@ -53,6 +53,7 @@ export const PluginListPage: React.FC<Props> = ({
searchQuery={searchQuery}
setSearchQuery={(query) => setPluginsSearchQuery(query)}
linkButton={linkButton}
placeholder="Search by name, author, description or type"
target="_blank"
/>

View File

@ -1,10 +1,11 @@
// Libraries
import React, { PureComponent } from 'react';
import { find } from 'lodash';
import { capitalize, find } from 'lodash';
// Types
import {
AppPlugin,
GrafanaPlugin,
GrafanaThemeV2,
NavModel,
NavModelItem,
PluginDependencies,
@ -13,11 +14,12 @@ import {
PluginMeta,
PluginMetaInfo,
PluginSignatureStatus,
PluginSignatureType,
PluginType,
UrlQueryMap,
} from '@grafana/data';
import { AppNotificationSeverity } from 'app/types';
import { Alert, LinkButton, PluginSignatureBadge, Tooltip } from '@grafana/ui';
import { Alert, LinkButton, PluginSignatureBadge, Tooltip, Badge, useStyles2, Icon } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { getPluginSettings } from './PluginSettingsCache';
@ -275,6 +277,8 @@ class PluginPage extends PureComponent<Props, State> {
return null;
}
const isSignatureValid = plugin.meta.signature === PluginSignatureStatus.valid;
if (plugin.meta.signature === PluginSignatureStatus.internal) {
return null;
}
@ -282,21 +286,32 @@ class PluginPage extends PureComponent<Props, State> {
return (
<Alert
aria-label={selectors.pages.PluginPage.signatureInfo}
severity={plugin.meta.signature !== PluginSignatureStatus.valid ? 'warning' : 'info'}
severity={isSignatureValid ? 'info' : 'warning'}
title="Plugin signature"
>
<PluginSignatureBadge
status={plugin.meta.signature}
<div
className={css`
margin-top: 0;
display: flex;
`}
/>
<br />
>
<PluginSignatureBadge
status={plugin.meta.signature}
className={css`
margin-top: 0;
`}
/>
{isSignatureValid && (
<PluginSignatureDetailsBadge
signatureType={plugin.meta.signatureType}
signatureOrg={plugin.meta.signatureOrg}
/>
)}
</div>
<br />
<p>
Grafana Labs checks each plugin to verify that it has a valid digital signature. Plugin signature verification
is part of our security measures to ensure plugins are safe and trustworthy.
{plugin.meta.signature !== PluginSignatureStatus.valid &&
{!isSignatureValid &&
'Grafana Labs cant guarantee the integrity of this unsigned plugin. Ask the plugin author to request it to be signed.'}
</p>
<a
@ -504,4 +519,71 @@ export function loadPlugin(pluginId: string): Promise<GrafanaPlugin> {
});
}
type PluginSignatureDetailsBadgeProps = {
signatureType?: PluginSignatureType;
signatureOrg?: string;
};
const PluginSignatureDetailsBadge: React.FC<PluginSignatureDetailsBadgeProps> = ({ signatureType, signatureOrg }) => {
const styles = useStyles2(getDetailsBadgeStyles);
if (!signatureType && !signatureOrg) {
return null;
}
const signatureTypeIcon =
signatureType === PluginSignatureType.grafana
? 'grafana'
: signatureType === PluginSignatureType.commercial || signatureType === PluginSignatureType.community
? 'shield'
: 'shield-exclamation';
const signatureTypeText = signatureType === PluginSignatureType.grafana ? 'Grafana Labs' : capitalize(signatureType);
return (
<>
{signatureType && (
<Badge
color="green"
className={styles.badge}
text={
<>
<strong className={styles.strong}>Level:&nbsp;</strong>
<Icon size="xs" name={signatureTypeIcon} />
&nbsp;
{signatureTypeText}
</>
}
/>
)}
{signatureOrg && (
<Badge
color="green"
className={styles.badge}
text={
<>
<strong className={styles.strong}>Signed by:</strong> {signatureOrg}
</>
}
/>
)}
</>
);
};
const getDetailsBadgeStyles = (theme: GrafanaThemeV2) => ({
badge: css`
background-color: ${theme.colors.background.canvas};
border-color: ${theme.colors.border.strong};
color: ${theme.colors.text.secondary};
margin-left: ${theme.spacing()};
`,
strong: css`
color: ${theme.colors.text.primary};
`,
icon: css`
margin-right: ${theme.spacing(0.5)};
`,
});
export default PluginPage;

View File

@ -4,7 +4,12 @@ export const getPlugins = (state: PluginsState) => {
const regex = new RegExp(state.searchQuery, 'i');
return state.plugins.filter((item) => {
return regex.test(item.name) || regex.test(item.info.author.name) || regex.test(item.info.description);
return (
regex.test(item.name) ||
regex.test(item.info.author.name) ||
regex.test(item.type) ||
regex.test(item.info.description)
);
});
};
export const getAllPluginsErrors = (state: PluginsState) => {

View File

@ -48,7 +48,8 @@
text-overflow: ellipsis;
overflow: hidden;
img {
img,
i {
width: 16px;
margin-right: 4px;
margin-bottom: 1px;