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:
co-authored by
nmarrs
Juan Cabanas
parent
e9883f6c61
commit
c7e8291bd1
@@ -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
|
||||
|
||||
+262
@@ -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}>
|
||||
|
||||
+263
@@ -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,
|
||||
|
||||
+368
@@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
+310
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user