mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Catalog: Allow extensions to render in the plugin catalog subtitle (#95239)
* Plugins Catalog: Allow plugin subtitle additional extensions to register and render * Add tests * Fix prettier issues * Fix typo * Empty commit
This commit is contained in:
parent
2a65de8aaa
commit
77b8b505ad
@ -0,0 +1,120 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { PluginSignatureStatus } from '@grafana/data';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
|
||||
import * as runtime from '../state/hooks';
|
||||
import { CatalogPlugin } from '../types';
|
||||
|
||||
import { PluginSubtitle, registerPluginSubtitleExtension } from './PluginSubtitle';
|
||||
|
||||
jest.mock('../state/hooks', () => ({
|
||||
useIsRemotePluginsAvailable: jest.fn().mockReturnValue(true),
|
||||
useInstallStatus: jest.fn().mockReturnValue({ error: null }),
|
||||
}));
|
||||
|
||||
describe('PluginSubtitle', () => {
|
||||
const basePlugin: CatalogPlugin = {
|
||||
description: 'Test description',
|
||||
downloads: 5,
|
||||
id: 'test-plugin',
|
||||
name: 'Test Plugin',
|
||||
orgName: 'Test',
|
||||
signature: PluginSignatureStatus.valid,
|
||||
isInstalled: false,
|
||||
hasUpdate: false,
|
||||
isCore: false,
|
||||
isDev: false,
|
||||
details: {
|
||||
links: [{ name: 'Website', url: 'http://test.com' }],
|
||||
versions: [{ version: '1.0.0', grafanaDependency: '>=9.0.0', isCompatible: true, createdAt: '2020-01-01' }],
|
||||
},
|
||||
publishedAt: '2020-09-01',
|
||||
updatedAt: '2021-06-28',
|
||||
isEnterprise: false,
|
||||
isDisabled: false,
|
||||
isDeprecated: false,
|
||||
isPublished: true,
|
||||
isManaged: false,
|
||||
isPreinstalled: { found: false, withVersion: false },
|
||||
info: {
|
||||
logos: {
|
||||
small: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/small',
|
||||
large: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/large',
|
||||
},
|
||||
keywords: ['test', 'plugin'],
|
||||
},
|
||||
popularity: 0,
|
||||
};
|
||||
|
||||
beforeEach(() => {});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders nothing when no plugin provided', () => {
|
||||
const { container } = render(<PluginSubtitle />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders plugin description', () => {
|
||||
render(<PluginSubtitle plugin={basePlugin} />);
|
||||
expect(screen.getByText('Test description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders links', () => {
|
||||
render(<PluginSubtitle plugin={basePlugin} />);
|
||||
expect(screen.getByText('Website')).toHaveAttribute('href', 'http://test.com');
|
||||
});
|
||||
|
||||
it('shows error alert when installation error exists', () => {
|
||||
jest.spyOn(runtime, 'useInstallStatus').mockReturnValueOnce({
|
||||
error: { message: 'Install failed', error: 'Details' },
|
||||
isInstalling: false,
|
||||
});
|
||||
render(<PluginSubtitle plugin={basePlugin} />);
|
||||
expect(screen.getByText('Install failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('warning when no permissions', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('renders install control warning when conditions are met', () => {
|
||||
const plugin = { ...basePlugin, isInstalled: false };
|
||||
render(<PluginSubtitle plugin={plugin} />);
|
||||
expect(screen.queryByText(/permission to install/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders uninstall control warning when conditions are met', () => {
|
||||
const plugin = { ...basePlugin, isInstalled: true };
|
||||
render(<PluginSubtitle plugin={plugin} />);
|
||||
expect(screen.queryByText(/permission to uninstall/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('no warning when has permissions', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('renders install control warning when conditions are met', () => {
|
||||
const plugin = { ...basePlugin, isInstalled: false };
|
||||
render(<PluginSubtitle plugin={plugin} />);
|
||||
expect(screen.queryByText(/permission to install/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders uninstall control warning when conditions are met', () => {
|
||||
const plugin = { ...basePlugin, isInstalled: true };
|
||||
render(<PluginSubtitle plugin={plugin} />);
|
||||
expect(screen.queryByText(/permission to uninstall/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('renders plugin subtitle extensions', () => {
|
||||
const TestExtension = () => <div>Extension Content</div>;
|
||||
registerPluginSubtitleExtension(TestExtension);
|
||||
render(<PluginSubtitle plugin={basePlugin} />);
|
||||
expect(screen.getByText('Extension Content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -3,7 +3,7 @@ import { Fragment } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { InstallControlsWarning } from '../components/InstallControls';
|
||||
import { getLatestCompatibleVersion, hasInstallControlWarning } from '../helpers';
|
||||
@ -14,6 +14,14 @@ interface Props {
|
||||
plugin?: CatalogPlugin;
|
||||
}
|
||||
|
||||
type PluginSubtitleExtension = (props: Props) => JSX.Element | null;
|
||||
|
||||
const pluginSubtitleExtensions: PluginSubtitleExtension[] = [];
|
||||
|
||||
export const registerPluginSubtitleExtension = (extension: PluginSubtitleExtension) => {
|
||||
pluginSubtitleExtensions.push(extension);
|
||||
};
|
||||
|
||||
export const PluginSubtitle = ({ plugin }: Props) => {
|
||||
const isRemotePluginsAvailable = useIsRemotePluginsAvailable();
|
||||
const styles = useStyles2(getStyles);
|
||||
@ -35,26 +43,33 @@ export const PluginSubtitle = ({ plugin }: Props) => {
|
||||
{typeof errorInstalling === 'string' ? errorInstalling : errorInstalling.error}
|
||||
</Alert>
|
||||
)}
|
||||
{plugin?.description && <div>{plugin?.description}</div>}
|
||||
{!config.featureToggles.pluginsDetailsRightPanel && !!plugin?.details?.links?.length && (
|
||||
<span>
|
||||
{plugin.details.links.map((link, index) => (
|
||||
<Fragment key={index}>
|
||||
{index > 0 && ' | '}
|
||||
<a href={link.url} className="external-link">
|
||||
{link.name}
|
||||
</a>
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
{hasInstallControlWarning(plugin, isRemotePluginsAvailable, latestCompatibleVersion) && (
|
||||
<InstallControlsWarning
|
||||
plugin={plugin}
|
||||
pluginStatus={pluginStatus}
|
||||
latestCompatibleVersion={latestCompatibleVersion}
|
||||
/>
|
||||
)}
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<div>
|
||||
{plugin?.description && <div>{plugin?.description}</div>}
|
||||
{!config.featureToggles.pluginsDetailsRightPanel && !!plugin?.details?.links?.length && (
|
||||
<span>
|
||||
{plugin.details.links.map((link, index) => (
|
||||
<Fragment key={index}>
|
||||
{index > 0 && ' | '}
|
||||
<a href={link.url} className="external-link">
|
||||
{link.name}
|
||||
</a>
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
{hasInstallControlWarning(plugin, isRemotePluginsAvailable, latestCompatibleVersion) && (
|
||||
<InstallControlsWarning
|
||||
plugin={plugin}
|
||||
pluginStatus={pluginStatus}
|
||||
latestCompatibleVersion={latestCompatibleVersion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{pluginSubtitleExtensions.map((extension) => {
|
||||
return <Fragment key={extension.name}>{extension({ plugin })}</Fragment>;
|
||||
})}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -25,7 +25,7 @@ export async function shouldLoadPluginInFrontendSandbox({
|
||||
pluginId,
|
||||
}: SandboxEligibilityCheckParams): Promise<boolean> {
|
||||
// basic check if the plugin is eligible for the sandbox
|
||||
if (!(await isPluginFrontendSandboxElegible({ isAngular, pluginId }))) {
|
||||
if (!(await isPluginFrontendSandboxEligible({ isAngular, pluginId }))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ export async function shouldLoadPluginInFrontendSandbox({
|
||||
* This is a basic check that checks if the plugin is eligible to run in the sandbox.
|
||||
* It does not check if the plugin is actually enabled for the sandbox.
|
||||
*/
|
||||
async function isPluginFrontendSandboxElegible({
|
||||
export async function isPluginFrontendSandboxEligible({
|
||||
isAngular,
|
||||
pluginId,
|
||||
}: SandboxEligibilityCheckParams): Promise<boolean> {
|
||||
@ -61,9 +61,14 @@ async function isPluginFrontendSandboxElegible({
|
||||
return false;
|
||||
}
|
||||
|
||||
// don't run grafana-signed plugins in sandbox
|
||||
const pluginMeta = await getPluginSettings(pluginId);
|
||||
if (pluginMeta.signatureType === PluginSignatureType.grafana) {
|
||||
try {
|
||||
// don't run grafana-signed plugins in sandbox
|
||||
const pluginMeta = await getPluginSettings(pluginId, { showErrorAlert: false });
|
||||
if (pluginMeta.signatureType === PluginSignatureType.grafana) {
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
// this can fail if we are trying to fetch settings of a non-installed plugin
|
||||
return false;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user