diff --git a/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts b/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts
new file mode 100644
index 00000000000..af1678a85a0
--- /dev/null
+++ b/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts
@@ -0,0 +1,239 @@
+import { CatalogPlugin } from '../types';
+
+// Exported from the Redux store
+export default {
+ description: 'Zabbix plugin for Grafana',
+ downloads: 35977451,
+ hasUpdate: false,
+ id: 'alexanderzobnin-zabbix-app',
+ info: {
+ logos: {
+ small: 'https://grafana.com/api/plugins/alexanderzobnin-zabbix-app/versions/4.2.2/logos/small',
+ large: 'https://grafana.com/api/plugins/alexanderzobnin-zabbix-app/versions/4.2.2/logos/large',
+ },
+ },
+ isCore: false,
+ isDev: false,
+ isEnterprise: false,
+ isInstalled: false,
+ name: 'Zabbix',
+ orgName: 'Alexander Zobnin',
+ popularity: 0.2093,
+ publishedAt: '2016-04-06T20:23:41.000Z',
+ type: 'app',
+ signature: 'valid',
+ signatureOrg: 'Alexander Zobnin',
+ signatureType: 'community',
+ updatedAt: '2021-08-25T15:03:49.000Z',
+ version: '4.2.2',
+ details: {
+ grafanaDependency: '>=8.0.0',
+ pluginDependencies: [],
+ links: [
+ {
+ name: 'GitHub',
+ url: 'https://github.com/alexanderzobnin/grafana-zabbix',
+ },
+ {
+ name: 'Docs',
+ url: 'https://alexanderzobnin.github.io/grafana-zabbix',
+ },
+ {
+ name: 'License',
+ url: 'https://github.com/alexanderzobnin/grafana-zabbix/blob/master/LICENSE',
+ },
+ ],
+ readme:
+ '
Zabbix plugin for Grafana
\n
\n
\n
\n
\n
\n
\n
\nVisualize your Zabbix metrics with the leading open source software for time series analytics.
\n
\nFeatures
\n\n- Select multiple metrics by using Regex
\n- Create interactive and reusable dashboards with template variables
\n- Show events on graphs with Annotations
\n- Display active problems with Triggers panel
\n- Transform and shape your data with metric processing functions (Avg, Median, Min, Max, Multiply, Summarize, Time shift, Alias)
\n- Find problems faster with Alerting feature
\n- Mix metrics from multiple data sources in the same dashboard or even graph
\n- Discover and share dashboards in the official library
\n
\nSee all features overview and dashboards examples at Grafana-Zabbix Live demo site.\nVisit plugins page at grafana.com and check out available Grafana data sources, panels and dashboards.
\nInstallation
\nInstall by using grafana-cli
\ngrafana-cli plugins install alexanderzobnin-zabbix-app\n
\nOr see more installation options in docs.
\nGetting started
\nFirst, configure Zabbix data source. Then you can create your first dashboard with step-by-step Getting started guide.
\nDocumentation
\n\nCommunity Resources, Feedback, and Support
\n\n- Found a bug? Want a new feature? Feel free to open an issue.
\n- Have a question? You also can open issue, but for questions, it would be better to use Grafana Community portal.
\n- Need additional support? Contact me for details alexanderzobnin@gmail.com
\n
\n
\n:copyright: 2015-2021 Alexander Zobnin alexanderzobnin@gmail.com
\nLicensed under the Apache 2.0 License
\n',
+ versions: [
+ {
+ version: '4.2.2',
+ createdAt: '2021-08-25T15:03:47.000Z',
+ },
+ {
+ version: '4.2.1',
+ createdAt: '2021-08-10T19:59:28.000Z',
+ },
+ {
+ version: '4.2.0',
+ createdAt: '2021-08-10T15:37:58.000Z',
+ },
+ {
+ version: '4.1.5',
+ createdAt: '2021-05-18T14:52:59.000Z',
+ },
+ {
+ version: '4.1.4',
+ createdAt: '2021-03-09T14:49:58.000Z',
+ },
+ {
+ version: '4.1.3',
+ createdAt: '2021-03-05T08:54:12.000Z',
+ },
+ {
+ version: '4.1.2',
+ createdAt: '2021-01-28T10:15:29.000Z',
+ },
+ {
+ version: '4.1.1',
+ createdAt: '2020-12-30T11:51:47.000Z',
+ },
+ {
+ version: '4.1.0',
+ createdAt: '2020-12-28T09:58:47.000Z',
+ },
+ {
+ version: '4.0.2',
+ createdAt: '2020-11-13T14:34:08.000Z',
+ },
+ {
+ version: '4.0.1',
+ createdAt: '2020-09-02T15:16:32.000Z',
+ },
+ {
+ version: '4.0.0',
+ createdAt: '2020-08-26T10:36:59.000Z',
+ },
+ {
+ version: '3.12.4',
+ createdAt: '2020-07-28T08:18:12.000Z',
+ },
+ {
+ version: '3.12.3',
+ createdAt: '2020-07-17T14:24:28.000Z',
+ },
+ {
+ version: '3.12.2',
+ createdAt: '2020-05-28T06:46:27.000Z',
+ },
+ {
+ version: '3.12.1',
+ createdAt: '2020-05-25T07:26:13.000Z',
+ },
+ {
+ version: '3.12.0',
+ createdAt: '2020-05-21T10:16:59.000Z',
+ },
+ {
+ version: '3.11.0',
+ createdAt: '2020-03-23T13:29:01.000Z',
+ },
+ {
+ version: '3.10.5',
+ createdAt: '2019-12-26T15:29:46.000Z',
+ },
+ {
+ version: '3.10.4',
+ createdAt: '2019-08-08T10:11:23.000Z',
+ },
+ {
+ version: '3.10.3',
+ createdAt: '2019-07-26T11:59:53.000Z',
+ },
+ {
+ version: '3.10.2',
+ createdAt: '2019-04-23T17:23:44.000Z',
+ },
+ {
+ version: '3.10.1',
+ createdAt: '2019-03-05T12:17:20.000Z',
+ },
+ {
+ version: '3.10.0',
+ createdAt: '2019-02-15T11:20:40.000Z',
+ },
+ {
+ version: '3.9.1',
+ createdAt: '2018-05-03T08:49:25.000Z',
+ },
+ {
+ version: '3.9.0',
+ createdAt: '2018-03-23T16:37:53.000Z',
+ },
+ {
+ version: '3.8.1',
+ createdAt: '2017-12-21T09:30:44.000Z',
+ },
+ {
+ version: '3.8.0',
+ createdAt: '2017-12-20T14:23:50.000Z',
+ },
+ {
+ version: '3.7.0',
+ createdAt: '2017-10-24T11:57:08.000Z',
+ },
+ {
+ version: '3.6.1',
+ createdAt: '2017-07-26T16:23:09.000Z',
+ },
+ {
+ version: '3.6.0',
+ createdAt: '2017-07-26T15:30:18.000Z',
+ },
+ {
+ version: '3.5.1',
+ createdAt: '2017-07-10T09:47:25.000Z',
+ },
+ {
+ version: '3.5.0',
+ createdAt: '2017-07-05T16:58:20.000Z',
+ },
+ {
+ version: '3.4.0',
+ createdAt: '2017-05-17T13:48:12.000Z',
+ },
+ {
+ version: '3.3.0',
+ createdAt: '2017-02-10T15:50:27.000Z',
+ },
+ {
+ version: '3.2.1',
+ createdAt: '2017-02-02T14:20:53.000Z',
+ },
+ {
+ version: '3.2.0',
+ createdAt: '2016-12-20T18:25:36.000Z',
+ },
+ {
+ version: '3.1.2',
+ createdAt: '2016-11-09T19:12:05.000Z',
+ },
+ {
+ version: '3.1.1',
+ createdAt: '2016-09-27T18:05:38.000Z',
+ },
+ {
+ version: '3.1.0',
+ createdAt: '2016-09-26T19:31:45.000Z',
+ },
+ {
+ version: '3.0.0',
+ createdAt: '2016-07-04T21:17:55.000Z',
+ },
+ {
+ version: '3.0.0-beta8',
+ createdAt: '2016-05-02T08:55:24.000Z',
+ },
+ {
+ version: '3.0.0-beta7',
+ createdAt: '2016-04-14T18:58:43.000Z',
+ },
+ {
+ version: '3.0.0-beta6',
+ createdAt: '2016-04-14T01:10:31.000Z',
+ },
+ {
+ version: '3.0.0-beta5',
+ createdAt: '2016-04-12T14:55:31.000Z',
+ },
+ {
+ version: '3.0.0-beta4',
+ createdAt: '2016-04-10T21:55:49.000Z',
+ },
+ {
+ version: '3.0.0-beta3',
+ createdAt: '2016-04-06T20:23:41.000Z',
+ },
+ ],
+ },
+} as CatalogPlugin;
diff --git a/public/app/features/plugins/admin/__mocks__/index.ts b/public/app/features/plugins/admin/__mocks__/index.ts
new file mode 100644
index 00000000000..3568b5d2a70
--- /dev/null
+++ b/public/app/features/plugins/admin/__mocks__/index.ts
@@ -0,0 +1,3 @@
+export { default as remotePluginMock } from './remotePlugin.mock';
+export { default as localPluginMock } from './localPlugin.mock';
+export * from './mockHelpers';
diff --git a/public/app/features/plugins/admin/__mocks__/localPlugin.mock.ts b/public/app/features/plugins/admin/__mocks__/localPlugin.mock.ts
new file mode 100644
index 00000000000..e1e148e0d5d
--- /dev/null
+++ b/public/app/features/plugins/admin/__mocks__/localPlugin.mock.ts
@@ -0,0 +1,72 @@
+import { LocalPlugin } from '../types';
+
+// Copied from /api/plugins
+export default {
+ name: 'Zabbix',
+ type: 'app',
+ id: 'alexanderzobnin-zabbix-app',
+ enabled: false,
+ pinned: false,
+ info: {
+ author: {
+ name: 'Alexander Zobnin',
+ url: 'https://github.com/alexanderzobnin',
+ },
+ description: 'Zabbix plugin for Grafana',
+ links: [
+ {
+ name: 'GitHub',
+ url: 'https://github.com/alexanderzobnin/grafana-zabbix',
+ },
+ {
+ name: 'Docs',
+ url: 'https://alexanderzobnin.github.io/grafana-zabbix',
+ },
+ {
+ name: 'License',
+ url: 'https://github.com/alexanderzobnin/grafana-zabbix/blob/master/LICENSE',
+ },
+ ],
+ logos: {
+ small: 'public/plugins/alexanderzobnin-zabbix-app/img/icn-zabbix-app.svg',
+ large: 'public/plugins/alexanderzobnin-zabbix-app/img/icn-zabbix-app.svg',
+ },
+ build: {
+ time: 1629903250076,
+ repo: 'git@github.com:alexanderzobnin/grafana-zabbix.git',
+ hash: 'e9db978235cd6d01a095a37f3aa711ea8ea0f7ab',
+ },
+ screenshots: [
+ {
+ path: 'public/plugins/alexanderzobnin-zabbix-app/img/screenshot-showcase.png',
+ name: 'Showcase',
+ },
+ {
+ path: 'public/plugins/alexanderzobnin-zabbix-app/img/screenshot-dashboard01.png',
+ name: 'Dashboard',
+ },
+ {
+ path: 'public/plugins/alexanderzobnin-zabbix-app/img/screenshot-annotations.png',
+ name: 'Annotations',
+ },
+ {
+ path: 'public/plugins/alexanderzobnin-zabbix-app/img/screenshot-metric_editor.png',
+ name: 'Metric Editor',
+ },
+ {
+ path: 'public/plugins/alexanderzobnin-zabbix-app/img/screenshot-triggers.png',
+ name: 'Triggers',
+ },
+ ],
+ version: '4.2.2',
+ updated: '2021-08-25',
+ },
+ latestVersion: '',
+ hasUpdate: false,
+ defaultNavUrl: '/plugins/alexanderzobnin-zabbix-app/',
+ category: '',
+ state: '',
+ signature: 'valid',
+ signatureType: 'community',
+ signatureOrg: 'Alexander Zobnin',
+} as LocalPlugin;
diff --git a/public/app/features/plugins/admin/__mocks__/mockHelpers.ts b/public/app/features/plugins/admin/__mocks__/mockHelpers.ts
new file mode 100644
index 00000000000..93b4d93a163
--- /dev/null
+++ b/public/app/features/plugins/admin/__mocks__/mockHelpers.ts
@@ -0,0 +1,82 @@
+import { setBackendSrv } from '@grafana/runtime';
+import { PluginsState } from 'app/types';
+import { API_ROOT, GRAFANA_API_ROOT } from '../constants';
+import { CatalogPlugin, LocalPlugin, RemotePlugin, Version } from '../types';
+import remotePluginMock from './remotePlugin.mock';
+import localPluginMock from './localPlugin.mock';
+import catalogPluginMock from './catalogPlugin.mock';
+
+// Returns a sample mock for a CatalogPlugin plugin with the possibility to extend it
+export const getCatalogPluginMock = (overrides?: Partial) => ({ ...catalogPluginMock, ...overrides });
+
+// Returns a sample mock for a local (installed) plugin with the possibility to extend it
+export const getLocalPluginMock = (overrides?: Partial) => ({ ...localPluginMock, ...overrides });
+
+// Returns a sample mock for a remote plugin with the possibility to extend it
+export const getRemotePluginMock = (overrides?: Partial) => ({ ...remotePluginMock, ...overrides });
+
+// Returns a mock for the Redux store state of plugins
+export const getPluginsStateMock = (plugins: CatalogPlugin[] = []): PluginsState => ({
+ // @ts-ignore - We don't need the rest of the properties here as we are using the "new" reducer (public/app/features/plugins/admin/state/reducer.ts)
+ items: {
+ ids: plugins.map(({ id }) => id),
+ entities: plugins.reduce((prev, current) => ({ ...prev, [current.id]: current }), {}),
+ },
+ requests: {
+ 'plugins/fetchAll': {
+ status: 'Fulfilled',
+ },
+ 'plugins/fetchDetails': {
+ status: 'Fulfilled',
+ },
+ },
+});
+
+// Mocks a plugin by considering what needs to be mocked from GCOM and what needs to be mocked locally (local Grafana API)
+export const mockPluginApis = ({
+ remote: remoteOverride,
+ local: localOverride,
+ versions,
+}: {
+ remote?: Partial;
+ local?: Partial;
+ versions?: Version[];
+}) => {
+ const remote = getRemotePluginMock(remoteOverride);
+ const local = getLocalPluginMock(localOverride);
+ const original = jest.requireActual('@grafana/runtime');
+ const originalBackendSrv = original.getBackendSrv();
+
+ setBackendSrv({
+ ...originalBackendSrv,
+ get: (path: string) => {
+ // Mock GCOM plugins (remote) if necessary
+ if (remote && path === `${GRAFANA_API_ROOT}/plugins`) {
+ return Promise.resolve({ items: [remote] });
+ }
+
+ // Mock GCOM single plugin page (remote) if necessary
+ if (remote && path === `${GRAFANA_API_ROOT}/plugins/${remote.slug}`) {
+ return Promise.resolve(remote);
+ }
+
+ // Mock versions
+ if (versions && path === `${GRAFANA_API_ROOT}/plugins/${remote.slug}/versions`) {
+ return Promise.resolve({ items: versions });
+ }
+
+ // Mock local plugin settings (installed) if necessary
+ if (local && path === `${API_ROOT}/${local.id}/settings`) {
+ return Promise.resolve(local);
+ }
+
+ // Mock local plugin listing (of necessary)
+ if (local && path === API_ROOT) {
+ return Promise.resolve([local]);
+ }
+
+ // Fall back to the original .get() in other cases
+ return originalBackendSrv.get(path);
+ },
+ });
+};
diff --git a/public/app/features/plugins/admin/__mocks__/remotePlugin.mock.ts b/public/app/features/plugins/admin/__mocks__/remotePlugin.mock.ts
new file mode 100644
index 00000000000..903d6b7435b
--- /dev/null
+++ b/public/app/features/plugins/admin/__mocks__/remotePlugin.mock.ts
@@ -0,0 +1,48 @@
+import { PluginSignatureType, PluginType } from '@grafana/data';
+import { RemotePlugin } from '../types';
+
+// Copied from /api/gnet/plugins/alexanderzobnin-zabbix-app
+export default {
+ createdAt: '2016-04-06T20:23:41.000Z',
+ description: 'Zabbix plugin for Grafana',
+ downloads: 33645089,
+ featured: 180,
+ id: 74,
+ typeId: 1,
+ typeName: 'Application',
+ internal: false,
+ links: [],
+ name: 'Zabbix',
+ orgId: 13056,
+ orgName: 'Alexander Zobnin',
+ orgSlug: 'alexanderzobnin',
+ orgUrl: 'https://github.com/alexanderzobnin',
+ url: 'https://github.com/alexanderzobnin/grafana-zabbix',
+ verified: false,
+ downloadSlug: 'alexanderzobnin-zabbix-app',
+ packages: {},
+ popularity: 0.2111,
+ signatureType: PluginSignatureType.community,
+ slug: 'alexanderzobnin-zabbix-app',
+ status: 'active',
+ typeCode: PluginType.app,
+ updatedAt: '2021-05-18T14:53:01.000Z',
+ version: '4.1.5',
+ versionStatus: 'active',
+ versionSignatureType: PluginSignatureType.community,
+ versionSignedByOrg: 'alexanderzobnin',
+ versionSignedByOrgName: 'Alexander Zobnin',
+ userId: 0,
+ readme:
+ 'Zabbix plugin for Grafana
\n:copyright: 2015-2021 Alexander Zobnin alexanderzobnin@gmail.com
\nLicensed under the Apache 2.0 License
',
+ json: {
+ dependencies: {
+ grafanaDependency: '>=7.3.0',
+ grafanaVersion: '7.3',
+ plugins: [],
+ },
+ info: {
+ links: [],
+ },
+ },
+} as RemotePlugin;
diff --git a/public/app/features/plugins/admin/api.ts b/public/app/features/plugins/admin/api.ts
index 7740d4a5969..5eec02b8c92 100644
--- a/public/app/features/plugins/admin/api.ts
+++ b/public/app/features/plugins/admin/api.ts
@@ -1,7 +1,16 @@
import { getBackendSrv } from '@grafana/runtime';
import { API_ROOT, GRAFANA_API_ROOT } from './constants';
-import { PluginDetails, Org, LocalPlugin, RemotePlugin, CatalogPlugin, CatalogPluginDetails } from './types';
import { mergeLocalsAndRemotes, mergeLocalAndRemote } from './helpers';
+import {
+ PluginDetails,
+ Org,
+ LocalPlugin,
+ RemotePlugin,
+ CatalogPlugin,
+ CatalogPluginDetails,
+ Version,
+ PluginVersion,
+} from './types';
export async function getCatalogPlugins(): Promise {
const [localPlugins, remotePlugins] = await Promise.all([getLocalPlugins(), getRemotePlugins()]);
@@ -69,10 +78,13 @@ async function getRemotePlugin(id: string, isInstalled: boolean): Promise {
+async function getPluginVersions(id: string): Promise {
try {
- const versions = await getBackendSrv().get(`${GRAFANA_API_ROOT}/plugins/${id}/versions`);
- return versions.items;
+ const versions: { items: PluginVersion[] } = await getBackendSrv().get(
+ `${GRAFANA_API_ROOT}/plugins/${id}/versions`
+ );
+
+ return (versions.items || []).map(({ version, createdAt }) => ({ version, createdAt }));
} catch (error) {
return [];
}
diff --git a/public/app/features/plugins/admin/pages/Browse.test.tsx b/public/app/features/plugins/admin/pages/Browse.test.tsx
index c0dbd0b5d50..e3ee579f084 100644
--- a/public/app/features/plugins/admin/pages/Browse.test.tsx
+++ b/public/app/features/plugins/admin/pages/Browse.test.tsx
@@ -3,38 +3,22 @@ import { Router } from 'react-router-dom';
import { render, RenderResult, waitFor, within } from '@testing-library/react';
import { Provider } from 'react-redux';
import { locationService } from '@grafana/runtime';
-import { PluginSignatureStatus, PluginSignatureType, PluginType } from '@grafana/data';
-import BrowsePage from './Browse';
+import { PluginType } from '@grafana/data';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { configureStore } from 'app/store/configureStore';
-import { LocalPlugin, RemotePlugin, PluginAdminRoutes } from '../types';
-import { API_ROOT, GRAFANA_API_ROOT } from '../constants';
+import { PluginAdminRoutes, CatalogPlugin } from '../types';
+import { getCatalogPluginMock, getPluginsStateMock } from '../__mocks__';
+import BrowsePage from './Browse';
+// Mock the config to enable the plugin catalog
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
- return {
- ...original,
- getBackendSrv: () => ({
- get: (path: string) => {
- switch (path) {
- case `${GRAFANA_API_ROOT}/plugins`:
- return Promise.resolve({ items: remote });
- case API_ROOT:
- return Promise.resolve(installed);
- default:
- return Promise.reject();
- }
- },
- }),
- config: {
- ...original.config,
- pluginAdminEnabled: true,
- },
- };
+
+ return { ...original, pluginAdminEnabled: true };
});
-function setup(path = '/plugins'): RenderResult {
- const store = configureStore();
+const renderBrowse = (path = '/plugins', plugins: CatalogPlugin[] = []): RenderResult => {
+ const store = configureStore({ plugins: getPluginsStateMock(plugins) });
locationService.push(path);
const props = getRouteComponentProps({
route: { routeName: PluginAdminRoutes.Home } as any,
@@ -47,108 +31,142 @@ function setup(path = '/plugins'): RenderResult {
);
-}
+};
describe('Browse list of plugins', () => {
describe('when filtering', () => {
it('should list installed plugins by default', async () => {
- const { queryByText } = setup('/plugins');
+ const { queryByText } = renderBrowse('/plugins', [
+ getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', isInstalled: true }),
+ getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', isInstalled: true }),
+ getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', isInstalled: true }),
+ getCatalogPluginMock({ id: 'plugin-4', name: 'Plugin 4', isInstalled: false }),
+ ]);
- await waitFor(() => queryByText('Installed'));
+ await waitFor(() => expect(queryByText('Plugin 1')).toBeInTheDocument());
+ expect(queryByText('Plugin 1')).toBeInTheDocument();
+ expect(queryByText('Plugin 2')).toBeInTheDocument();
+ expect(queryByText('Plugin 3')).toBeInTheDocument();
- for (const plugin of installed) {
- expect(queryByText(plugin.name)).toBeInTheDocument();
- }
-
- for (const plugin of remote) {
- expect(queryByText(plugin.name)).toBeNull();
- }
+ expect(queryByText('Plugin 4')).toBeNull();
});
it('should list all plugins (except core plugins) when filtering by all', async () => {
- const { queryByText } = setup('/plugins?filterBy=all&filterByType=all');
+ const { queryByText } = renderBrowse('/plugins?filterBy=all&filterByType=all', [
+ getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', isInstalled: true }),
+ getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', isInstalled: false }),
+ getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', isInstalled: true }),
+ getCatalogPluginMock({ id: 'plugin-4', name: 'Plugin 4', isInstalled: true, isCore: true }),
+ ]);
- await waitFor(() => expect(queryByText('Diagram')).toBeInTheDocument());
- for (const plugin of remote) {
- expect(queryByText(plugin.name)).toBeInTheDocument();
- }
+ await waitFor(() => expect(queryByText('Plugin 1')).toBeInTheDocument());
+ expect(queryByText('Plugin 2')).toBeInTheDocument();
+ expect(queryByText('Plugin 3')).toBeInTheDocument();
- expect(queryByText('Alert Manager')).not.toBeInTheDocument();
+ // Core plugins should not be listed
+ expect(queryByText('Plugin 4')).not.toBeInTheDocument();
});
it('should list installed plugins (including core plugins) when filtering by installed', async () => {
- const { queryByText } = setup('/plugins?filterBy=installed');
+ const { queryByText } = renderBrowse('/plugins?filterBy=installed', [
+ getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', isInstalled: true }),
+ getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', isInstalled: false }),
+ getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', isInstalled: true }),
+ getCatalogPluginMock({ id: 'plugin-4', name: 'Plugin 4', isInstalled: true, isCore: true }),
+ ]);
- await waitFor(() => queryByText('Installed'));
+ await waitFor(() => expect(queryByText('Plugin 1')).toBeInTheDocument());
+ expect(queryByText('Plugin 3')).toBeInTheDocument();
+ expect(queryByText('Plugin 4')).toBeInTheDocument();
- for (const plugin of installed) {
- expect(queryByText(plugin.name)).toBeInTheDocument();
- }
-
- for (const plugin of remote) {
- expect(queryByText(plugin.name)).not.toBeInTheDocument();
- }
+ // Not showing not installed plugins
+ expect(queryByText('Plugin 2')).not.toBeInTheDocument();
});
- it('should list enterprise plugins', async () => {
- const { queryByText } = setup('/plugins?filterBy=all&q=wavefront');
+ it('should list enterprise plugins when querying for them', async () => {
+ const { queryByText } = renderBrowse('/plugins?filterBy=all&q=wavefront', [
+ getCatalogPluginMock({ id: 'wavefront', name: 'Wavefront', isInstalled: true, isEnterprise: true }),
+ getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', isInstalled: true, isCore: true }),
+ getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', isInstalled: true }),
+ ]);
await waitFor(() => expect(queryByText('Wavefront')).toBeInTheDocument());
+
+ // Should not show plugins that don't match the query
+ expect(queryByText('Plugin 2')).not.toBeInTheDocument();
+ expect(queryByText('Plugin 3')).not.toBeInTheDocument();
});
it('should list only datasource plugins when filtering by datasource', async () => {
- const { queryByText } = setup('/plugins?filterBy=all&filterByType=datasource');
+ const { queryByText } = renderBrowse('/plugins?filterBy=all&filterByType=datasource', [
+ getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', type: PluginType.app }),
+ getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', type: PluginType.datasource }),
+ getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', type: PluginType.panel }),
+ ]);
- await waitFor(() => expect(queryByText('Wavefront')).toBeInTheDocument());
+ await waitFor(() => expect(queryByText('Plugin 2')).toBeInTheDocument());
- expect(queryByText('Alert Manager')).not.toBeInTheDocument();
- expect(queryByText('Diagram')).not.toBeInTheDocument();
- expect(queryByText('Zabbix')).not.toBeInTheDocument();
- expect(queryByText('ACE.SVG')).not.toBeInTheDocument();
+ // Other plugin types shouldn't be shown
+ expect(queryByText('Plugin 1')).not.toBeInTheDocument();
+ expect(queryByText('Plugin 3')).not.toBeInTheDocument();
});
it('should list only panel plugins when filtering by panel', async () => {
- const { queryByText } = setup('/plugins?filterBy=all&filterByType=panel');
+ const { queryByText } = renderBrowse('/plugins?filterBy=all&filterByType=panel', [
+ getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', type: PluginType.app }),
+ getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', type: PluginType.datasource }),
+ getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', type: PluginType.panel }),
+ ]);
- await waitFor(() => expect(queryByText('Diagram')).toBeInTheDocument());
- expect(queryByText('ACE.SVG')).toBeInTheDocument();
+ await waitFor(() => expect(queryByText('Plugin 3')).toBeInTheDocument());
- expect(queryByText('Wavefront')).not.toBeInTheDocument();
- expect(queryByText('Alert Manager')).not.toBeInTheDocument();
- expect(queryByText('Zabbix')).not.toBeInTheDocument();
+ // Other plugin types shouldn't be shown
+ expect(queryByText('Plugin 1')).not.toBeInTheDocument();
+ expect(queryByText('Plugin 2')).not.toBeInTheDocument();
});
it('should list only app plugins when filtering by app', async () => {
- const { queryByText } = setup('/plugins?filterBy=all&filterByType=app');
+ const { queryByText } = renderBrowse('/plugins?filterBy=all&filterByType=app', [
+ getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', type: PluginType.app }),
+ getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', type: PluginType.datasource }),
+ getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', type: PluginType.panel }),
+ ]);
- await waitFor(() => expect(queryByText('Zabbix')).toBeInTheDocument());
+ await waitFor(() => expect(queryByText('Plugin 1')).toBeInTheDocument());
- expect(queryByText('Wavefront')).not.toBeInTheDocument();
- expect(queryByText('Alert Manager')).not.toBeInTheDocument();
- expect(queryByText('Diagram')).not.toBeInTheDocument();
- expect(queryByText('ACE.SVG')).not.toBeInTheDocument();
+ // Other plugin types shouldn't be shown
+ expect(queryByText('Plugin 2')).not.toBeInTheDocument();
+ expect(queryByText('Plugin 3')).not.toBeInTheDocument();
});
});
describe('when searching', () => {
it('should only list plugins matching search', async () => {
- const { queryByText } = setup('/plugins?filterBy=all&q=zabbix');
+ const { queryByText } = renderBrowse('/plugins?filterBy=all&q=zabbix', [
+ getCatalogPluginMock({ id: 'zabbix', name: 'Zabbix' }),
+ getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2' }),
+ getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3' }),
+ ]);
await waitFor(() => expect(queryByText('Zabbix')).toBeInTheDocument());
- expect(queryByText('Wavefront')).not.toBeInTheDocument();
- expect(queryByText('Alert Manager')).not.toBeInTheDocument();
- expect(queryByText('Diagram')).not.toBeInTheDocument();
- expect(queryByText('Redis Application')).not.toBeInTheDocument();
+ // Other plugin types shouldn't be shown
+ expect(queryByText('Plugin 2')).not.toBeInTheDocument();
+ expect(queryByText('Plugin 3')).not.toBeInTheDocument();
});
});
describe('when sorting', () => {
it('should sort plugins by name in ascending alphabetical order', async () => {
- const { findByTestId } = setup('/plugins?filterBy=all');
+ const { findByTestId } = renderBrowse('/plugins?filterBy=all', [
+ getCatalogPluginMock({ id: 'wavefront', name: 'Wavefront' }),
+ getCatalogPluginMock({ id: 'redis-application', name: 'Redis Application' }),
+ getCatalogPluginMock({ id: 'zabbix', name: 'Zabbix' }),
+ getCatalogPluginMock({ id: 'diagram', name: 'Diagram' }),
+ getCatalogPluginMock({ id: 'acesvg', name: 'ACE.SVG' }),
+ ]);
const pluginList = await findByTestId('plugin-list');
const pluginHeadings = within(pluginList).queryAllByRole('heading');
-
expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
'ACE.SVG',
'Diagram',
@@ -159,11 +177,16 @@ describe('Browse list of plugins', () => {
});
it('should sort plugins by name in descending alphabetical order', async () => {
- const { findByTestId } = setup('/plugins?filterBy=all&sortBy=nameDesc');
+ const { findByTestId } = renderBrowse('/plugins?filterBy=all&sortBy=nameDesc', [
+ getCatalogPluginMock({ id: 'wavefront', name: 'Wavefront' }),
+ getCatalogPluginMock({ id: 'redis-application', name: 'Redis Application' }),
+ getCatalogPluginMock({ id: 'zabbix', name: 'Zabbix' }),
+ getCatalogPluginMock({ id: 'diagram', name: 'Diagram' }),
+ getCatalogPluginMock({ id: 'acesvg', name: 'ACE.SVG' }),
+ ]);
const pluginList = await findByTestId('plugin-list');
const pluginHeadings = within(pluginList).queryAllByRole('heading');
-
expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
'Zabbix',
'Wavefront',
@@ -174,11 +197,16 @@ describe('Browse list of plugins', () => {
});
it('should sort plugins by date in ascending updated order', async () => {
- const { findByTestId } = setup('/plugins?filterBy=all&sortBy=updated');
+ const { findByTestId } = renderBrowse('/plugins?filterBy=all&sortBy=updated', [
+ getCatalogPluginMock({ id: '1', name: 'Wavefront', updatedAt: '2021-04-01T00:00:00.000Z' }),
+ getCatalogPluginMock({ id: '2', name: 'Redis Application', updatedAt: '2021-02-01T00:00:00.000Z' }),
+ getCatalogPluginMock({ id: '3', name: 'Zabbix', updatedAt: '2021-01-01T00:00:00.000Z' }),
+ getCatalogPluginMock({ id: '4', name: 'Diagram', updatedAt: '2021-05-01T00:00:00.000Z' }),
+ getCatalogPluginMock({ id: '5', name: 'ACE.SVG', updatedAt: '2021-02-01T00:00:00.000Z' }),
+ ]);
const pluginList = await findByTestId('plugin-list');
const pluginHeadings = within(pluginList).queryAllByRole('heading');
-
expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
'Diagram',
'Wavefront',
@@ -189,26 +217,36 @@ describe('Browse list of plugins', () => {
});
it('should sort plugins by date in ascending published order', async () => {
- const { findByTestId } = setup('/plugins?filterBy=all&sortBy=published');
+ const { findByTestId } = renderBrowse('/plugins?filterBy=all&sortBy=published', [
+ getCatalogPluginMock({ id: '1', name: 'Wavefront', publishedAt: '2021-04-01T00:00:00.000Z' }),
+ getCatalogPluginMock({ id: '2', name: 'Redis Application', publishedAt: '2021-02-01T00:00:00.000Z' }),
+ getCatalogPluginMock({ id: '3', name: 'Zabbix', publishedAt: '2021-01-01T00:00:00.000Z' }),
+ getCatalogPluginMock({ id: '4', name: 'Diagram', publishedAt: '2021-05-01T00:00:00.000Z' }),
+ getCatalogPluginMock({ id: '5', name: 'ACE.SVG', publishedAt: '2021-02-01T00:00:00.000Z' }),
+ ]);
const pluginList = await findByTestId('plugin-list');
const pluginHeadings = within(pluginList).queryAllByRole('heading');
-
expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
'Diagram',
+ 'Wavefront',
'Redis Application',
'ACE.SVG',
- 'Wavefront',
'Zabbix',
]);
});
it('should sort plugins by number of downloads in ascending order', async () => {
- const { findByTestId } = setup('/plugins?filterBy=all&sortBy=downloads');
+ const { findByTestId } = renderBrowse('/plugins?filterBy=all&sortBy=downloads', [
+ getCatalogPluginMock({ id: '1', name: 'Wavefront', downloads: 30 }),
+ getCatalogPluginMock({ id: '2', name: 'Redis Application', downloads: 10 }),
+ getCatalogPluginMock({ id: '3', name: 'Zabbix', downloads: 50 }),
+ getCatalogPluginMock({ id: '4', name: 'Diagram', downloads: 20 }),
+ getCatalogPluginMock({ id: '5', name: 'ACE.SVG', downloads: 40 }),
+ ]);
const pluginList = await findByTestId('plugin-list');
const pluginHeadings = within(pluginList).queryAllByRole('heading');
-
expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
'Zabbix',
'ACE.SVG',
@@ -219,215 +257,3 @@ describe('Browse list of plugins', () => {
});
});
});
-
-const installed: LocalPlugin[] = [
- {
- name: 'Alert Manager',
- type: PluginType.datasource,
- id: 'alertmanager',
- enabled: true,
- pinned: false,
- info: {
- author: {
- name: 'Prometheus alertmanager',
- url: 'https://grafana.com',
- },
- description: '',
- links: [
- {
- name: 'Learn more',
- url: 'https://prometheus.io/docs/alerting/latest/alertmanager/',
- },
- ],
- logos: {
- small: 'public/app/plugins/datasource/alertmanager/img/logo.svg',
- large: 'public/app/plugins/datasource/alertmanager/img/logo.svg',
- },
- build: {},
- screenshots: null,
- version: '',
- updated: '',
- },
- latestVersion: '',
- hasUpdate: false,
- defaultNavUrl: '/plugins/alertmanager/',
- category: '',
- state: 'alpha',
- signature: PluginSignatureStatus.internal,
- signatureType: PluginSignatureType.core,
- signatureOrg: '',
- },
- {
- name: 'Diagram',
- type: PluginType.panel,
- id: 'jdbranham-diagram-panel',
- enabled: true,
- pinned: false,
- info: {
- author: { name: 'Jeremy Branham', url: 'https://savantly.net' },
- description: 'Display diagrams and charts with colored metric indicators',
- links: [
- {
- name: 'Project site',
- url: 'https://github.com/jdbranham/grafana-diagram',
- },
- {
- name: 'Apache License',
- url: 'https://github.com/jdbranham/grafana-diagram/blob/master/LICENSE',
- },
- ],
- logos: {
- small: 'public/plugins/jdbranham-diagram-panel/img/logo.svg',
- large: 'public/plugins/jdbranham-diagram-panel/img/logo.svg',
- },
- build: {},
- screenshots: [],
- version: '1.7.3',
- updated: '2021-07-20',
- },
- latestVersion: '1.7.3',
- hasUpdate: true,
- defaultNavUrl: '/plugins/jdbranham-diagram-panel/',
- category: '',
- state: '',
- signature: PluginSignatureStatus.missing,
- signatureType: PluginSignatureType.core,
- signatureOrg: '',
- },
- {
- name: 'Redis Application',
- type: PluginType.app,
- id: 'redis-app',
- enabled: false,
- pinned: false,
- info: {
- author: {
- name: 'RedisGrafana',
- url: 'https://redisgrafana.github.io',
- },
- description: 'Provides Application pages and custom panels for Redis Data Source.',
- links: [
- { name: 'Website', url: 'https://redisgrafana.github.io' },
- {
- name: 'License',
- url: 'https://github.com/RedisGrafana/grafana-redis-app/blob/master/LICENSE',
- },
- ],
- logos: {
- small: 'public/plugins/redis-app/img/logo.svg',
- large: 'public/plugins/redis-app/img/logo.svg',
- },
- build: {},
- screenshots: [],
- version: '2.0.1',
- updated: '2021-07-07',
- },
- latestVersion: '2.0.1',
- hasUpdate: false,
- defaultNavUrl: '/plugins/redis-app/',
- category: '',
- state: '',
- signature: PluginSignatureStatus.valid,
- signatureType: PluginSignatureType.commercial,
- signatureOrg: 'RedisGrafana',
- },
-];
-
-const remote: RemotePlugin[] = [
- {
- status: 'active',
- id: 74,
- typeId: 1,
- typeName: 'Application',
- typeCode: PluginType.app,
- slug: 'alexanderzobnin-zabbix-app',
- name: 'Zabbix',
- description: 'Zabbix plugin for Grafana',
- version: '4.1.5',
- versionStatus: 'active',
- versionSignatureType: PluginSignatureType.community,
- versionSignedByOrg: 'alexanderzobnin',
- versionSignedByOrgName: 'Alexander Zobnin',
- userId: 0,
- orgId: 13056,
- orgName: 'Alexander Zobnin',
- orgSlug: 'alexanderzobnin',
- orgUrl: 'https://github.com/alexanderzobnin',
- url: 'https://github.com/alexanderzobnin/grafana-zabbix',
- createdAt: '2016-04-06T20:23:41.000Z',
- updatedAt: '2021-05-18T14:53:01.000Z',
- downloads: 34387994,
- verified: false,
- featured: 180,
- internal: false,
- downloadSlug: 'alexanderzobnin-zabbix-app',
- popularity: 0.2019,
- signatureType: PluginSignatureType.community,
- packages: {},
- links: [],
- },
- {
- status: 'enterprise',
- id: 658,
- typeId: 2,
- typeName: 'Data Source',
- typeCode: PluginType.datasource,
- slug: 'grafana-wavefront-datasource',
- name: 'Wavefront',
- description: 'Wavefront Datasource',
- version: '1.0.8',
- versionStatus: 'active',
- versionSignatureType: PluginSignatureType.grafana,
- versionSignedByOrg: 'grafana',
- versionSignedByOrgName: 'Grafana Labs',
- userId: 0,
- orgId: 5000,
- orgName: 'Grafana Labs',
- orgSlug: 'grafana',
- orgUrl: 'https://grafana.org',
- url: 'https://github.com/grafana/wavefront-datasource/',
- createdAt: '2020-09-01T13:02:57.000Z',
- updatedAt: '2021-07-12T18:41:03.000Z',
- downloads: 7818,
- verified: false,
- featured: 0,
- internal: false,
- downloadSlug: 'grafana-wavefront-datasource',
- popularity: 0.0107,
- signatureType: PluginSignatureType.grafana,
- packages: {},
- links: [],
- },
- {
- status: 'active',
- id: 659,
- typeId: 3,
- typeName: 'Panel',
- typeCode: PluginType.panel,
- slug: 'aceiot-svg-panel',
- name: 'ACE.SVG',
- description: 'SVG Visualization Panel',
- version: '0.0.10',
- versionStatus: 'active',
- versionSignatureType: PluginSignatureType.community,
- versionSignedByOrg: 'aceiot',
- versionSignedByOrgName: 'Andrew Rodgers',
- userId: 0,
- orgId: 409764,
- orgName: 'Andrew Rodgers',
- orgSlug: 'aceiot',
- orgUrl: '',
- url: 'https://github.com/ACE-IoT-Solutions/ace-svg-react',
- createdAt: '2020-09-01T14:46:44.000Z',
- updatedAt: '2021-06-28T14:01:36.000Z',
- downloads: 101569,
- verified: false,
- featured: 0,
- internal: false,
- downloadSlug: 'aceiot-svg-panel',
- popularity: 0.0134,
- signatureType: PluginSignatureType.community,
- packages: {},
- links: [],
- },
-];
diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
index 4cfa60aa45c..6597c5bab9b 100644
--- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
+++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
@@ -3,93 +3,41 @@ import { Provider } from 'react-redux';
import { render, RenderResult, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { config } from '@grafana/runtime';
-import { PluginSignatureStatus, PluginSignatureType, PluginType } from '@grafana/data';
import { configureStore } from 'app/store/configureStore';
import PluginDetailsPage from './PluginDetails';
-import { API_ROOT, GRAFANA_API_ROOT } from '../constants';
-import { LocalPlugin, RemotePlugin } from '../types';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
+import { CatalogPlugin } from '../types';
+import { mockPluginApis, getCatalogPluginMock, getPluginsStateMock } from '../__mocks__';
+// Mock the config to enable the plugin catalog
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
+ const mockedRuntime = { ...original };
- return {
- ...original,
- getBackendSrv: () => ({
- get: (path: string) => {
- switch (path) {
- case `${GRAFANA_API_ROOT}/plugins/not-installed/versions`:
- case `${GRAFANA_API_ROOT}/plugins/enterprise/versions`:
- return Promise.resolve([]);
- case `${GRAFANA_API_ROOT}/plugins/installed/versions`:
- return Promise.resolve({
- items: [
- {
- version: '1.0.0',
- createdAt: '2016-04-06T20:23:41.000Z',
- },
- ],
- });
- case API_ROOT:
- return Promise.resolve([
- localPlugin(),
- localPlugin({ id: 'installed', signature: PluginSignatureStatus.valid }),
- localPlugin({ id: 'has-update', signature: PluginSignatureStatus.valid }),
- localPlugin({ id: 'core', signature: PluginSignatureStatus.internal }),
- ]);
- case `${GRAFANA_API_ROOT}/plugins/core`:
- return Promise.resolve(localPlugin({ id: 'core', signature: PluginSignatureStatus.internal }));
- case `${GRAFANA_API_ROOT}/plugins/not-installed`:
- return Promise.resolve(remotePlugin());
- case `${GRAFANA_API_ROOT}/plugins/has-update`:
- return Promise.resolve(remotePlugin({ slug: 'has-update', version: '2.0.0' }));
- case `${GRAFANA_API_ROOT}/plugins/installed`:
- return Promise.resolve(remotePlugin({ slug: 'installed' }));
- case `${GRAFANA_API_ROOT}/plugins/enterprise`:
- return Promise.resolve(remotePlugin({ status: 'enterprise' }));
- case `${GRAFANA_API_ROOT}/plugins`:
- return Promise.resolve({
- items: [
- remotePlugin({ slug: 'not-installed' }),
- remotePlugin({ slug: 'installed' }),
- remotePlugin({ slug: 'has-update', version: '2.0.0' }),
- remotePlugin({ slug: 'enterprise', status: 'enterprise' }),
- ],
- });
- default:
- return Promise.reject();
- }
- },
- }),
- config: {
- ...original.config,
- bootData: {
- ...original.config.bootData,
- user: {
- ...original.config.bootData.user,
- isGrafanaAdmin: true,
- },
- },
- buildInfo: {
- ...original.config.buildInfo,
- version: 'v7.5.0',
- },
- pluginAdminEnabled: true,
- },
- };
+ mockedRuntime.config.bootData.user.isGrafanaAdmin = true;
+ mockedRuntime.config.buildInfo.version = 'v8.1.0';
+ mockedRuntime.config.pluginAdminEnabled = true;
+
+ return mockedRuntime;
});
-function setup(pluginId: string): RenderResult {
- const props = getRouteComponentProps({ match: { params: { pluginId }, isExact: true, url: '', path: '' } });
- const store = configureStore();
+const renderPluginDetails = (pluginOverride: Partial): RenderResult => {
+ const plugin = getCatalogPluginMock(pluginOverride);
+ const { id } = plugin;
+ const props = getRouteComponentProps({ match: { params: { pluginId: id }, isExact: true, url: '', path: '' } });
+ const store = configureStore({
+ plugins: getPluginsStateMock([plugin]),
+ });
+
return render(
);
-}
+};
describe('Plugin details page', () => {
+ const id = 'my-plugin';
let dateNow: any;
beforeAll(() => {
@@ -104,15 +52,50 @@ describe('Plugin details page', () => {
dateNow.mockRestore();
});
- it('should display an overview (plugin readme) by default', async () => {
- const { queryByText } = setup('not-installed');
+ // We are doing this very basic test to see if the API fetching and data-munging is working correctly from a high-level.
+ it('(SMOKE TEST) - should fetch and merge the remote and local plugin API responses correctly ', async () => {
+ const id = 'smoke-test-plugin';
+
+ mockPluginApis({
+ remote: { slug: id },
+ local: { id },
+ });
+
+ const props = getRouteComponentProps({ match: { params: { pluginId: id }, isExact: true, url: '', path: '' } });
+ const store = configureStore();
+ const { queryByText } = render(
+
+
+
+ );
await waitFor(() => expect(queryByText(/licensed under the apache 2.0 license/i)).toBeInTheDocument());
});
- it('should display version history', async () => {
- const { queryByText, getByText, getByRole } = setup('installed');
+ it('should display an overview (plugin readme) by default', async () => {
+ const { queryByText } = renderPluginDetails({ id });
+
+ await waitFor(() => expect(queryByText(/licensed under the apache 2.0 license/i)).toBeInTheDocument());
+ });
+
+ it('should display version history in case it is available', async () => {
+ const { queryByText, getByText, getByRole } = renderPluginDetails({
+ id,
+ details: {
+ links: [],
+ versions: [
+ {
+ version: '1.0.0',
+ createdAt: '2016-04-06T20:23:41.000Z',
+ },
+ ],
+ },
+ });
+
+ // Check if version information is available
await waitFor(() => expect(queryByText(/version history/i)).toBeInTheDocument());
+
+ // Go to the versions tab
userEvent.click(getByText(/version history/i));
expect(
getByRole('columnheader', {
@@ -136,35 +119,42 @@ describe('Plugin details page', () => {
).toBeInTheDocument();
});
- it("should display install button for a plugin that isn't installed", async () => {
- const { queryByRole } = setup('not-installed');
+ it("should display an install button for a plugin that isn't installed", async () => {
+ const { queryByRole } = renderPluginDetails({ id, isInstalled: false });
await waitFor(() => expect(queryByRole('button', { name: /install/i })).toBeInTheDocument());
expect(queryByRole('button', { name: /uninstall/i })).not.toBeInTheDocument();
});
- it('should display uninstall button for an installed plugin', async () => {
- const { queryByRole } = setup('installed');
+ it('should display an uninstall button for an already installed plugin', async () => {
+ const { queryByRole } = renderPluginDetails({ id, isInstalled: true });
+
await waitFor(() => expect(queryByRole('button', { name: /uninstall/i })).toBeInTheDocument());
});
it('should display update and uninstall buttons for a plugin with update', async () => {
- const { queryByRole } = setup('has-update');
+ const { queryByRole } = renderPluginDetails({ id, isInstalled: true, hasUpdate: true });
+ // Displays an "update" button
await waitFor(() => expect(queryByRole('button', { name: /update/i })).toBeInTheDocument());
+
+ // Does not display "install" and "uninstall" buttons
+ expect(queryByRole('button', { name: /install/i })).toBeInTheDocument();
expect(queryByRole('button', { name: /uninstall/i })).toBeInTheDocument();
});
- it('should display install button for enterprise plugins if license is valid', async () => {
+ it('should display an install button for enterprise plugins if license is valid', async () => {
config.licenseInfo.hasValidLicense = true;
- const { queryByRole } = setup('enterprise');
+
+ const { queryByRole } = renderPluginDetails({ id, isInstalled: false, isEnterprise: true });
await waitFor(() => expect(queryByRole('button', { name: /install/i })).toBeInTheDocument());
});
it('should not display install button for enterprise plugins if license is invalid', async () => {
config.licenseInfo.hasValidLicense = false;
- const { queryByRole, queryByText } = setup('enterprise');
+
+ const { queryByRole, queryByText } = renderPluginDetails({ id, isInstalled: true, isEnterprise: true });
await waitFor(() => expect(queryByRole('button', { name: /install/i })).not.toBeInTheDocument());
expect(queryByText(/no valid Grafana Enterprise license detected/i)).toBeInTheDocument();
@@ -172,126 +162,50 @@ describe('Plugin details page', () => {
});
it('should not display install / uninstall buttons for core plugins', async () => {
- const { queryByRole } = setup('core');
+ const { queryByRole } = renderPluginDetails({ id, isInstalled: true, isCore: true });
+ await waitFor(() => expect(queryByRole('button', { name: /update/i })).not.toBeInTheDocument());
await waitFor(() => expect(queryByRole('button', { name: /(un)?install/i })).not.toBeInTheDocument());
});
- it('should display install link with pluginAdminExternalManageEnabled true', async () => {
+ it('should display install link with `config.pluginAdminExternalManageEnabled` set to true', async () => {
config.pluginAdminExternalManageEnabled = true;
- const { queryByRole } = setup('not-installed');
+
+ const { queryByRole } = renderPluginDetails({ id, isInstalled: false });
await waitFor(() => expect(queryByRole('link', { name: /install via grafana.com/i })).toBeInTheDocument());
});
- it('should display uninstall link for an installed plugin with pluginAdminExternalManageEnabled true', async () => {
+ it('should display uninstall link for an installed plugin with `config.pluginAdminExternalManageEnabled` set to true', async () => {
config.pluginAdminExternalManageEnabled = true;
- const { queryByRole } = setup('installed');
+
+ const { queryByRole } = renderPluginDetails({ id, isInstalled: true });
+
await waitFor(() => expect(queryByRole('link', { name: /uninstall via grafana.com/i })).toBeInTheDocument());
});
- it('should display update and uninstall links for a plugin with update and pluginAdminExternalManageEnabled true', async () => {
+ it('should display update and uninstall links for a plugin with an available update and `config.pluginAdminExternalManageEnabled` set to true', async () => {
config.pluginAdminExternalManageEnabled = true;
- const { queryByRole } = setup('has-update');
+
+ const { queryByRole } = renderPluginDetails({ id, isInstalled: true, hasUpdate: true });
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');
+ const { queryByText } = renderPluginDetails({
+ id,
+ details: {
+ pluginDependencies: [],
+ grafanaDependency: '>=8.0.0',
+ links: [],
+ },
+ });
// Wait for the dependencies part to be loaded
await waitFor(() => expect(queryByText(/dependencies:/i)).toBeInTheDocument());
- expect(queryByText('Grafana >=7.3.0')).toBeInTheDocument();
+ expect(queryByText('Grafana >=8.0.0')).toBeInTheDocument();
});
});
-
-function remotePlugin(plugin: Partial = {}): RemotePlugin {
- return {
- createdAt: '2016-04-06T20:23:41.000Z',
- description: 'Zabbix plugin for Grafana',
- downloads: 33645089,
- featured: 180,
- id: 74,
- typeId: 1,
- typeName: 'Application',
- internal: false,
- links: [],
- name: 'Zabbix',
- orgId: 13056,
- orgName: 'Alexander Zobnin',
- orgSlug: 'alexanderzobnin',
- orgUrl: 'https://github.com/alexanderzobnin',
- url: 'https://github.com/alexanderzobnin/grafana-zabbix',
- verified: false,
- downloadSlug: 'alexanderzobnin-zabbix-app',
- packages: {},
- popularity: 0.2111,
- signatureType: PluginSignatureType.community,
- slug: 'alexanderzobnin-zabbix-app',
- status: 'active',
- typeCode: PluginType.app,
- updatedAt: '2021-05-18T14:53:01.000Z',
- version: '4.1.5',
- versionStatus: 'active',
- versionSignatureType: PluginSignatureType.community,
- versionSignedByOrg: 'alexanderzobnin',
- versionSignedByOrgName: 'Alexander Zobnin',
- userId: 0,
- readme:
- 'Zabbix plugin for Grafana
\n:copyright: 2015-2021 Alexander Zobnin alexanderzobnin@gmail.com
\nLicensed under the Apache 2.0 License
',
- json: {
- dependencies: {
- grafanaDependency: '>=7.3.0',
- grafanaVersion: '7.3',
- plugins: [],
- },
- info: {
- links: [],
- },
- },
- ...plugin,
- };
-}
-
-function localPlugin(plugin: Partial = {}): LocalPlugin {
- return {
- name: 'Akumuli',
- type: PluginType.datasource,
- id: 'akumuli-datasource',
- enabled: true,
- pinned: false,
- info: {
- author: {
- name: 'Eugene Lazin',
- url: 'https://akumuli.org',
- },
- description: 'Datasource plugin for Akumuli time-series database',
- links: [
- {
- name: 'Project site',
- url: 'https://github.com/akumuli/Akumuli',
- },
- ],
- logos: {
- small: 'public/plugins/akumuli-datasource/img/logo.svg.png',
- large: 'public/plugins/akumuli-datasource/img/logo.svg.png',
- },
- build: {},
- screenshots: null,
- version: '1.3.12',
- updated: '2019-12-19',
- },
- latestVersion: '1.3.12',
- hasUpdate: false,
- defaultNavUrl: '/plugins/akumuli-datasource/',
- category: '',
- state: '',
- signature: PluginSignatureStatus.valid,
- signatureType: PluginSignatureType.core,
- signatureOrg: 'Grafana Labs',
- ...plugin,
- };
-}
diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts
index 08e0aed2b4e..9cd9f897669 100644
--- a/public/app/features/plugins/admin/types.ts
+++ b/public/app/features/plugins/admin/types.ts
@@ -220,3 +220,21 @@ export type ReducerState = PluginsState & {
// TODO
export type PluginCatalogStoreState = StoreState & { plugins: ReducerState };
+
+// The data that we receive when fetching "/api/gnet/plugins//versions"
+export type PluginVersion = {
+ id: number;
+ pluginId: number;
+ pluginSlug: string;
+ version: string;
+ url: string;
+ commit: string;
+ description: string;
+ createdAt: string;
+ updatedAt?: string;
+ downloads: number;
+ verified: boolean;
+ status: string;
+ downloadSlug: string;
+ links: Array<{ rel: string; href: string }>;
+};