Files
grafana/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
Levente Balogh 1133e56006 Plugins Catalog: migrate state handling to Redux (#38876)
* feat(Plugins/Catalog): start adding necessary apis

* feat(PLugins/Catalog): add extra helpers for merging local & remote plugins

* feat(Plugins/Catalog): add plugin details as an optional field of CatalogPlugin

* feat(PLugins/Catalog): add scaffolding for the new redux model

* feat(PLugins/Catalog): export reducers based on a feature-flag

* refactor(Plugins/Admin): rename api methods

* feat(Plugin/Catalog): add an api method for fetching a single plugin

* feat(Plugins/Admin): try cleaning stuff around plugin fetching

* ffeat(Plugins/Catalog): return the catalog reducer when the feature flag is set

* refactor(Plugins/Admin): fix typings

* feat(Plugins/Admin): use the new reducer for the browse page

* feat(catalog): introduce selectors to search and filter plugins list

* refactor(Plugins/Details): rename page prop type

* refactor(Plugins/Admin): add a const for a state prefix

* refactor(Plugins/Admin): use the state prefix in the actions

* feat(Plugins/Admin): add types for the requests

* refactor(Plugins/Admin): add request info to the reducer

* refactor(Plugins/Admin): add request handling to the hooks & selectors

* refactor(Plugins/Details): start using the data stored in Redux

* refactor(Plugins/Admin): rename selector to start with "select"

* fix(Plugins/Admin): only fetch plugins once

* refactor(Plugins/Admin): make the tab selection work in details

* refactor(catalog): put back loading and error states in plugin list

* refactor(Plugins/Admin): use CatalogPlugin for <PluginDetailsSignature />

* feat(Plugins/Admin): add an api method for fetching plugin details

* refactor(Plugins/Admin): add action for updating the details

* irefactor(Plugins/Admin): show basic plugin details info

* refactor(Plugin Details): migrate the plugin details header

* refactor(Plugins/Admin): make the config and dashboards tabs work

* refactor(Plugins/Admin): add old reducer state to the new one

* feat(catalog): introduce actions, reducers and hooks for install & uninstall

* refactor(catalog): wire up InstallControls component to redux

* refactor(catalog): move parentUrl inside PluginDetailsHeader and uncomment InstallControls

* feat(catalog): introduce code for plugin updates to install action

* refactor(Plugins/Admin): add backward compatible actions

* test(catalog): update PluginDetails and Browse tests to work with catalog store

* refactor(Plugins/Admin): make the dashboards and panels work again

* refactor(Plugins/Admin): fix linter and typescript errors

* fix(Plugins/Admin): put the local-only plugins to the beginning of the list

* fix(Plugins/Admin): fix the mocks in the tests for PluginDetails

* refactor(Plugins/Admin): remove unecessary hook usePluginsByFilter()

* refactor(Plugins/Admin): extract the useTabs() hook to its own file

* refactor(Plugins/Admin): remove unused helpers and types

* fix(Plugins/Admin): show the first tab when uninstalling an app plugin

This can cause the user to find themselves on a dissappeared tab, as the
config and dashboards tabs are removed.

* fix(catalog): correct logic for checking if activeTabIndex is greater than total tabs

* fix(Plugins/Admin): fix race-condition between fetching plugin details and all plugins

* fix(Plugins): fix strict type errors

* chore(catalog): remove todos

* feat(catalog): render an alert in PluginDetails when a plugin cannot be found

* feat(catalog): use the proper store state

* refactor(Plugins/Admin): fetch local and remote plugins in parallell

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* style(catalog): fix prettier error in api

* fix(catalog): prevent throwing error if InstallControlsButton is unmounted during install

* refactor(Plugins/Admin): add a separate hook for filtering & sorting plugins

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
2021-09-09 12:20:35 +02:00

288 lines
10 KiB
TypeScript

import React from 'react';
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';
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
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,
},
};
});
function setup(pluginId: string): RenderResult {
const props = getRouteComponentProps({ match: { params: { pluginId }, isExact: true, url: '', path: '' } });
const store = configureStore();
return render(
<Provider store={store}>
<PluginDetailsPage {...props} />
</Provider>
);
}
describe('Plugin details page', () => {
let dateNow: any;
beforeAll(() => {
dateNow = jest.spyOn(Date, 'now').mockImplementation(() => 1609470000000); // 2021-01-01 04:00:00
});
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
dateNow.mockRestore();
});
it('should display an overview (plugin readme) by default', async () => {
const { queryByText } = setup('not-installed');
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');
await waitFor(() => expect(queryByText(/version history/i)).toBeInTheDocument());
userEvent.click(getByText(/version history/i));
expect(
getByRole('columnheader', {
name: /version/i,
})
).toBeInTheDocument();
expect(
getByRole('columnheader', {
name: /last updated/i,
})
).toBeInTheDocument();
expect(
getByRole('cell', {
name: /1\.0\.0/i,
})
).toBeInTheDocument();
expect(
getByRole('cell', {
name: /5 years ago/i,
})
).toBeInTheDocument();
});
it("should display install button for a plugin that isn't installed", async () => {
const { queryByRole } = setup('not-installed');
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');
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');
await waitFor(() => expect(queryByRole('button', { name: /update/i })).toBeInTheDocument());
expect(queryByRole('button', { name: /uninstall/i })).toBeInTheDocument();
});
it('should display install button for enterprise plugins if license is valid', async () => {
config.licenseInfo.hasValidLicense = true;
const { queryByRole } = setup('enterprise');
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');
await waitFor(() => expect(queryByRole('button', { name: /install/i })).not.toBeInTheDocument());
expect(queryByText(/no valid Grafana Enterprise license detected/i)).toBeInTheDocument();
expect(queryByRole('link', { name: /learn more/i })).toBeInTheDocument();
});
it('should not display install / uninstall buttons for core plugins', async () => {
const { queryByRole } = setup('core');
await waitFor(() => expect(queryByRole('button', { name: /(un)?install/i })).not.toBeInTheDocument());
});
it('should display install link with pluginAdminExternalManageEnabled true', async () => {
config.pluginAdminExternalManageEnabled = true;
const { queryByRole } = setup('not-installed');
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 () => {
config.pluginAdminExternalManageEnabled = true;
const { queryByRole } = setup('installed');
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 () => {
config.pluginAdminExternalManageEnabled = true;
const { queryByRole } = setup('has-update');
await waitFor(() => expect(queryByRole('link', { name: /update via grafana.com/i })).toBeInTheDocument());
expect(queryByRole('link', { name: /uninstall via grafana.com/i })).toBeInTheDocument();
});
});
function remotePlugin(plugin: Partial<RemotePlugin> = {}): 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:
'<h1>Zabbix plugin for Grafana</h1>\n<p>:copyright: 2015-2021 Alexander Zobnin alexanderzobnin@gmail.com</p>\n<p>Licensed under the Apache 2.0 License</p>',
json: {
dependencies: {
grafanaDependency: '>=7.3.0',
grafanaVersion: '7.3',
},
info: {
links: [],
},
},
...plugin,
};
}
function localPlugin(plugin: Partial<LocalPlugin> = {}): 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,
};
}