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

CircleCI\nVersion\ncodecov\nChange Log\nDocs\nTwitter URL\nDonate

\n

Visualize your Zabbix metrics with the leading open source software for time series analytics.

\n

Dashboard

\n

Features

\n\n

See 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.

\n

Installation

\n

Install by using grafana-cli

\n
grafana-cli plugins install alexanderzobnin-zabbix-app\n
\n

Or see more installation options in docs.

\n

Getting started

\n

First, configure Zabbix data source. Then you can create your first dashboard with step-by-step Getting started guide.

\n

Documentation

\n\n

Community Resources, Feedback, and Support

\n\n
\n

:copyright: 2015-2021 Alexander Zobnin alexanderzobnin@gmail.com

\n

Licensed 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

\n

Licensed 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

\n

Licensed 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 }>; +};