Transform: Minimum scene should not throw transformation errors (#103189)

* Transform: Minimum scene should not throw transformation errors
This commit is contained in:
Ivan Ortega Alba
2025-04-02 13:36:42 +02:00
committed by GitHub
parent aaa61d0919
commit cbf25e994a
2 changed files with 289 additions and 93 deletions
@@ -55,6 +55,7 @@ import {
getPersistedDSFor,
getElementDatasource,
transformSceneToSaveModelSchemaV2,
validateDashboardSchemaV2,
} from './transformSceneToSaveModelSchemaV2';
// Mock dependencies
@@ -416,6 +417,14 @@ describe('transformSceneToSaveModelSchemaV2', () => {
expect(result.annotations?.[2].spec.datasource?.type).toBe('loki');
});
it('should transform the minimum scene to save model schema v2', () => {
const minimalScene = new DashboardScene({});
expect(() => {
transformSceneToSaveModelSchemaV2(minimalScene);
}).not.toThrow();
});
describe('getPersistedDSFor query', () => {
it('should respect datasource reference mapping when determining query datasource', () => {
// Setup test data
@@ -889,3 +898,182 @@ function createAnnotationLayers() {
}),
];
}
describe('validateDashboardSchemaV2', () => {
const validDashboard = {
title: 'Test Dashboard',
timeSettings: {
from: 'now-1h',
to: 'now',
autoRefresh: '5s',
hideTimepicker: false,
timezone: 'UTC',
autoRefreshIntervals: ['5s', '10s', '30s'],
quickRanges: [],
weekStart: 'monday',
nowDelay: '1m',
fiscalYearStartMonth: 1,
},
variables: [],
elements: {},
annotations: [],
layout: {
kind: 'GridLayout',
spec: {
items: [],
},
},
};
it('should validate a valid dashboard', () => {
expect(validateDashboardSchemaV2(validDashboard)).toBe(true);
});
it('should throw error if dashboard is not an object', () => {
expect(() => validateDashboardSchemaV2(null)).toThrow('Dashboard is not an object or is null');
expect(() => validateDashboardSchemaV2(undefined)).toThrow('Dashboard is not an object or is null');
expect(() => validateDashboardSchemaV2('string')).toThrow('Dashboard is not an object or is null');
expect(() => validateDashboardSchemaV2(123)).toThrow('Dashboard is not an object or is null');
expect(() => validateDashboardSchemaV2(true)).toThrow('Dashboard is not an object or is null');
expect(() => validateDashboardSchemaV2([])).toThrow('Dashboard is not an object or is null');
});
it('should validate required properties', () => {
const requiredProps = {
title: 'Title is not a string',
timeSettings: 'TimeSettings is not an object or is null',
variables: 'Variables is not an array',
elements: 'Elements is not an object or is null',
annotations: 'Annotations is not an array',
layout: 'Layout is not an object or is null',
};
for (const [prop, message] of Object.entries(requiredProps)) {
const invalidDashboard = { ...validDashboard };
delete invalidDashboard[prop as keyof typeof invalidDashboard];
expect(() => validateDashboardSchemaV2(invalidDashboard)).toThrow(message);
}
});
it('should validate timeSettings required properties', () => {
const timeSettingsErrors = {
from: 'From is not a string',
to: 'To is not a string',
autoRefresh: 'AutoRefresh is not a string',
hideTimepicker: 'HideTimepicker is not a boolean',
} as const;
for (const [prop, message] of Object.entries(timeSettingsErrors)) {
const invalidDashboard = {
...validDashboard,
timeSettings: { ...validDashboard.timeSettings },
};
delete invalidDashboard.timeSettings[prop as keyof typeof invalidDashboard.timeSettings];
expect(() => validateDashboardSchemaV2(invalidDashboard)).toThrow(message);
}
});
it('should validate optional properties when present', () => {
const invalidDashboard = {
...validDashboard,
description: 123, // Should be string
cursorSync: 'Invalid', // Should be one of ['Off', 'Crosshair', 'Tooltip']
liveNow: 'true', // Should be boolean
preload: 'true', // Should be boolean
editable: 'true', // Should be boolean
links: 'not-an-array', // Should be array
tags: 'not-an-array', // Should be array
id: 'not-a-number', // Should be number
};
expect(() => validateDashboardSchemaV2(invalidDashboard)).toThrow('Description is not a string');
expect(() => validateDashboardSchemaV2({ ...validDashboard, cursorSync: 'Invalid' })).toThrow(
'CursorSync is not a valid value'
);
expect(() => validateDashboardSchemaV2({ ...validDashboard, liveNow: 'true' })).toThrow('LiveNow is not a boolean');
expect(() => validateDashboardSchemaV2({ ...validDashboard, preload: 'true' })).toThrow('Preload is not a boolean');
expect(() => validateDashboardSchemaV2({ ...validDashboard, editable: 'true' })).toThrow(
'Editable is not a boolean'
);
expect(() => validateDashboardSchemaV2({ ...validDashboard, links: 'not-an-array' })).toThrow(
'Links is not an array'
);
expect(() => validateDashboardSchemaV2({ ...validDashboard, tags: 'not-an-array' })).toThrow(
'Tags is not an array'
);
expect(() => validateDashboardSchemaV2({ ...validDashboard, id: 'not-a-number' })).toThrow('ID is not a number');
});
it('should validate optional timeSettings properties when present', () => {
const invalidTimeSettings = {
...validDashboard.timeSettings,
autoRefreshIntervals: 'not-an-array',
timezone: 123,
quickRanges: 'not-an-array',
weekStart: 'invalid-day',
nowDelay: 123,
fiscalYearStartMonth: 'not-a-number',
};
expect(() => validateDashboardSchemaV2({ ...validDashboard, timeSettings: invalidTimeSettings })).toThrow(
'AutoRefreshIntervals is not an array'
);
expect(() =>
validateDashboardSchemaV2({ ...validDashboard, timeSettings: { ...validDashboard.timeSettings, timezone: 123 } })
).toThrow('Timezone is not a string');
expect(() =>
validateDashboardSchemaV2({
...validDashboard,
timeSettings: { ...validDashboard.timeSettings, quickRanges: 'not-an-array' },
})
).toThrow('QuickRanges is not an array');
expect(() =>
validateDashboardSchemaV2({
...validDashboard,
timeSettings: { ...validDashboard.timeSettings, weekStart: 'invalid-day' },
})
).toThrow('WeekStart should be one of "saturday", "sunday" or "monday"');
expect(() =>
validateDashboardSchemaV2({ ...validDashboard, timeSettings: { ...validDashboard.timeSettings, nowDelay: 123 } })
).toThrow('NowDelay is not a string');
expect(() =>
validateDashboardSchemaV2({
...validDashboard,
timeSettings: { ...validDashboard.timeSettings, fiscalYearStartMonth: 'not-a-number' },
})
).toThrow('FiscalYearStartMonth is not a number');
});
it('should validate layout kind and structure', () => {
// Missing kind
expect(() => validateDashboardSchemaV2({ ...validDashboard, layout: { spec: { items: [] } } })).toThrow(
'Layout kind is required'
);
// Invalid GridLayout
expect(() => validateDashboardSchemaV2({ ...validDashboard, layout: { kind: 'GridLayout' } })).toThrow(
'Layout spec is not an object or is null'
);
expect(() => validateDashboardSchemaV2({ ...validDashboard, layout: { kind: 'GridLayout', spec: {} } })).toThrow(
'Layout spec items is not an array'
);
// Invalid RowsLayout
expect(() => validateDashboardSchemaV2({ ...validDashboard, layout: { kind: 'RowsLayout' } })).toThrow(
'Layout spec is not an object or is null'
);
expect(() => validateDashboardSchemaV2({ ...validDashboard, layout: { kind: 'RowsLayout', spec: {} } })).toThrow(
'Layout spec items is not an array'
);
// Valid GridLayout
expect(validateDashboardSchemaV2({ ...validDashboard, layout: { kind: 'GridLayout', spec: { items: [] } } })).toBe(
true
);
// Valid RowsLayout
expect(validateDashboardSchemaV2({ ...validDashboard, layout: { kind: 'RowsLayout', spec: { rows: [] } } })).toBe(
true
);
});
});
@@ -77,10 +77,10 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
description: sceneDash.description,
cursorSync: getCursorSync(sceneDash),
liveNow: getLiveNow(sceneDash),
preload: sceneDash.preload,
editable: sceneDash.editable,
links: sceneDash.links,
tags: sceneDash.tags,
preload: sceneDash.preload ?? defaultDashboardV2Spec().preload,
editable: sceneDash.editable ?? defaultDashboardV2Spec().editable,
links: sceneDash.links ?? defaultDashboardV2Spec().links,
tags: sceneDash.tags ?? defaultDashboardV2Spec().tags,
// EOF dashboard settings
// time settings
@@ -444,98 +444,18 @@ export function getDefaultDataSourceRef(): DataSourceRef {
}
// Function to know if the dashboard transformed is a valid DashboardV2Spec
function validateDashboardSchemaV2(dash: unknown): dash is DashboardV2Spec {
if (typeof dash !== 'object' || dash === null) {
export function validateDashboardSchemaV2(dash: unknown): dash is DashboardV2Spec {
if (typeof dash !== 'object' || dash === null || Array.isArray(dash)) {
throw new Error('Dashboard is not an object or is null');
}
if ('title' in dash && typeof dash.title !== 'string') {
// Required properties
if (!('title' in dash) || typeof dash.title !== 'string') {
throw new Error('Title is not a string');
}
if ('description' in dash && dash.description !== undefined && typeof dash.description !== 'string') {
throw new Error('Description is not a string');
}
if ('cursorSync' in dash && typeof dash.cursorSync !== 'string') {
const validCursorSyncValues = ((): string[] => {
const typeValues: DashboardCursorSync[] = ['Off', 'Crosshair', 'Tooltip'];
return typeValues;
})();
if (
'cursorSync' in dash &&
(typeof dash.cursorSync !== 'string' || !validCursorSyncValues.includes(dash.cursorSync))
) {
throw new Error('CursorSync is not a string');
}
}
if ('liveNow' in dash && typeof dash.liveNow !== 'boolean') {
throw new Error('LiveNow is not a boolean');
}
if ('preload' in dash && typeof dash.preload !== 'boolean') {
throw new Error('Preload is not a boolean');
}
if ('editable' in dash && typeof dash.editable !== 'boolean') {
throw new Error('Editable is not a boolean');
}
if ('links' in dash && !Array.isArray(dash.links)) {
throw new Error('Links is not an array');
}
if ('tags' in dash && !Array.isArray(dash.tags)) {
throw new Error('Tags is not an array');
}
if ('id' in dash && dash.id !== undefined && typeof dash.id !== 'number') {
throw new Error('ID is not a number');
}
// Time settings
if (!('timeSettings' in dash) || typeof dash.timeSettings !== 'object' || dash.timeSettings === null) {
throw new Error('TimeSettings is not an object or is null');
}
if (!('timezone' in dash.timeSettings) || typeof dash.timeSettings.timezone !== 'string') {
throw new Error('Timezone is not a string');
}
if (!('from' in dash.timeSettings) || typeof dash.timeSettings.from !== 'string') {
throw new Error('From is not a string');
}
if (!('to' in dash.timeSettings) || typeof dash.timeSettings.to !== 'string') {
throw new Error('To is not a string');
}
if (!('autoRefresh' in dash.timeSettings) || typeof dash.timeSettings.autoRefresh !== 'string') {
throw new Error('AutoRefresh is not a string');
}
if (!('autoRefreshIntervals' in dash.timeSettings) || !Array.isArray(dash.timeSettings.autoRefreshIntervals)) {
throw new Error('AutoRefreshIntervals is not an array');
}
if (
'quickRanges' in dash.timeSettings &&
dash.timeSettings.quickRanges &&
!Array.isArray(dash.timeSettings.quickRanges)
) {
throw new Error('QuickRanges is not an array');
}
if (!('hideTimepicker' in dash.timeSettings) || typeof dash.timeSettings.hideTimepicker !== 'boolean') {
throw new Error('HideTimepicker is not a boolean');
}
if (
'weekStart' in dash.timeSettings &&
typeof dash.timeSettings.weekStart === 'string' &&
!['saturday', 'sunday', 'monday'].includes(dash.timeSettings.weekStart)
) {
throw new Error('WeekStart should be one of "saturday", "sunday" or "monday"');
}
if (!('fiscalYearStartMonth' in dash.timeSettings) || typeof dash.timeSettings.fiscalYearStartMonth !== 'number') {
throw new Error('FiscalYearStartMonth is not a number');
}
if (
'nowDelay' in dash.timeSettings &&
dash.timeSettings.nowDelay !== undefined &&
typeof dash.timeSettings.nowDelay !== 'string'
) {
throw new Error('NowDelay is not a string');
}
// Other sections
if (!('variables' in dash) || !Array.isArray(dash.variables)) {
throw new Error('Variables is not an array');
}
@@ -545,17 +465,105 @@ function validateDashboardSchemaV2(dash: unknown): dash is DashboardV2Spec {
if (!('annotations' in dash) || !Array.isArray(dash.annotations)) {
throw new Error('Annotations is not an array');
}
// Layout
if (!('layout' in dash) || typeof dash.layout !== 'object' || dash.layout === null) {
throw new Error('Layout is not an object or is null');
}
if (!('kind' in dash.layout) || dash.layout.kind === 'GridLayout') {
validateGridLayout(dash.layout);
// Optional properties - only validate if present
if ('description' in dash && dash.description !== undefined && typeof dash.description !== 'string') {
throw new Error('Description is not a string');
}
if ('cursorSync' in dash && dash.cursorSync !== undefined) {
const validCursorSyncValues = ((): string[] => {
const typeValues: DashboardCursorSync[] = ['Off', 'Crosshair', 'Tooltip'];
return typeValues;
})();
if (typeof dash.cursorSync !== 'string' || !validCursorSyncValues.includes(dash.cursorSync)) {
throw new Error('CursorSync is not a valid value');
}
}
if ('liveNow' in dash && dash.liveNow !== undefined && typeof dash.liveNow !== 'boolean') {
throw new Error('LiveNow is not a boolean');
}
if ('preload' in dash && dash.preload !== undefined && typeof dash.preload !== 'boolean') {
throw new Error('Preload is not a boolean');
}
if ('editable' in dash && dash.editable !== undefined && typeof dash.editable !== 'boolean') {
throw new Error('Editable is not a boolean');
}
if ('links' in dash && dash.links !== undefined && !Array.isArray(dash.links)) {
throw new Error('Links is not an array');
}
if ('tags' in dash && dash.tags !== undefined && !Array.isArray(dash.tags)) {
throw new Error('Tags is not an array');
}
if ('id' in dash && dash.id !== undefined && typeof dash.id !== 'number') {
throw new Error('ID is not a number');
}
if (!('kind' in dash.layout) || dash.layout.kind === 'RowsLayout') {
// Time settings validation
const timeSettings = dash.timeSettings;
// Required time settings
if (!('from' in timeSettings) || typeof timeSettings.from !== 'string') {
throw new Error('From is not a string');
}
if (!('to' in timeSettings) || typeof timeSettings.to !== 'string') {
throw new Error('To is not a string');
}
if (!('autoRefresh' in timeSettings) || typeof timeSettings.autoRefresh !== 'string') {
throw new Error('AutoRefresh is not a string');
}
if (!('hideTimepicker' in timeSettings) || typeof timeSettings.hideTimepicker !== 'boolean') {
throw new Error('HideTimepicker is not a boolean');
}
// Optional time settings with defaults
if (
'autoRefreshIntervals' in timeSettings &&
timeSettings.autoRefreshIntervals !== undefined &&
!Array.isArray(timeSettings.autoRefreshIntervals)
) {
throw new Error('AutoRefreshIntervals is not an array');
}
if ('timezone' in timeSettings && timeSettings.timezone !== undefined && typeof timeSettings.timezone !== 'string') {
throw new Error('Timezone is not a string');
}
if (
'quickRanges' in timeSettings &&
timeSettings.quickRanges !== undefined &&
!Array.isArray(timeSettings.quickRanges)
) {
throw new Error('QuickRanges is not an array');
}
if ('weekStart' in timeSettings && timeSettings.weekStart !== undefined) {
if (
typeof timeSettings.weekStart !== 'string' ||
!['saturday', 'sunday', 'monday'].includes(timeSettings.weekStart)
) {
throw new Error('WeekStart should be one of "saturday", "sunday" or "monday"');
}
}
if ('nowDelay' in timeSettings && timeSettings.nowDelay !== undefined && typeof timeSettings.nowDelay !== 'string') {
throw new Error('NowDelay is not a string');
}
if (
'fiscalYearStartMonth' in timeSettings &&
timeSettings.fiscalYearStartMonth !== undefined &&
typeof timeSettings.fiscalYearStartMonth !== 'number'
) {
throw new Error('FiscalYearStartMonth is not a number');
}
// Layout validation
if (!('kind' in dash.layout)) {
throw new Error('Layout kind is required');
}
if (dash.layout.kind === 'GridLayout') {
validateGridLayout(dash.layout);
} else if (dash.layout.kind === 'RowsLayout') {
validateRowsLayout(dash.layout);
}