Create compatibility modal for mvp
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { DashboardJson } from 'app/features/manage-dashboards/types';
|
||||
|
||||
import { CompatibilityModal } from './CompatibilityModal';
|
||||
import { checkDashboardCompatibility, CompatibilityCheckResult } from './api/compatibilityApi';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getDataSourceSrv: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./api/compatibilityApi', () => ({
|
||||
checkDashboardCompatibility: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockGetDataSourceSrv = getDataSourceSrv as jest.MockedFunction<typeof getDataSourceSrv>;
|
||||
const mockCheckDashboardCompatibility = checkDashboardCompatibility as jest.MockedFunction<
|
||||
typeof checkDashboardCompatibility
|
||||
>;
|
||||
|
||||
// Suppress console.error for expected errors in tests
|
||||
const originalError = console.error;
|
||||
beforeAll(() => {
|
||||
console.error = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.error = originalError;
|
||||
});
|
||||
|
||||
// Prometheus-specific query type (extends DataQuery)
|
||||
interface PrometheusQuery extends DataQuery {
|
||||
expr: string;
|
||||
}
|
||||
|
||||
// Test fixtures
|
||||
const createMockDashboard = (overrides: Partial<DashboardJson> = {}): DashboardJson => {
|
||||
// Create a minimal dashboard for testing purposes
|
||||
// Panels array is intentionally minimal - only includes fields needed for compatibility check
|
||||
const dashboard: DashboardJson = {
|
||||
title: 'Test Dashboard',
|
||||
uid: 'test-uid',
|
||||
schemaVersion: 39,
|
||||
version: 1,
|
||||
panels: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'graph',
|
||||
title: 'CPU Usage',
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'prometheus-uid-123',
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
expr: 'rate(cpu_usage_total[5m])',
|
||||
} as PrometheusQuery,
|
||||
],
|
||||
},
|
||||
] as unknown as DashboardJson['panels'],
|
||||
...overrides,
|
||||
};
|
||||
return dashboard;
|
||||
};
|
||||
|
||||
const createMockCompatibilityResult = (score = 100): CompatibilityCheckResult => ({
|
||||
compatibilityScore: score,
|
||||
datasourceResults: [
|
||||
{
|
||||
uid: 'prometheus-uid',
|
||||
type: 'prometheus',
|
||||
name: 'Test Prometheus',
|
||||
totalQueries: 5,
|
||||
checkedQueries: 5,
|
||||
totalMetrics: 10,
|
||||
foundMetrics: score === 100 ? 10 : Math.floor(10 * (score / 100)),
|
||||
missingMetrics: score === 100 ? [] : ['missing_metric_1', 'missing_metric_2'],
|
||||
compatibilityScore: score,
|
||||
queryBreakdown: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const defaultProps: ComponentProps<typeof CompatibilityModal> = {
|
||||
isOpen: true,
|
||||
onDismiss: jest.fn(),
|
||||
dashboardJson: createMockDashboard(),
|
||||
datasourceUid: 'prometheus-uid',
|
||||
};
|
||||
|
||||
describe('CompatibilityModal', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Default mock: datasource found
|
||||
mockGetDataSourceSrv.mockReturnValue({
|
||||
getInstanceSettings: jest.fn().mockReturnValue({
|
||||
uid: 'prometheus-uid',
|
||||
type: 'prometheus',
|
||||
name: 'Test Prometheus',
|
||||
}),
|
||||
} as unknown as ReturnType<typeof getDataSourceSrv>);
|
||||
|
||||
// Default mock: successful API call
|
||||
mockCheckDashboardCompatibility.mockResolvedValue(createMockCompatibilityResult(100));
|
||||
});
|
||||
|
||||
describe('Modal visibility', () => {
|
||||
it('should render modal when isOpen is true', async () => {
|
||||
render(<CompatibilityModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Dashboard Compatibility Check for Test Dashboard')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render modal content when isOpen is false', async () => {
|
||||
render(<CompatibilityModal {...defaultProps} isOpen={false} />);
|
||||
|
||||
// Wait for any async updates to settle
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Dashboard Compatibility Check for Test Dashboard')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should include dashboard title in modal title', async () => {
|
||||
const dashboardWithCustomTitle = createMockDashboard({ title: 'My Custom Dashboard' });
|
||||
render(<CompatibilityModal {...defaultProps} dashboardJson={dashboardWithCustomTitle} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Dashboard Compatibility Check for My Custom Dashboard')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading state', () => {
|
||||
it('should show loading spinner and message while checking compatibility', async () => {
|
||||
// Make API call pending
|
||||
mockCheckDashboardCompatibility.mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve(createMockCompatibilityResult(100)), 1000))
|
||||
);
|
||||
|
||||
render(<CompatibilityModal {...defaultProps} />);
|
||||
|
||||
// Should show loading state immediately
|
||||
expect(screen.getByText('Checking compatibility...')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('Spinner')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error state', () => {
|
||||
it('should show error alert when dashboard is v2 schema', async () => {
|
||||
// Create a v2 dashboard (has 'elements' property instead of 'panels')
|
||||
const v2Dashboard = { elements: {}, schemaVersion: 40 } as unknown as DashboardJson;
|
||||
|
||||
render(<CompatibilityModal {...defaultProps} dashboardJson={v2Dashboard} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Error checking compatibility')).toBeInTheDocument();
|
||||
expect(screen.getByText('Failed to check dashboard compatibility. Please try again.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// API should not be called for v2 dashboards
|
||||
expect(mockCheckDashboardCompatibility).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show error alert when datasource is not found', async () => {
|
||||
mockGetDataSourceSrv.mockReturnValue({
|
||||
getInstanceSettings: jest.fn().mockReturnValue(null),
|
||||
} as unknown as ReturnType<typeof getDataSourceSrv>);
|
||||
|
||||
render(<CompatibilityModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Error checking compatibility')).toBeInTheDocument();
|
||||
expect(screen.getByText('Failed to check dashboard compatibility. Please try again.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error alert when API call fails', async () => {
|
||||
mockCheckDashboardCompatibility.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
render(<CompatibilityModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Error checking compatibility')).toBeInTheDocument();
|
||||
expect(screen.getByText('Failed to check dashboard compatibility. Please try again.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show retry button in error state', async () => {
|
||||
mockCheckDashboardCompatibility.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
render(<CompatibilityModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should retry API call when retry button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// First call fails
|
||||
mockCheckDashboardCompatibility.mockRejectedValueOnce(new Error('API Error'));
|
||||
// Second call succeeds
|
||||
mockCheckDashboardCompatibility.mockResolvedValueOnce(createMockCompatibilityResult(100));
|
||||
|
||||
render(<CompatibilityModal {...defaultProps} />);
|
||||
|
||||
// Wait for error state
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Error checking compatibility')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click retry button
|
||||
const retryButton = screen.getByRole('button', { name: /retry/i });
|
||||
await user.click(retryButton);
|
||||
|
||||
// Should eventually show success state (loading may be too fast to catch)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Compatibility Score')).toBeInTheDocument();
|
||||
expect(screen.getByText('100%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// API should have been called twice
|
||||
expect(mockCheckDashboardCompatibility).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Success state', () => {
|
||||
it('should show compatibility score when check succeeds', async () => {
|
||||
mockCheckDashboardCompatibility.mockResolvedValue(createMockCompatibilityResult(100));
|
||||
|
||||
render(<CompatibilityModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Compatibility Score')).toBeInTheDocument();
|
||||
expect(screen.getByText('100%')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display partial compatibility score', async () => {
|
||||
mockCheckDashboardCompatibility.mockResolvedValue(createMockCompatibilityResult(75));
|
||||
|
||||
render(<CompatibilityModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Compatibility Score')).toBeInTheDocument();
|
||||
expect(screen.getByText('75%')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display low compatibility score', async () => {
|
||||
mockCheckDashboardCompatibility.mockResolvedValue(createMockCompatibilityResult(25));
|
||||
|
||||
render(<CompatibilityModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Compatibility Score')).toBeInTheDocument();
|
||||
expect(screen.getByText('25%')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('API call behavior', () => {
|
||||
it('should call checkDashboardCompatibility with correct parameters', async () => {
|
||||
const dashboardJson = createMockDashboard();
|
||||
render(<CompatibilityModal {...defaultProps} dashboardJson={dashboardJson} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCheckDashboardCompatibility).toHaveBeenCalledWith(dashboardJson, [
|
||||
{
|
||||
uid: 'prometheus-uid',
|
||||
type: 'prometheus',
|
||||
name: 'Test Prometheus',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call API when modal is closed', async () => {
|
||||
render(<CompatibilityModal {...defaultProps} isOpen={false} />);
|
||||
|
||||
// Wait for any async updates to settle
|
||||
await waitFor(() => {
|
||||
expect(mockCheckDashboardCompatibility).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should trigger API call when modal opens', async () => {
|
||||
const { rerender } = render(<CompatibilityModal {...defaultProps} isOpen={false} />);
|
||||
|
||||
// API should not be called yet
|
||||
expect(mockCheckDashboardCompatibility).not.toHaveBeenCalled();
|
||||
|
||||
// Open modal
|
||||
rerender(<CompatibilityModal {...defaultProps} isOpen={true} />);
|
||||
|
||||
// API should now be called
|
||||
await waitFor(() => {
|
||||
expect(mockCheckDashboardCompatibility).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Modal interactions', () => {
|
||||
it('should call onDismiss when modal is closed', async () => {
|
||||
const onDismiss = jest.fn();
|
||||
render(<CompatibilityModal {...defaultProps} onDismiss={onDismiss} />);
|
||||
|
||||
// Wait for modal to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Dashboard Compatibility Check for Test Dashboard')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Find and click close button (X button in modal header)
|
||||
const closeButton = screen.getByLabelText('Close');
|
||||
await userEvent.click(closeButton);
|
||||
|
||||
expect(onDismiss).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { Modal, useStyles2, Stack, Alert, Button, Spinner, Text } from '@grafana/ui';
|
||||
import { DashboardJson } from 'app/features/manage-dashboards/types';
|
||||
|
||||
import { checkDashboardCompatibility } from './api/compatibilityApi';
|
||||
|
||||
interface CompatibilityModalProps {
|
||||
/** Controls modal visibility */
|
||||
isOpen: boolean;
|
||||
/** Handler called when modal is dismissed */
|
||||
onDismiss: () => void;
|
||||
/** Dashboard JSON to check (v1 or v2 schema) */
|
||||
dashboardJson: DashboardJson;
|
||||
/** UID of the datasource to check compatibility against */
|
||||
datasourceUid: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal component that checks dashboard compatibility with a datasource.
|
||||
*
|
||||
* This modal is self-contained and handles its own data fetching. When opened,
|
||||
* it automatically triggers a compatibility check by calling the dashboard validator
|
||||
* backend API. It displays loading, error, or success states accordingly.
|
||||
*
|
||||
* This component is generic and works with any dashboard source:
|
||||
* - Community dashboards (GnetDashboard)
|
||||
* - Plugin-provided dashboards (PluginDashboard)
|
||||
* - User-created dashboards
|
||||
*
|
||||
* Features #12-15 will add detailed result displays (color-coded scores, missing
|
||||
* metrics lists, and query breakdowns).
|
||||
*/
|
||||
export const CompatibilityModal = ({ isOpen, onDismiss, dashboardJson, datasourceUid }: CompatibilityModalProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
// Fetch compatibility results when modal opens
|
||||
const {
|
||||
value: result,
|
||||
loading,
|
||||
error,
|
||||
retry,
|
||||
} = useAsyncRetry(async () => {
|
||||
// Don't trigger API call if modal is closed
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate dashboard is v1 schema (reject v2 for MVP)
|
||||
// isDashboardV2Spec checks for 'elements' property at runtime
|
||||
if ('elements' in dashboardJson) {
|
||||
throw new Error(
|
||||
t(
|
||||
'compatibility-modal.v2-not-supported',
|
||||
'Dashboard v2 schema is not yet supported. Compatibility checking is currently only available for v1 dashboards. Support for v2 dashboards is coming soon.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch datasource details to build mapping
|
||||
const ds = getDataSourceSrv().getInstanceSettings(datasourceUid);
|
||||
if (!ds) {
|
||||
throw new Error(
|
||||
t('compatibility-modal.datasource-not-found', 'Datasource not found with UID: {{uid}}', {
|
||||
uid: datasourceUid,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Call compatibility check API with validated dashboard
|
||||
return await checkDashboardCompatibility(dashboardJson, [
|
||||
{
|
||||
uid: ds.uid,
|
||||
type: ds.type,
|
||||
name: ds.name,
|
||||
},
|
||||
]);
|
||||
}, [isOpen, dashboardJson, datasourceUid]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('compatibility-modal.title', 'Dashboard Compatibility Check for {{dashboardName}}', {
|
||||
dashboardName: dashboardJson.title || 'Dashboard',
|
||||
})}
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
className={styles.modal}
|
||||
contentClassName={styles.modalContent}
|
||||
>
|
||||
<div className={styles.contentContainer}>
|
||||
{/* Loading State */}
|
||||
{loading && (
|
||||
<Stack direction="column" alignItems="center" gap={2}>
|
||||
<Spinner size="xl" />
|
||||
<Text>
|
||||
<Trans i18nKey="compatibility-modal.checking">Checking compatibility...</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{!loading && error && (
|
||||
<Stack direction="column" alignItems="center" gap={2}>
|
||||
<Alert title={t('compatibility-modal.error-title', 'Error checking compatibility')} severity="error">
|
||||
<Trans i18nKey="compatibility-modal.error-description">
|
||||
Failed to check dashboard compatibility. Please try again.
|
||||
</Trans>
|
||||
</Alert>
|
||||
<Button variant="secondary" onClick={retry}>
|
||||
<Trans i18nKey="compatibility-modal.retry">Retry</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Success State - Placeholder for Features #12-15 */}
|
||||
{!loading && !error && result && (
|
||||
<Stack direction="column" gap={3}>
|
||||
<div>
|
||||
<Text element="h3">
|
||||
<Trans i18nKey="compatibility-modal.score-title">Compatibility Score</Trans>
|
||||
</Text>
|
||||
<Text element="p" variant="h2">
|
||||
{result.compatibilityScore}%
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Feature #12: CompatibilityScoreDisplay with color coding */}
|
||||
{/* - Large score display with color coding (green >=80%, yellow 50-79%, red <50%) */}
|
||||
{/* - Icon based on score range (check-circle, warning, exclamation-circle) */}
|
||||
{/* - Descriptive text: 'Highly Compatible', 'Partially Compatible', 'Low Compatibility' */}
|
||||
|
||||
{/* Feature #13: DatasourceResultSection component */}
|
||||
{/* - Display datasource name and type */}
|
||||
{/* - Show total queries vs checked queries count */}
|
||||
{/* - Show total metrics vs found metrics count */}
|
||||
{/* - Display number of missing metrics */}
|
||||
|
||||
{/* Feature #14: MissingMetricsList component */}
|
||||
{/* - Collapsible/expandable section with missing metrics */}
|
||||
{/* - Show count of missing metrics in header */}
|
||||
{/* - Display bullet list of missing metric names when expanded */}
|
||||
{/* - Add copy-to-clipboard button for metric names */}
|
||||
{/* - Show 'All metrics found!' message when missingMetrics array is empty */}
|
||||
|
||||
{/* Feature #15: QueryBreakdownTable component */}
|
||||
{/* - Collapsible section with 'Show panel breakdown' toggle */}
|
||||
{/* - Table with columns: Panel Title, Panel ID, Query Ref, Metrics Found/Total, Compatibility % */}
|
||||
{/* - Color-code compatibility percentage in table cells */}
|
||||
{/* - Add sorting capability by compatibility score */}
|
||||
{/* - Show expandable row details with missing metrics list per query */}
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
modal: css({
|
||||
width: '90%',
|
||||
maxWidth: '1200px',
|
||||
height: '80vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
modalContent: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
padding: theme.spacing(3),
|
||||
height: '100%',
|
||||
}),
|
||||
contentContainer: css({
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}),
|
||||
};
|
||||
}
|
||||
+48
-37
@@ -1,5 +1,6 @@
|
||||
import { BackendSrv, getBackendSrv } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema/src/veneer/dashboard.types';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { DashboardJson } from 'app/features/manage-dashboards/types';
|
||||
|
||||
import { checkDashboardCompatibility, CompatibilityCheckResult, DatasourceMapping } from './compatibilityApi';
|
||||
|
||||
@@ -23,46 +24,56 @@ const createMockBackendSrv = (overrides: Partial<BackendSrv> = {}): BackendSrv =
|
||||
...overrides,
|
||||
}) as unknown as BackendSrv;
|
||||
|
||||
// Prometheus-specific query type (extends DataQuery)
|
||||
interface PrometheusQuery extends DataQuery {
|
||||
expr: string;
|
||||
}
|
||||
|
||||
// Test fixtures
|
||||
const createMockDashboard = (overrides: Partial<Dashboard> = {}): Dashboard => ({
|
||||
title: 'Test Dashboard',
|
||||
uid: 'test-uid',
|
||||
schemaVersion: 39,
|
||||
version: 1,
|
||||
panels: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'graph',
|
||||
title: 'CPU Usage',
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'prometheus-uid-123',
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
expr: 'rate(cpu_usage_total[5m])',
|
||||
const createMockDashboard = (overrides: Partial<DashboardJson> = {}): DashboardJson => {
|
||||
// Create a minimal dashboard for testing purposes
|
||||
// Panels array is intentionally minimal - only includes fields needed for compatibility check
|
||||
const dashboard: DashboardJson = {
|
||||
title: 'Test Dashboard',
|
||||
uid: 'test-uid',
|
||||
schemaVersion: 39,
|
||||
version: 1,
|
||||
panels: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'graph',
|
||||
title: 'CPU Usage',
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'prometheus-uid-123',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'graph',
|
||||
title: 'Memory Usage',
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'prometheus-uid-123',
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
expr: 'rate(cpu_usage_total[5m])',
|
||||
} as PrometheusQuery,
|
||||
],
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
expr: 'memory_usage_bytes',
|
||||
{
|
||||
id: 2,
|
||||
type: 'graph',
|
||||
title: 'Memory Usage',
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'prometheus-uid-123',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
});
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
expr: 'memory_usage_bytes',
|
||||
} as PrometheusQuery,
|
||||
],
|
||||
},
|
||||
] as unknown as DashboardJson['panels'],
|
||||
...overrides,
|
||||
};
|
||||
return dashboard;
|
||||
};
|
||||
|
||||
const createMockDatasourceMappings = (): DatasourceMapping[] => [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema/src/veneer/dashboard.types';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { DashboardJson } from 'app/features/manage-dashboards/types';
|
||||
|
||||
/**
|
||||
* Represents a datasource mapping for compatibility checking.
|
||||
@@ -19,8 +19,8 @@ export interface DatasourceMapping {
|
||||
* Request body for dashboard compatibility check API call
|
||||
*/
|
||||
export interface CheckCompatibilityRequest {
|
||||
/** Complete dashboard JSON object (v1 schema with panels array) */
|
||||
dashboardJson: Dashboard;
|
||||
/** Complete dashboard JSON object (supports both v1 and v2 schemas) */
|
||||
dashboardJson: DashboardJson;
|
||||
/** Array of datasource mappings to check compatibility against */
|
||||
datasourceMappings: DatasourceMapping[];
|
||||
}
|
||||
@@ -88,10 +88,13 @@ export interface CompatibilityCheckResult {
|
||||
* validation service, which extracts metrics from dashboard queries and checks
|
||||
* if those metrics exist in the target datasource(s).
|
||||
*
|
||||
* @param dashboardJson Complete dashboard JSON object (must be v1 schema)
|
||||
* Note: The backend currently only supports v1 dashboards (with panels array).
|
||||
* V2 dashboards (with elements) will be rejected by the backend with an appropriate error.
|
||||
*
|
||||
* @param dashboardJson Complete dashboard JSON object (v1 or v2 schema)
|
||||
* @param datasourceMappings Array of datasource mappings to validate against
|
||||
* @returns Promise resolving to compatibility check results
|
||||
* @throws CompatibilityCheckError if the API call fails
|
||||
* @throws Error if the API call fails or dashboard schema is unsupported
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -105,7 +108,7 @@ export interface CompatibilityCheckResult {
|
||||
* ```
|
||||
*/
|
||||
export async function checkDashboardCompatibility(
|
||||
dashboardJson: Dashboard,
|
||||
dashboardJson: DashboardJson,
|
||||
datasourceMappings: DatasourceMapping[]
|
||||
): Promise<CompatibilityCheckResult> {
|
||||
// Get current organization ID from user context
|
||||
|
||||
Reference in New Issue
Block a user