From c7e8291bd1916fe0f641aff6497bccce6b8c4758 Mon Sep 17 00:00:00 2001 From: Alexa Vargas <239999+axelavargas@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:10:19 +0100 Subject: [PATCH] Dashboard Library: Add basic unit test to suggested dashboards flow (#113825) * Create unit tests for communityDashboardHelper * Add unit test for autoMapDatasources * Create unit test for DashboardCard * --wip-- [skip ci] * Fix test * fix linting * Add unit test to the dashboardLibraryApi * update codeowners for dashboard library code * fix: improve test coverage and fix failing tests - Fix image handling tests in DashboardCard to verify actual behavior - Fix UID filtering test in autoMapDatasources to use correct mock data - Add test for dimThumbnail prop - Add test for undefined inputs edge case - All 82 tests now passing * render function modified * merge with template dashboard modifications. tests modified --------- Co-authored-by: nmarrs Co-authored-by: Juan Cabanas --- .github/CODEOWNERS | 1 + .../CommunityDashboardMappingForm.test.tsx | 262 +++++++++++++ .../DashboardLibrary/DashboardCard.test.tsx | 349 +++++++++++++++++ .../DashboardLibrary/DashboardCard.tsx | 4 +- .../api/dashboardLibraryApi.test.ts | 263 +++++++++++++ .../api/dashboardLibraryApi.ts | 2 +- .../utils/autoMapDatasources.test.ts | 368 ++++++++++++++++++ .../utils/communityDashboardHelpers.test.ts | 310 +++++++++++++++ 8 files changed, 1557 insertions(+), 2 deletions(-) create mode 100644 public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardMappingForm.test.tsx create mode 100644 public/app/features/dashboard/dashgrid/DashboardLibrary/DashboardCard.test.tsx create mode 100644 public/app/features/dashboard/dashgrid/DashboardLibrary/api/dashboardLibraryApi.test.ts create mode 100644 public/app/features/dashboard/dashgrid/DashboardLibrary/utils/autoMapDatasources.test.ts create mode 100644 public/app/features/dashboard/dashgrid/DashboardLibrary/utils/communityDashboardHelpers.test.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3cb63cf9969..39a480c8a85 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1101,6 +1101,7 @@ eslint-suppressions.json @grafanabot # Grafana Sharing Squad /public/app/features/dashboard-scene/sharing/ @grafana/sharing-squad /public/app/features/dashboard/components/ShareModal/ @grafana/sharing-squad +/public/app/features/dashboard/dashgrid/DashboardLibrary/ @grafana/sharing-squad /public/app/features/manage-dashboards/components/SnapshotListTable.tsx @grafana/sharing-squad /pkg/services/dashboardsnapshots/ @grafana/sharing-squad /public/app/features/explore/QueryLibrary/ @grafana/sharing-squad diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardMappingForm.test.tsx b/public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardMappingForm.test.tsx new file mode 100644 index 00000000000..a59122a6ca7 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardMappingForm.test.tsx @@ -0,0 +1,262 @@ +import { screen, waitFor } from '@testing-library/react'; +import { render } from 'test/test-utils'; + +import { DataSourceInput, DashboardInput, InputType } from 'app/features/manage-dashboards/state/reducers'; + +import { CommunityDashboardMappingForm } from './CommunityDashboardMappingForm'; +import { CONTENT_KINDS, ContentKind, EVENT_LOCATIONS, EventLocation } from './interactions'; +import { InputMapping } from './utils/autoMapDatasources'; + +interface CommunityDashboardMappingFormProps { + dashboardName: string; + libraryItemId: string; + eventLocation: EventLocation; + contentKind: ContentKind; + datasourceTypes: string[]; + unmappedDsInputs: DataSourceInput[]; + constantInputs: DashboardInput[]; + existingMappings: InputMapping[]; + onBack: () => void; + onPreview: (mappings: InputMapping[]) => void; +} + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getDataSourceSrv: () => ({ + getInstanceSettings: jest.fn((uid: string) => ({ + uid, + name: `DataSource ${uid}`, + type: 'prometheus', + })), + }), +})); + +jest.mock('app/features/datasources/components/picker/DataSourcePicker', () => ({ + DataSourcePicker: ({ + onChange, + placeholder, + }: { + onChange: (ds: { uid: string; name: string; type: string }) => void; + placeholder?: string; + }) => ( + + ), +})); + +// Helper functions +const createMockDataSourceInput = (overrides: Partial = {}): DataSourceInput => + ({ + name: 'DS_PROMETHEUS', + pluginId: 'prometheus', + type: InputType.DataSource, + label: 'Prometheus', + value: '', + info: 'Prometheus datasource', + ...overrides, + }) as DataSourceInput; + +const createMockConstantInput = (overrides: Partial = {}): DashboardInput => + ({ + name: 'var_instance', + type: InputType.Constant, + label: 'Instance', + value: 'default', + description: 'Instance name', + info: 'Instance name', + pluginId: undefined, + ...overrides, + }) as DashboardInput; + +const createMockExistingMapping = (overrides: Partial = {}): InputMapping => ({ + name: 'DS_LOKI', + type: 'datasource', + pluginId: 'loki', + value: 'loki-uid', + ...overrides, +}); + +function setupForm(overrides: Partial = {}) { + const defaultProps: CommunityDashboardMappingFormProps = { + dashboardName: 'Test Dashboard', + libraryItemId: '123', + eventLocation: EVENT_LOCATIONS.MODAL_COMMUNITY_TAB, + contentKind: CONTENT_KINDS.COMMUNITY_DASHBOARD, + datasourceTypes: ['prometheus'], + unmappedDsInputs: [], + constantInputs: [], + existingMappings: [], + onBack: jest.fn(), + onPreview: jest.fn(), + ...overrides, + }; + + return render(); +} + +describe('CommunityDashboardMappingForm', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Rendering', () => { + it('should render description text', () => { + setupForm(); + + expect( + screen.getByText('This dashboard requires datasource configuration. Select datasources for each input below.') + ).toBeInTheDocument(); + }); + + it('should render back button', () => { + setupForm(); + + expect(screen.getByRole('button', { name: /back to dashboards/i })).toBeInTheDocument(); + }); + + it('should render preview button', () => { + setupForm(); + + expect(screen.getByRole('button', { name: /preview dashboard/i })).toBeInTheDocument(); + }); + }); + + describe('Auto-mapped datasources alert', () => { + it('should show alert when existing mappings are provided', () => { + setupForm({ existingMappings: [createMockExistingMapping()] }); + + expect(screen.getByText(/1 datasources were automatically configured/i)).toBeInTheDocument(); + expect(screen.getByText(/loki → DataSource loki-uid/i)).toBeInTheDocument(); + }); + + it('should not show alert when no existing mappings', () => { + setupForm(); + + expect(screen.queryByText(/datasources were automatically configured/i)).not.toBeInTheDocument(); + }); + }); + + describe('Datasource inputs', () => { + it('should render datasource configuration section when unmapped inputs exist', () => { + setupForm({ unmappedDsInputs: [createMockDataSourceInput()] }); + + expect(screen.getByText('Datasource Configuration')).toBeInTheDocument(); + expect(screen.getByText('Prometheus')).toBeInTheDocument(); + }); + + it('should render multiple datasource inputs', () => { + setupForm({ + unmappedDsInputs: [ + createMockDataSourceInput({ name: 'DS_PROM', label: 'Prometheus' }), + createMockDataSourceInput({ name: 'DS_LOKI', label: 'Loki', pluginId: 'loki' }), + ], + }); + + expect(screen.getByText('Prometheus')).toBeInTheDocument(); + expect(screen.getByText('Loki')).toBeInTheDocument(); + }); + + it('should not render datasource section when no unmapped inputs', () => { + setupForm(); + + expect(screen.queryByText('Datasource Configuration')).not.toBeInTheDocument(); + }); + }); + + describe('Constant inputs', () => { + it('should render dashboard variables section when constant inputs exist', () => { + setupForm({ constantInputs: [createMockConstantInput()] }); + + expect(screen.getByText('Dashboard Variables')).toBeInTheDocument(); + expect(screen.getByText('Instance')).toBeInTheDocument(); + }); + + it('should render input field with default value', () => { + setupForm({ constantInputs: [createMockConstantInput({ value: 'my-default-value' })] }); + + expect(screen.getByDisplayValue('my-default-value')).toBeInTheDocument(); + }); + + it('should allow editing constant input values', async () => { + const { user } = setupForm({ constantInputs: [createMockConstantInput()] }); + + const input = screen.getByDisplayValue('default'); + await user.clear(input); + await user.type(input, 'new-value'); + + expect(screen.getByDisplayValue('new-value')).toBeInTheDocument(); + }); + + it('should render multiple constant inputs', () => { + setupForm({ + constantInputs: [ + createMockConstantInput({ name: 'var_instance', label: 'Instance' }), + createMockConstantInput({ name: 'var_env', label: 'Environment', value: 'prod' }), + ], + }); + + expect(screen.getByText('Instance')).toBeInTheDocument(); + expect(screen.getByText('Environment')).toBeInTheDocument(); + expect(screen.getByDisplayValue('prod')).toBeInTheDocument(); + }); + }); + + describe('Button interactions', () => { + it('should call onBack when back button is clicked', async () => { + const mockOnBack = jest.fn(); + const { user } = setupForm({ onBack: mockOnBack }); + + await user.click(screen.getByRole('button', { name: /back to dashboards/i })); + + expect(mockOnBack).toHaveBeenCalledTimes(1); + }); + + it('should enable preview button when no unmapped datasources', () => { + setupForm(); + + expect(screen.getByRole('button', { name: /preview dashboard/i })).toBeEnabled(); + }); + + it('should disable preview button when datasources are not mapped', () => { + setupForm({ unmappedDsInputs: [createMockDataSourceInput()] }); + + expect(screen.getByRole('button', { name: /preview dashboard/i })).toBeDisabled(); + }); + + it('should enable preview button after all datasources are mapped', async () => { + const { user } = setupForm({ unmappedDsInputs: [createMockDataSourceInput()] }); + + expect(screen.getByRole('button', { name: /preview dashboard/i })).toBeDisabled(); + + await user.click(screen.getByRole('button', { name: /prometheus datasource/i })); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /preview dashboard/i })).toBeEnabled(); + }); + }); + + it('should call onPreview with all mappings when preview is clicked', async () => { + const mockOnPreview = jest.fn(); + const existingMappings = [createMockExistingMapping()]; + const { user } = setupForm({ onPreview: mockOnPreview, existingMappings }); + + await user.click(screen.getByRole('button', { name: /preview dashboard/i })); + + expect(mockOnPreview).toHaveBeenCalledWith(existingMappings); + }); + + it('should call onPreview with combined mappings including constants', async () => { + const mockOnPreview = jest.fn(); + const constantInputs = [createMockConstantInput({ name: 'var_test', value: 'test-value' })]; + const existingMappings = [createMockExistingMapping()]; + + const { user } = setupForm({ onPreview: mockOnPreview, constantInputs, existingMappings }); + + await user.click(screen.getByRole('button', { name: /preview dashboard/i })); + + expect(mockOnPreview).toHaveBeenCalledWith([ + ...existingMappings, + { name: 'var_test', type: 'constant', value: 'test-value' }, + ]); + }); + }); +}); diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/DashboardCard.test.tsx b/public/app/features/dashboard/dashgrid/DashboardLibrary/DashboardCard.test.tsx new file mode 100644 index 00000000000..939f1bcdb89 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/DashboardCard.test.tsx @@ -0,0 +1,349 @@ +import { screen } from '@testing-library/react'; +import { render } from 'test/test-utils'; + +import { PluginDashboard } from 'app/types/plugins'; + +import { DashboardCard } from './DashboardCard'; +import { GnetDashboard } from './types'; + +// Helper functions for creating mock objects +const createMockPluginDashboard = (overrides: Partial = {}): PluginDashboard => ({ + dashboardId: 1, + description: 'Test description', + imported: false, + importedRevision: 0, + importedUri: '', + importedUrl: '', + path: '', + pluginId: 'test-plugin', + removed: false, + revision: 1, + slug: 'test-dashboard', + title: 'Test Dashboard', + uid: 'test-uid', + ...overrides, +}); + +const createMockGnetDashboard = (overrides: Partial = {}): GnetDashboard => ({ + id: 123, + name: 'Test Dashboard', + description: 'Test description', + datasource: 'Prometheus', + orgName: 'Test Org', + userName: 'testuser', + publishedAt: '', + updatedAt: '', + downloads: 0, + ...overrides, +}); + +const createMockDetails = (overrides = {}) => ({ + id: '123', + datasource: 'Prometheus', + dependencies: ['Prometheus'], + publishedBy: 'Test Org', + lastUpdate: '1 Jan 2025', + grafanaComUrl: 'https://grafana.com/grafana/dashboards/123-test/', + ...overrides, +}); + +describe('DashboardCard', () => { + const mockOnClick = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render title as heading', () => { + const dashboard = createMockPluginDashboard(); + render( + + ); + + expect(screen.getByRole('heading', { name: 'My Dashboard' })).toBeInTheDocument(); + }); + + it('should render image when imageUrl is provided', () => { + const dashboard = createMockPluginDashboard(); + render( + + ); + + const image = screen.getByRole('img', { name: 'Test Dashboard' }); + expect(image).toBeInTheDocument(); + expect(image).toHaveAttribute('src', 'https://example.com/image.png'); + }); + + it('should show "No preview available" when imageUrl is not provided', () => { + const dashboard = createMockPluginDashboard(); + render( + + ); + + expect(screen.getByText('No preview available')).toBeInTheDocument(); + expect(screen.queryByRole('img')).not.toBeInTheDocument(); + }); + + it('should render description when provided', () => { + const dashboard = createMockPluginDashboard({ description: 'My custom description' }); + render( + + ); + + expect(screen.getByText('My custom description')).toBeInTheDocument(); + }); + + it('should not render description when empty', () => { + const dashboard = createMockPluginDashboard({ description: '' }); + render( + + ); + + expect(screen.getByRole('heading', { name: 'Test Dashboard' })).toBeInTheDocument(); + expect(screen.queryByTestId('dashboard-card-description')).not.toBeInTheDocument(); + }); + + describe('Button interactions', () => { + it('should trigger onClick when button is clicked', async () => { + const { user } = render( + + ); + + await user.click(screen.getByRole('button', { name: 'Use dashboard' })); + + expect(mockOnClick).toHaveBeenCalledTimes(1); + }); + + it('should display template button text', () => { + render( + + ); + + expect(screen.getByRole('button', { name: 'Use template' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Use dashboard' })).not.toBeInTheDocument(); + }); + + it('should display dashboard button text', () => { + render( + + ); + + expect(screen.getByRole('button', { name: 'Use dashboard' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Use template' })).not.toBeInTheDocument(); + }); + }); + + describe('Badge display', () => { + it('should show datasource-provided badge when flag is true', () => { + render( + + ); + + expect(screen.getByText('Data source provided')).toBeInTheDocument(); + }); + + it('should not show badge when flag is false', () => { + render( + + ); + + expect(screen.queryByText('Data source provided')).not.toBeInTheDocument(); + }); + + it('should not show badge by default', () => { + render( + + ); + + expect(screen.queryByText('Data source provided')).not.toBeInTheDocument(); + }); + }); + + describe('Details tooltip', () => { + it('should show details icon button when details are provided', () => { + const details = createMockDetails(); + render( + + ); + + expect(screen.getByRole('button', { name: 'Details' })).toBeInTheDocument(); + }); + + it('should not show details icon button when details are not provided', () => { + render( + + ); + + expect(screen.queryByRole('button', { name: 'Details' })).not.toBeInTheDocument(); + }); + + it('should display details information in tooltip', async () => { + const details = createMockDetails({ + id: '456', + datasource: 'Loki', + dependencies: ['Loki', 'Prometheus'], + publishedBy: 'Grafana Labs', + lastUpdate: '15 Dec 2024', + grafanaComUrl: 'https://grafana.com/grafana/dashboards/456-loki/', + }); + + const { user } = render( + + ); + + await user.hover(screen.getByRole('button', { name: 'Details' })); + + expect(await screen.findByText('456')).toBeInTheDocument(); + expect(screen.getByText('Loki')).toBeInTheDocument(); + expect(screen.getByText('Loki | Prometheus')).toBeInTheDocument(); + expect(screen.getByText('Grafana Labs')).toBeInTheDocument(); + expect(screen.getByText('15 Dec 2024')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'View on Grafana.com' })).toHaveAttribute( + 'href', + 'https://grafana.com/grafana/dashboards/456-loki/' + ); + }); + + it('should not show Grafana.com link when grafanaComUrl is not provided', async () => { + const details = createMockDetails({ grafanaComUrl: undefined }); + + const { user } = render( + + ); + + await user.hover(screen.getByRole('button', { name: 'Details' })); + + expect(await screen.findByText('123')).toBeInTheDocument(); + expect(screen.queryByRole('link', { name: 'View on Grafana.com' })).not.toBeInTheDocument(); + }); + }); + + describe('Image handling', () => { + it('should render image correctly for logo vs thumbnail', () => { + const { rerender } = render( + + ); + + let image = screen.getByRole('img'); + expect(image).toBeInTheDocument(); + expect(image).toHaveAttribute('src', 'https://example.com/logo.png'); + + rerender( + + ); + + image = screen.getByRole('img'); + expect(image).toBeInTheDocument(); + expect(image).toHaveAttribute('src', 'https://example.com/screenshot.png'); + }); + + it('should render image when dimThumbnail is true', () => { + render( + + ); + + const image = screen.getByRole('img'); + expect(image).toBeInTheDocument(); + expect(image).toHaveAttribute('src', 'https://example.com/screenshot.png'); + }); + }); + + describe('GnetDashboard support', () => { + it('should render with GnetDashboard', () => { + const dashboard = createMockGnetDashboard({ name: 'Community Dashboard' }); + render( + + ); + + expect(screen.getByRole('heading', { name: 'Community Dashboard' })).toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/DashboardCard.tsx b/public/app/features/dashboard/dashgrid/DashboardLibrary/DashboardCard.tsx index 0f308b0a4ad..4d992320c09 100644 --- a/public/app/features/dashboard/dashgrid/DashboardLibrary/DashboardCard.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/DashboardCard.tsx @@ -77,7 +77,9 @@ function DashboardCardComponent({
{dashboard.description && ( - {dashboard.description} + + {dashboard.description} + )}
diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/api/dashboardLibraryApi.test.ts b/public/app/features/dashboard/dashgrid/DashboardLibrary/api/dashboardLibraryApi.test.ts new file mode 100644 index 00000000000..c662b90e372 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/api/dashboardLibraryApi.test.ts @@ -0,0 +1,263 @@ +import { BackendSrv, getBackendSrv } from '@grafana/runtime'; +import { DashboardJson } from 'app/features/manage-dashboards/types'; +import { PluginDashboard } from 'app/types/plugins'; + +import { GnetDashboard } from '../types'; + +import { + fetchCommunityDashboard, + fetchCommunityDashboards, + fetchProvisionedDashboards, + FetchCommunityDashboardsParams, + GnetDashboardResponse, +} from './dashboardLibraryApi'; + +jest.mock('@grafana/runtime', () => ({ + getBackendSrv: jest.fn(), +})); + +const mockGetBackendSrv = getBackendSrv as jest.MockedFunction; + +// Helper to create mock BackendSrv +const createMockBackendSrv = (overrides: Partial = {}): BackendSrv => + ({ + get: jest.fn(), + ...overrides, + }) as unknown as BackendSrv; + +// Helper functions for creating mock objects +const createMockGnetDashboard = (overrides: Partial = {}): GnetDashboard => ({ + id: 1, + name: 'Test Dashboard', + description: 'Test Description', + downloads: 100, + datasource: 'Prometheus', + ...overrides, +}); + +const createMockPluginDashboard = (overrides: Partial = {}): PluginDashboard => ({ + dashboardId: 1, + uid: 'dash-uid', + title: 'Test Dashboard', + pluginId: 'prometheus', + path: 'dashboards/test.json', + description: 'Test plugin dashboard', + imported: false, + importedRevision: 0, + importedUri: '', + importedUrl: '', + removed: false, + revision: 1, + slug: 'test-dashboard', + ...overrides, +}); + +const defaultFetchParams: FetchCommunityDashboardsParams = { + orderBy: 'downloads', + direction: 'desc', + page: 1, + pageSize: 10, + includeLogo: true, + includeScreenshots: true, +}; + +describe('dashboardLibraryApi', () => { + let mockGet: jest.MockedFunction; + let consoleWarnSpy: jest.SpyInstance; + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + mockGet = jest.fn(); + mockGetBackendSrv.mockReturnValue(createMockBackendSrv({ get: mockGet })); + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + }); + + afterEach(() => { + jest.clearAllMocks(); + consoleWarnSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + + describe('fetchCommunityDashboards', () => { + it('should fetch community dashboards with correct query parameters', async () => { + const mockDashboards = [createMockGnetDashboard({ id: 1 }), createMockGnetDashboard({ id: 2 })]; + const mockResponse = { + page: 1, + pages: 5, + items: mockDashboards, + }; + + mockGet.mockResolvedValue(mockResponse); + + const result = await fetchCommunityDashboards(defaultFetchParams); + + expect(mockGet).toHaveBeenCalledWith( + '/api/gnet/dashboards?orderBy=downloads&direction=desc&page=1&pageSize=10&includeLogo=1&includeScreenshots=true', + undefined, + undefined, + { showErrorAlert: false } + ); + + expect(result).toEqual({ + page: 1, + pages: 5, + items: mockDashboards, + }); + }); + + it('should include dataSourceSlugIn when provided', async () => { + mockGet.mockResolvedValue({ page: 1, pages: 1, items: [] }); + + await fetchCommunityDashboards({ + ...defaultFetchParams, + dataSourceSlugIn: 'prometheus', + }); + + expect(mockGet).toHaveBeenCalledWith( + expect.stringContaining('dataSourceSlugIn=prometheus'), + undefined, + undefined, + { showErrorAlert: false } + ); + }); + + it('should include filter when provided', async () => { + mockGet.mockResolvedValue({ page: 1, pages: 1, items: [] }); + + await fetchCommunityDashboards({ + ...defaultFetchParams, + filter: 'kubernetes', + }); + + expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('filter=kubernetes'), undefined, undefined, { + showErrorAlert: false, + }); + }); + + it('should handle unexpected response format and return empty array', async () => { + const mockResponse = { + page: 1, + pages: 1, + }; + + mockGet.mockResolvedValue(mockResponse); + + const result = await fetchCommunityDashboards(defaultFetchParams); + + expect(consoleWarnSpy).toHaveBeenCalledWith('Unexpected API response format from Grafana.com:', mockResponse); + expect(result).toEqual({ + page: 1, + pages: 1, + items: [], + }); + }); + + it('should use fallback values when page/pages are missing', async () => { + const items = [createMockGnetDashboard()]; + + mockGet.mockResolvedValue({ + items, + }); + + const result = await fetchCommunityDashboards({ + ...defaultFetchParams, + page: 3, + }); + + expect(result.page).toBe(3); + expect(result.pages).toBe(1); + }); + }); + + describe('fetchCommunityDashboard', () => { + it('should fetch a single dashboard by gnetId', async () => { + const gnetId = 12345; + const mockResponse: GnetDashboardResponse = { + json: { + title: 'Test Dashboard', + panels: [], + schemaVersion: 41, + } as DashboardJson, + dependencies: { + items: [ + { + pluginSlug: 'prometheus', + pluginTypeCode: 'datasource', + }, + ], + }, + }; + + mockGet.mockResolvedValue(mockResponse); + + const result = await fetchCommunityDashboard(gnetId); + + expect(mockGet).toHaveBeenCalledWith('/api/gnet/dashboards/12345'); + expect(result).toEqual(mockResponse); + }); + + it('should handle dashboard without dependencies', async () => { + const gnetId = 999; + const mockResponse: GnetDashboardResponse = { + json: { + title: 'Simple Dashboard', + panels: [], + schemaVersion: 41, + } as DashboardJson, + }; + + mockGet.mockResolvedValue(mockResponse); + + const result = await fetchCommunityDashboard(gnetId); + + expect(result).toEqual(mockResponse); + expect(result.dependencies).toBeUndefined(); + }); + }); + + describe('fetchProvisionedDashboards', () => { + it('should fetch provisioned dashboards for a datasource type', async () => { + const datasourceType = 'prometheus'; + const mockDashboards: PluginDashboard[] = [ + createMockPluginDashboard({ uid: 'dash-1', title: 'Dashboard 1' }), + createMockPluginDashboard({ uid: 'dash-2', title: 'Dashboard 2' }), + ]; + + mockGet.mockResolvedValue(mockDashboards); + + const result = await fetchProvisionedDashboards(datasourceType); + + expect(mockGet).toHaveBeenCalledWith('api/plugins/prometheus/dashboards', undefined, undefined, { + showErrorAlert: false, + }); + expect(result).toEqual(mockDashboards); + }); + + it('should return empty array when response is not an array', async () => { + mockGet.mockResolvedValue({ error: 'Not found' }); + + const result = await fetchProvisionedDashboards('unknown-plugin'); + + expect(result).toEqual([]); + }); + + it('should handle API errors gracefully', async () => { + const error = new Error('Network error'); + mockGet.mockRejectedValue(error); + + const result = await fetchProvisionedDashboards('prometheus'); + + expect(consoleErrorSpy).toHaveBeenCalledWith('Error loading provisioned dashboards', error); + expect(result).toEqual([]); + }); + + it('should return empty array for datasource with no provisioned dashboards', async () => { + mockGet.mockResolvedValue([]); + + const result = await fetchProvisionedDashboards('mysql'); + + expect(result).toEqual([]); + }); + }); +}); diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/api/dashboardLibraryApi.ts b/public/app/features/dashboard/dashgrid/DashboardLibrary/api/dashboardLibraryApi.ts index 6f3a4045368..ac74a089f66 100644 --- a/public/app/features/dashboard/dashgrid/DashboardLibrary/api/dashboardLibraryApi.ts +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/api/dashboardLibraryApi.ts @@ -71,7 +71,7 @@ export async function fetchCommunityDashboards( // Grafana.com API returns format: { page: number, pages: number, items: GnetDashboard[] } // We normalize it to use "dashboards" instead of "items" for consistency - if (result) { + if (result && Array.isArray(result.items)) { return { page: result.page || params.page, pages: result.pages || 1, diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/autoMapDatasources.test.ts b/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/autoMapDatasources.test.ts new file mode 100644 index 00000000000..88bb5a0213d --- /dev/null +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/autoMapDatasources.test.ts @@ -0,0 +1,368 @@ +import { DataSourceSrv, getDataSourceSrv } from '@grafana/runtime'; +import { Input } from 'app/features/dashboard/components/DashExportModal/DashboardExporter'; +import { DashboardInput, DataSourceInput, InputType } from 'app/features/manage-dashboards/state/reducers'; + +import { + isDataSourceInput, + tryAutoMapDatasources, + parseConstantInputs, + mapConstantInputs, + mapUserSelectedDatasources, +} from './autoMapDatasources'; + +jest.mock('@grafana/runtime', () => ({ + getDataSourceSrv: jest.fn(), +})); + +const mockGetDataSourceSrv = getDataSourceSrv as jest.MockedFunction; + +// Helper to create partial DataSourceSrv mock +const createMockDataSourceSrv = (overrides: Partial = {}): DataSourceSrv => ({ + get: jest.fn(), + getList: jest.fn(), + getInstanceSettings: jest.fn(), + reload: jest.fn(), + registerRuntimeDataSource: jest.fn(), + ...overrides, +}); + +// Helper functions for creating mock objects +const createMockDataSourceInput = (overrides: Partial = {}): DataSourceInput => + ({ + name: 'DS_PROMETHEUS', + pluginId: 'prometheus', + type: InputType.DataSource, + label: 'Prometheus', + value: '', + info: 'Prometheus datasource', + ...overrides, + }) as DataSourceInput; + +const createMockConstantInput = (overrides: Partial = {}): DashboardInput => + ({ + name: 'var_instance', + type: InputType.Constant, + label: 'Instance', + value: 'default', + description: 'Instance name', + info: 'Instance name', + pluginId: undefined, + ...overrides, + }) as DashboardInput; + +const createMockInput = (overrides: Partial = {}): Input => + ({ + name: 'test_input', + type: 'constant', + label: 'Test', + value: 'default', + description: 'Test input', + ...overrides, + }) as Input; + +describe('autoMapDatasources', () => { + describe('isDataSourceInput', () => { + it('should return true for datasource input with pluginId', () => { + const input = { ...createMockInput({ type: 'datasource' }), pluginId: 'prometheus' }; + + expect(isDataSourceInput(input)).toBe(true); + }); + + it('should return false for constant input', () => { + const input = createMockInput({ type: 'constant' }); + + expect(isDataSourceInput(input)).toBe(false); + }); + + it('should return false for datasource input missing pluginId', () => { + const input = createMockInput({ type: 'datasource' }); + + expect(isDataSourceInput(input)).toBe(false); + }); + }); + + describe('tryAutoMapDatasources', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should auto-map when current datasource matches required type', () => { + const input = createMockDataSourceInput({ pluginId: 'prometheus' }); + const currentDatasourceUid = 'prom-uid'; + + mockGetDataSourceSrv.mockReturnValue( + createMockDataSourceSrv({ + getList: jest.fn().mockReturnValue([ + { uid: 'prom-uid', type: 'prometheus' }, + { uid: 'prom-uid-2', type: 'prometheus' }, + ]), + }) + ); + + const result = tryAutoMapDatasources([input], currentDatasourceUid); + + expect(result.allMapped).toBe(true); + expect(result.mappings).toHaveLength(1); + expect(result.mappings[0]).toEqual({ + name: 'DS_PROMETHEUS', + type: 'datasource', + pluginId: 'prometheus', + value: 'prom-uid', + }); + expect(result.unmappedDsInputs).toHaveLength(0); + }); + + it('should auto-map single compatible datasource when current datasource is same type', () => { + const input = createMockDataSourceInput({ pluginId: 'prometheus' }); + const currentDatasourceUid = 'other-uid'; + + mockGetDataSourceSrv.mockReturnValue( + createMockDataSourceSrv({ + getList: jest.fn().mockReturnValue([{ uid: 'prom-uid', type: 'prometheus' }]), + getInstanceSettings: jest.fn().mockReturnValue({ type: 'prometheus' }), + }) + ); + + const result = tryAutoMapDatasources([input], currentDatasourceUid); + + expect(result.allMapped).toBe(true); + expect(result.mappings).toHaveLength(1); + expect(result.mappings[0].value).toBe('prom-uid'); + }); + + it('should not auto-map single compatible datasource when current datasource is different type', () => { + const input = createMockDataSourceInput({ pluginId: 'prometheus' }); + const currentDatasourceUid = 'loki-uid'; + + mockGetDataSourceSrv.mockReturnValue( + createMockDataSourceSrv({ + getList: jest.fn().mockReturnValue([{ uid: 'prom-uid', type: 'prometheus' }]), + getInstanceSettings: jest.fn().mockReturnValue({ type: 'loki' }), + }) + ); + + const result = tryAutoMapDatasources([input], currentDatasourceUid); + + expect(result.allMapped).toBe(false); + expect(result.mappings).toHaveLength(0); + expect(result.unmappedDsInputs).toHaveLength(1); + }); + + it('should not auto-map when multiple compatible datasources exist', () => { + const input = createMockDataSourceInput({ pluginId: 'prometheus' }); + const currentDatasourceUid = 'loki-uid'; + + mockGetDataSourceSrv.mockReturnValue( + createMockDataSourceSrv({ + getList: jest.fn().mockReturnValue([ + { uid: 'prom-uid-1', type: 'prometheus' }, + { uid: 'prom-uid-2', type: 'prometheus' }, + ]), + }) + ); + + const result = tryAutoMapDatasources([input], currentDatasourceUid); + + expect(result.allMapped).toBe(false); + expect(result.mappings).toHaveLength(0); + expect(result.unmappedDsInputs).toHaveLength(1); + }); + + it('should return unmapped when no compatible datasources exist', () => { + const input = createMockDataSourceInput({ pluginId: 'prometheus' }); + const currentDatasourceUid = 'loki-uid'; + + mockGetDataSourceSrv.mockReturnValue( + createMockDataSourceSrv({ + getList: jest.fn().mockReturnValue([]), + }) + ); + + const result = tryAutoMapDatasources([input], currentDatasourceUid); + + expect(result.allMapped).toBe(false); + expect(result.mappings).toHaveLength(0); + expect(result.unmappedDsInputs).toHaveLength(1); + }); + + it('should handle empty inputs array', () => { + const result = tryAutoMapDatasources([], 'any-uid'); + + expect(result.allMapped).toBe(true); + expect(result.mappings).toHaveLength(0); + expect(result.unmappedDsInputs).toHaveLength(0); + }); + + it('should filter out datasources without UIDs', () => { + const input = createMockDataSourceInput({ pluginId: 'prometheus' }); + const currentDatasourceUid = 'prom-uid'; // Current datasource matches the required type + + mockGetDataSourceSrv.mockReturnValue( + createMockDataSourceSrv({ + getList: jest.fn().mockReturnValue([ + { uid: 'prom-uid', type: 'prometheus' }, + { type: 'prometheus' }, // Missing UID - should be filtered + { uid: undefined, type: 'prometheus' }, // Undefined UID - should be filtered + ]), + }) + ); + + const result = tryAutoMapDatasources([input], currentDatasourceUid); + + // Should auto-map since current datasource matches and there's only one valid datasource with UID + expect(result.allMapped).toBe(true); + expect(result.mappings).toHaveLength(1); + expect(result.mappings[0].value).toBe('prom-uid'); + }); + }); + + describe('parseConstantInputs', () => { + it('should parse constant inputs from __inputs array', () => { + const allInputs: Input[] = [ + createMockInput({ name: 'var_instance', type: 'constant', label: 'Instance', description: 'Instance name' }), + createMockInput({ + name: 'var_env', + type: 'constant', + label: 'Environment', + value: 'prod', + description: 'Environment', + }), + ]; + + const result = parseConstantInputs(allInputs); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + name: 'var_instance', + label: 'Instance', + description: 'Instance name', + info: 'Instance name', + value: 'default', + type: InputType.Constant, + pluginId: undefined, + }); + }); + + it('should filter out datasource inputs', () => { + const allInputs: Input[] = [ + createMockInput({ name: 'var_instance', type: 'constant', description: 'Instance name' }), + createMockInput({ name: 'DS_PROM', type: 'datasource', description: 'Prometheus datasource' }), + ]; + + const result = parseConstantInputs(allInputs); + + expect(result).toHaveLength(1); + expect(result[0].name).toBe('var_instance'); + }); + + it('should handle empty inputs array', () => { + const result = parseConstantInputs([]); + + expect(result).toHaveLength(0); + }); + + it('should handle null inputs', () => { + const result = parseConstantInputs(null!); + + expect(result).toHaveLength(0); + }); + + it('should handle undefined inputs', () => { + const result = parseConstantInputs(undefined!); + + expect(result).toHaveLength(0); + }); + + it('should use label as fallback when label is missing', () => { + const input = createMockInput({ name: 'var_instance', type: 'constant', label: '' }); + const result = parseConstantInputs([input]); + + expect(result[0].label).toBe('var_instance'); + }); + + it('should use default info when description is missing', () => { + const input = createMockInput({ name: 'var_instance', type: 'constant', description: '' }); + const result = parseConstantInputs([input]); + + expect(result[0].info).toBe('Specify a string constant'); + }); + }); + + describe('mapConstantInputs', () => { + it('should use user-provided values when available', () => { + const constantInputs: DashboardInput[] = [createMockConstantInput()]; + const userValues = { var_instance: 'custom-value' }; + + const result = mapConstantInputs(constantInputs, userValues); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + name: 'var_instance', + type: 'constant', + value: 'custom-value', + }); + }); + + it('should fall back to default values when user values not provided', () => { + const constantInputs: DashboardInput[] = [createMockConstantInput()]; + const userValues = {}; + + const result = mapConstantInputs(constantInputs, userValues); + + expect(result[0].value).toBe('default'); + }); + + it('should handle empty constantInputs array', () => { + const result = mapConstantInputs([], {}); + + expect(result).toHaveLength(0); + }); + }); + + describe('mapUserSelectedDatasources', () => { + it('should map user selections to InputMapping format', () => { + const unmappedInputs: DataSourceInput[] = [createMockDataSourceInput()]; + const userSelectedDsMappings = { + DS_PROMETHEUS: { + name: 'DS_PROMETHEUS', + pluginId: 'prometheus', + datasource: { uid: 'selected-uid' }, + }, + }; + + const result = mapUserSelectedDatasources(unmappedInputs, userSelectedDsMappings); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + name: 'DS_PROMETHEUS', + type: 'datasource', + pluginId: 'prometheus', + value: 'selected-uid', + }); + }); + + it('should handle missing datasource selections', () => { + const unmappedInputs: DataSourceInput[] = [createMockDataSourceInput()]; + const userSelectedDsMappings = {}; + + const result = mapUserSelectedDatasources(unmappedInputs, userSelectedDsMappings); + + expect(result[0].value).toBe(''); + }); + + it('should handle undefined datasource in selection', () => { + const unmappedInputs: DataSourceInput[] = [createMockDataSourceInput()]; + const userSelectedDsMappings = { + DS_PROMETHEUS: { + name: 'DS_PROMETHEUS', + pluginId: 'prometheus', + datasource: undefined, + }, + }; + + const result = mapUserSelectedDatasources(unmappedInputs, userSelectedDsMappings); + + expect(result[0].value).toBe(''); + }); + }); +}); diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/communityDashboardHelpers.test.ts b/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/communityDashboardHelpers.test.ts new file mode 100644 index 00000000000..58e77b7d13f --- /dev/null +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/communityDashboardHelpers.test.ts @@ -0,0 +1,310 @@ +import { locationService } from '@grafana/runtime'; +import { InputType, DataSourceInput, DashboardInput } from 'app/features/manage-dashboards/state/reducers'; +import { DashboardJson } from 'app/features/manage-dashboards/types'; + +import { DASHBOARD_LIBRARY_ROUTES } from '../../types'; +import { fetchCommunityDashboard } from '../api/dashboardLibraryApi'; +import { CONTENT_KINDS, CREATION_ORIGINS, EVENT_LOCATIONS, SOURCE_ENTRY_POINTS } from '../interactions'; +import { GnetDashboard } from '../types'; + +import { InputMapping, tryAutoMapDatasources, parseConstantInputs } from './autoMapDatasources'; +import { + buildDashboardDetails, + buildGrafanaComUrl, + createSlug, + getLogoUrl, + navigateToTemplate, + onUseCommunityDashboard, +} from './communityDashboardHelpers'; + +jest.mock('../api/dashboardLibraryApi', () => ({ + fetchCommunityDashboard: jest.fn(), +})); + +jest.mock('./autoMapDatasources', () => ({ + ...jest.requireActual('./autoMapDatasources'), + tryAutoMapDatasources: jest.fn(), + parseConstantInputs: jest.fn(), +})); + +// Mock function references +const mockFetchCommunityDashboard = fetchCommunityDashboard as jest.MockedFunction; +const mockTryAutoMapDatasources = tryAutoMapDatasources as jest.MockedFunction; +const mockParseConstantInputs = parseConstantInputs as jest.MockedFunction; + +// Helper functions for creating mock objects +const createMockGnetDashboard = (overrides: Partial = {}): GnetDashboard => ({ + id: 123, + name: 'Test Dashboard', + description: '', + datasource: 'Prometheus', + orgName: 'Test Org', + userName: 'testuser', + publishedAt: '', + updatedAt: '2025-11-05T16:55:41.000Z', + downloads: 0, + ...overrides, +}); + +const createMockDashboardJson = (overrides: Partial = {}): DashboardJson => + ({ + __inputs: [], + title: 'Test Dashboard', + panels: [], + schemaVersion: 41, + uid: 'test-uid', + version: 1, + editable: true, + graphTooltip: 0, + timezone: 'browser', + ...overrides, + }) as DashboardJson; + +describe('communityDashboardHelpers', () => { + describe('createSlug', () => { + it('should convert to lower case', () => { + expect(createSlug('Test')).toBe('test'); + }); + + it('should replace non-alphanumeric characters with hyphens', () => { + expect(createSlug('Test@#example')).toBe('test-example'); + }); + + it('should remove leading and trailing hyphens', () => { + expect(createSlug('-test-')).toBe('test'); + }); + }); + + describe('buildGrafanaComUrl', () => { + it('should build a valid URL', () => { + const gnetDashboard = createMockGnetDashboard({ + id: 1, + name: 'Test', + }); + + expect(buildGrafanaComUrl(gnetDashboard)).toBe('https://grafana.com/grafana/dashboards/1-test/'); + }); + }); + + describe('buildDashboardDetails', () => { + it('should build a valid dashboard details object', () => { + const gnetDashboard = createMockGnetDashboard({ + id: 1, + name: 'Test', + datasource: 'Test', + orgName: 'Org', + updatedAt: '2025-11-05T16:55:41.000Z', + }); + + const result = buildDashboardDetails(gnetDashboard); + + expect(result).toEqual({ + id: '1', + datasource: 'Test', + dependencies: ['Test'], + publishedBy: 'Org', + lastUpdate: expect.any(String), // Date format varies by locale + grafanaComUrl: 'https://grafana.com/grafana/dashboards/1-test/', + }); + + // Verify the date was formatted (not the raw ISO string) + expect(result.lastUpdate).not.toBe('2025-11-05T16:55:41.000Z'); + expect(result.lastUpdate).toContain('2025'); + expect(result.lastUpdate).toMatch(/Nov/); + }); + }); + + describe('getLogoUrl', () => { + it('should return an empty string if no logo is found', () => { + const gnetDashboard = createMockGnetDashboard(); + + expect(getLogoUrl(gnetDashboard)).toBe(''); + }); + + it('should return a valid logo URL', () => { + const gnetDashboard = createMockGnetDashboard({ + logos: { + large: { + content: 'aGVsbG8=', + type: 'image/png', + filename: '/dashboards/abc/large_logo/logo.png', + }, + }, + }); + + expect(getLogoUrl(gnetDashboard)).toBe('data:image/png;base64,aGVsbG8='); + }); + }); + + describe('navigateToTemplate', () => { + it('should navigate to the template route with the correct parameters', () => { + const dashboardTitle = 'Test Dashboard'; + const gnetId = 123; + const datasourceUid = 'test-datasource'; + const mappings: InputMapping[] = []; + const eventLocation = EVENT_LOCATIONS.EMPTY_DASHBOARD; + const contentKind = CONTENT_KINDS.COMMUNITY_DASHBOARD; + + const mockLocationServicePush = jest.fn(); + locationService.push = mockLocationServicePush; + + navigateToTemplate(dashboardTitle, gnetId, datasourceUid, mappings, eventLocation, contentKind); + + expect(mockLocationServicePush).toHaveBeenCalledWith({ + pathname: DASHBOARD_LIBRARY_ROUTES.Template, + search: expect.any(String), + }); + + const callArgs = mockLocationServicePush.mock.calls[0][0]; + const searchParams = new URLSearchParams(callArgs.search); + + expect(searchParams.get('title')).toBe('Test Dashboard'); + expect(searchParams.get('gnetId')).toBe('123'); + expect(searchParams.get('datasource')).toBe('test-datasource'); + expect(searchParams.get('sourceEntryPoint')).toBe(SOURCE_ENTRY_POINTS.DATASOURCE_PAGE); + expect(searchParams.get('creationOrigin')).toBe(CREATION_ORIGINS.DASHBOARD_LIBRARY_COMMUNITY_DASHBOARD); + expect(searchParams.get('contentKind')).toBe(CONTENT_KINDS.COMMUNITY_DASHBOARD); + expect(searchParams.get('eventLocation')).toBe(EVENT_LOCATIONS.EMPTY_DASHBOARD); + expect(searchParams.get('mappings')).toBe('[]'); + }); + }); + + describe('onUseCommunityDashboard', () => { + async function setup(options?: { + dashboard?: Partial; + dashboardJson?: Partial; + autoMapResult?: { + allMapped: boolean; + mappings: InputMapping[]; + unmappedDsInputs: DataSourceInput[]; + }; + constantInputs?: DashboardInput[]; + onShowMapping?: jest.Mock; + }) { + const dashboard = createMockGnetDashboard(options?.dashboard); + const dashboardJson = createMockDashboardJson(options?.dashboardJson); + + mockFetchCommunityDashboard.mockResolvedValue({ json: dashboardJson }); + + mockTryAutoMapDatasources.mockReturnValue( + options?.autoMapResult ?? { + allMapped: true, + mappings: [], + unmappedDsInputs: [], + } + ); + + mockParseConstantInputs.mockReturnValue(options?.constantInputs ?? []); + + await onUseCommunityDashboard({ + dashboard, + datasourceUid: 'test-ds-uid', + datasourceType: 'prometheus', + eventLocation: 'empty_dashboard', + onShowMapping: options?.onShowMapping, + }); + } + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should navigate directly when all datasources are auto-mapped and no constants', async () => { + await setup({ + autoMapResult: { + allMapped: true, + mappings: [{ name: 'DS_PROM', type: 'datasource', value: 'prom-uid', pluginId: 'prometheus' }], + unmappedDsInputs: [], + }, + }); + + expect(locationService.push).toHaveBeenCalled(); + expect(locationService.push).toHaveBeenCalledWith( + expect.objectContaining({ + pathname: expect.any(String), + search: expect.stringContaining('gnetId=123'), + }) + ); + }); + + it('should show mapping form when datasources are unmapped', async () => { + const mockOnShowMapping = jest.fn(); + + await setup({ + autoMapResult: { + allMapped: false, + mappings: [], + unmappedDsInputs: [ + { + name: 'DS_PROM', + pluginId: 'prometheus', + type: InputType.DataSource, + info: 'prometheus', + value: '', + label: 'Prometheus', + }, + ], + }, + onShowMapping: mockOnShowMapping, + }); + + expect(mockOnShowMapping).toHaveBeenCalled(); + expect(locationService.push).not.toHaveBeenCalled(); + expect(mockOnShowMapping).toHaveBeenCalledWith( + expect.objectContaining({ + dashboardName: 'Test Dashboard', + unmappedDsInputs: expect.arrayContaining([expect.objectContaining({ name: 'DS_PROM' })]), + }) + ); + }); + + it('should show mapping form when constants exist even if datasources are mapped', async () => { + const mockOnShowMapping = jest.fn(); + + await setup({ + autoMapResult: { + allMapped: true, + mappings: [{ name: 'DS_PROM', type: 'datasource', value: 'prom-uid', pluginId: 'prometheus' }], + unmappedDsInputs: [], + }, + constantInputs: [ + { + name: 'var_instance', + label: 'Instance', + description: 'Instance name', + info: 'Enter instance name', + value: 'default', + type: InputType.Constant, + }, + ], + onShowMapping: mockOnShowMapping, + }); + + expect(mockOnShowMapping).toHaveBeenCalled(); + expect(locationService.push).not.toHaveBeenCalled(); + expect(mockOnShowMapping).toHaveBeenCalledWith( + expect.objectContaining({ + dashboardName: 'Test Dashboard', + constantInputs: expect.arrayContaining([expect.objectContaining({ name: 'var_instance' })]), + }) + ); + }); + + it('should handle API errors gracefully', async () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + mockFetchCommunityDashboard.mockRejectedValue(new Error('API failed')); + + await onUseCommunityDashboard({ + dashboard: createMockGnetDashboard(), + datasourceUid: 'test-ds-uid', + datasourceType: 'prometheus', + eventLocation: 'empty_dashboard', + }); + + expect(consoleErrorSpy).toHaveBeenCalledWith('Error loading community dashboard:', expect.any(Error)); + expect(locationService.push).not.toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); + }); + }); +});