Plugins: Add test-ids for plugin details, remove lastCommitDate (#109173)

add test-ids for plugin details, remove lastCommitDate
This commit is contained in:
Yulia Shanyrova
2025-08-13 10:18:49 +02:00
committed by GitHub
parent 2ecc076bbf
commit 47aa892d24
5 changed files with 69 additions and 303 deletions
+2 -10
View File
@@ -35,7 +35,6 @@ export async function getPluginDetails(id: string): Promise<CatalogPluginDetails
versions,
statusContext: remote?.statusContext ?? '',
iam: remote?.json?.iam,
lastCommitDate: remote?.lastCommitDate,
changelog: remote?.changelog || localChangelog,
licenseUrl: remote?.licenseUrl,
documentationUrl: remote?.documentationUrl,
@@ -171,9 +170,7 @@ export async function installPlugin(id: string, version?: string) {
// on the backend.
return await getBackendSrv().post(
`${API_ROOT}/${id}/install`,
{
version,
},
{ version },
{
// Error is displayed in the page
showErrorAlert: false,
@@ -195,9 +192,4 @@ export async function updatePluginSettings(id: string, data: Partial<PluginMeta>
return response?.data;
}
export const api = {
getRemotePlugins,
getInstalledPlugins: getLocalPlugins,
installPlugin,
uninstallPlugin,
};
export const api = { getRemotePlugins, getInstalledPlugins: getLocalPlugins, installPlugin, uninstallPlugin };
@@ -22,11 +22,7 @@ import { formatDate } from 'app/core/internationalization/dates';
import { CatalogPlugin } from '../types';
type Props = {
pluginExtentionsInfo: PageInfoItem[];
plugin: CatalogPlugin;
width?: string;
};
type Props = { pluginExtentionsInfo: PageInfoItem[]; plugin: CatalogPlugin; width?: string };
export function PluginDetailsPanel(props: Props): React.ReactElement | null {
const { pluginExtentionsInfo, plugin, width = '250px' } = props;
@@ -57,11 +53,19 @@ export function PluginDetailsPanel(props: Props): React.ReactElement | null {
const onClickReportConcern = (pluginId: string) => {
setReportAbuseModalOpen(true);
reportInteraction('plugin_detail_report_concern', {
plugin_id: pluginId,
});
reportInteraction('plugin_detail_report_concern', { plugin_id: pluginId });
};
function createTestId(text: string) {
// Convert to string and handle null/undefined
const str = String(text || '');
return str
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '-');
}
return (
<>
<Stack direction="column" gap={3} shrink={0} grow={0} width={width} data-testid="plugin-details-panel">
@@ -70,35 +74,25 @@ export function PluginDetailsPanel(props: Props): React.ReactElement | null {
{pluginExtentionsInfo.map((infoItem, index) => {
return (
<Stack key={index} wrap direction="column" gap={0.5}>
<Text color="secondary">{infoItem.label + ':'}</Text>
<div className={styles.pluginVersionDetails}>{infoItem.value}</div>
<Text color="secondary" data-testid={`${createTestId(infoItem.label)}-label`}>
{infoItem.label + ':'}
</Text>
<div data-testid={`${createTestId(infoItem.label)}-value`} className={styles.pluginVersionDetails}>
{infoItem.value}
</div>
</Stack>
);
})}
{plugin.updatedAt && (
<Stack direction="column" gap={0.5}>
<Text color="secondary">
<Text color="secondary" data-testid="latest-release-date-label">
<Trans i18nKey="plugins.details.labels.latestReleaseDate">Latest release date:</Trans>
</Text>{' '}
<Text>
<Text data-testid="latest-release-date-value">
{formatDate(new Date(plugin.updatedAt), { day: 'numeric', month: 'short', year: 'numeric' })}
</Text>
</Stack>
)}
{plugin?.details?.lastCommitDate && (
<Stack direction="column" gap={0.5}>
<Text color="secondary">
<Trans i18nKey="plugins.details.labels.lastCommitDate">Last commit date:</Trans>
</Text>{' '}
<Text>
{formatDate(new Date(plugin.details.lastCommitDate), {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</Text>
</Stack>
)}
</Stack>
</Box>
{shouldRenderLinks && (
@@ -271,9 +265,5 @@ export function PluginDetailsPanel(props: Props): React.ReactElement | null {
}
export const getStyles = (theme: GrafanaTheme2) => {
return {
pluginVersionDetails: css({
wordBreak: 'break-word',
}),
};
return { pluginVersionDetails: css({ wordBreak: 'break-word' }) };
};
@@ -36,13 +36,7 @@ jest.mock('@grafana/runtime', () => {
return runtime;
});
jest.mock('../hooks/usePluginConfig.tsx', () => ({
usePluginConfig: jest.fn(() => ({
value: {
meta: {},
},
})),
}));
jest.mock('../hooks/usePluginConfig.tsx', () => ({ usePluginConfig: jest.fn(() => ({ value: { meta: {} } })) }));
jest.mock('app/core/core', () => ({
contextSrv: {
@@ -53,19 +47,11 @@ jest.mock('app/core/core', () => ({
const renderPluginDetails = (
pluginOverride: Partial<CatalogPlugin>,
{
pageId,
pluginsStateOverride,
}: {
pageId?: PluginTabIds;
pluginsStateOverride?: ReducerState;
} = {}
{ pageId, pluginsStateOverride }: { pageId?: PluginTabIds; pluginsStateOverride?: ReducerState } = {}
) => {
const plugin = getCatalogPluginMock(pluginOverride);
const { id } = plugin;
const store = configureStore({
plugins: pluginsStateOverride || getPluginsStateMock([plugin]),
});
const store = configureStore({ plugins: pluginsStateOverride || getPluginsStateMock([plugin]) });
return render(
<Routes>
@@ -86,10 +72,7 @@ describe('Plugin details page', () => {
dateNow = jest.spyOn(Date, 'now').mockImplementation(() => 1609470000000); // 2021-01-01 04:00:00
// Enabling / disabling the plugin is currently reloading the page to propagate the changes
Object.defineProperty(window, 'location', {
configurable: true,
value: { reload: jest.fn() },
});
Object.defineProperty(window, 'location', { configurable: true, value: { reload: jest.fn() } });
});
afterEach(() => {
@@ -106,21 +89,14 @@ describe('Plugin details page', () => {
describe('viewed as user with grafana admin permissions', () => {
beforeAll(() => {
mockUserPermissions({
isAdmin: true,
isDataSourceEditor: true,
isOrgAdmin: true,
});
mockUserPermissions({ isAdmin: true, isDataSourceEditor: true, isOrgAdmin: true });
});
// 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 },
});
mockPluginApis({ remote: { slug: id }, local: { id } });
const { queryByText } = renderPluginDetails({ id });
@@ -139,12 +115,7 @@ describe('Plugin details page', () => {
// @ts-ignore
usePluginConfig.mockReturnValue({
value: {
meta: {
type: PluginType.app,
enabled: false,
pinned: false,
jsonData: {},
},
meta: { type: PluginType.app, enabled: false, pinned: false, jsonData: {} },
configPages: [
{
title: 'Config',
@@ -158,11 +129,7 @@ describe('Plugin details page', () => {
},
});
const { queryByText } = renderPluginDetails({
name,
isInstalled: true,
type: PluginType.app,
});
const { queryByText } = renderPluginDetails({ name, isInstalled: true, type: PluginType.app });
expect(await queryByText(/custom config page/i)).toBeInTheDocument();
});
@@ -236,34 +203,13 @@ describe('Plugin details page', () => {
it('should display version history if the plugin is published', async () => {
const versions = [
{
version: '1.2.0',
createdAt: '2018-04-06T20:23:41.000Z',
isCompatible: false,
grafanaDependency: '>=8.3.0',
},
{
version: '1.1.0',
createdAt: '2017-04-06T20:23:41.000Z',
isCompatible: true,
grafanaDependency: '>=8.0.0',
},
{
version: '1.0.0',
createdAt: '2016-04-06T20:23:41.000Z',
isCompatible: true,
grafanaDependency: '>=7.0.0',
},
{ version: '1.2.0', createdAt: '2018-04-06T20:23:41.000Z', isCompatible: false, grafanaDependency: '>=8.3.0' },
{ version: '1.1.0', createdAt: '2017-04-06T20:23:41.000Z', isCompatible: true, grafanaDependency: '>=8.0.0' },
{ version: '1.0.0', createdAt: '2016-04-06T20:23:41.000Z', isCompatible: true, grafanaDependency: '>=7.0.0' },
];
const { findByRole, queryByText, getByRole } = renderPluginDetails(
{
id,
details: {
links: [],
versions,
},
},
{ id, details: { links: [], versions } },
{ pageId: PluginTabIds.VERSIONS }
);
@@ -404,11 +350,7 @@ describe('Plugin details page', () => {
it('should display grafana dependencies for a plugin if they are available', async () => {
const { queryByText } = renderPluginDetails({
id,
details: {
pluginDependencies: [],
grafanaDependency: '>=8.0.0',
links: [],
},
details: { pluginDependencies: [], grafanaDependency: '>=8.0.0', links: [] },
});
// Wait for the dependencies part to be loaded
@@ -419,10 +361,7 @@ describe('Plugin details page', () => {
// @ts-ignore
api.uninstallPlugin = jest.fn();
setBackendSrv({
...originalBackendSrv,
get: jest.fn().mockResolvedValue({ panels: [] }),
});
setBackendSrv({ ...originalBackendSrv, get: jest.fn().mockResolvedValue({ panels: [] }) });
const { queryByText, getByRole, findByRole, user } = renderPluginDetails({
id,
@@ -432,14 +371,7 @@ describe('Plugin details page', () => {
pluginDependencies: [],
grafanaDependency: '>=8.0.0',
links: [],
versions: [
{
version: '1.0.0',
createdAt: '',
isCompatible: true,
grafanaDependency: '>=8.0.0',
},
],
versions: [{ version: '1.0.0', createdAt: '', isCompatible: true, grafanaDependency: '>=8.0.0' }],
},
});
@@ -470,12 +402,7 @@ describe('Plugin details page', () => {
// Mock the store like if the remote plugins request was rejected
const pluginsStateOverride = {
...state,
requests: {
...state.requests,
[fetchRemotePlugins.typePrefix]: {
status: RequestStatus.Rejected,
},
},
requests: { ...state.requests, [fetchRemotePlugins.typePrefix]: { status: RequestStatus.Rejected } },
};
// Does not show an Install button
@@ -521,11 +448,7 @@ describe('Plugin details page', () => {
it('should display a "Create" button as a post installation step for installed data source plugins', async () => {
const name = 'Akumuli';
const { queryByText } = renderPluginDetails({
name,
isInstalled: true,
type: PluginType.datasource,
});
const { queryByText } = renderPluginDetails({ name, isInstalled: true, type: PluginType.datasource });
await waitFor(() => queryByText('Uninstall'));
expect(queryByText('Add new data source')).toBeInTheDocument();
@@ -546,11 +469,7 @@ describe('Plugin details page', () => {
it('should not display post installation step for panel plugins', async () => {
const name = 'Akumuli';
const { queryByText } = renderPluginDetails({
name,
isInstalled: true,
type: PluginType.panel,
});
const { queryByText } = renderPluginDetails({ name, isInstalled: true, type: PluginType.panel });
await waitFor(() => queryByText('Uninstall'));
expect(queryByText('Add new data source')).toBeNull();
@@ -560,21 +479,9 @@ describe('Plugin details page', () => {
const name = 'Akumuli';
// @ts-ignore
usePluginConfig.mockReturnValue({
value: {
meta: {
enabled: false,
pinned: false,
jsonData: {},
},
},
});
usePluginConfig.mockReturnValue({ value: { meta: { enabled: false, pinned: false, jsonData: {} } } });
const { queryByText, queryByRole } = renderPluginDetails({
name,
isInstalled: true,
type: PluginType.app,
});
const { queryByText, queryByRole } = renderPluginDetails({ name, isInstalled: true, type: PluginType.app });
await waitFor(() => queryByText('Uninstall'));
@@ -586,21 +493,9 @@ describe('Plugin details page', () => {
const name = 'Akumuli';
// @ts-ignore
usePluginConfig.mockReturnValue({
value: {
meta: {
enabled: true,
pinned: false,
jsonData: {},
},
},
});
usePluginConfig.mockReturnValue({ value: { meta: { enabled: true, pinned: false, jsonData: {} } } });
const { queryByText, queryByRole } = renderPluginDetails({
name,
isInstalled: true,
type: PluginType.app,
});
const { queryByText, queryByRole } = renderPluginDetails({ name, isInstalled: true, type: PluginType.app });
await waitFor(() => queryByText('Uninstall'));
@@ -616,15 +511,7 @@ describe('Plugin details page', () => {
api.updatePluginSettings = jest.fn();
// @ts-ignore
usePluginConfig.mockReturnValue({
value: {
meta: {
enabled: false,
pinned: false,
jsonData: {},
},
},
});
usePluginConfig.mockReturnValue({ value: { meta: { enabled: false, pinned: false, jsonData: {} } } });
const { queryByText, getByRole, user } = renderPluginDetails({
id,
@@ -641,11 +528,7 @@ describe('Plugin details page', () => {
// Check if the API request was initiated
expect(api.updatePluginSettings).toHaveBeenCalledTimes(1);
expect(api.updatePluginSettings).toHaveBeenCalledWith(id, {
enabled: true,
pinned: true,
jsonData: {},
});
expect(api.updatePluginSettings).toHaveBeenCalledWith(id, { enabled: true, pinned: true, jsonData: {} });
});
it('should be possible to disable an app plugin', async () => {
@@ -656,15 +539,7 @@ describe('Plugin details page', () => {
api.updatePluginSettings = jest.fn();
// @ts-ignore
usePluginConfig.mockReturnValue({
value: {
meta: {
enabled: true,
pinned: true,
jsonData: {},
},
},
});
usePluginConfig.mockReturnValue({ value: { meta: { enabled: true, pinned: true, jsonData: {} } } });
const { queryByText, getByRole, user } = renderPluginDetails({
id,
@@ -681,11 +556,7 @@ describe('Plugin details page', () => {
// Check if the API request was initiated
expect(api.updatePluginSettings).toHaveBeenCalledTimes(1);
expect(api.updatePluginSettings).toHaveBeenCalledWith(id, {
enabled: false,
pinned: false,
jsonData: {},
});
expect(api.updatePluginSettings).toHaveBeenCalledWith(id, { enabled: false, pinned: false, jsonData: {} });
});
it('should not display versions tab for plugins not published to gcom', async () => {
@@ -742,37 +613,25 @@ describe('Plugin details page', () => {
});
it('shows a "angular warning" if the plugin uses Angular', async () => {
const { queryByText } = renderPluginDetails({
angularDetected: true,
});
const { queryByText } = renderPluginDetails({ angularDetected: true });
await waitFor(() => expect(queryByText(/angular plugin/i)).toBeInTheDocument);
});
it('does not show an "angular warning" if the plugin is not using Angular', async () => {
const { queryByText } = renderPluginDetails({
angularDetected: false,
});
const { queryByText } = renderPluginDetails({ angularDetected: false });
await waitFor(() => expect(queryByText(/angular plugin/i)).not.toBeInTheDocument);
});
it('should display a deprecation warning if the plugin is deprecated', async () => {
const { findByRole } = renderPluginDetails({
id,
isInstalled: true,
isDeprecated: true,
});
const { findByRole } = renderPluginDetails({ id, isInstalled: true, isDeprecated: true });
expect(await findByRole('link', { name: 'deprecated' })).toBeInTheDocument();
});
it('should not display a deprecation warning in the plugin is not deprecated', async () => {
const { queryByRole } = renderPluginDetails({
id,
isInstalled: true,
isDeprecated: false,
});
const { queryByRole } = renderPluginDetails({ id, isInstalled: true, isDeprecated: false });
await waitFor(() => expect(queryByRole('link', { name: 'deprecated' })).not.toBeInTheDocument());
});
@@ -783,10 +642,7 @@ describe('Plugin details page', () => {
id,
isInstalled: true,
isDeprecated: true,
details: {
statusContext,
links: [],
},
details: { statusContext, links: [] },
});
expect(await findByRole('link', { name: 'deprecated' })).toBeInTheDocument();
@@ -800,10 +656,7 @@ describe('Plugin details page', () => {
id,
isInstalled: true,
isDeprecated: true,
details: {
statusContext,
links: [],
},
details: { statusContext, links: [] },
});
expect(await findByRole('link', { name: 'deprecated' })).toBeInTheDocument();
@@ -816,11 +669,7 @@ describe('Plugin details page', () => {
describe('viewed as user without grafana admin permissions', () => {
beforeAll(() => {
mockUserPermissions({
isAdmin: false,
isDataSourceEditor: false,
isOrgAdmin: false,
});
mockUserPermissions({ isAdmin: false, isDataSourceEditor: false, isOrgAdmin: false });
});
it("should not display an install button for a plugin that isn't installed", async () => {
@@ -858,20 +707,12 @@ describe('Plugin details page', () => {
describe('viewed as user without data source edit permissions', () => {
beforeAll(() => {
mockUserPermissions({
isAdmin: true,
isDataSourceEditor: false,
isOrgAdmin: true,
});
mockUserPermissions({ isAdmin: true, isDataSourceEditor: false, isOrgAdmin: true });
});
it('should not display the data source post installation step', async () => {
const name = 'Akumuli';
const { queryByText } = renderPluginDetails({
name,
isInstalled: true,
type: PluginType.app,
});
const { queryByText } = renderPluginDetails({ name, isInstalled: true, type: PluginType.app });
await waitFor(() => queryByText('Uninstall'));
expect(queryByText('Add new data source')).toBeNull();
@@ -880,11 +721,7 @@ describe('Plugin details page', () => {
describe('Display plugin details right panel', () => {
beforeAll(() => {
mockUserPermissions({
isAdmin: true,
isDataSourceEditor: false,
isOrgAdmin: true,
});
mockUserPermissions({ isAdmin: true, isDataSourceEditor: false, isOrgAdmin: true });
});
it('should display Latest release date and report a concern information', async () => {
@@ -903,21 +740,6 @@ describe('Plugin details page', () => {
expect(queryByText('Last updated:')).toBeNull();
});
it('should display last commit date information', async () => {
const id = 'right-panel-test-plugin';
const lastCommitDate = '2023-10-26T16:54:55.000Z';
const { queryByText } = renderPluginDetails({ id, details: { lastCommitDate, links: [] } });
expect(queryByText('Last commit date:')).toBeVisible();
expect(queryByText('Oct 26, 2023')).toBeVisible();
});
it('should not display last commit date if there is no lastCommit data', async () => {
const id = 'right-panel-test-plugin';
const lastCommitDate = undefined;
const { queryByText } = renderPluginDetails({ id, details: { lastCommitDate, links: [] } });
expect(queryByText('Last commit date:')).toBeNull();
});
it('should not display Report Abuse if the plugin is Core', async () => {
const id = 'right-panel-test-plugin';
const isCore = true;
+10 -47
View File
@@ -74,16 +74,12 @@ export interface Screenshots {
export interface CatalogPluginDetails {
readme?: string;
versions?: Version[];
links: Array<{
name: string;
url: string;
}>;
links: Array<{ name: string; url: string }>;
grafanaDependency?: string;
pluginDependencies?: PluginDependencies['plugins'];
statusContext?: string;
iam?: IdentityAccessManagement;
changelog?: string;
lastCommitDate?: string;
licenseUrl?: string;
documentationUrl?: string;
sponsorshipUrl?: string;
@@ -95,10 +91,7 @@ export interface CatalogPluginDetails {
}
export interface CatalogPluginInfo {
logos: {
large: string;
small: string;
};
logos: { large: string; small: string };
keywords: string[];
}
@@ -115,13 +108,7 @@ export type RemotePlugin = {
json?: {
dependencies: PluginDependencies;
iam?: IdentityAccessManagement;
info: {
links: Array<{
name: string;
url: string;
}>;
screenshots?: Screenshots[] | null;
};
info: { links: Array<{ name: string; url: string }>; screenshots?: Screenshots[] | null };
};
links: Array<{ rel: string; href: string }>;
name: string;
@@ -129,12 +116,7 @@ export type RemotePlugin = {
orgName: string;
orgSlug: string;
orgUrl: string;
packages: {
[arch: string]: {
packageName: string;
downloadUrl: string;
};
};
packages: { [arch: string]: { packageName: string; downloadUrl: string } };
popularity: number;
readme?: string;
signatureType: PluginSignatureType | '';
@@ -154,7 +136,6 @@ export type RemotePlugin = {
versionSignedByOrgName: string;
versionStatus: string;
angularDetected?: boolean;
lastCommitDate?: string;
licenseUrl?: string;
documentationUrl?: string;
sponsorshipUrl?: string;
@@ -184,16 +165,10 @@ export type LocalPlugin = WithAccessControlMetadata & {
author: Rel;
description: string;
links?: Rel[];
logos: {
small: string;
large: string;
};
logos: { small: string; large: string };
keywords: string[];
build: Build;
screenshots?: Array<{
path: string;
name: string;
}> | null;
screenshots?: Array<{ path: string; name: string }> | null;
version: string;
updated: string;
};
@@ -255,11 +230,7 @@ export interface Org {
avatarUrl: string;
}
export type CatalogPluginsState = {
loading: boolean;
error?: Error;
plugins: CatalogPlugin[];
};
export type CatalogPluginsState = { loading: boolean; error?: Error; plugins: CatalogPlugin[] };
export enum PluginStatus {
INSTALL = 'INSTALL',
@@ -300,10 +271,7 @@ export enum RequestStatus {
Fulfilled = 'Fulfilled',
Rejected = 'Rejected',
}
export type RemotePluginResponse = {
plugins: RemotePlugin[];
error?: Error;
};
export type RemotePluginResponse = { plugins: RemotePlugin[]; error?: Error };
export type RequestInfo = {
status: RequestStatus;
@@ -350,11 +318,6 @@ export type PluginVersion = {
angularDetected?: boolean;
};
export type InstancePlugin = {
pluginSlug: string;
version: string;
};
export type InstancePlugin = { pluginSlug: string; version: string };
export type ProvisionedPlugin = {
slug: string;
};
export type ProvisionedPlugin = { slug: string };
-1
View File
@@ -10862,7 +10862,6 @@
"downloads": "Downloads",
"from": "From",
"installedVersion": "Installed Version",
"lastCommitDate": "Last commit date:",
"latestReleaseDate": "Latest release date:",
"latestVersion": "Latest Version",
"license": "License",