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 {
grafanaDependency?: string;
grafanaVersion: string;
plugins: PluginDependencyInfo[];
}

View File

@ -16,13 +16,15 @@ export async function getCatalogPlugin(id: string): Promise<CatalogPlugin> {
}
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 isInstalled = Boolean(local);
const [remote, versions] = await Promise.all([getRemotePlugin(id, isInstalled), getPluginVersions(id)]);
const dependencies = remote?.json?.dependencies;
return {
grafanaDependency: remote?.json?.dependencies?.grafanaDependency || '',
grafanaDependency: dependencies?.grafanaDependency || dependencies?.grafanaVersion || '',
pluginDependencies: dependencies?.plugins || [],
links: remote?.json?.info.links || local?.info.links || [],
readme: remote?.readme,
versions,

View File

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

View File

@ -1,5 +1,5 @@
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';
export type PluginTypeCode = 'app' | 'panel' | 'datasource';
@ -44,6 +44,7 @@ export interface CatalogPluginDetails {
url: string;
}>;
grafanaDependency?: string;
pluginDependencies?: PluginDependencies['plugins'];
}
export interface CatalogPluginInfo {
@ -62,10 +63,7 @@ export type RemotePlugin = {
id: number;
internal: boolean;
json?: {
dependencies: {
grafanaDependency: string;
grafanaVersion: string;
};
dependencies: PluginDependencies;
info: {
links: Array<{
name: string;

View File

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