Commit 3d89f045 by Ryan McKinley Committed by GitHub

Plugins: show signing status on datasources and plugins (#23542)

* show signing status

* show signing status

* Progress on signed badge style

* Progress on signing status look and updated card background

* Updates

* Transforms card tweak

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
parent 68ba60ad
......@@ -14,6 +14,14 @@ export enum PluginType {
renderer = 'renderer',
}
export enum PluginSignatureStatus {
internal = 'internal', // core plugin, no signature
valid = 'valid', // signed and accurate MANIFEST
invalid = 'invalid', // invalid signature
modified = 'modified', // valid signature, but content mismatch
unsigned = 'unsigned', // no MANIFEST file
}
export interface PluginMeta<T extends KeyValue = {}> {
id: string;
name: string;
......@@ -38,6 +46,7 @@ export interface PluginMeta<T extends KeyValue = {}> {
enterprise?: boolean;
latestVersion?: string;
pinned?: boolean;
signature?: PluginSignatureStatus;
}
interface PluginDependencyInfo {
......
......@@ -2,6 +2,7 @@
import { GrafanaTheme } from '@grafana/data';
import { renderGeneratedFileBanner } from '../utils/generatedFileBanner';
import { styleMixins } from '.';
export const darkThemeVarsTemplate = (theme: GrafanaTheme) =>
`${renderGeneratedFileBanner('grafana-ui/src/themes/dark.ts', 'grafana-ui/src/themes/_variables.dark.scss.tmpl.ts')}
......@@ -140,9 +141,9 @@ $code-tag-bg: $dark-1;
$code-tag-border: $dark-9;
// cards
$card-background: linear-gradient(135deg, $dark-4, $dark-3);
$card-background-hover: linear-gradient(135deg, $dark-5, $dark-6);
$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, 0.1), 1px 1px 0 0 rgba(0, 0, 0, 0.3);
$card-background: ${theme.colors.bg2};
$card-background-hover: ${styleMixins.hoverColor(theme.colors.bg2, theme)};
$card-shadow: none;
// Lists
$list-item-bg: $card-background;
......
......@@ -2,6 +2,7 @@
import { GrafanaTheme } from '@grafana/data';
import { renderGeneratedFileBanner } from '../utils/generatedFileBanner';
import { styleMixins } from '.';
export const lightThemeVarsTemplate = (theme: GrafanaTheme) =>
`${renderGeneratedFileBanner('grafana-ui/src/themes/light.ts', 'grafana-ui/src/themes/_variable.light.scss.tmpl.ts')}
......@@ -133,9 +134,9 @@ $code-tag-bg: $gray-6;
$code-tag-border: $gray-4;
// cards
$card-background: linear-gradient(135deg, $gray-6, $gray-7);
$card-background-hover: linear-gradient(135deg, $gray-6, $gray-5);
$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, 0.1), 1px 1px 0 0 rgba(0, 0, 0, 0.1);
$card-background: ${theme.colors.bg2};
$card-background-hover: ${styleMixins.hoverColor(theme.colors.bg2, theme)};
$card-shadow: none;
// Lists
$list-item-bg: $gray-7;
......
......@@ -129,7 +129,7 @@ const darkTheme: GrafanaTheme = {
linkExternal: basicColors.blue85,
},
shadows: {
listItem: '-1px -1px 0 0 hsla(0, 0%, 100%, 0.1), 1px 1px 0 0 rgba(0, 0, 0, 0.3)',
listItem: 'none',
},
};
......
......@@ -130,7 +130,7 @@ const lightTheme: GrafanaTheme = {
textHeading: basicColors.gray25,
},
shadows: {
listItem: '-1px -1px 0 0 hsla(0, 0%, 100%, 0.1), 1px 1px 0 0 rgba(0, 0, 0, 0.1)',
listItem: 'none',
},
};
......
import React, { PureComponent } from 'react';
import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector';
import { FilterInput } from '../FilterInput/FilterInput';
import { LinkButton } from '@grafana/ui';
export interface Props {
searchQuery: string;
layoutMode?: LayoutMode;
onSetLayoutMode?: (mode: LayoutMode) => {};
setSearchQuery: (value: string) => {};
linkButton: { href: string; title: string };
target?: string;
......@@ -14,7 +11,7 @@ export interface Props {
export default class OrgActionBar extends PureComponent<Props> {
render() {
const { searchQuery, layoutMode, onSetLayoutMode, linkButton, setSearchQuery, target } = this.props;
const { searchQuery, linkButton, setSearchQuery, target } = this.props;
const linkProps = { href: linkButton.href };
if (target) {
......@@ -31,7 +28,6 @@ export default class OrgActionBar extends PureComponent<Props> {
onChange={setSearchQuery}
placeholder={'Search by name or type'}
/>
<LayoutSelector mode={layoutMode} onLayoutModeChanged={(mode: LayoutMode) => onSetLayoutMode(mode)} />
</div>
<div className="page-action-bar__spacer" />
<LinkButton {...linkProps}>{linkButton.title}</LinkButton>
......
......@@ -14,9 +14,6 @@ exports[`Render should render component 1`] = `
placeholder="Search by name or type"
value=""
/>
<LayoutSelector
onLayoutModeChanged={[Function]}
/>
</div>
<div
className="page-action-bar__spacer"
......
import React from 'react';
import {
Container,
CustomScrollbar,
InfoBox,
ValuePicker,
Button,
useTheme,
VerticalGroup,
stylesFactory,
} from '@grafana/ui';
import { Container, CustomScrollbar, ValuePicker, Button, useTheme, VerticalGroup, stylesFactory } from '@grafana/ui';
import {
DataFrame,
DataTransformerConfig,
......@@ -120,33 +111,39 @@ export class TransformationsEditor extends React.PureComponent<Props> {
);
};
renderNoAddedTransformsState() {
return (
<>
<p className="muted">
Transformations allow you to combine, re-order, hide and rename specific parts the the data set before being
visualized. <br />
Choose one of the transformations below to start with:
</p>
<VerticalGroup>
{standardTransformersRegistry.list().map(t => {
return (
<TransformationCard
title={t.name}
description={t.description}
actions={<Button>Select</Button>}
onClick={() => {
this.onTransformationAdd({ value: t.id });
}}
/>
);
})}
</VerticalGroup>
</>
);
}
render() {
const hasTransformationsConfigured = this.props.transformations.length > 0;
return (
<CustomScrollbar autoHeightMin="100%">
<Container padding="md">
{!hasTransformationsConfigured && (
<InfoBox>
<p>
Transformations allow you to combine, re-order, hide and rename specific parts the the data set before
being visualized. Choose one of the transformations below to start with:
</p>
<VerticalGroup>
{standardTransformersRegistry.list().map(t => {
return (
<TransformationCard
title={t.name}
description={t.description}
actions={<Button>Select</Button>}
onClick={() => {
this.onTransformationAdd({ value: t.id });
}}
/>
);
})}
</VerticalGroup>
</InfoBox>
)}
{!hasTransformationsConfigured && this.renderNoAddedTransformsState()}
{hasTransformationsConfigured && this.renderTransformationEditors()}
{hasTransformationsConfigured && this.renderTransformationSelector()}
</Container>
......@@ -166,8 +163,13 @@ const getTransformationCardStyles = stylesFactory((theme: GrafanaTheme) => {
card: css`
background: ${theme.colors.bg2};
width: 100%;
border: none;
padding: ${theme.spacing.sm};
&:hover {
background: ${theme.colors.bg3};
box-shadow: none;
border: none;
}
`,
};
......
......@@ -64,7 +64,6 @@ export class DataSourcesListPage extends PureComponent<Props> {
layoutMode,
searchQuery,
setDataSourcesSearchQuery,
setDataSourcesLayoutMode,
hasFetched,
} = this.props;
......@@ -81,9 +80,7 @@ export class DataSourcesListPage extends PureComponent<Props> {
{hasFetched &&
dataSourcesCount > 0 && [
<OrgActionBar
layoutMode={layoutMode}
searchQuery={searchQuery}
onSetLayoutMode={mode => setDataSourcesLayoutMode(mode)}
setSearchQuery={query => setDataSourcesSearchQuery(query)}
linkButton={linkButton}
key="action-bar"
......
......@@ -11,6 +11,7 @@ import { addDataSource, loadDataSourcePlugins } from './state/actions';
import { getDataSourcePlugins } from './state/selectors';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
import { setDataSourceTypeSearchQuery } from './state/reducers';
import { PluginSignatureBadge } from '../plugins/PluginSignatureBadge';
import { Card } from 'app/core/components/Card/Card';
export interface Props {
......@@ -146,7 +147,34 @@ const DataSourceTypeCard: FC<DataSourceTypeCardProps> = props => {
}
className={isPhantom && 'add-data-source-item--phantom'}
onClick={onClick}
/>
aria-label={e2e.pages.AddDataSource.selectors.dataSourcePlugins(plugin.name)}
>
<img className="add-data-source-item-logo" src={plugin.info.logos.small} />
<div className="add-data-source-item-text-wrapper">
<span className="add-data-source-item-text">{plugin.name}</span>
{plugin.info.description && <span className="add-data-source-item-desc">{plugin.info.description}</span>}
{!isPhantom && (
<div>
<PluginSignatureBadge status={plugin.signature} />
</div>
)}
</div>
<div className="add-data-source-item-actions">
{learnMoreLink && (
<LinkButton
variant="secondary"
href={`${learnMoreLink.url}?utm_source=grafana_add_ds`}
target="_blank"
rel="noopener"
onClick={onLearnMoreClick}
icon="external-link-alt"
>
{learnMoreLink.name}
</LinkButton>
)}
{!isPhantom && <Button>Select</Button>}
</div>
</Card>
);
};
......
......@@ -18,14 +18,12 @@ exports[`Render should render action bar and datasources 1`] = `
>
<OrgActionBar
key="action-bar"
layoutMode="grid"
linkButton={
Object {
"href": "datasources/new",
"title": "Add data source",
}
}
onSetLayoutMode={[Function]}
searchQuery=""
setSearchQuery={[Function]}
/>
......
import React, { FC } from 'react';
import classNames from 'classnames';
import PluginListItem from './PluginListItem';
import { PluginMeta } from '@grafana/data';
import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
interface Props {
plugins: PluginMeta[];
layoutMode: LayoutMode;
}
const PluginList: FC<Props> = props => {
const { plugins, layoutMode } = props;
const listStyle = classNames({
'card-section': true,
'card-list-layout-grid': layoutMode === LayoutModes.Grid,
'card-list-layout-list': layoutMode === LayoutModes.List,
});
const { plugins } = props;
return (
<section className={listStyle}>
<section className="card-section card-list-layout-list">
<ol className="card-list">
{plugins.map((plugin, index) => {
return <PluginListItem plugin={plugin} key={`${plugin.name}-${index}`} />;
......
import React, { FC } from 'react';
import { PluginMeta } from '@grafana/data';
import { PluginSignatureBadge } from './PluginSignatureBadge';
interface Props {
plugin: PluginMeta;
......@@ -13,6 +14,7 @@ const PluginListItem: FC<Props> = props => {
<a className="card-item" href={`plugins/${plugin.id}/`}>
<div className="card-item-header">
<div className="card-item-type">{plugin.type}</div>
<PluginSignatureBadge status={plugin.signature} />
{plugin.hasUpdate && (
<div className="card-item-notice">
<span bs-tooltip="plugin.latestVersion">Update available!</span>
......
import React from 'react';
import { shallow } from 'enzyme';
import { PluginListPage, Props } from './PluginListPage';
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
import { NavModel, PluginMeta } from '@grafana/data';
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
import { setPluginsLayoutMode, setPluginsSearchQuery } from './state/reducers';
import { setPluginsSearchQuery } from './state/reducers';
const setup = (propOverrides?: object) => {
const props: Props = {
......@@ -19,8 +18,6 @@ const setup = (propOverrides?: object) => {
plugins: [] as PluginMeta[],
searchQuery: '',
setPluginsSearchQuery: mockToolkitActionCreator(setPluginsSearchQuery),
setPluginsLayoutMode: mockToolkitActionCreator(setPluginsLayoutMode),
layoutMode: LayoutModes.Grid,
loadPlugins: jest.fn(),
hasFetched: false,
};
......
......@@ -6,20 +6,17 @@ import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
import PluginList from './PluginList';
import { loadPlugins } from './state/actions';
import { getNavModel } from 'app/core/selectors/navModel';
import { getLayoutMode, getPlugins, getPluginsSearchQuery } from './state/selectors';
import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
import { getPlugins, getPluginsSearchQuery } from './state/selectors';
import { NavModel, PluginMeta } from '@grafana/data';
import { StoreState } from 'app/types';
import { setPluginsLayoutMode, setPluginsSearchQuery } from './state/reducers';
import { setPluginsSearchQuery } from './state/reducers';
export interface Props {
navModel: NavModel;
plugins: PluginMeta[];
layoutMode: LayoutMode;
searchQuery: string;
hasFetched: boolean;
loadPlugins: typeof loadPlugins;
setPluginsLayoutMode: typeof setPluginsLayoutMode;
setPluginsSearchQuery: typeof setPluginsSearchQuery;
}
......@@ -33,15 +30,7 @@ export class PluginListPage extends PureComponent<Props> {
}
render() {
const {
hasFetched,
navModel,
plugins,
layoutMode,
setPluginsLayoutMode,
setPluginsSearchQuery,
searchQuery,
} = this.props;
const { hasFetched, navModel, plugins, setPluginsSearchQuery, searchQuery } = this.props;
const linkButton = {
href: 'https://grafana.com/plugins?utm_source=grafana_plugin_list',
......@@ -54,12 +43,10 @@ export class PluginListPage extends PureComponent<Props> {
<>
<OrgActionBar
searchQuery={searchQuery}
layoutMode={layoutMode}
onSetLayoutMode={mode => setPluginsLayoutMode(mode)}
setSearchQuery={query => setPluginsSearchQuery(query)}
linkButton={linkButton}
/>
{hasFetched && plugins && plugins && <PluginList plugins={plugins} layoutMode={layoutMode} />}
{hasFetched && plugins && plugins && <PluginList plugins={plugins} />}
</>
</Page.Contents>
</Page>
......@@ -71,7 +58,6 @@ function mapStateToProps(state: StoreState) {
return {
navModel: getNavModel(state.navIndex, 'plugins'),
plugins: getPlugins(state.plugins),
layoutMode: getLayoutMode(state.plugins),
searchQuery: getPluginsSearchQuery(state.plugins),
hasFetched: state.plugins.hasFetched,
};
......@@ -79,7 +65,6 @@ function mapStateToProps(state: StoreState) {
const mapDispatchToProps = {
loadPlugins,
setPluginsLayoutMode,
setPluginsSearchQuery,
};
......
import React from 'react';
import { Icon, stylesFactory, useTheme, IconName, Tooltip } from '@grafana/ui';
import { GrafanaTheme, PluginSignatureStatus, getColorFromHexRgbOrName } from '@grafana/data';
import { css } from 'emotion';
import tinycolor from 'tinycolor2';
interface Props {
status: PluginSignatureStatus;
}
export const PluginSignatureBadge: React.FC<Props> = ({ status }) => {
const theme = useTheme();
const display = getSignatureDisplayModel(status);
const styles = getStyles(theme, display);
return (
<Tooltip content={display.tooltip} placement="left">
<div className={styles.wrapper}>
<Icon name={display.icon} size="sm" />
<span>{display.text}</span>
</div>
</Tooltip>
);
};
interface DisplayModel {
text: string;
icon: IconName;
color: string;
tooltip: string;
}
function getSignatureDisplayModel(signature: PluginSignatureStatus): DisplayModel {
switch (signature) {
case PluginSignatureStatus.internal:
return { text: 'Core', icon: 'cube', color: 'blue', tooltip: 'Core plugin that is bundled with Grafana' };
case PluginSignatureStatus.valid:
return { text: 'Signed', icon: 'lock', color: 'green', tooltip: 'Signed and verified plugin' };
case PluginSignatureStatus.invalid:
return {
text: 'Invalid',
icon: 'exclamation-triangle',
color: 'red',
tooltip: 'Invalid plugin signature',
};
case PluginSignatureStatus.modified:
return {
text: 'Modified',
icon: 'exclamation-triangle',
color: 'red',
tooltip: 'Valid signature but content has been modified',
};
}
return { text: 'Unsigned', icon: 'exclamation-triangle', color: 'red', tooltip: 'Unsigned external plugin' };
}
const getStyles = stylesFactory((theme: GrafanaTheme, model: DisplayModel) => {
let sourceColor = getColorFromHexRgbOrName(model.color);
let borderColor = '';
let bgColor = '';
let textColor = '';
if (theme.isDark) {
bgColor = tinycolor(sourceColor)
.darken(38)
.toString();
borderColor = tinycolor(sourceColor)
.darken(25)
.toString();
textColor = tinycolor(sourceColor)
.lighten(45)
.toString();
} else {
bgColor = tinycolor(sourceColor)
.lighten(30)
.toString();
borderColor = tinycolor(sourceColor)
.lighten(15)
.toString();
textColor = tinycolor(sourceColor)
.darken(40)
.toString();
}
return {
wrapper: css`
font-size: ${theme.typography.size.sm};
display: inline-flex;
padding: 1px 4px;
border-radius: 3px;
margin-top: 6px;
background: ${bgColor};
border: 1px solid ${borderColor};
color: ${textColor};
> span {
position: relative;
top: 1px;
margin-left: 2px;
}
`,
};
});
PluginSignatureBadge.displayName = 'PluginSignatureBadge';
......@@ -2,7 +2,7 @@
exports[`Render should render component 1`] = `
<section
className="card-section card-list-layout-grid"
className="card-section card-list-layout-list"
>
<ol
className="card-list"
......
......@@ -16,6 +16,7 @@ exports[`Render should render component 1`] = `
>
panel
</div>
<PluginSignatureBadge />
</div>
<div
className="card-item-body"
......@@ -62,6 +63,7 @@ exports[`Render should render has plugin section 1`] = `
>
panel
</div>
<PluginSignatureBadge />
<div
className="card-item-notice"
>
......
......@@ -17,14 +17,12 @@ exports[`Render should render component 1`] = `
isLoading={true}
>
<OrgActionBar
layoutMode="grid"
linkButton={
Object {
"href": "https://grafana.com/plugins?utm_source=grafana_plugin_list",
"title": "Find more plugins on Grafana.com",
}
}
onSetLayoutMode={[Function]}
searchQuery=""
setSearchQuery={[Function]}
/>
......@@ -49,19 +47,16 @@ exports[`Render should render list 1`] = `
isLoading={false}
>
<OrgActionBar
layoutMode="grid"
linkButton={
Object {
"href": "https://grafana.com/plugins?utm_source=grafana_plugin_list",
"title": "Find more plugins on Grafana.com",
}
}
onSetLayoutMode={[Function]}
searchQuery=""
setSearchQuery={[Function]}
/>
<PluginList
layoutMode="grid"
plugins={Array []}
/>
</PageContents>
......
......@@ -6,11 +6,9 @@ import {
pluginDashboardsLoaded,
pluginsLoaded,
pluginsReducer,
setPluginsLayoutMode,
setPluginsSearchQuery,
} from './reducers';
import { PluginMetaInfo, PluginType } from '@grafana/data';
import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
describe('pluginsReducer', () => {
describe('when pluginsLoaded is dispatched', () => {
......@@ -58,18 +56,6 @@ describe('pluginsReducer', () => {
});
});
describe('when setPluginsLayoutMode is dispatched', () => {
it('then state should be correct', () => {
reducerTester<PluginsState>()
.givenReducer(pluginsReducer, { ...initialState })
.whenActionIsDispatched(setPluginsLayoutMode(LayoutModes.List))
.thenStateShouldEqual({
...initialState,
layoutMode: LayoutModes.List,
});
});
});
describe('when pluginDashboardsLoad is dispatched', () => {
it('then state should be correct', () => {
reducerTester<PluginsState>()
......
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { PluginMeta, PanelPlugin } from '@grafana/data';
import { PluginsState } from 'app/types';
import { LayoutMode, LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
import { PluginDashboard } from '../../../types/plugins';
export const initialState: PluginsState = {
plugins: [],
searchQuery: '',
layoutMode: LayoutModes.Grid,
hasFetched: false,
dashboards: [],
isLoadingPluginDashboards: false,
......@@ -25,9 +23,6 @@ const pluginsSlice = createSlice({
setPluginsSearchQuery: (state, action: PayloadAction<string>) => {
state.searchQuery = action.payload;
},
setPluginsLayoutMode: (state, action: PayloadAction<LayoutMode>) => {
state.layoutMode = action.payload;
},
pluginDashboardsLoad: (state, action: PayloadAction<undefined>) => {
state.isLoadingPluginDashboards = true;
state.dashboards = [];
......@@ -46,7 +41,6 @@ export const {
pluginsLoaded,
pluginDashboardsLoad,
pluginDashboardsLoaded,
setPluginsLayoutMode,
setPluginsSearchQuery,
panelPluginLoaded,
} = pluginsSlice.actions;
......
......@@ -9,4 +9,3 @@ export const getPlugins = (state: PluginsState) => {
};
export const getPluginsSearchQuery = (state: PluginsState) => state.searchQuery;
export const getLayoutMode = (state: PluginsState) => state.layoutMode;
......@@ -24,7 +24,6 @@ export interface PanelPluginsIndex {
export interface PluginsState {
plugins: PluginMeta[];
searchQuery: string;
layoutMode: string;
hasFetched: boolean;
dashboards: PluginDashboard[];
isLoadingPluginDashboards: boolean;
......
......@@ -143,9 +143,9 @@ $code-tag-bg: $dark-1;
$code-tag-border: $dark-9;
// cards
$card-background: linear-gradient(135deg, $dark-4, $dark-3);
$card-background-hover: linear-gradient(135deg, $dark-5, $dark-6);
$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, 0.1), 1px 1px 0 0 rgba(0, 0, 0, 0.3);
$card-background: #202226;
$card-background-hover: #25272b;
$card-shadow: none;
// Lists
$list-item-bg: $card-background;
......
......@@ -136,9 +136,9 @@ $code-tag-bg: $gray-6;
$code-tag-border: $gray-4;
// cards
$card-background: linear-gradient(135deg, $gray-6, $gray-7);
$card-background-hover: linear-gradient(135deg, $gray-6, $gray-5);
$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, 0.1), 1px 1px 0 0 rgba(0, 0, 0, 0.1);
$card-background: #f1f5f9;
$card-background-hover: #eaf0f6;
$card-shadow: none;
// Lists
$list-item-bg: $gray-7;
......
......@@ -24,7 +24,6 @@
display: flex;
align-items: center;
cursor: pointer;
box-shadow: $card-shadow;
background: $panel-editor-viz-item-bg;
border: 1px solid transparent;
border-radius: 3px;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment