mirror of
https://github.com/grafana/grafana.git
synced 2025-02-09 23:16:16 -06:00
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:
parent
ec3d8b590a
commit
8f62e42554
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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"
|
||||
/>
|
||||
|
||||
|
@ -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 can’t 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: </strong>
|
||||
<Icon size="xs" name={signatureTypeIcon} />
|
||||
|
||||
{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;
|
||||
|
@ -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) => {
|
||||
|
@ -48,7 +48,8 @@
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
img,
|
||||
i {
|
||||
width: 16px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 1px;
|
||||
|
Loading…
Reference in New Issue
Block a user