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
pull/32938/head^2
Jack Westbrook 4 years ago committed by GitHub
parent ec3d8b590a
commit 8f62e42554
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      packages/grafana-data/src/types/plugin.ts
  2. 2
      packages/grafana-ui/src/components/Badge/Badge.tsx
  3. 2
      packages/grafana-ui/src/types/icon.ts
  4. 1
      public/app/features/plugins/PluginListPage.tsx
  5. 92
      public/app/features/plugins/PluginPage.tsx
  6. 7
      public/app/features/plugins/state/selectors.ts
  7. 3
      public/sass/pages/_plugins.scss

@ -27,6 +27,14 @@ export enum PluginSignatureStatus {
missing = 'missing', // missing signature file 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 */ /** Describes error code returned from Grafana plugins API call */
export enum PluginErrorCode { export enum PluginErrorCode {
missingSignature = 'signatureMissing', missingSignature = 'signatureMissing',
@ -65,6 +73,8 @@ export interface PluginMeta<T extends KeyValue = {}> {
latestVersion?: string; latestVersion?: string;
pinned?: boolean; pinned?: boolean;
signature?: PluginSignatureStatus; signature?: PluginSignatureStatus;
signatureType?: PluginSignatureType;
signatureOrg?: string;
live?: boolean; live?: boolean;
} }

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

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

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

@ -1,10 +1,11 @@
// Libraries // Libraries
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { find } from 'lodash'; import { capitalize, find } from 'lodash';
// Types // Types
import { import {
AppPlugin, AppPlugin,
GrafanaPlugin, GrafanaPlugin,
GrafanaThemeV2,
NavModel, NavModel,
NavModelItem, NavModelItem,
PluginDependencies, PluginDependencies,
@ -13,11 +14,12 @@ import {
PluginMeta, PluginMeta,
PluginMetaInfo, PluginMetaInfo,
PluginSignatureStatus, PluginSignatureStatus,
PluginSignatureType,
PluginType, PluginType,
UrlQueryMap, UrlQueryMap,
} from '@grafana/data'; } from '@grafana/data';
import { AppNotificationSeverity } from 'app/types'; 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 Page from 'app/core/components/Page/Page';
import { getPluginSettings } from './PluginSettingsCache'; import { getPluginSettings } from './PluginSettingsCache';
@ -275,6 +277,8 @@ class PluginPage extends PureComponent<Props, State> {
return null; return null;
} }
const isSignatureValid = plugin.meta.signature === PluginSignatureStatus.valid;
if (plugin.meta.signature === PluginSignatureStatus.internal) { if (plugin.meta.signature === PluginSignatureStatus.internal) {
return null; return null;
} }
@ -282,8 +286,13 @@ class PluginPage extends PureComponent<Props, State> {
return ( return (
<Alert <Alert
aria-label={selectors.pages.PluginPage.signatureInfo} aria-label={selectors.pages.PluginPage.signatureInfo}
severity={plugin.meta.signature !== PluginSignatureStatus.valid ? 'warning' : 'info'} severity={isSignatureValid ? 'info' : 'warning'}
title="Plugin signature" title="Plugin signature"
>
<div
className={css`
display: flex;
`}
> >
<PluginSignatureBadge <PluginSignatureBadge
status={plugin.meta.signature} status={plugin.meta.signature}
@ -291,12 +300,18 @@ class PluginPage extends PureComponent<Props, State> {
margin-top: 0; margin-top: 0;
`} `}
/> />
<br /> {isSignatureValid && (
<PluginSignatureDetailsBadge
signatureType={plugin.meta.signatureType}
signatureOrg={plugin.meta.signatureOrg}
/>
)}
</div>
<br /> <br />
<p> <p>
Grafana Labs checks each plugin to verify that it has a valid digital signature. Plugin signature verification 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. is part of our security measures to ensure plugins are safe and trustworthy.
{plugin.meta.signature !== PluginSignatureStatus.valid && {!isSignatureValid &&
'Grafana Labs can’t guarantee the integrity of this unsigned plugin. Ask the plugin author to request it to be signed.'} 'Grafana Labs can’t guarantee the integrity of this unsigned plugin. Ask the plugin author to request it to be signed.'}
</p> </p>
<a <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; export default PluginPage;

@ -4,7 +4,12 @@ export const getPlugins = (state: PluginsState) => {
const regex = new RegExp(state.searchQuery, 'i'); const regex = new RegExp(state.searchQuery, 'i');
return state.plugins.filter((item) => { 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) => { export const getAllPluginsErrors = (state: PluginsState) => {

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

Loading…
Cancel
Save