mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Refactor installation buttons component (#37642)
* refactor(catalog): split out installcontrols into multiple components * test(catalog): update tests for plugindetails page * refactor(catalog): rename installcontrols -> index * refactor(catalog): remove redundant curlies Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com> * tests(plugindetails): fix assertions and naming of tests Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com> * refactor(installcontrols): prefer enum over duplicate union type, rename disabled prop * refactor(installcontrols): use PluginStatus enum for installcontrols pluginStatus * refactor(installcontrols): remove redundant curlies Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
This commit is contained in:
@@ -1,186 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { css, cx } from '@emotion/css';
|
|
||||||
import { satisfies } from 'semver';
|
|
||||||
|
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
import { Button, HorizontalGroup, Icon, LinkButton, useStyles2 } from '@grafana/ui';
|
|
||||||
import { AppEvents, GrafanaTheme2 } from '@grafana/data';
|
|
||||||
|
|
||||||
import appEvents from 'app/core/app_events';
|
|
||||||
import { CatalogPluginDetails, ActionTypes } from '../types';
|
|
||||||
import { api } from '../api';
|
|
||||||
import { isGrafanaAdmin } from '../helpers';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
plugin: CatalogPluginDetails;
|
|
||||||
isInflight: boolean;
|
|
||||||
hasUpdate: boolean;
|
|
||||||
hasInstalledPanel: boolean;
|
|
||||||
isInstalled: boolean;
|
|
||||||
dispatch: React.Dispatch<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const InstallControls = ({ plugin, isInflight, hasUpdate, isInstalled, hasInstalledPanel, dispatch }: Props) => {
|
|
||||||
const isExternallyManaged = config.pluginAdminExternalManageEnabled;
|
|
||||||
const externalManageLink = getExternalManageLink(plugin);
|
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
|
|
||||||
if (!plugin) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onInstall = async () => {
|
|
||||||
dispatch({ type: ActionTypes.INFLIGHT });
|
|
||||||
try {
|
|
||||||
await api.installPlugin(plugin.id, plugin.version);
|
|
||||||
appEvents.emit(AppEvents.alertSuccess, [`Installed ${plugin.name}`]);
|
|
||||||
dispatch({ type: ActionTypes.INSTALLED, payload: plugin.type === 'panel' });
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({ type: ActionTypes.ERROR, payload: { error } });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUninstall = async () => {
|
|
||||||
dispatch({ type: ActionTypes.INFLIGHT });
|
|
||||||
try {
|
|
||||||
await api.uninstallPlugin(plugin.id);
|
|
||||||
appEvents.emit(AppEvents.alertSuccess, [`Uninstalled ${plugin.name}`]);
|
|
||||||
dispatch({ type: ActionTypes.UNINSTALLED });
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({ type: ActionTypes.ERROR, payload: error });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUpdate = async () => {
|
|
||||||
dispatch({ type: ActionTypes.INFLIGHT });
|
|
||||||
try {
|
|
||||||
await api.installPlugin(plugin.id, plugin.version);
|
|
||||||
appEvents.emit(AppEvents.alertSuccess, [`Updated ${plugin.name}`]);
|
|
||||||
dispatch({ type: ActionTypes.UPDATED });
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({ type: ActionTypes.ERROR, payload: error });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const grafanaDependency = plugin.grafanaDependency;
|
|
||||||
const unsupportedGrafanaVersion = grafanaDependency
|
|
||||||
? !satisfies(config.buildInfo.version, grafanaDependency, {
|
|
||||||
// needed for when running against master
|
|
||||||
includePrerelease: true,
|
|
||||||
})
|
|
||||||
: false;
|
|
||||||
|
|
||||||
const isDevelopmentBuild = Boolean(plugin.isDev);
|
|
||||||
const isEnterprise = plugin.isEnterprise;
|
|
||||||
const isCore = plugin.isCore;
|
|
||||||
const hasPermission = isGrafanaAdmin();
|
|
||||||
|
|
||||||
if (isCore) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEnterprise && !config.licenseInfo?.hasValidLicense) {
|
|
||||||
return (
|
|
||||||
<HorizontalGroup height="auto" align="center">
|
|
||||||
<span className={styles.message}>No valid Grafana Enterprise license detected.</span>
|
|
||||||
<LinkButton
|
|
||||||
href={`${getExternalManageLink(plugin)}?utm_source=grafana_catalog_learn_more`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
size="sm"
|
|
||||||
fill="text"
|
|
||||||
icon="external-link-alt"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</LinkButton>
|
|
||||||
</HorizontalGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDevelopmentBuild) {
|
|
||||||
return (
|
|
||||||
<div className={styles.message}>This is a development build of the plugin and can't be uninstalled.</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasPermission && !isExternallyManaged) {
|
|
||||||
const pluginStatus = isInstalled ? 'uninstall' : hasUpdate ? 'update' : 'install';
|
|
||||||
const message = `You do not have permission to ${pluginStatus} this plugin.`;
|
|
||||||
return <div className={styles.message}>{message}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInstalled) {
|
|
||||||
return (
|
|
||||||
<HorizontalGroup height="auto">
|
|
||||||
{hasUpdate &&
|
|
||||||
(isExternallyManaged ? (
|
|
||||||
<LinkButton href={externalManageLink} target="_blank" rel="noopener noreferrer">
|
|
||||||
{'Update via grafana.com'}
|
|
||||||
</LinkButton>
|
|
||||||
) : (
|
|
||||||
<Button disabled={isInflight || !hasPermission} onClick={onUpdate}>
|
|
||||||
{isInflight ? 'Updating' : 'Update'}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{isExternallyManaged ? (
|
|
||||||
<LinkButton variant="destructive" href={externalManageLink} target="_blank" rel="noopener noreferrer">
|
|
||||||
{'Uninstall via grafana.com'}
|
|
||||||
</LinkButton>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Button variant="destructive" disabled={isInflight || !hasPermission} onClick={onUninstall}>
|
|
||||||
{isInflight && !hasUpdate ? 'Uninstalling' : 'Uninstall'}
|
|
||||||
</Button>
|
|
||||||
{hasInstalledPanel && (
|
|
||||||
<div className={cx(styles.message, styles.messageMargin)}>
|
|
||||||
Please refresh your browser window before using this plugin.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</HorizontalGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unsupportedGrafanaVersion) {
|
|
||||||
return (
|
|
||||||
<div className={styles.message}>
|
|
||||||
<Icon name="exclamation-triangle" />
|
|
||||||
This plugin doesn't support your version of Grafana.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HorizontalGroup height="auto">
|
|
||||||
{isExternallyManaged ? (
|
|
||||||
<LinkButton href={externalManageLink} target="_blank" rel="noopener noreferrer">
|
|
||||||
{'Install via grafana.com'}
|
|
||||||
</LinkButton>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Button disabled={isInflight || !hasPermission} onClick={onInstall}>
|
|
||||||
{isInflight ? 'Installing' : 'Install'}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</HorizontalGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function getExternalManageLink(plugin: CatalogPluginDetails): string {
|
|
||||||
return `https://grafana.com/grafana/plugins/${plugin.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getStyles = (theme: GrafanaTheme2) => {
|
|
||||||
return {
|
|
||||||
message: css`
|
|
||||||
color: ${theme.colors.text.secondary};
|
|
||||||
`,
|
|
||||||
messageMargin: css`
|
|
||||||
margin-left: ${theme.spacing()};
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { HorizontalGroup, LinkButton } from '@grafana/ui';
|
||||||
|
import { getExternalManageLink } from '../../helpers';
|
||||||
|
import { PluginStatus } from '../../types';
|
||||||
|
|
||||||
|
type ExternallyManagedButtonProps = {
|
||||||
|
pluginId: string;
|
||||||
|
pluginStatus: PluginStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ExternallyManagedButton({ pluginId, pluginStatus }: ExternallyManagedButtonProps) {
|
||||||
|
const externalManageLink = getExternalManageLink(pluginId);
|
||||||
|
|
||||||
|
if (pluginStatus === PluginStatus.UPDATE) {
|
||||||
|
return (
|
||||||
|
<HorizontalGroup height="auto">
|
||||||
|
<LinkButton href={externalManageLink} target="_blank" rel="noopener noreferrer">
|
||||||
|
Update via grafana.com
|
||||||
|
</LinkButton>
|
||||||
|
<LinkButton variant="destructive" href={externalManageLink} target="_blank" rel="noopener noreferrer">
|
||||||
|
Uninstall via grafana.com
|
||||||
|
</LinkButton>
|
||||||
|
</HorizontalGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginStatus === PluginStatus.UNINSTALL) {
|
||||||
|
return (
|
||||||
|
<LinkButton variant="destructive" href={externalManageLink} target="_blank" rel="noopener noreferrer">
|
||||||
|
Uninstall via grafana.com
|
||||||
|
</LinkButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LinkButton href={externalManageLink} target="_blank" rel="noopener noreferrer">
|
||||||
|
Install via grafana.com
|
||||||
|
</LinkButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { AppEvents } from '@grafana/data';
|
||||||
|
import { Button, HorizontalGroup, useStyles2 } from '@grafana/ui';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
import { api } from '../../api';
|
||||||
|
import { ActionTypes, CatalogPlugin, PluginStatus } from '../../types';
|
||||||
|
import { getStyles } from './index';
|
||||||
|
|
||||||
|
type InstallControlsButtonProps = {
|
||||||
|
isInProgress: boolean;
|
||||||
|
hasInstalledPanel: boolean;
|
||||||
|
dispatch: React.Dispatch<any>;
|
||||||
|
plugin: CatalogPlugin;
|
||||||
|
pluginStatus: PluginStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function InstallControlsButton({
|
||||||
|
isInProgress,
|
||||||
|
dispatch,
|
||||||
|
plugin,
|
||||||
|
pluginStatus,
|
||||||
|
hasInstalledPanel,
|
||||||
|
}: InstallControlsButtonProps) {
|
||||||
|
const uninstallBtnText = isInProgress ? 'Uninstalling' : 'Uninstall';
|
||||||
|
const updateBtnText = isInProgress ? 'Updating' : 'Update';
|
||||||
|
const installBtnText = isInProgress ? 'Installing' : 'Install';
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const onInstall = async () => {
|
||||||
|
dispatch({ type: ActionTypes.INFLIGHT });
|
||||||
|
try {
|
||||||
|
await api.installPlugin(plugin.id, plugin.version);
|
||||||
|
appEvents.emit(AppEvents.alertSuccess, [`Installed ${plugin.name}`]);
|
||||||
|
dispatch({ type: ActionTypes.INSTALLED, payload: plugin.type === 'panel' });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({ type: ActionTypes.ERROR, payload: { error } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUninstall = async () => {
|
||||||
|
dispatch({ type: ActionTypes.INFLIGHT });
|
||||||
|
try {
|
||||||
|
await api.uninstallPlugin(plugin.id);
|
||||||
|
appEvents.emit(AppEvents.alertSuccess, [`Uninstalled ${plugin.name}`]);
|
||||||
|
dispatch({ type: ActionTypes.UNINSTALLED });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({ type: ActionTypes.ERROR, payload: error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdate = async () => {
|
||||||
|
dispatch({ type: ActionTypes.INFLIGHT });
|
||||||
|
try {
|
||||||
|
await api.installPlugin(plugin.id, plugin.version);
|
||||||
|
appEvents.emit(AppEvents.alertSuccess, [`Updated ${plugin.name}`]);
|
||||||
|
dispatch({ type: ActionTypes.UPDATED });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({ type: ActionTypes.ERROR, payload: error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pluginStatus === PluginStatus.UNINSTALL) {
|
||||||
|
return (
|
||||||
|
<HorizontalGroup height="auto">
|
||||||
|
<Button variant="destructive" disabled={isInProgress} onClick={onUninstall}>
|
||||||
|
{uninstallBtnText}
|
||||||
|
</Button>
|
||||||
|
{hasInstalledPanel && (
|
||||||
|
<div className={styles.message}>Please refresh your browser window before using this plugin.</div>
|
||||||
|
)}
|
||||||
|
</HorizontalGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginStatus === PluginStatus.UPDATE) {
|
||||||
|
return (
|
||||||
|
<HorizontalGroup height="auto">
|
||||||
|
<Button disabled={isInProgress} onClick={onUpdate}>
|
||||||
|
{updateBtnText}
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" disabled={isInProgress} onClick={onUninstall}>
|
||||||
|
{uninstallBtnText}
|
||||||
|
</Button>
|
||||||
|
</HorizontalGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button disabled={isInProgress} onClick={onInstall}>
|
||||||
|
{installBtnText}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { satisfies } from 'semver';
|
||||||
|
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { HorizontalGroup, Icon, LinkButton, useStyles2 } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
|
||||||
|
import { CatalogPluginDetails, PluginStatus } from '../../types';
|
||||||
|
import { isGrafanaAdmin, getExternalManageLink } from '../../helpers';
|
||||||
|
import { ExternallyManagedButton } from './ExternallyManagedButton';
|
||||||
|
import { InstallControlsButton } from './InstallControlsButton';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
plugin: CatalogPluginDetails;
|
||||||
|
isInflight: boolean;
|
||||||
|
hasUpdate: boolean;
|
||||||
|
hasInstalledPanel: boolean;
|
||||||
|
isInstalled: boolean;
|
||||||
|
dispatch: React.Dispatch<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InstallControls = ({ plugin, isInflight, hasUpdate, isInstalled, hasInstalledPanel, dispatch }: Props) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const isExternallyManaged = config.pluginAdminExternalManageEnabled;
|
||||||
|
const hasPermission = isGrafanaAdmin();
|
||||||
|
const grafanaDependency = plugin.grafanaDependency;
|
||||||
|
const unsupportedGrafanaVersion = grafanaDependency
|
||||||
|
? !satisfies(config.buildInfo.version, grafanaDependency, {
|
||||||
|
// needed for when running against master
|
||||||
|
includePrerelease: true,
|
||||||
|
})
|
||||||
|
: false;
|
||||||
|
const pluginStatus = isInstalled ? (hasUpdate ? PluginStatus.UPDATE : PluginStatus.UNINSTALL) : PluginStatus.INSTALL;
|
||||||
|
|
||||||
|
if (plugin.isCore) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.isEnterprise && !config.licenseInfo?.hasValidLicense) {
|
||||||
|
return (
|
||||||
|
<HorizontalGroup height="auto" align="center">
|
||||||
|
<span className={styles.message}>No valid Grafana Enterprise license detected.</span>
|
||||||
|
<LinkButton
|
||||||
|
href={`${getExternalManageLink(plugin.id)}?utm_source=grafana_catalog_learn_more`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
size="sm"
|
||||||
|
fill="text"
|
||||||
|
icon="external-link-alt"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</LinkButton>
|
||||||
|
</HorizontalGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.isDev) {
|
||||||
|
return (
|
||||||
|
<div className={styles.message}>This is a development build of the plugin and can't be uninstalled.</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPermission && !isExternallyManaged) {
|
||||||
|
const message = `You do not have permission to ${pluginStatus} this plugin.`;
|
||||||
|
return <div className={styles.message}>{message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unsupportedGrafanaVersion) {
|
||||||
|
return (
|
||||||
|
<div className={styles.message}>
|
||||||
|
<Icon name="exclamation-triangle" />
|
||||||
|
This plugin doesn't support your version of Grafana.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExternallyManaged) {
|
||||||
|
return <ExternallyManagedButton pluginId={plugin.id} pluginStatus={pluginStatus} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InstallControlsButton
|
||||||
|
isInProgress={isInflight}
|
||||||
|
dispatch={dispatch}
|
||||||
|
plugin={plugin}
|
||||||
|
pluginStatus={pluginStatus}
|
||||||
|
hasInstalledPanel={hasInstalledPanel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
message: css`
|
||||||
|
color: ${theme.colors.text.secondary};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -3,16 +3,7 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import { PluginSignatureStatus } from '@grafana/data';
|
import { PluginSignatureStatus } from '@grafana/data';
|
||||||
import { PluginBadges } from './PluginBadges';
|
import { PluginBadges } from './PluginBadges';
|
||||||
import { CatalogPlugin } from '../types';
|
import { CatalogPlugin } from '../types';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
const runtimeMock = jest.requireMock('@grafana/runtime');
|
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
|
||||||
config: {
|
|
||||||
licenseInfo: {
|
|
||||||
hasValidLicense: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('PluginBadges', () => {
|
describe('PluginBadges', () => {
|
||||||
const plugin: CatalogPlugin = {
|
const plugin: CatalogPlugin = {
|
||||||
@@ -39,6 +30,10 @@ describe('PluginBadges', () => {
|
|||||||
isEnterprise: false,
|
isEnterprise: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders a plugin signature badge', () => {
|
it('renders a plugin signature badge', () => {
|
||||||
render(<PluginBadges plugin={plugin} />);
|
render(<PluginBadges plugin={plugin} />);
|
||||||
|
|
||||||
@@ -53,14 +48,14 @@ describe('PluginBadges', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders an enterprise badge (when a license is valid)', () => {
|
it('renders an enterprise badge (when a license is valid)', () => {
|
||||||
runtimeMock.config.licenseInfo.hasValidLicense = true;
|
config.licenseInfo.hasValidLicense = true;
|
||||||
render(<PluginBadges plugin={{ ...plugin, isEnterprise: true }} />);
|
render(<PluginBadges plugin={{ ...plugin, isEnterprise: true }} />);
|
||||||
expect(screen.getByText(/enterprise/i)).toBeVisible();
|
expect(screen.getByText(/enterprise/i)).toBeVisible();
|
||||||
expect(screen.queryByRole('button', { name: /learn more/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: /learn more/i })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders an enterprise badge with icon and link (when a license is invalid)', () => {
|
it('renders an enterprise badge with icon and link (when a license is invalid)', () => {
|
||||||
runtimeMock.config.licenseInfo.hasValidLicense = false;
|
config.licenseInfo.hasValidLicense = false;
|
||||||
render(<PluginBadges plugin={{ ...plugin, isEnterprise: true }} />);
|
render(<PluginBadges plugin={{ ...plugin, isEnterprise: true }} />);
|
||||||
expect(screen.getByText(/enterprise/i)).toBeVisible();
|
expect(screen.getByText(/enterprise/i)).toBeVisible();
|
||||||
expect(screen.getByLabelText(/lock icon/i)).toBeInTheDocument();
|
expect(screen.getByLabelText(/lock icon/i)).toBeInTheDocument();
|
||||||
|
|||||||
@@ -166,3 +166,5 @@ export const matchesKeyword: PluginFilter = (plugin, query) => {
|
|||||||
|
|
||||||
return fields.some((f) => f.includes(query.toLowerCase()));
|
return fields.some((f) => f.includes(query.toLowerCase()));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getExternalManageLink = (pluginId: string) => `https://grafana.com/grafana/plugins/${pluginId}`;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, RenderResult, waitFor } from '@testing-library/react';
|
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 { PluginSignatureStatus, PluginSignatureType, PluginType } from '@grafana/data';
|
||||||
import PluginDetailsPage from './PluginDetails';
|
import PluginDetailsPage from './PluginDetails';
|
||||||
import { API_ROOT, GRAFANA_API_ROOT } from '../constants';
|
import { API_ROOT, GRAFANA_API_ROOT } from '../constants';
|
||||||
@@ -17,12 +19,30 @@ jest.mock('@grafana/runtime', () => {
|
|||||||
case `${GRAFANA_API_ROOT}/plugins/not-installed/versions`:
|
case `${GRAFANA_API_ROOT}/plugins/not-installed/versions`:
|
||||||
case `${GRAFANA_API_ROOT}/plugins/enterprise/versions`:
|
case `${GRAFANA_API_ROOT}/plugins/enterprise/versions`:
|
||||||
return Promise.resolve([]);
|
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:
|
case API_ROOT:
|
||||||
return Promise.resolve([localPlugin(), corePlugin()]);
|
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`:
|
case `${GRAFANA_API_ROOT}/plugins/core`:
|
||||||
return Promise.resolve(corePlugin());
|
return Promise.resolve(localPlugin({ id: 'core', signature: PluginSignatureStatus.internal }));
|
||||||
case `${GRAFANA_API_ROOT}/plugins/not-installed`:
|
case `${GRAFANA_API_ROOT}/plugins/not-installed`:
|
||||||
return Promise.resolve(remotePlugin());
|
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`:
|
case `${GRAFANA_API_ROOT}/plugins/enterprise`:
|
||||||
return Promise.resolve(remotePlugin({ status: 'enterprise' }));
|
return Promise.resolve(remotePlugin({ status: 'enterprise' }));
|
||||||
default:
|
default:
|
||||||
@@ -53,18 +73,84 @@ function setup(pluginId: string): RenderResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('Plugin details page', () => {
|
describe('Plugin details page', () => {
|
||||||
it('should display install button for uninstalled plugins', async () => {
|
let dateNow: any;
|
||||||
const { getByText } = setup('not-installed');
|
|
||||||
|
|
||||||
const expected = 'Install';
|
beforeAll(() => {
|
||||||
|
dateNow = jest.spyOn(Date, 'now').mockImplementation(() => 1609470000000); // 2021-01-01 04:00:00
|
||||||
await waitFor(() => expect(getByText(expected)).toBeInTheDocument());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not display install button for enterprise plugins', async () => {
|
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');
|
const { queryByRole } = setup('enterprise');
|
||||||
|
|
||||||
await waitFor(() => expect(queryByRole('button', { name: /(un)?install/i })).not.toBeInTheDocument());
|
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 () => {
|
it('should not display install / uninstall buttons for core plugins', async () => {
|
||||||
@@ -72,6 +158,27 @@ describe('Plugin details page', () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(queryByRole('button', { name: /(un)?install/i })).not.toBeInTheDocument());
|
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 {
|
function remotePlugin(plugin: Partial<RemotePlugin> = {}): RemotePlugin {
|
||||||
@@ -106,7 +213,8 @@ function remotePlugin(plugin: Partial<RemotePlugin> = {}): RemotePlugin {
|
|||||||
versionSignedByOrg: 'alexanderzobnin',
|
versionSignedByOrg: 'alexanderzobnin',
|
||||||
versionSignedByOrgName: 'Alexander Zobnin',
|
versionSignedByOrgName: 'Alexander Zobnin',
|
||||||
userId: 0,
|
userId: 0,
|
||||||
readme: '',
|
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: {
|
json: {
|
||||||
dependencies: {
|
dependencies: {
|
||||||
grafanaDependency: '>=7.3.0',
|
grafanaDependency: '>=7.3.0',
|
||||||
@@ -122,65 +230,40 @@ function remotePlugin(plugin: Partial<RemotePlugin> = {}): RemotePlugin {
|
|||||||
|
|
||||||
function localPlugin(plugin: Partial<LocalPlugin> = {}): LocalPlugin {
|
function localPlugin(plugin: Partial<LocalPlugin> = {}): LocalPlugin {
|
||||||
return {
|
return {
|
||||||
category: '',
|
name: 'Akumuli',
|
||||||
defaultNavUrl: '/plugins/alertmanager/',
|
type: PluginType.datasource,
|
||||||
|
id: 'akumuli-datasource',
|
||||||
|
enabled: true,
|
||||||
|
pinned: false,
|
||||||
info: {
|
info: {
|
||||||
author: {
|
author: {
|
||||||
name: 'Prometheus alertmanager',
|
name: 'Eugene Lazin',
|
||||||
url: 'https://grafana.com',
|
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: {},
|
build: {},
|
||||||
description: '',
|
screenshots: null,
|
||||||
links: [],
|
version: '1.3.12',
|
||||||
logos: {
|
updated: '2019-12-19',
|
||||||
small: '',
|
|
||||||
large: '',
|
|
||||||
},
|
|
||||||
updated: '',
|
|
||||||
version: '',
|
|
||||||
},
|
},
|
||||||
enabled: true,
|
latestVersion: '1.3.12',
|
||||||
hasUpdate: false,
|
hasUpdate: false,
|
||||||
id: 'alertmanager',
|
defaultNavUrl: '/plugins/akumuli-datasource/',
|
||||||
latestVersion: '',
|
category: '',
|
||||||
name: 'Alert Manager',
|
|
||||||
pinned: false,
|
|
||||||
signature: PluginSignatureStatus.internal,
|
|
||||||
signatureOrg: '',
|
|
||||||
signatureType: '',
|
|
||||||
state: 'alpha',
|
|
||||||
type: PluginType.datasource,
|
|
||||||
...plugin,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function corePlugin(plugin: Partial<LocalPlugin> = {}): LocalPlugin {
|
|
||||||
return {
|
|
||||||
category: 'sql',
|
|
||||||
defaultNavUrl: '/plugins/postgres/',
|
|
||||||
enabled: true,
|
|
||||||
hasUpdate: false,
|
|
||||||
id: 'core',
|
|
||||||
info: {
|
|
||||||
author: { name: 'Grafana Labs', url: 'https://grafana.com' },
|
|
||||||
build: {},
|
|
||||||
description: 'Data source for PostgreSQL and compatible databases',
|
|
||||||
links: [],
|
|
||||||
logos: {
|
|
||||||
small: 'public/app/plugins/datasource/postgres/img/postgresql_logo.svg',
|
|
||||||
large: 'public/app/plugins/datasource/postgres/img/postgresql_logo.svg',
|
|
||||||
},
|
|
||||||
updated: '',
|
|
||||||
version: '',
|
|
||||||
},
|
|
||||||
latestVersion: '',
|
|
||||||
name: 'PostgreSQL',
|
|
||||||
pinned: false,
|
|
||||||
signature: PluginSignatureStatus.internal,
|
|
||||||
signatureOrg: '',
|
|
||||||
signatureType: '',
|
|
||||||
state: '',
|
state: '',
|
||||||
type: PluginType.datasource,
|
signature: PluginSignatureStatus.valid,
|
||||||
|
signatureType: 'community',
|
||||||
|
signatureOrg: 'Grafana Labs',
|
||||||
...plugin,
|
...plugin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,3 +223,9 @@ export type PluginsByFilterType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PluginFilter = (plugin: CatalogPlugin, query: string) => boolean;
|
export type PluginFilter = (plugin: CatalogPlugin, query: string) => boolean;
|
||||||
|
|
||||||
|
export enum PluginStatus {
|
||||||
|
INSTALL = 'INSTALL',
|
||||||
|
UNINSTALL = 'UNINSTALL',
|
||||||
|
UPDATE = 'UPDATE',
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user