Transform: Minimum scene should not throw transformation errors (#103189)
* Transform: Minimum scene should not throw transformation errors
This commit is contained in:
+188
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+101
-93
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user