mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* 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>
288 lines
10 KiB
TypeScript
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,
|
|
};
|
|
}
|