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>1>go to the <3>plugin catalog3>.",
"title-plugin-not-found": "Plugin not found"