Plugins Catalog: show Grafana and plugin dependencies (#39062)

* fix(@grafana/data): add a missing optional field to the plugin types

* refactor(Plugins/ADmin): use the type from @grafana/data for plugin dependencies

* fix(Datasources/Graphite): add missing `state` to useEffect dependencies

* refactor(Plugins/Admin): remove unnecessary comment

* feat(Plugins/Admin): add plugin and grafana dependencies to the CatalogPluginDetails

* feat(Plugins/ADmin): show Grafana dependency under plugin details

* feat(Plugins/Admin): show grafana and plugin dependencies for a plugin

* test(Plugins/Admin): add a smoke test for plugin dependencies

* refactor(Plugins/Admin): remove unused style from the header
This commit is contained in:
Levente Balogh 2021-09-10 11:32:21 +02:00 committed by GitHub
parent fc73bc1161
commit 9173898fd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 99 additions and 13 deletions

View File

@ -87,6 +87,7 @@ interface PluginDependencyInfo {
} }
export interface PluginDependencies { export interface PluginDependencies {
grafanaDependency?: string;
grafanaVersion: string; grafanaVersion: string;
plugins: PluginDependencyInfo[]; plugins: PluginDependencyInfo[];
} }

View File

@ -16,13 +16,15 @@ export async function getCatalogPlugin(id: string): Promise<CatalogPlugin> {
} }
export async function getPluginDetails(id: string): Promise<CatalogPluginDetails> { export async function getPluginDetails(id: string): Promise<CatalogPluginDetails> {
const localPlugins = await getLocalPlugins(); // /api/plugins/<id>/settings const localPlugins = await getLocalPlugins();
const local = localPlugins.find((p) => p.id === id); const local = localPlugins.find((p) => p.id === id);
const isInstalled = Boolean(local); const isInstalled = Boolean(local);
const [remote, versions] = await Promise.all([getRemotePlugin(id, isInstalled), getPluginVersions(id)]); const [remote, versions] = await Promise.all([getRemotePlugin(id, isInstalled), getPluginVersions(id)]);
const dependencies = remote?.json?.dependencies;
return { return {
grafanaDependency: remote?.json?.dependencies?.grafanaDependency || '', grafanaDependency: dependencies?.grafanaDependency || dependencies?.grafanaVersion || '',
pluginDependencies: dependencies?.plugins || [],
links: remote?.json?.info.links || local?.info.links || [], links: remote?.json?.info.links || local?.info.links || [],
readme: remote?.readme, readme: remote?.readme,
versions, versions,

View File

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { css } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, Icon } from '@grafana/ui'; import { useStyles2, Icon } from '@grafana/ui';
import { InstallControls } from './InstallControls'; import { InstallControls } from './InstallControls';
import { PluginDetailsHeaderSignature } from './PluginDetailsHeaderSignature'; import { PluginDetailsHeaderSignature } from './PluginDetailsHeaderSignature';
import { PluginDetailsHeaderDependencies } from './PluginDetailsHeaderDependencies';
import { PluginLogo } from './PluginLogo'; import { PluginLogo } from './PluginLogo';
import { CatalogPlugin } from '../types'; import { CatalogPlugin } from '../types';
@ -47,7 +48,7 @@ export function PluginDetailsHeader({ plugin, currentUrl, parentUrl }: Props): R
</ol> </ol>
</nav> </nav>
<div className={styles.headerInformation}> <div className={styles.headerInformationRow}>
{/* Org name */} {/* Org name */}
<span>{plugin.orgName}</span> <span>{plugin.orgName}</span>
@ -73,6 +74,11 @@ export function PluginDetailsHeader({ plugin, currentUrl, parentUrl }: Props): R
<PluginDetailsHeaderSignature plugin={plugin} /> <PluginDetailsHeaderSignature plugin={plugin} />
</div> </div>
<PluginDetailsHeaderDependencies
plugin={plugin}
className={cx(styles.headerInformationRow, styles.headerInformationRowSecondary)}
/>
<p>{plugin.description}</p> <p>{plugin.description}</p>
<InstallControls plugin={plugin} /> <InstallControls plugin={plugin} />
@ -106,11 +112,11 @@ export const getStyles = (theme: GrafanaTheme2) => {
} }
} }
`, `,
headerInformation: css` headerInformationRow: css`
display: flex; display: flex;
align-items: center; align-items: center;
margin-top: ${theme.spacing()}; margin-top: ${theme.spacing()};
margin-bottom: ${theme.spacing(3)}; margin-bottom: ${theme.spacing()};
& > * { & > * {
&::after { &::after {
@ -124,6 +130,9 @@ export const getStyles = (theme: GrafanaTheme2) => {
} }
font-size: ${theme.typography.h4.fontSize}; font-size: ${theme.typography.h4.fontSize};
`, `,
headerInformationRowSecondary: css`
font-size: ${theme.typography.body.fontSize};
`,
headerOrgName: css` headerOrgName: css`
font-size: ${theme.typography.h4.fontSize}; font-size: ${theme.typography.h4.fontSize};
`, `,

View File

@ -0,0 +1,66 @@
import React from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, Icon } from '@grafana/ui';
import { CatalogPlugin } from '../types';
type Props = {
plugin: CatalogPlugin;
className?: string;
};
const PluginIconClassName: Record<string, string> = {
datasource: 'gicon gicon-datasources',
panel: 'icon-gf icon-gf-panel',
app: 'icon-gf icon-gf-apps',
page: 'icon-gf icon-gf-endpoint-tiny',
dashboard: 'gicon gicon-dashboard',
default: 'icon-gf icon-gf-apps',
};
export function PluginDetailsHeaderDependencies({ plugin, className }: Props): React.ReactElement | null {
const styles = useStyles2(getStyles);
const pluginDependencies = plugin.details?.pluginDependencies;
const grafanaDependency = plugin.details?.grafanaDependency;
const hasNoDependencyInfo = !grafanaDependency && (!pluginDependencies || !pluginDependencies.length);
if (hasNoDependencyInfo) {
return null;
}
return (
<div className={className}>
<div className={styles.textBold}>Dependencies:</div>
{/* Grafana dependency */}
{Boolean(grafanaDependency) && (
<div>
<Icon name="grafana" />
Grafana {grafanaDependency}
</div>
)}
{/* Plugin dependencies */}
{pluginDependencies && pluginDependencies.length > 0 && (
<div>
{pluginDependencies.map((p) => {
return (
<span key={p.name}>
<i className={PluginIconClassName[p.type] || PluginIconClassName.default} />
{p.name} {p.version}
</span>
);
})}
</div>
)}
</div>
);
}
export const getStyles = (theme: GrafanaTheme2) => {
return {
textBold: css`
font-weight: ${theme.typography.fontWeightBold};
`,
};
};

View File

@ -197,6 +197,15 @@ describe('Plugin details page', () => {
await waitFor(() => expect(queryByRole('link', { name: /update via grafana.com/i })).toBeInTheDocument()); await waitFor(() => expect(queryByRole('link', { name: /update via grafana.com/i })).toBeInTheDocument());
expect(queryByRole('link', { name: /uninstall via grafana.com/i })).toBeInTheDocument(); expect(queryByRole('link', { name: /uninstall via grafana.com/i })).toBeInTheDocument();
}); });
it('should display grafana dependencies for a plugin if they are available', async () => {
const { queryByText } = setup('not-installed');
// Wait for the dependencies part to be loaded
await waitFor(() => expect(queryByText(/dependencies:/i)).toBeInTheDocument());
expect(queryByText('Grafana >=7.3.0')).toBeInTheDocument();
});
}); });
function remotePlugin(plugin: Partial<RemotePlugin> = {}): RemotePlugin { function remotePlugin(plugin: Partial<RemotePlugin> = {}): RemotePlugin {
@ -237,6 +246,7 @@ function remotePlugin(plugin: Partial<RemotePlugin> = {}): RemotePlugin {
dependencies: { dependencies: {
grafanaDependency: '>=7.3.0', grafanaDependency: '>=7.3.0',
grafanaVersion: '7.3', grafanaVersion: '7.3',
plugins: [],
}, },
info: { info: {
links: [], links: [],

View File

@ -1,5 +1,5 @@
import { EntityState } from '@reduxjs/toolkit'; import { EntityState } from '@reduxjs/toolkit';
import { PluginType, PluginSignatureStatus, PluginSignatureType } from '@grafana/data'; import { PluginType, PluginSignatureStatus, PluginSignatureType, PluginDependencies } from '@grafana/data';
import { StoreState, PluginsState } from 'app/types'; import { StoreState, PluginsState } from 'app/types';
export type PluginTypeCode = 'app' | 'panel' | 'datasource'; export type PluginTypeCode = 'app' | 'panel' | 'datasource';
@ -44,6 +44,7 @@ export interface CatalogPluginDetails {
url: string; url: string;
}>; }>;
grafanaDependency?: string; grafanaDependency?: string;
pluginDependencies?: PluginDependencies['plugins'];
} }
export interface CatalogPluginInfo { export interface CatalogPluginInfo {
@ -62,10 +63,7 @@ export type RemotePlugin = {
id: number; id: number;
internal: boolean; internal: boolean;
json?: { json?: {
dependencies: { dependencies: PluginDependencies;
grafanaDependency: string;
grafanaVersion: string;
};
info: { info: {
links: Array<{ links: Array<{
name: string; name: string;

View File

@ -50,13 +50,13 @@ export const GraphiteQueryEditorContext = ({
if (state) { if (state) {
dispatch(actions.queriesChanged(queries)); dispatch(actions.queriesChanged(queries));
} }
}, [dispatch, queries]); }, [dispatch, queries, state]);
useEffect(() => { useEffect(() => {
if (state && state.target?.target !== query.target) { if (state && state.target?.target !== query.target) {
dispatch(actions.queryChanged(query)); dispatch(actions.queryChanged(query));
} }
}, [dispatch, query]); }, [dispatch, query, state]);
if (!state) { if (!state) {
dispatch( dispatch(