diff --git a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.test.tsx b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.test.tsx new file mode 100644 index 00000000000..83a148a2647 --- /dev/null +++ b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.test.tsx @@ -0,0 +1,124 @@ +import { renderHook } from '@testing-library/react'; + +import { PluginSignatureStatus, PluginSignatureType, PluginType } from '@grafana/data'; +import { config } from '@grafana/runtime'; + +import { CatalogPlugin } from '../types'; + +import { usePluginDetailsTabs } from './usePluginDetailsTabs'; + +jest.mock('react-router-dom-v5-compat', () => ({ + useLocation: () => ({ pathname: '/plugins/test-plugin' }), +})); + +jest.mock('./usePluginConfig'); + +jest.mock('app/core/core', () => ({ + contextSrv: { + hasPermissionInMetadata: jest.fn(() => true), + }, +})); + +const mockUsePluginConfig = jest.requireMock('./usePluginConfig').usePluginConfig; + +const mockPlugin: CatalogPlugin = { + id: 'test-plugin', + name: 'Test Plugin', + description: 'Test plugin description', + type: PluginType.app, + isPublished: true, + isInstalled: true, + isCore: false, + isDev: false, + isDisabled: false, + isDeprecated: false, + isEnterprise: false, + isFullyInstalled: true, + isManaged: false, + isPreinstalled: { found: false, withVersion: false }, + hasUpdate: false, + info: { + logos: { small: '', large: '' }, + keywords: [], + }, + orgName: 'Test', + signature: PluginSignatureStatus.valid, + signatureType: PluginSignatureType.grafana, + signatureOrg: 'Grafana', + popularity: 1, + downloads: 100, + updatedAt: '2023-01-01', + publishedAt: '2023-01-01', + angularDetected: false, + accessControl: {}, +}; + +const mockPluginConfig = { + meta: { + id: 'test-plugin', + name: 'Test Plugin', + type: PluginType.app, + module: 'test', + baseUrl: '', + info: { + author: { name: 'Test' }, + description: 'Test plugin', + logos: { small: '', large: '' }, + links: [], + screenshots: [], + updated: '2023-01-01', + version: '1.0.0', + keywords: [], + }, + }, + configPages: [ + { + id: 'config-page-1', + title: 'Configuration', + icon: 'cog', + body: () => null, + }, + ], +}; + +describe('usePluginDetailsTabs', () => { + beforeEach(() => { + jest.clearAllMocks(); + config.featureToggles.externalServiceAccounts = false; + }); + + it('should not include config-specific tabs while plugin config is loading', () => { + // simulate the race condition: plugin is loaded but config is still loading + mockUsePluginConfig.mockReturnValue({ + loading: true, + error: undefined, + value: null, + }); + + const { result } = renderHook(() => usePluginDetailsTabs(mockPlugin, undefined, false)); + + // should NOT include config page tabs while loading + const configTab = result.current.navModel.children?.find((tab) => tab.id === 'config-page-1'); + expect(configTab).toBeUndefined(); + + // should include basic tabs + const overviewTab = result.current.navModel.children?.find((tab) => tab.id === 'overview'); + expect(overviewTab).toBeDefined(); + }); + + it('should include config-specific tabs when plugin config is loaded', () => { + // simulate config loaded successfully + mockUsePluginConfig.mockReturnValue({ + loading: false, + error: undefined, + value: mockPluginConfig, + }); + + const { result } = renderHook(() => usePluginDetailsTabs(mockPlugin, undefined, false)); + + // should NOW include config page tabs + const configTab = result.current.navModel.children?.find((tab) => tab.id === 'config-page-1'); + expect(configTab).toBeDefined(); + expect(configTab?.text).toBe('Configuration'); + }); +}); diff --git a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx index 6f0f076d54b..77ea7115922 100644 --- a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx @@ -85,7 +85,8 @@ export const usePluginDetailsTabs = ( } // Not extending the tabs with the config pages if the plugin is not installed - if (!pluginConfig) { + // also wait if the plugin config is still loading to avoid showing default tabs prematurely + if (!pluginConfig || loading) { return navModelChildren; } @@ -151,7 +152,7 @@ export const usePluginDetailsTabs = ( } return navModelChildren; - }, [plugin, pluginConfig, pathname, isPublished, currentPageId, isNarrowScreen]); + }, [plugin, pluginConfig, pathname, isPublished, currentPageId, isNarrowScreen, loading]); const navModel: NavModelItem = { text: plugin?.name ?? '',