Files
grafana/public/app/features/plugins/admin/components/PluginActions.test.tsx
Hugo Häggmark 16ca230898 Plugins: Fix better UX for disabled Angular plugins (#101333)
* Feat: better UX for Angular plugins

* Chore: fix i18n

* Update public/app/features/plugins/admin/components/PluginDetailsDisabledError.tsx

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>

* Update public/app/features/plugins/admin/components/PluginDetailsDisabledError.tsx

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>

* Chore: fixes after PR feedback

* Test: testing uninstall in cloud

* Chore: fix weird merge

* Chore: fix test import

* Chore: comment out an expec

* Chore: revert test of uninstall on cloud

* Chore: adds tooltip and removes admin message

* Trigger build

* Chore: fix for cloud

* Trigger build

---------

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
2025-03-17 06:26:49 +01:00

278 lines
10 KiB
TypeScript

import { render, screen } from 'test/test-utils';
import { PluginErrorCode, PluginSignatureStatus, PluginSignatureType } from '@grafana/data';
import * as helpers from '../helpers';
import * as hooks from '../state/hooks';
import { initialState } from '../state/reducer';
import { CatalogPlugin, PluginStatus, ReducerState, Version } from '../types';
import { getInstallControlsDisabled, getPluginStatus, PluginActions } from './PluginActions';
describe('PluginActions', () => {
let plugins: ReducerState;
beforeEach(() => {
plugins = { ...initialState };
jest.spyOn(helpers, 'isInstallControlsEnabled').mockReturnValue(true);
jest.spyOn(helpers, 'hasInstallControlWarning').mockReturnValue(false);
jest.spyOn(hooks, 'useIsRemotePluginsAvailable').mockReturnValue(true);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('render', () => {
it('should render nothing when no plugin is provided', () => {
render(<PluginActions />, { preloadedState: { plugins } });
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
it('should render install button for non-installed plugin', () => {
render(<PluginActions plugin={createPluginStub()} />, { preloadedState: { plugins } });
expect(screen.getByRole('button', { name: /install/i })).toBeInTheDocument();
});
it('should render uninstall button for installed plugin', () => {
const installedPlugin = createPluginStub({ isInstalled: true });
render(<PluginActions plugin={installedPlugin} />, { preloadedState: { plugins } });
expect(screen.getByRole('button', { name: /uninstall/i })).toBeInTheDocument();
});
it('should render update button for plugin with update', () => {
const pluginWithUpdate = createPluginStub({ isInstalled: true, hasUpdate: true });
render(<PluginActions plugin={pluginWithUpdate} />, { preloadedState: { plugins } });
expect(screen.getByRole('button', { name: /update/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /uninstall/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /uninstall/i })).toHaveAttribute('aria-disabled', 'false');
});
it('should not render install controls for core plugins', () => {
const corePlugin = createPluginStub({ isCore: true });
render(<PluginActions plugin={corePlugin} />, { preloadedState: { plugins } });
expect(screen.queryByRole('button', { name: /install|uninstall|update/i })).not.toBeInTheDocument();
});
it('should not render install controls for disabled plugins', () => {
const disabledPlugin = createPluginStub({ isDisabled: true });
render(<PluginActions plugin={disabledPlugin} />, { preloadedState: { plugins } });
expect(screen.queryByRole('button', { name: /install|uninstall|update/i })).not.toBeInTheDocument();
});
it('should not render install controls for provisioned plugins', () => {
const provisionedPlugin = createPluginStub({ isProvisioned: true });
render(<PluginActions plugin={provisionedPlugin} />, { preloadedState: { plugins } });
expect(screen.queryByRole('button', { name: /install|uninstall|update/i })).not.toBeInTheDocument();
});
it('should not render install controls when install controls are disabled', () => {
jest.spyOn(helpers, 'isInstallControlsEnabled').mockReturnValue(false);
render(<PluginActions plugin={createPluginStub()} />, { preloadedState: { plugins } });
expect(screen.queryByRole('button', { name: /install|uninstall|update/i })).not.toBeInTheDocument();
});
it('should render install controls when there is an installed disabled angular plugin with a non-angular version available', async () => {
jest.spyOn(helpers, 'getLatestCompatibleVersion').mockReturnValue(createVersion({ angularDetected: false }));
const disabledAngularPlugin = createPluginStub({
isInstalled: true,
isDisabled: true,
error: PluginErrorCode.angular,
});
render(<PluginActions plugin={disabledAngularPlugin} />, { preloadedState: { plugins } });
expect(screen.getByRole('button', { name: /update/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /uninstall/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /uninstall/i })).toHaveAttribute('aria-disabled', 'true');
});
it('should not render install controls when there is an installed disabled angular plugin with no non-angular version available', () => {
jest.spyOn(helpers, 'getLatestCompatibleVersion').mockReturnValue(createVersion({ angularDetected: true }));
const disabledAngularPlugin = createPluginStub({
isInstalled: true,
isDisabled: true,
error: PluginErrorCode.angular,
});
render(<PluginActions plugin={disabledAngularPlugin} />, { preloadedState: { plugins } });
expect(screen.queryByRole('button', { name: /install|uninstall|update/i })).not.toBeInTheDocument();
});
});
describe('getPluginStatus', () => {
describe('regular plugins', () => {
it('should return INSTALL for non-installed plugins', () => {
const plugin = createPluginStub({ isInstalled: false });
expect(getPluginStatus(plugin, undefined)).toBe(PluginStatus.INSTALL);
});
it('should return UPDATE for installed plugins with updates', () => {
const plugin = createPluginStub({ isInstalled: true, hasUpdate: true });
expect(getPluginStatus(plugin, undefined)).toBe(PluginStatus.UPDATE);
});
it('should return UNINSTALL for installed plugins without updates', () => {
const plugin = createPluginStub({ isInstalled: true, hasUpdate: false });
expect(getPluginStatus(plugin, undefined)).toBe(PluginStatus.UNINSTALL);
});
});
describe('angular plugins', () => {
it('should return INSTALL for non-installed angular plugins', () => {
const plugin = createPluginStub({
isInstalled: false,
error: PluginErrorCode.angular,
});
expect(getPluginStatus(plugin, undefined)).toBe(PluginStatus.INSTALL);
});
it('should return UPDATE for installed angular plugins with non-angular version available', () => {
const plugin = createPluginStub({
isInstalled: true,
error: PluginErrorCode.angular,
});
const latestVersion = createVersion({ angularDetected: false });
expect(getPluginStatus(plugin, latestVersion)).toBe(PluginStatus.UPDATE);
});
it('should return UNINSTALL for installed angular plugins with only angular versions available', () => {
const plugin = createPluginStub({
isInstalled: true,
error: PluginErrorCode.angular,
});
const latestVersion = createVersion({ angularDetected: true });
expect(getPluginStatus(plugin, latestVersion)).toBe(PluginStatus.UNINSTALL);
});
it('should return UNINSTALL for installed angular plugins with no version info', () => {
const plugin = createPluginStub({
isInstalled: true,
error: PluginErrorCode.angular,
});
expect(getPluginStatus(plugin, undefined)).toBe(PluginStatus.UNINSTALL);
});
});
describe('disabled plugins', () => {
it('should handle disabled angular plugins', () => {
const plugin = createPluginStub({
isInstalled: true,
isDisabled: true,
error: PluginErrorCode.angular,
});
expect(getPluginStatus(plugin, undefined)).toBe(PluginStatus.UNINSTALL);
});
it('should handle disabled regular plugins', () => {
const plugin = createPluginStub({
isInstalled: true,
isDisabled: true,
});
expect(getPluginStatus(plugin, undefined)).toBe(PluginStatus.UNINSTALL);
});
});
});
describe('getInstallControlsDisabled', () => {
it('should return false for disabled angular plugins that have a non-angular version available', () => {
const plugin = createPluginStub({ isDisabled: true, error: PluginErrorCode.angular });
const latestVersion = createVersion({ angularDetected: false });
expect(getInstallControlsDisabled(plugin, latestVersion)).toBe(false);
});
it('should return true for disabled regular plugins', () => {
const plugin = createPluginStub({ isDisabled: true });
expect(getInstallControlsDisabled(plugin, undefined)).toBe(true);
});
it('should return true for core plugins', () => {
const plugin = createPluginStub({ isCore: true });
expect(getInstallControlsDisabled(plugin, undefined)).toBe(true);
});
it('should return true for provisioned plugins', () => {
const plugin = createPluginStub({ isProvisioned: true });
expect(getInstallControlsDisabled(plugin, undefined)).toBe(true);
});
it('should return false for regular plugins', () => {
const plugin = createPluginStub({});
expect(getInstallControlsDisabled(plugin, undefined)).toBe(false);
});
it('should return true when install controls are not enabled', () => {
jest.spyOn(helpers, 'isInstallControlsEnabled').mockReturnValue(false);
const plugin = createPluginStub({});
expect(getInstallControlsDisabled(plugin, undefined)).toBe(true);
});
});
});
function createPluginStub(overrides?: Partial<CatalogPlugin>): CatalogPlugin {
return {
name: 'Test Plugin',
id: 'test-plugin',
description: 'Test plugin',
isCore: false,
isInstalled: false,
isDisabled: false,
isProvisioned: false,
hasUpdate: false,
signature: PluginSignatureStatus.valid,
signatureType: PluginSignatureType.grafana,
signatureOrg: 'grafana',
info: {
logos: { small: '', large: '' },
keywords: [],
},
error: undefined,
downloads: 0,
popularity: 0,
orgName: 'Test Org',
publishedAt: '',
updatedAt: '',
isPublished: true,
isDev: false,
isEnterprise: false,
isDeprecated: false,
isManaged: false,
isPreinstalled: { found: false, withVersion: false },
...overrides,
};
}
function createVersion(overrides?: Partial<Version>): Version {
return {
version: '1.0.0',
createdAt: '',
updatedAt: '',
isCompatible: true,
grafanaDependency: '',
angularDetected: false,
...overrides,
};
}