mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins Catalog: add support for enable/disable app plugins (#41801)
* refactor(plugins): add empty line between methods * feat(api): add an API function for updating plugin settings * feat(plugins): add a "getting started" guide for enabling / disabling app plugins * test(plugins/admin): add tests for enable/disable functionality * refactor(plugins/admin): update the name of the test cases Now that we have multiple type of post-installation steps it probably makes sense.
This commit is contained in:
@@ -48,6 +48,7 @@ class AppRootPage extends Component<Props, State> {
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
return nextProps.location.pathname.startsWith('/a/');
|
||||
}
|
||||
|
||||
async loadPluginSettings() {
|
||||
const { params } = this.props.match;
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { PluginError, renderMarkdown } from '@grafana/data';
|
||||
import { PluginError, PluginMeta, renderMarkdown } from '@grafana/data';
|
||||
import { API_ROOT, GCOM_API_ROOT } from './constants';
|
||||
import { LocalPlugin, RemotePlugin, CatalogPluginDetails, Version, PluginVersion } from './types';
|
||||
import { isLocalPluginVisible, isRemotePluginVisible } from './helpers';
|
||||
@@ -99,6 +99,16 @@ export async function uninstallPlugin(id: string) {
|
||||
return await getBackendSrv().post(`${API_ROOT}/${id}/uninstall`);
|
||||
}
|
||||
|
||||
export async function updatePluginSettings(id: string, data: Partial<PluginMeta>) {
|
||||
const response = await getBackendSrv().datasourceRequest({
|
||||
url: `/api/plugins/${id}/settings`,
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
|
||||
return response?.data;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
getRemotePlugins,
|
||||
getInstalledPlugins: getLocalPlugins,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { PluginMeta } from '@grafana/data';
|
||||
import { Button } from '@grafana/ui';
|
||||
import { usePluginConfig } from '../../hooks/usePluginConfig';
|
||||
import { updatePluginSettings } from '../../api';
|
||||
import React from 'react';
|
||||
import { CatalogPlugin } from '../../types';
|
||||
|
||||
type Props = {
|
||||
plugin: CatalogPlugin;
|
||||
};
|
||||
|
||||
export function GetStartedWithApp({ plugin }: Props): React.ReactElement | null {
|
||||
const { value: pluginConfig } = usePluginConfig(plugin);
|
||||
|
||||
if (!pluginConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { enabled, jsonData } = pluginConfig?.meta;
|
||||
|
||||
const enable = () =>
|
||||
updatePluginSettingsAndReload(plugin.id, {
|
||||
enabled: true,
|
||||
pinned: true,
|
||||
jsonData,
|
||||
});
|
||||
|
||||
const disable = () => {
|
||||
updatePluginSettingsAndReload(plugin.id, {
|
||||
enabled: false,
|
||||
pinned: false,
|
||||
jsonData,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!enabled && (
|
||||
<Button variant="primary" onClick={enable}>
|
||||
Enable
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{enabled && (
|
||||
<Button variant="destructive" onClick={disable}>
|
||||
Disable
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const updatePluginSettingsAndReload = async (id: string, data: Partial<PluginMeta>) => {
|
||||
try {
|
||||
await updatePluginSettings(id, data);
|
||||
|
||||
// Reloading the page as the plugin meta changes made here wouldn't be propagated throughout the app.
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
console.error('Error while updating the plugin', e);
|
||||
}
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import React, { ReactElement } from 'react';
|
||||
import { PluginType } from '@grafana/data';
|
||||
import { CatalogPlugin } from '../../types';
|
||||
import { GetStartedWithDataSource } from './GetStartedWithDataSource';
|
||||
import { GetStartedWithApp } from './GetStartedWithApp';
|
||||
|
||||
type Props = {
|
||||
plugin: CatalogPlugin;
|
||||
@@ -15,6 +16,8 @@ export function GetStartedWithPlugin({ plugin }: Props): ReactElement | null {
|
||||
switch (plugin.type) {
|
||||
case PluginType.datasource:
|
||||
return <GetStartedWithDataSource plugin={plugin} />;
|
||||
case PluginType.app:
|
||||
return <GetStartedWithApp plugin={plugin} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '../types';
|
||||
import * as api from '../api';
|
||||
import { fetchRemotePlugins } from '../state/actions';
|
||||
import { usePluginConfig } from '../hooks/usePluginConfig';
|
||||
import { PluginErrorCode, PluginSignatureStatus, PluginType, dateTimeFormatTimeAgo } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
@@ -28,6 +29,14 @@ jest.mock('@grafana/runtime', () => {
|
||||
return mockedRuntime;
|
||||
});
|
||||
|
||||
jest.mock('../hooks/usePluginConfig.tsx', () => ({
|
||||
usePluginConfig: jest.fn(() => ({
|
||||
value: {
|
||||
meta: {},
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
const renderPluginDetails = (
|
||||
pluginOverride: Partial<CatalogPlugin>,
|
||||
{
|
||||
@@ -65,10 +74,17 @@ const renderPluginDetails = (
|
||||
|
||||
describe('Plugin details page', () => {
|
||||
const id = 'my-plugin';
|
||||
const originalWindowLocation = window.location;
|
||||
let dateNow: any;
|
||||
|
||||
beforeAll(() => {
|
||||
dateNow = jest.spyOn(Date, 'now').mockImplementation(() => 1609470000000); // 2021-01-01 04:00:00
|
||||
|
||||
// Enabling / disabling the plugin is currently reloading the page to propagate the changes
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: { reload: jest.fn() },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -79,6 +95,7 @@ describe('Plugin details page', () => {
|
||||
|
||||
afterAll(() => {
|
||||
dateNow.mockRestore();
|
||||
Object.defineProperty(window, 'location', { configurable: true, value: originalWindowLocation });
|
||||
});
|
||||
|
||||
describe('viewed as user with grafana admin permissions', () => {
|
||||
@@ -442,7 +459,7 @@ describe('Plugin details page', () => {
|
||||
expect(rendered.getByText(message)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display post installation step for installed data source plugins', async () => {
|
||||
it('should display a "Create" button as a post installation step for installed data source plugins', async () => {
|
||||
const name = 'Akumuli';
|
||||
const { queryByText } = renderPluginDetails({
|
||||
name,
|
||||
@@ -454,7 +471,7 @@ describe('Plugin details page', () => {
|
||||
expect(queryByText(`Create a ${name} data source`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display post installation step for disabled data source plugins', async () => {
|
||||
it('should not display a "Create" button as a post installation step for disabled data source plugins', async () => {
|
||||
const name = 'Akumuli';
|
||||
const { queryByText } = renderPluginDetails({
|
||||
name,
|
||||
@@ -479,16 +496,136 @@ describe('Plugin details page', () => {
|
||||
expect(queryByText(`Create a ${name} data source`)).toBeNull();
|
||||
});
|
||||
|
||||
it('should not display post installation step for app plugins', async () => {
|
||||
it('should display an enable button for app plugins that are not enabled as a post installation step', async () => {
|
||||
const name = 'Akumuli';
|
||||
const { queryByText } = renderPluginDetails({
|
||||
|
||||
// @ts-ignore
|
||||
usePluginConfig.mockReturnValue({
|
||||
value: {
|
||||
meta: {
|
||||
enabled: false,
|
||||
pinned: false,
|
||||
jsonData: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { queryByText, queryByRole } = renderPluginDetails({
|
||||
name,
|
||||
isInstalled: true,
|
||||
type: PluginType.app,
|
||||
});
|
||||
|
||||
await waitFor(() => queryByText('Uninstall'));
|
||||
expect(queryByText(`Create a ${name} data source`)).toBeNull();
|
||||
|
||||
expect(queryByRole('button', { name: /enable/i })).toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: /disable/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display a disable button for app plugins that are enabled as a post installation step', async () => {
|
||||
const name = 'Akumuli';
|
||||
|
||||
// @ts-ignore
|
||||
usePluginConfig.mockReturnValue({
|
||||
value: {
|
||||
meta: {
|
||||
enabled: true,
|
||||
pinned: false,
|
||||
jsonData: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { queryByText, queryByRole } = renderPluginDetails({
|
||||
name,
|
||||
isInstalled: true,
|
||||
type: PluginType.app,
|
||||
});
|
||||
|
||||
await waitFor(() => queryByText('Uninstall'));
|
||||
|
||||
expect(queryByRole('button', { name: /disable/i })).toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: /enable/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should be possible to enable an app plugin', async () => {
|
||||
const id = 'akumuli-datasource';
|
||||
const name = 'Akumuli';
|
||||
|
||||
// @ts-ignore
|
||||
api.updatePluginSettings = jest.fn();
|
||||
|
||||
// @ts-ignore
|
||||
usePluginConfig.mockReturnValue({
|
||||
value: {
|
||||
meta: {
|
||||
enabled: false,
|
||||
pinned: false,
|
||||
jsonData: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { queryByText, getByRole } = renderPluginDetails({
|
||||
id,
|
||||
name,
|
||||
isInstalled: true,
|
||||
type: PluginType.app,
|
||||
});
|
||||
|
||||
// Wait for the header to be loaded
|
||||
await waitFor(() => queryByText('Uninstall'));
|
||||
|
||||
// Click on "Enable"
|
||||
userEvent.click(getByRole('button', { name: /enable/i }));
|
||||
|
||||
// Check if the API request was initiated
|
||||
expect(api.updatePluginSettings).toHaveBeenCalledTimes(1);
|
||||
expect(api.updatePluginSettings).toHaveBeenCalledWith(id, {
|
||||
enabled: true,
|
||||
pinned: true,
|
||||
jsonData: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should be possible to disable an app plugin', async () => {
|
||||
const id = 'akumuli-datasource';
|
||||
const name = 'Akumuli';
|
||||
|
||||
// @ts-ignore
|
||||
api.updatePluginSettings = jest.fn();
|
||||
|
||||
// @ts-ignore
|
||||
usePluginConfig.mockReturnValue({
|
||||
value: {
|
||||
meta: {
|
||||
enabled: true,
|
||||
pinned: true,
|
||||
jsonData: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { queryByText, getByRole } = renderPluginDetails({
|
||||
id,
|
||||
name,
|
||||
isInstalled: true,
|
||||
type: PluginType.app,
|
||||
});
|
||||
|
||||
// Wait for the header to be loaded
|
||||
await waitFor(() => queryByText('Uninstall'));
|
||||
|
||||
// Click on "Disable"
|
||||
userEvent.click(getByRole('button', { name: /disable/i }));
|
||||
|
||||
// Check if the API request was initiated
|
||||
expect(api.updatePluginSettings).toHaveBeenCalledTimes(1);
|
||||
expect(api.updatePluginSettings).toHaveBeenCalledWith(id, {
|
||||
enabled: false,
|
||||
pinned: false,
|
||||
jsonData: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not display versions tab for plugins not published to gcom', async () => {
|
||||
|
||||
Reference in New Issue
Block a user