From 8f62e4255464b267c190e90dc9df35f5b197fa9c Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Fri, 30 Apr 2021 11:00:41 +0200 Subject: [PATCH] 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 --- packages/grafana-data/src/types/plugin.ts | 10 ++ .../grafana-ui/src/components/Badge/Badge.tsx | 2 +- packages/grafana-ui/src/types/icon.ts | 2 + .../app/features/plugins/PluginListPage.tsx | 1 + public/app/features/plugins/PluginPage.tsx | 100 ++++++++++++++++-- .../app/features/plugins/state/selectors.ts | 7 +- public/sass/pages/_plugins.scss | 3 +- 7 files changed, 113 insertions(+), 12 deletions(-) diff --git a/packages/grafana-data/src/types/plugin.ts b/packages/grafana-data/src/types/plugin.ts index d7cbad6819b..9d9c211ed71 100644 --- a/packages/grafana-data/src/types/plugin.ts +++ b/packages/grafana-data/src/types/plugin.ts @@ -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 { latestVersion?: string; pinned?: boolean; signature?: PluginSignatureStatus; + signatureType?: PluginSignatureType; + signatureOrg?: string; live?: boolean; } diff --git a/packages/grafana-ui/src/components/Badge/Badge.tsx b/packages/grafana-ui/src/components/Badge/Badge.tsx index 1dcd5f6c0dd..053f063e51d 100644 --- a/packages/grafana-ui/src/components/Badge/Badge.tsx +++ b/packages/grafana-ui/src/components/Badge/Badge.tsx @@ -12,7 +12,7 @@ import { HorizontalGroup } from '../Layout/Layout'; export type BadgeColor = 'blue' | 'red' | 'green' | 'orange' | 'purple'; export interface BadgeProps extends HTMLAttributes { - text: string; + text: React.ReactNode; color: BadgeColor; icon?: IconName; tooltip?: string; diff --git a/packages/grafana-ui/src/types/icon.ts b/packages/grafana-ui/src/types/icon.ts index eecc155d255..f7122c59f15 100644 --- a/packages/grafana-ui/src/types/icon.ts +++ b/packages/grafana-ui/src/types/icon.ts @@ -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', diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index b5cd7d8dd63..b0e6ab2b3c4 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -53,6 +53,7 @@ export const PluginListPage: React.FC = ({ searchQuery={searchQuery} setSearchQuery={(query) => setPluginsSearchQuery(query)} linkButton={linkButton} + placeholder="Search by name, author, description or type" target="_blank" /> diff --git a/public/app/features/plugins/PluginPage.tsx b/public/app/features/plugins/PluginPage.tsx index b3d96b0fe55..52e7ed7f402 100644 --- a/public/app/features/plugins/PluginPage.tsx +++ b/public/app/features/plugins/PluginPage.tsx @@ -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 { 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 { return ( - -
+ > + + {isSignatureValid && ( + + )} +

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 can’t guarantee the integrity of this unsigned plugin. Ask the plugin author to request it to be signed.'}

{ }); } +type PluginSignatureDetailsBadgeProps = { + signatureType?: PluginSignatureType; + signatureOrg?: string; +}; + +const PluginSignatureDetailsBadge: React.FC = ({ 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 && ( + + Level:  + +   + {signatureTypeText} + + } + /> + )} + {signatureOrg && ( + + Signed by: {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; diff --git a/public/app/features/plugins/state/selectors.ts b/public/app/features/plugins/state/selectors.ts index 12714fdc6bd..774844ba723 100644 --- a/public/app/features/plugins/state/selectors.ts +++ b/public/app/features/plugins/state/selectors.ts @@ -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) => { diff --git a/public/sass/pages/_plugins.scss b/public/sass/pages/_plugins.scss index eddc6b3eabc..1be0a7b4275 100644 --- a/public/sass/pages/_plugins.scss +++ b/public/sass/pages/_plugins.scss @@ -48,7 +48,8 @@ text-overflow: ellipsis; overflow: hidden; - img { + img, + i { width: 16px; margin-right: 4px; margin-bottom: 1px;