From 47aa892d2429b8e8e4e1e8e073b33687cdb59396 Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Wed, 13 Aug 2025 10:18:49 +0200 Subject: [PATCH] Plugins: Add test-ids for plugin details, remove lastCommitDate (#109173) add test-ids for plugin details, remove lastCommitDate --- public/app/features/plugins/admin/api.ts | 12 +- .../admin/components/PluginDetailsPanel.tsx | 52 ++-- .../admin/pages/PluginDetails.test.tsx | 250 +++--------------- public/app/features/plugins/admin/types.ts | 57 +--- public/locales/en-US/grafana.json | 1 - 5 files changed, 69 insertions(+), 303 deletions(-) diff --git a/public/app/features/plugins/admin/api.ts b/public/app/features/plugins/admin/api.ts index 20440a5ee05..74a072ba054 100644 --- a/public/app/features/plugins/admin/api.ts +++ b/public/app/features/plugins/admin/api.ts @@ -35,7 +35,6 @@ export async function getPluginDetails(id: string): Promise return response?.data; } -export const api = { - getRemotePlugins, - getInstalledPlugins: getLocalPlugins, - installPlugin, - uninstallPlugin, -}; +export const api = { getRemotePlugins, getInstalledPlugins: getLocalPlugins, installPlugin, uninstallPlugin }; diff --git a/public/app/features/plugins/admin/components/PluginDetailsPanel.tsx b/public/app/features/plugins/admin/components/PluginDetailsPanel.tsx index bf30e8e2a5b..155218a4501 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPanel.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPanel.tsx @@ -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 ( <> @@ -70,35 +74,25 @@ export function PluginDetailsPanel(props: Props): React.ReactElement | null { {pluginExtentionsInfo.map((infoItem, index) => { return ( - {infoItem.label + ':'} -
{infoItem.value}
+ + {infoItem.label + ':'} + +
+ {infoItem.value} +
); })} {plugin.updatedAt && ( - + Latest release date: {' '} - + {formatDate(new Date(plugin.updatedAt), { day: 'numeric', month: 'short', year: 'numeric' })} )} - {plugin?.details?.lastCommitDate && ( - - - Last commit date: - {' '} - - {formatDate(new Date(plugin.details.lastCommitDate), { - day: 'numeric', - month: 'short', - year: 'numeric', - })} - - - )}
{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' }) }; }; diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx index b7aaed5cef0..dcd34e386fc 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx @@ -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, - { - 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( @@ -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; diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index eaedf1766ee..3cc66bba0b9 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -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 }; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a1d5bb3caac..5d551355482 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -10862,7 +10862,6 @@ "downloads": "Downloads", "from": "From", "installedVersion": "Installed Version", - "lastCommitDate": "Last commit date:", "latestReleaseDate": "Latest release date:", "latestVersion": "Latest Version", "license": "License",