= {}): 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();
+ });
+ });
+});