From 63237f0be851d65acfb09ede64c1aa9c5c6b5454 Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Mon, 11 Aug 2025 14:45:21 +0200 Subject: [PATCH] Plugins: All plugins are up to date message if now new updates available (#109175) * add message for no updates abailable * add tests to PluginList --- .../admin/components/PluginList.test.tsx | 97 +++++++++++++++++++ .../plugins/admin/components/PluginList.tsx | 11 ++- public/locales/en-US/grafana.json | 3 + 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 public/app/features/plugins/admin/components/PluginList.test.tsx diff --git a/public/app/features/plugins/admin/components/PluginList.test.tsx b/public/app/features/plugins/admin/components/PluginList.test.tsx new file mode 100644 index 00000000000..3588bd3f895 --- /dev/null +++ b/public/app/features/plugins/admin/components/PluginList.test.tsx @@ -0,0 +1,97 @@ +import { render, screen } from '@testing-library/react'; + +import { PluginSignatureStatus, PluginSignatureType, PluginType } from '@grafana/data'; +import { config } from '@grafana/runtime'; + +import { CatalogPlugin } from '../types'; + +import { PluginList } from './PluginList'; + +jest.mock('react-router-dom-v5-compat', () => ({ useLocation: jest.fn(), useSearchParams: jest.fn() })); + +const mockUseLocation = jest.requireMock('react-router-dom-v5-compat').useLocation; +const mockUseSearchParams = jest.requireMock('react-router-dom-v5-compat').useSearchParams; + +const mockPlugin: CatalogPlugin = { + description: 'Test plugin description', + downloads: 1000, + hasUpdate: false, + id: 'test-plugin', + info: { logos: { small: 'small-logo-url', large: 'large-logo-url' }, keywords: ['test', 'plugin'] }, + isDev: false, + isCore: false, + isEnterprise: false, + isInstalled: true, + isDisabled: false, + isDeprecated: false, + isManaged: false, + isPreinstalled: { found: false, withVersion: false }, + isPublished: true, + name: 'Test Plugin', + orgName: 'Test Org', + signature: PluginSignatureStatus.valid, + signatureType: PluginSignatureType.grafana, + signatureOrg: 'Test Signature Org', + popularity: 4, + publishedAt: '2023-01-01', + type: PluginType.app, + updatedAt: '2023-12-01', + installedVersion: '1.0.0', + angularDetected: false, + isFullyInstalled: true, + accessControl: {}, +}; + +const mockPlugin2: CatalogPlugin = { + ...mockPlugin, + id: 'test-plugin-2', + name: 'Test Plugin 2', + type: PluginType.datasource, +}; + +describe('PluginList', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUseLocation.mockReturnValue({ pathname: '/plugins' }); + mockUseSearchParams.mockReturnValue([new URLSearchParams(), jest.fn()]); + config.appSubUrl = ''; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render plugins when not loading and plugins exist', () => { + const plugins = [mockPlugin, mockPlugin2]; + render(); + + expect(screen.getByTestId('plugin-list')).toBeInTheDocument(); + expect(screen.getByText('Test Plugin')).toBeInTheDocument(); + expect(screen.getByText('Test Plugin 2')).toBeInTheDocument(); + }); + + it('should show "All plugins are up to date" message when filterBy=has-update and no plugins', () => { + mockUseSearchParams.mockReturnValue([new URLSearchParams('filterBy=has-update'), jest.fn()]); + + render(); + + expect(screen.getByText('All plugins are up to date')).toBeInTheDocument(); + }); + + it('should show "No plugins found" message when filterBy=all and not loading', () => { + mockUseSearchParams.mockReturnValue([new URLSearchParams('filterBy=all'), jest.fn()]); + + render(); + + expect(screen.getByText('No plugins found')).toBeInTheDocument(); + }); + + it('should not show empty state when filterBy=has-update but plugins exist', () => { + mockUseSearchParams.mockReturnValue([new URLSearchParams('filterBy=has-update'), jest.fn()]); + + render(); + + expect(screen.queryByText('All plugins are up to date')).not.toBeInTheDocument(); + expect(screen.getByText('Test Plugin')).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/plugins/admin/components/PluginList.tsx b/public/app/features/plugins/admin/components/PluginList.tsx index 74b7a0cd2b0..6ba9fff955f 100644 --- a/public/app/features/plugins/admin/components/PluginList.tsx +++ b/public/app/features/plugins/admin/components/PluginList.tsx @@ -1,4 +1,4 @@ -import { useLocation } from 'react-router-dom-v5-compat'; +import { useLocation, useSearchParams } from 'react-router-dom-v5-compat'; import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; @@ -15,9 +15,18 @@ interface Props { export const PluginList = ({ plugins, isLoading }: Props) => { const { pathname } = useLocation(); + const [searchParams] = useSearchParams(); const pathName = config.appSubUrl + (pathname.endsWith('/') ? pathname.slice(0, -1) : pathname); + if (searchParams.get('filterBy') === 'has-update' && !isLoading && plugins.length === 0) { + return ( + + ); + } if (!isLoading && plugins.length === 0) { return ; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 4cc86e24c64..98441d484f4 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -10941,6 +10941,9 @@ "label-plugin-id": "Plugin Id", "label-severity": "Severity" }, + "no-updates-available": { + "message": "All plugins are up to date" + }, "not-found-plugin": { "body-plugin-not-found": "That plugin cannot be found. Please check the url is correct or <1>go to the <3>plugin catalog.", "title-plugin-not-found": "Plugin not found"