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 <nathanielmarrs@gmail.com>
Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com>
This commit is contained in:
Alexa Vargas
2025-11-17 12:10:19 -03:00
committed by GitHub
co-authored by nmarrs Juan Cabanas
parent e9883f6c61
commit c7e8291bd1
8 changed files with 1557 additions and 2 deletions
+1
View File
@@ -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
@@ -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;
}) => (
<button onClick={() => onChange({ uid: 'test-ds-uid', name: 'Test DS', type: 'prometheus' })}>{placeholder}</button>
),
}));
// Helper functions
const createMockDataSourceInput = (overrides: Partial<DataSourceInput> = {}): DataSourceInput =>
({
name: 'DS_PROMETHEUS',
pluginId: 'prometheus',
type: InputType.DataSource,
label: 'Prometheus',
value: '',
info: 'Prometheus datasource',
...overrides,
}) as DataSourceInput;
const createMockConstantInput = (overrides: Partial<DashboardInput> = {}): 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> = {}): InputMapping => ({
name: 'DS_LOKI',
type: 'datasource',
pluginId: 'loki',
value: 'loki-uid',
...overrides,
});
function setupForm(overrides: Partial<CommunityDashboardMappingFormProps> = {}) {
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(<CommunityDashboardMappingForm {...defaultProps} />);
}
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' },
]);
});
});
});
@@ -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> = {}): 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> = {}): 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(
<DashboardCard title="My Dashboard" dashboard={dashboard} onClick={mockOnClick} kind="suggested_dashboard" />
);
expect(screen.getByRole('heading', { name: 'My Dashboard' })).toBeInTheDocument();
});
it('should render image when imageUrl is provided', () => {
const dashboard = createMockPluginDashboard();
render(
<DashboardCard
title="Test Dashboard"
imageUrl="https://example.com/image.png"
dashboard={dashboard}
onClick={mockOnClick}
kind="suggested_dashboard"
/>
);
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(
<DashboardCard title="Test Dashboard" dashboard={dashboard} onClick={mockOnClick} kind="suggested_dashboard" />
);
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(
<DashboardCard title="Test Dashboard" dashboard={dashboard} onClick={mockOnClick} kind="suggested_dashboard" />
);
expect(screen.getByText('My custom description')).toBeInTheDocument();
});
it('should not render description when empty', () => {
const dashboard = createMockPluginDashboard({ description: '' });
render(
<DashboardCard title="Test Dashboard" dashboard={dashboard} onClick={mockOnClick} kind="suggested_dashboard" />
);
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(
<DashboardCard
title="Test Dashboard"
dashboard={createMockPluginDashboard()}
onClick={mockOnClick}
kind="suggested_dashboard"
/>
);
await user.click(screen.getByRole('button', { name: 'Use dashboard' }));
expect(mockOnClick).toHaveBeenCalledTimes(1);
});
it('should display template button text', () => {
render(
<DashboardCard
title="Test Dashboard"
dashboard={createMockPluginDashboard()}
onClick={mockOnClick}
kind="template_dashboard"
/>
);
expect(screen.getByRole('button', { name: 'Use template' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Use dashboard' })).not.toBeInTheDocument();
});
it('should display dashboard button text', () => {
render(
<DashboardCard
title="Test Dashboard"
dashboard={createMockPluginDashboard()}
onClick={mockOnClick}
kind="suggested_dashboard"
/>
);
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(
<DashboardCard
title="Test Dashboard"
dashboard={createMockPluginDashboard()}
onClick={mockOnClick}
showDatasourceProvidedBadge={true}
kind="suggested_dashboard"
/>
);
expect(screen.getByText('Data source provided')).toBeInTheDocument();
});
it('should not show badge when flag is false', () => {
render(
<DashboardCard
title="Test Dashboard"
dashboard={createMockPluginDashboard()}
onClick={mockOnClick}
showDatasourceProvidedBadge={false}
kind="suggested_dashboard"
/>
);
expect(screen.queryByText('Data source provided')).not.toBeInTheDocument();
});
it('should not show badge by default', () => {
render(
<DashboardCard
title="Test Dashboard"
dashboard={createMockPluginDashboard()}
onClick={mockOnClick}
kind="suggested_dashboard"
/>
);
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(
<DashboardCard
title="Test Dashboard"
dashboard={createMockPluginDashboard()}
details={details}
onClick={mockOnClick}
kind="suggested_dashboard"
/>
);
expect(screen.getByRole('button', { name: 'Details' })).toBeInTheDocument();
});
it('should not show details icon button when details are not provided', () => {
render(
<DashboardCard
title="Test Dashboard"
dashboard={createMockPluginDashboard()}
onClick={mockOnClick}
kind="suggested_dashboard"
/>
);
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(
<DashboardCard
title="Test Dashboard"
dashboard={createMockPluginDashboard()}
details={details}
onClick={mockOnClick}
kind="suggested_dashboard"
/>
);
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(
<DashboardCard
title="Test Dashboard"
dashboard={createMockPluginDashboard()}
details={details}
onClick={mockOnClick}
kind="suggested_dashboard"
/>
);
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(
<DashboardCard
title="Test Dashboard"
imageUrl="https://example.com/logo.png"
dashboard={createMockPluginDashboard()}
onClick={mockOnClick}
isLogo={true}
kind="suggested_dashboard"
/>
);
let image = screen.getByRole('img');
expect(image).toBeInTheDocument();
expect(image).toHaveAttribute('src', 'https://example.com/logo.png');
rerender(
<DashboardCard
title="Test Dashboard"
imageUrl="https://example.com/screenshot.png"
dashboard={createMockPluginDashboard()}
onClick={mockOnClick}
isLogo={false}
kind="suggested_dashboard"
/>
);
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(
<DashboardCard
title="Test Dashboard"
imageUrl="https://example.com/screenshot.png"
dashboard={createMockPluginDashboard()}
onClick={mockOnClick}
dimThumbnail={true}
showDatasourceProvidedBadge={true}
kind="suggested_dashboard"
/>
);
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(
<DashboardCard
title="Community Dashboard"
dashboard={dashboard}
onClick={mockOnClick}
kind="suggested_dashboard"
/>
);
expect(screen.getByRole('heading', { name: 'Community Dashboard' })).toBeInTheDocument();
});
});
});
@@ -77,7 +77,9 @@ function DashboardCardComponent({
</div>
<div title={dashboard.description || ''} className={styles.descriptionWrapper}>
{dashboard.description && (
<Card.Description className={styles.description}>{dashboard.description}</Card.Description>
<Card.Description data-testid="dashboard-card-description" className={styles.description}>
{dashboard.description}
</Card.Description>
)}
</div>
<Card.Actions className={styles.actionsContainer}>
@@ -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<typeof getBackendSrv>;
// Helper to create mock BackendSrv
const createMockBackendSrv = (overrides: Partial<BackendSrv> = {}): BackendSrv =>
({
get: jest.fn(),
...overrides,
}) as unknown as BackendSrv;
// Helper functions for creating mock objects
const createMockGnetDashboard = (overrides: Partial<GnetDashboard> = {}): GnetDashboard => ({
id: 1,
name: 'Test Dashboard',
description: 'Test Description',
downloads: 100,
datasource: 'Prometheus',
...overrides,
});
const createMockPluginDashboard = (overrides: Partial<PluginDashboard> = {}): 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<BackendSrv['get']>;
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([]);
});
});
});
@@ -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,
@@ -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<typeof getDataSourceSrv>;
// Helper to create partial DataSourceSrv mock
const createMockDataSourceSrv = (overrides: Partial<DataSourceSrv> = {}): 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> = {}): DataSourceInput =>
({
name: 'DS_PROMETHEUS',
pluginId: 'prometheus',
type: InputType.DataSource,
label: 'Prometheus',
value: '',
info: 'Prometheus datasource',
...overrides,
}) as DataSourceInput;
const createMockConstantInput = (overrides: Partial<DashboardInput> = {}): 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> = {}): 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('');
});
});
});
@@ -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<typeof fetchCommunityDashboard>;
const mockTryAutoMapDatasources = tryAutoMapDatasources as jest.MockedFunction<typeof tryAutoMapDatasources>;
const mockParseConstantInputs = parseConstantInputs as jest.MockedFunction<typeof parseConstantInputs>;
// Helper functions for creating mock objects
const createMockGnetDashboard = (overrides: Partial<GnetDashboard> = {}): 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> = {}): 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<GnetDashboard>;
dashboardJson?: Partial<DashboardJson>;
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();
});
});
});