mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
fc73bc1161
commit
9173898fd6
@ -87,6 +87,7 @@ interface PluginDependencyInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginDependencies {
|
export interface PluginDependencies {
|
||||||
|
grafanaDependency?: string;
|
||||||
grafanaVersion: string;
|
grafanaVersion: string;
|
||||||
plugins: PluginDependencyInfo[];
|
plugins: PluginDependencyInfo[];
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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};
|
||||||
`,
|
`,
|
||||||
|
@ -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};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
@ -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: [],
|
||||||
|
@ -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;
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user