mirror of https://github.com/grafana/grafana
prometheushacktoberfestmetricsmonitoringalertinggrafanagoinfluxdbmysqlpostgresanalyticsdata-visualizationdashboardbusiness-intelligenceelasticsearch
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1291 lines
42 KiB
1291 lines
42 KiB
import { VariableRefresh } from '@grafana/data';
|
|
import { config } from '@grafana/runtime';
|
|
import {
|
|
AdHocFiltersVariable,
|
|
behaviors,
|
|
ConstantVariable,
|
|
CustomVariable,
|
|
DataSourceVariable,
|
|
GroupByVariable,
|
|
IntervalVariable,
|
|
QueryVariable,
|
|
SceneGridLayout,
|
|
SceneRefreshPicker,
|
|
SceneTimePicker,
|
|
SceneTimeRange,
|
|
SceneVariableSet,
|
|
TextBoxVariable,
|
|
VizPanel,
|
|
SceneDataQuery,
|
|
SceneQueryRunner,
|
|
sceneUtils,
|
|
dataLayers,
|
|
} from '@grafana/scenes';
|
|
import {
|
|
DashboardCursorSync as DashboardCursorSyncV1,
|
|
VariableHide as VariableHideV1,
|
|
VariableSort as VariableSortV1,
|
|
} from '@grafana/schema/dist/esm/index.gen';
|
|
import {
|
|
GridLayoutSpec,
|
|
AutoGridLayoutSpec,
|
|
RowsLayoutSpec,
|
|
TabsLayoutSpec,
|
|
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
|
|
|
|
import { DashboardEditPane } from '../edit-pane/DashboardEditPane';
|
|
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
|
import { DashboardControls } from '../scene/DashboardControls';
|
|
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
|
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
|
|
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
|
import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem';
|
|
import { AutoGridLayout } from '../scene/layout-auto-grid/AutoGridLayout';
|
|
import { AutoGridLayoutManager } from '../scene/layout-auto-grid/AutoGridLayoutManager';
|
|
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
|
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
|
|
import { RowItem } from '../scene/layout-rows/RowItem';
|
|
import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager';
|
|
import { TabItem } from '../scene/layout-tabs/TabItem';
|
|
import { TabsLayoutManager } from '../scene/layout-tabs/TabsLayoutManager';
|
|
import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
|
|
|
|
import {
|
|
getPersistedDSFor,
|
|
getElementDatasource,
|
|
transformSceneToSaveModelSchemaV2,
|
|
validateDashboardSchemaV2,
|
|
getDataQueryKind,
|
|
getAutoAssignedDSRef,
|
|
getVizPanelQueries,
|
|
} from './transformSceneToSaveModelSchemaV2';
|
|
|
|
// Mock dependencies
|
|
jest.mock('../utils/dashboardSceneGraph', () => {
|
|
const original = jest.requireActual('../utils/dashboardSceneGraph');
|
|
return {
|
|
...original,
|
|
dashboardSceneGraph: {
|
|
...original.dashboardSceneGraph,
|
|
getElementIdentifierForVizPanel: jest.fn().mockImplementation((panel) => {
|
|
// Return the panel key if it exists, otherwise use panel-1 as default
|
|
return panel?.state?.key || 'panel-1';
|
|
}),
|
|
getPanelLinks: jest.fn().mockImplementation(() => {
|
|
return new VizPanelLinks({
|
|
rawLinks: [
|
|
{ title: 'Test Link 1', url: 'http://test1.com', targetBlank: true },
|
|
{ title: 'Test Link 2', url: 'http://test2.com' },
|
|
],
|
|
menu: new VizPanelLinksMenu({}),
|
|
});
|
|
}),
|
|
},
|
|
};
|
|
});
|
|
|
|
jest.mock('../utils/utils', () => {
|
|
const original = jest.requireActual('../utils/utils');
|
|
return {
|
|
...original,
|
|
getDashboardSceneFor: jest.fn().mockImplementation(() => ({
|
|
serializer: {
|
|
getDSReferencesMapping: jest.fn().mockReturnValue({
|
|
panels: new Map([['panel-1', new Set(['A'])]]),
|
|
variables: new Set(),
|
|
annotations: new Set(),
|
|
}),
|
|
},
|
|
})),
|
|
};
|
|
});
|
|
|
|
function setupDashboardScene(state: Partial<DashboardSceneState>): DashboardScene {
|
|
return new DashboardScene(state);
|
|
}
|
|
|
|
jest.mock('@grafana/runtime', () => ({
|
|
...jest.requireActual('@grafana/runtime'),
|
|
config: {
|
|
...jest.requireActual('@grafana/runtime').config,
|
|
bootData: {
|
|
settings: {
|
|
defaultDatasource: 'loki',
|
|
datasources: {
|
|
Prometheus: {
|
|
name: 'Prometheus',
|
|
meta: { id: 'prometheus' },
|
|
type: 'datasource',
|
|
},
|
|
'-- Grafana --': {
|
|
name: 'Grafana',
|
|
meta: { id: 'grafana' },
|
|
type: 'datasource',
|
|
},
|
|
loki: {
|
|
name: 'Loki',
|
|
meta: {
|
|
id: 'loki',
|
|
name: 'Loki',
|
|
type: 'datasource',
|
|
info: { version: '1.0.0' },
|
|
module: 'app/plugins/datasource/loki/module',
|
|
baseUrl: '/plugins/loki',
|
|
},
|
|
type: 'datasource',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
|
|
describe('transformSceneToSaveModelSchemaV2', () => {
|
|
let dashboardScene: DashboardScene;
|
|
let prevFeatureToggleValue: boolean;
|
|
|
|
beforeAll(() => {
|
|
prevFeatureToggleValue = !!config.featureToggles.groupByVariable;
|
|
config.featureToggles.groupByVariable = true;
|
|
});
|
|
|
|
afterAll(() => {
|
|
config.featureToggles.groupByVariable = prevFeatureToggleValue;
|
|
});
|
|
|
|
beforeEach(() => {
|
|
// The intention is to have a complete dashboard scene
|
|
// with all the possible properties set
|
|
dashboardScene = setupDashboardScene({
|
|
$data: new DashboardDataLayerSet({ annotationLayers: createAnnotationLayers() }),
|
|
id: 1,
|
|
title: 'Test Dashboard',
|
|
description: 'Test Description',
|
|
preload: true,
|
|
tags: ['tag1', 'tag2'],
|
|
uid: 'test-uid',
|
|
version: 1,
|
|
$timeRange: new SceneTimeRange({
|
|
timeZone: 'UTC',
|
|
from: 'now-1h',
|
|
to: 'now',
|
|
weekStart: 'monday',
|
|
fiscalYearStartMonth: 1,
|
|
UNSAFE_nowDelay: '1m',
|
|
refreshOnActivate: {
|
|
afterMs: 10,
|
|
percent: 0.1,
|
|
},
|
|
}),
|
|
controls: new DashboardControls({
|
|
refreshPicker: new SceneRefreshPicker({
|
|
refresh: '5s',
|
|
intervals: ['5s', '10s', '30s'],
|
|
autoEnabled: true,
|
|
autoMinInterval: '5s',
|
|
autoValue: '5s',
|
|
isOnCanvas: true,
|
|
primary: true,
|
|
withText: true,
|
|
minRefreshInterval: '5s',
|
|
}),
|
|
timePicker: new SceneTimePicker({
|
|
isOnCanvas: true,
|
|
hidePicker: true,
|
|
}),
|
|
}),
|
|
links: [
|
|
{
|
|
title: 'Test Link',
|
|
url: 'http://test.com',
|
|
asDropdown: false,
|
|
icon: '',
|
|
includeVars: false,
|
|
keepTime: false,
|
|
tags: [],
|
|
targetBlank: false,
|
|
tooltip: '',
|
|
type: 'link',
|
|
},
|
|
],
|
|
body: new DefaultGridLayoutManager({
|
|
grid: new SceneGridLayout({
|
|
isLazy: false,
|
|
children: [
|
|
new DashboardGridItem({
|
|
y: 0,
|
|
height: 10,
|
|
body: new VizPanel({
|
|
key: 'panel-1',
|
|
pluginId: 'timeseries',
|
|
title: 'Test Panel',
|
|
titleItems: [
|
|
new VizPanelLinks({
|
|
rawLinks: [
|
|
{ title: 'Test Link 1', url: 'http://test1.com', targetBlank: true },
|
|
{ title: 'Test Link 2', url: 'http://test2.com' },
|
|
],
|
|
menu: new VizPanelLinksMenu({}),
|
|
}),
|
|
],
|
|
description: 'Test Description',
|
|
hoverHeader: true,
|
|
hoverHeaderOffset: 10,
|
|
fieldConfig: {
|
|
defaults: {
|
|
mappings: [],
|
|
max: undefined,
|
|
},
|
|
overrides: [],
|
|
},
|
|
displayMode: 'transparent',
|
|
pluginVersion: '7.0.0',
|
|
$timeRange: new SceneTimeRange({
|
|
timeZone: 'UTC',
|
|
from: 'now-3h',
|
|
to: 'now',
|
|
}),
|
|
}),
|
|
// Props related to repeatable panels
|
|
// repeatedPanels?: VizPanel[],
|
|
// variableName?: string,
|
|
// itemHeight?: number,
|
|
// repeatDirection?: RepeatDirection,
|
|
// maxPerRow?: number,
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
meta: {},
|
|
editPane: new DashboardEditPane(),
|
|
$behaviors: [
|
|
new behaviors.CursorSync({
|
|
sync: DashboardCursorSyncV1.Crosshair,
|
|
}),
|
|
new behaviors.LiveNowTimer({
|
|
enabled: true,
|
|
}),
|
|
],
|
|
$variables: new SceneVariableSet({
|
|
// Test each of the variables
|
|
variables: [
|
|
new QueryVariable({
|
|
name: 'queryVar',
|
|
label: 'Query Variable',
|
|
description: 'A query variable',
|
|
skipUrlSync: false,
|
|
hide: VariableHideV1.hideLabel,
|
|
value: 'value1',
|
|
text: 'text1',
|
|
query: {
|
|
expr: 'label_values(node_boot_time_seconds)',
|
|
refId: 'A',
|
|
},
|
|
definition: 'definition1',
|
|
datasource: { uid: 'datasource1', type: 'prometheus' },
|
|
sort: VariableSortV1.alphabeticalDesc,
|
|
refresh: VariableRefresh.onDashboardLoad,
|
|
regex: 'regex1',
|
|
allValue: '*',
|
|
includeAll: true,
|
|
isMulti: true,
|
|
}),
|
|
new CustomVariable({
|
|
name: 'customVar',
|
|
label: 'Custom Variable',
|
|
description: 'A custom variable',
|
|
skipUrlSync: false,
|
|
hide: VariableHideV1.dontHide,
|
|
value: 'option1',
|
|
text: 'option1',
|
|
query: 'option1, option2',
|
|
options: [
|
|
{ label: 'option1', value: 'option1' },
|
|
{ label: 'option2', value: 'option2' },
|
|
],
|
|
isMulti: true,
|
|
allValue: 'All',
|
|
includeAll: true,
|
|
}),
|
|
new DataSourceVariable({
|
|
name: 'datasourceVar',
|
|
label: 'Datasource Variable',
|
|
description: 'A datasource variable',
|
|
skipUrlSync: false,
|
|
hide: VariableHideV1.dontHide,
|
|
value: 'value1',
|
|
text: 'text1',
|
|
regex: 'regex1',
|
|
pluginId: 'datasource1',
|
|
defaultOptionEnabled: true,
|
|
}),
|
|
new ConstantVariable({
|
|
name: 'constantVar',
|
|
label: 'Constant Variable',
|
|
description: 'A constant variable',
|
|
skipUrlSync: false,
|
|
hide: VariableHideV1.dontHide,
|
|
value: 'value4',
|
|
}),
|
|
new IntervalVariable({
|
|
name: 'intervalVar',
|
|
label: 'Interval Variable',
|
|
description: 'An interval variable',
|
|
skipUrlSync: false,
|
|
hide: VariableHideV1.dontHide,
|
|
value: '1m',
|
|
intervals: ['1m', '5m', '10m'],
|
|
autoEnabled: false,
|
|
autoMinInterval: '1m',
|
|
autoStepCount: 10,
|
|
}),
|
|
new TextBoxVariable({
|
|
name: 'textVar',
|
|
label: 'Text Variable',
|
|
description: 'A text variable',
|
|
skipUrlSync: false,
|
|
hide: VariableHideV1.dontHide,
|
|
value: 'value6',
|
|
}),
|
|
new GroupByVariable({
|
|
name: 'groupByVar',
|
|
label: 'Group By Variable',
|
|
description: 'A group by variable',
|
|
skipUrlSync: false,
|
|
hide: VariableHideV1.dontHide,
|
|
value: 'value7',
|
|
text: 'text7',
|
|
datasource: { uid: 'datasource2', type: 'prometheus' },
|
|
defaultOptions: [
|
|
{ text: 'option1', value: 'option1' },
|
|
{ text: 'option2', value: 'option2' },
|
|
],
|
|
isMulti: false,
|
|
includeAll: false,
|
|
}),
|
|
new AdHocFiltersVariable({
|
|
name: 'adhocVar',
|
|
label: 'Adhoc Variable',
|
|
description: 'An adhoc variable',
|
|
skipUrlSync: false,
|
|
hide: VariableHideV1.dontHide,
|
|
datasource: { uid: 'datasource3', type: 'prometheus' },
|
|
baseFilters: [
|
|
{
|
|
key: 'key1',
|
|
operator: '=',
|
|
value: 'value1',
|
|
condition: 'AND',
|
|
},
|
|
{
|
|
key: 'key2',
|
|
operator: '=',
|
|
value: 'value2',
|
|
condition: 'OR',
|
|
},
|
|
],
|
|
filters: [
|
|
{
|
|
key: 'key3',
|
|
operator: '=',
|
|
value: 'value3',
|
|
condition: 'AND',
|
|
},
|
|
],
|
|
defaultKeys: [
|
|
{
|
|
text: 'defaultKey1',
|
|
value: 'defaultKey1',
|
|
group: 'defaultGroup1',
|
|
expandable: true,
|
|
},
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
it('should transform scene to save model schema v2', () => {
|
|
const result = transformSceneToSaveModelSchemaV2(dashboardScene);
|
|
expect(result).toMatchSnapshot();
|
|
|
|
// Check that the annotation layers are correctly transformed
|
|
expect(result.annotations).toHaveLength(2);
|
|
});
|
|
|
|
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
|
|
const queryWithoutDS: SceneDataQuery = {
|
|
refId: 'A',
|
|
// No datasource defined originally
|
|
};
|
|
const queryWithDS: SceneDataQuery = {
|
|
refId: 'B',
|
|
datasource: { uid: 'prometheus', type: 'prometheus' },
|
|
};
|
|
|
|
// Mock query runner with runtime-resolved datasource
|
|
const queryRunner = new SceneQueryRunner({
|
|
queries: [queryWithoutDS, queryWithDS],
|
|
datasource: { uid: 'default-ds', type: 'default' },
|
|
});
|
|
|
|
// Get a reference to the DS references mapping
|
|
const dsReferencesMap = new Set(['A']);
|
|
|
|
// Test the query without DS originally - should return undefined
|
|
const resultA = getPersistedDSFor(queryWithoutDS, dsReferencesMap, 'query', queryRunner);
|
|
expect(resultA).toBeUndefined();
|
|
|
|
// Test the query with DS originally - should return the original datasource
|
|
const resultB = getPersistedDSFor(queryWithDS, dsReferencesMap, 'query', queryRunner);
|
|
expect(resultB).toEqual({ uid: 'prometheus', type: 'prometheus' });
|
|
|
|
// Test a query with no DS originally but not in the mapping - should get the runner's datasource
|
|
const queryNotInMapping: SceneDataQuery = {
|
|
refId: 'C',
|
|
// No datasource, but not in mapping
|
|
};
|
|
const resultC = getPersistedDSFor(queryNotInMapping, dsReferencesMap, 'query', queryRunner);
|
|
expect(resultC).toEqual({ uid: 'default-ds', type: 'default' });
|
|
});
|
|
});
|
|
|
|
describe('getPersistedDSFor variable', () => {
|
|
it('should respect datasource reference mapping when determining variable datasource', () => {
|
|
// Setup test data - variable without datasource
|
|
const variableWithoutDS = new QueryVariable({
|
|
name: 'A',
|
|
// No datasource defined originally
|
|
});
|
|
|
|
// Variable with datasource
|
|
const variableWithDS = new QueryVariable({
|
|
name: 'B',
|
|
datasource: { uid: 'prometheus', type: 'prometheus' },
|
|
});
|
|
|
|
// Get a reference to the DS references mapping
|
|
const dsReferencesMap = new Set(['A']);
|
|
|
|
// Test the variable without DS originally - should return undefined
|
|
const resultA = getPersistedDSFor(variableWithoutDS, dsReferencesMap, 'variable');
|
|
expect(resultA).toBeUndefined();
|
|
|
|
// Test the variable with DS originally - should return the original datasource
|
|
const resultB = getPersistedDSFor(variableWithDS, dsReferencesMap, 'variable');
|
|
expect(resultB).toEqual({ uid: 'prometheus', type: 'prometheus' });
|
|
|
|
// Test a variable with no DS originally but not in the mapping - should get empty object
|
|
const variableNotInMapping = new QueryVariable({
|
|
name: 'C',
|
|
// No datasource, but not in mapping
|
|
});
|
|
const resultC = getPersistedDSFor(variableNotInMapping, dsReferencesMap, 'variable');
|
|
expect(resultC).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe('getDataQueryKind', () => {
|
|
it('should preserve original query datasource type when available', () => {
|
|
// 1. Test with a query that has its own datasource type
|
|
const queryWithDS: SceneDataQuery = {
|
|
refId: 'A',
|
|
datasource: { uid: 'prometheus-1', type: 'prometheus' },
|
|
};
|
|
|
|
// Create a query runner with a different datasource type
|
|
const queryRunner = new SceneQueryRunner({
|
|
datasource: { uid: 'default-ds', type: 'loki' },
|
|
queries: [],
|
|
});
|
|
|
|
// Should use the query's own datasource type (prometheus)
|
|
expect(getDataQueryKind(queryWithDS, queryRunner)).toBe('prometheus');
|
|
});
|
|
|
|
it('should use queryRunner datasource type as fallback when query has no datasource', () => {
|
|
// 2. Test with a query that has no datasource
|
|
const queryWithoutDS: SceneDataQuery = {
|
|
refId: 'A',
|
|
};
|
|
|
|
// Create a query runner with a datasource
|
|
const queryRunner = new SceneQueryRunner({
|
|
datasource: { uid: 'influxdb-1', type: 'influxdb' },
|
|
queries: [],
|
|
});
|
|
|
|
// Should fall back to queryRunner's datasource type
|
|
expect(getDataQueryKind(queryWithoutDS, queryRunner)).toBe('influxdb');
|
|
});
|
|
|
|
it('should fall back to default datasource when neither query nor queryRunner has datasource type', () => {
|
|
// 3. Test with neither query nor queryRunner having a datasource type
|
|
const queryWithoutDS: SceneDataQuery = {
|
|
refId: 'A',
|
|
};
|
|
|
|
// Create a query runner with no datasource
|
|
const queryRunner = new SceneQueryRunner({
|
|
queries: [],
|
|
});
|
|
|
|
expect(getDataQueryKind(queryWithoutDS, queryRunner)).toBe('loki');
|
|
|
|
// Also verify the function's behavior by checking the args
|
|
expect(queryWithoutDS.datasource?.type).toBeUndefined(); // No query datasource
|
|
expect(queryRunner.state.datasource?.type).toBeUndefined(); // No queryRunner datasource
|
|
});
|
|
});
|
|
|
|
it('should test annotation with legacyOptions field', () => {
|
|
// Create a scene with an annotation layer that has options
|
|
const annotationWithOptions = new DashboardAnnotationsDataLayer({
|
|
key: 'layerWithLegacyOptions',
|
|
query: {
|
|
datasource: {
|
|
type: 'prometheus',
|
|
uid: 'abc123',
|
|
},
|
|
name: 'annotation-with-options',
|
|
enable: true,
|
|
iconColor: 'red',
|
|
legacyOptions: {
|
|
expr: 'rate(http_requests_total[5m])',
|
|
queryType: 'range',
|
|
legendFormat: '{{method}} {{endpoint}}',
|
|
useValueAsTime: true,
|
|
},
|
|
// Some other properties that aren't in the annotation spec
|
|
// and should be moved to options
|
|
customProp1: 'value1',
|
|
customProp2: 'value2',
|
|
},
|
|
name: 'layerWithOptions',
|
|
isEnabled: true,
|
|
isHidden: false,
|
|
});
|
|
|
|
const scene = setupDashboardScene({
|
|
$data: new DashboardDataLayerSet({
|
|
annotationLayers: [annotationWithOptions],
|
|
}),
|
|
body: new DefaultGridLayoutManager({
|
|
grid: new SceneGridLayout({ children: [] }),
|
|
}),
|
|
});
|
|
|
|
const result = transformSceneToSaveModelSchemaV2(scene);
|
|
|
|
// Verify the annotation options are properly serialized
|
|
expect(result.annotations.length).toBe(1);
|
|
expect(result.annotations[0].spec.legacyOptions).toBeDefined();
|
|
expect(result.annotations[0].spec.legacyOptions).toEqual({
|
|
expr: 'rate(http_requests_total[5m])',
|
|
queryType: 'range',
|
|
legendFormat: '{{method}} {{endpoint}}',
|
|
useValueAsTime: true,
|
|
customProp1: 'value1',
|
|
customProp2: 'value2',
|
|
});
|
|
|
|
// Ensure these properties are not at the root level
|
|
expect(result).not.toHaveProperty('annotations[0].spec.expr');
|
|
expect(result).not.toHaveProperty('annotations[0].spec.queryType');
|
|
expect(result).not.toHaveProperty('annotations[0].spec.legendFormat');
|
|
expect(result).not.toHaveProperty('annotations[0].spec.useValueAsTime');
|
|
expect(result).not.toHaveProperty('annotations[0].spec.customProp1');
|
|
expect(result).not.toHaveProperty('annotations[0].spec.customProp2');
|
|
});
|
|
});
|
|
|
|
describe('getElementDatasource', () => {
|
|
it('should handle panel query datasources correctly', () => {
|
|
// Create test elements
|
|
const vizPanel = new VizPanel({
|
|
key: 'panel-1',
|
|
pluginId: 'timeseries',
|
|
});
|
|
|
|
const queryWithDS: SceneDataQuery = {
|
|
refId: 'B',
|
|
datasource: { uid: 'prometheus', type: 'prometheus' },
|
|
};
|
|
|
|
const queryWithoutDS: SceneDataQuery = {
|
|
refId: 'A',
|
|
};
|
|
|
|
// Mock query runner
|
|
const queryRunner = new SceneQueryRunner({
|
|
queries: [queryWithoutDS, queryWithDS],
|
|
datasource: { uid: 'default-ds', type: 'default' },
|
|
});
|
|
|
|
// Mock dsReferencesMapping
|
|
const dsReferencesMapping = {
|
|
panels: new Map(new Set([['panel-1', new Set<string>(['A'])]])),
|
|
variables: new Set<string>(),
|
|
annotations: new Set<string>(),
|
|
};
|
|
|
|
// Call the function with the panel and query with DS
|
|
const resultWithDS = getElementDatasource(vizPanel, queryWithDS, 'panel', queryRunner, dsReferencesMapping);
|
|
expect(resultWithDS).toEqual({ uid: 'prometheus', type: 'prometheus' });
|
|
|
|
// Call the function with the panel and query without DS
|
|
const resultWithoutDS = getElementDatasource(vizPanel, queryWithoutDS, 'panel', queryRunner, dsReferencesMapping);
|
|
expect(resultWithoutDS).toBeUndefined();
|
|
});
|
|
|
|
it('should handle variable datasources correctly', () => {
|
|
// Create a variable set
|
|
const variableSet = new SceneVariableSet({
|
|
variables: [
|
|
new QueryVariable({
|
|
name: 'A',
|
|
// No datasource
|
|
}),
|
|
new QueryVariable({
|
|
name: 'B',
|
|
datasource: { uid: 'prometheus', type: 'prometheus' },
|
|
}),
|
|
],
|
|
});
|
|
|
|
// Variable with DS
|
|
const variableWithDS = variableSet.getByName('B');
|
|
|
|
// Variable without DS
|
|
const variableWithoutDS = variableSet.getByName('A');
|
|
|
|
// Mock dsReferencesMapping
|
|
const dsReferencesMapping = {
|
|
panels: new Map(new Set([['panel-1', new Set<string>(['A'])]])),
|
|
variables: new Set<string>(['A']),
|
|
annotations: new Set<string>(),
|
|
};
|
|
|
|
// Call the function with variables
|
|
if (variableWithDS && sceneUtils.isQueryVariable(variableWithDS)) {
|
|
const resultWithDS = getElementDatasource(
|
|
variableSet,
|
|
variableWithDS,
|
|
'variable',
|
|
undefined,
|
|
dsReferencesMapping
|
|
);
|
|
expect(resultWithDS).toEqual({ uid: 'prometheus', type: 'prometheus' });
|
|
}
|
|
|
|
if (variableWithoutDS && sceneUtils.isQueryVariable(variableWithoutDS)) {
|
|
// Test with auto-assigned variable (in the mapping)
|
|
const resultWithoutDS = getElementDatasource(variableSet, variableWithoutDS, 'variable');
|
|
expect(resultWithoutDS).toEqual(undefined);
|
|
}
|
|
});
|
|
|
|
it('should return undefined for non-query variables', () => {
|
|
// Create a variable set with non-query variable
|
|
const variableSet = new SceneVariableSet({
|
|
variables: [
|
|
new ConstantVariable({
|
|
name: 'constant',
|
|
value: 'value',
|
|
}),
|
|
],
|
|
});
|
|
|
|
// Non-query variable
|
|
const constantVar = variableSet.getByName('constant');
|
|
|
|
// Call the function
|
|
// @ts-expect-error
|
|
const result = getElementDatasource(variableSet, constantVar, 'variable');
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should return undefined for non-query variables', () => {
|
|
// Create a variable set with non-query variable types
|
|
const variableSet = new SceneVariableSet({
|
|
variables: [
|
|
// Use TextBoxVariable which is not a QueryVariable
|
|
new TextBoxVariable({
|
|
name: 'textVar',
|
|
value: 'text-value',
|
|
}),
|
|
],
|
|
});
|
|
|
|
// Non-query variable - this is safe because getElementDatasource checks if it's a query variable
|
|
const textVar = variableSet.getByName('textVar');
|
|
|
|
// Call the function
|
|
// @ts-expect-error
|
|
const result = getElementDatasource(variableSet, textVar, 'variable');
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should handle annotation datasources correctly', () => {
|
|
// Use the dataLayers.AnnotationsDataLayer directly
|
|
const annotationLayer = new dataLayers.AnnotationsDataLayer({
|
|
key: 'annotation-1',
|
|
name: 'Test Annotation',
|
|
isEnabled: true,
|
|
isHidden: false,
|
|
query: {
|
|
name: 'Test Annotation',
|
|
enable: true,
|
|
hide: false,
|
|
iconColor: 'red',
|
|
datasource: { uid: 'prometheus', type: 'prometheus' },
|
|
},
|
|
});
|
|
|
|
// Create an annotation query without datasource
|
|
const annotationWithoutDS = {
|
|
name: 'No DS Annotation',
|
|
enable: true,
|
|
hide: false,
|
|
iconColor: 'blue',
|
|
};
|
|
|
|
// Mock dsReferencesMapping
|
|
const dsReferencesMapping = {
|
|
panels: new Map([['panel-1', new Set(['A'])]]),
|
|
variables: new Set<string>(),
|
|
annotations: new Set<string>(['No DS Annotation']),
|
|
};
|
|
|
|
// Test with annotation that has datasource defined
|
|
const resultWithDS = getElementDatasource(
|
|
annotationLayer,
|
|
annotationLayer.state.query,
|
|
'annotation',
|
|
undefined,
|
|
dsReferencesMapping
|
|
);
|
|
expect(resultWithDS).toEqual({ uid: 'prometheus', type: 'prometheus' });
|
|
|
|
// Test with annotation that has no datasource defined
|
|
const resultWithoutDS = getElementDatasource(
|
|
annotationLayer,
|
|
annotationWithoutDS,
|
|
'annotation',
|
|
undefined,
|
|
dsReferencesMapping
|
|
);
|
|
expect(resultWithoutDS).toBeUndefined();
|
|
});
|
|
|
|
it('should handle invalid input combinations', () => {
|
|
const vizPanel = new VizPanel({
|
|
key: 'panel-1',
|
|
pluginId: 'timeseries',
|
|
});
|
|
|
|
const variableSet = new SceneVariableSet({
|
|
variables: [
|
|
new QueryVariable({
|
|
name: 'A',
|
|
}),
|
|
],
|
|
});
|
|
|
|
const variable = variableSet.getByName('A');
|
|
const query: SceneDataQuery = { refId: 'A' };
|
|
|
|
if (variable && sceneUtils.isQueryVariable(variable)) {
|
|
// Panel with variable
|
|
expect(getElementDatasource(vizPanel, variable, 'panel')).toBeUndefined();
|
|
}
|
|
// Variable set with query
|
|
expect(getElementDatasource(variableSet, query, 'variable')).toBeUndefined();
|
|
});
|
|
|
|
it('should throw error when invalid type is passed to getAutoAssignedDSRef', () => {
|
|
const vizPanel = new VizPanel({
|
|
key: 'panel-1',
|
|
pluginId: 'timeseries',
|
|
});
|
|
|
|
const dsReferencesMapping = {
|
|
panels: new Map([['panel-1', new Set(['A'])]]),
|
|
variables: new Set<string>(),
|
|
annotations: new Set<string>(),
|
|
};
|
|
|
|
expect(() => {
|
|
// @ts-expect-error - intentionally passing invalid type to test error handling
|
|
getAutoAssignedDSRef(vizPanel, 'invalid-type', dsReferencesMapping);
|
|
}).toThrow('Invalid type invalid-type for getAutoAssignedDSRef');
|
|
});
|
|
});
|
|
|
|
describe('getVizPanelQueries', () => {
|
|
it('should handle panel query datasources correctly', () => {
|
|
const queryWithDS: SceneDataQuery = {
|
|
refId: 'B',
|
|
datasource: { uid: 'prometheus-uid', type: 'prometheus' },
|
|
};
|
|
|
|
const queryWithoutDS: SceneDataQuery = {
|
|
refId: 'A',
|
|
};
|
|
|
|
// Mock query runner
|
|
const queryRunner = new SceneQueryRunner({
|
|
queries: [queryWithoutDS, queryWithDS],
|
|
datasource: { uid: 'default-ds', type: 'default' },
|
|
});
|
|
// Create test elements
|
|
const vizPanel = new VizPanel({
|
|
key: 'panel-1',
|
|
pluginId: 'timeseries',
|
|
$data: queryRunner,
|
|
});
|
|
|
|
// Mock dsReferencesMapping
|
|
const dsReferencesMapping = {
|
|
panels: new Map(new Set([['panel-1', new Set<string>(['A'])]])),
|
|
variables: new Set<string>(),
|
|
annotations: new Set<string>(),
|
|
};
|
|
|
|
const result = getVizPanelQueries(vizPanel, dsReferencesMapping);
|
|
expect(result.length).toBe(2);
|
|
expect(result[0].spec.query.kind).toBe('DataQuery');
|
|
expect(result[0].spec.query.datasource).toBeUndefined(); // ignore datasource if it wasn't provided
|
|
expect(result[0].spec.query.group).toBe('default');
|
|
expect(result[0].spec.query.version).toBe('v0');
|
|
|
|
expect(result[1].spec.query.kind).toBe('DataQuery');
|
|
expect(result[1].spec.query.datasource?.name).toBe('prometheus-uid');
|
|
expect(result[1].spec.query.group).toBe('prometheus');
|
|
expect(result[1].spec.query.version).toBe('v0');
|
|
});
|
|
});
|
|
|
|
function getMinimalSceneState(body: DashboardLayoutManager): Partial<DashboardSceneState> {
|
|
return {
|
|
id: 1,
|
|
title: 'Test Dashboard',
|
|
description: 'Test Description',
|
|
preload: true,
|
|
tags: ['tag1', 'tag2'],
|
|
uid: 'test-uid',
|
|
version: 1,
|
|
|
|
controls: new DashboardControls({
|
|
refreshPicker: new SceneRefreshPicker({
|
|
refresh: '5s',
|
|
intervals: ['5s', '10s', '30s'],
|
|
autoEnabled: true,
|
|
autoMinInterval: '5s',
|
|
autoValue: '5s',
|
|
isOnCanvas: true,
|
|
primary: true,
|
|
withText: true,
|
|
minRefreshInterval: '5s',
|
|
}),
|
|
timePicker: new SceneTimePicker({
|
|
isOnCanvas: true,
|
|
hidePicker: true,
|
|
quickRanges: [
|
|
{
|
|
display: 'Last 6 hours',
|
|
from: 'now-6h',
|
|
to: 'now',
|
|
},
|
|
{
|
|
display: 'Last 3 days',
|
|
from: 'now-3d',
|
|
to: 'now',
|
|
},
|
|
],
|
|
}),
|
|
}),
|
|
|
|
$timeRange: new SceneTimeRange({
|
|
timeZone: 'UTC',
|
|
from: 'now-1h',
|
|
to: 'now',
|
|
weekStart: 'monday',
|
|
fiscalYearStartMonth: 1,
|
|
UNSAFE_nowDelay: '1m',
|
|
refreshOnActivate: {
|
|
afterMs: 10,
|
|
percent: 0.1,
|
|
},
|
|
}),
|
|
|
|
body,
|
|
};
|
|
}
|
|
|
|
describe('dynamic layouts', () => {
|
|
it('should transform scene with rows layout with default grids in rows to save model schema v2', () => {
|
|
const scene = setupDashboardScene(
|
|
getMinimalSceneState(
|
|
new RowsLayoutManager({
|
|
rows: [
|
|
new RowItem({
|
|
layout: new DefaultGridLayoutManager({
|
|
grid: new SceneGridLayout({
|
|
children: [
|
|
new DashboardGridItem({
|
|
y: 0,
|
|
height: 10,
|
|
body: new VizPanel({}),
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
}),
|
|
],
|
|
})
|
|
)
|
|
);
|
|
|
|
const result = transformSceneToSaveModelSchemaV2(scene);
|
|
expect(result.layout.kind).toBe('RowsLayout');
|
|
const rowsLayout = result.layout.spec as RowsLayoutSpec;
|
|
expect(rowsLayout.rows.length).toBe(1);
|
|
expect(rowsLayout.rows[0].kind).toBe('RowsLayoutRow');
|
|
expect(rowsLayout.rows[0].spec.layout.kind).toBe('GridLayout');
|
|
});
|
|
|
|
it('should transform scene with rows layout with multiple rows with different grids to save model schema v2', () => {
|
|
const scene = setupDashboardScene(
|
|
getMinimalSceneState(
|
|
new RowsLayoutManager({
|
|
rows: [
|
|
new RowItem({
|
|
layout: new AutoGridLayoutManager({
|
|
layout: new AutoGridLayout({
|
|
children: [
|
|
new AutoGridItem({
|
|
body: new VizPanel({}),
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
}),
|
|
new RowItem({
|
|
layout: new DefaultGridLayoutManager({
|
|
grid: new SceneGridLayout({
|
|
children: [
|
|
new DashboardGridItem({
|
|
y: 0,
|
|
height: 10,
|
|
body: new VizPanel({}),
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
}),
|
|
],
|
|
})
|
|
)
|
|
);
|
|
|
|
const result = transformSceneToSaveModelSchemaV2(scene);
|
|
expect(result.layout.kind).toBe('RowsLayout');
|
|
const rowsLayout = result.layout.spec as RowsLayoutSpec;
|
|
expect(rowsLayout.rows.length).toBe(2);
|
|
expect(rowsLayout.rows[0].kind).toBe('RowsLayoutRow');
|
|
expect(rowsLayout.rows[0].spec.layout.kind).toBe('AutoGridLayout');
|
|
const layout1 = rowsLayout.rows[0].spec.layout.spec as AutoGridLayoutSpec;
|
|
expect(layout1.items[0].kind).toBe('AutoGridLayoutItem');
|
|
|
|
expect(rowsLayout.rows[1].spec.layout.kind).toBe('GridLayout');
|
|
const layout2 = rowsLayout.rows[1].spec.layout.spec as GridLayoutSpec;
|
|
expect(layout2.items[0].kind).toBe('GridLayoutItem');
|
|
});
|
|
|
|
it('should transform scene with auto grid layout to schema v2', () => {
|
|
const scene = setupDashboardScene(
|
|
getMinimalSceneState(
|
|
new AutoGridLayoutManager({
|
|
columnWidth: 100,
|
|
rowHeight: 'standard',
|
|
maxColumnCount: 4,
|
|
fillScreen: true,
|
|
layout: new AutoGridLayout({
|
|
children: [
|
|
new AutoGridItem({
|
|
body: new VizPanel({}),
|
|
}),
|
|
new AutoGridItem({
|
|
body: new VizPanel({}),
|
|
}),
|
|
],
|
|
}),
|
|
})
|
|
)
|
|
);
|
|
const result = transformSceneToSaveModelSchemaV2(scene);
|
|
expect(result.layout.kind).toBe('AutoGridLayout');
|
|
const respGridLayout = result.layout.spec as AutoGridLayoutSpec;
|
|
expect(respGridLayout.columnWidthMode).toBe('custom');
|
|
expect(respGridLayout.columnWidth).toBe(100);
|
|
expect(respGridLayout.rowHeightMode).toBe('standard');
|
|
expect(respGridLayout.rowHeight).toBeUndefined();
|
|
expect(respGridLayout.maxColumnCount).toBe(4);
|
|
expect(respGridLayout.fillScreen).toBe(true);
|
|
expect(respGridLayout.items.length).toBe(2);
|
|
expect(respGridLayout.items[0].kind).toBe('AutoGridLayoutItem');
|
|
});
|
|
|
|
it('should transform scene with tabs layout to schema v2', () => {
|
|
const tabs = [
|
|
new TabItem({
|
|
layout: new DefaultGridLayoutManager({
|
|
grid: new SceneGridLayout({
|
|
children: [
|
|
new DashboardGridItem({
|
|
y: 0,
|
|
height: 10,
|
|
body: new VizPanel({}),
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
}),
|
|
];
|
|
|
|
const scene = setupDashboardScene(getMinimalSceneState(new TabsLayoutManager({ tabs })));
|
|
const result = transformSceneToSaveModelSchemaV2(scene);
|
|
expect(result.layout.kind).toBe('TabsLayout');
|
|
const tabsLayout = result.layout.spec as TabsLayoutSpec;
|
|
expect(tabsLayout.tabs.length).toBe(1);
|
|
expect(tabsLayout.tabs[0].kind).toBe('TabsLayoutTab');
|
|
expect(tabsLayout.tabs[0].spec.layout.kind).toBe('GridLayout');
|
|
});
|
|
});
|
|
|
|
// Instead of reusing annotation layer objects, create a factory function to generate new ones each time
|
|
function createAnnotationLayers() {
|
|
return [
|
|
new DashboardAnnotationsDataLayer({
|
|
key: 'layer1',
|
|
query: {
|
|
datasource: {
|
|
type: 'grafana',
|
|
uid: '-- Grafana --',
|
|
},
|
|
name: 'query1',
|
|
enable: true,
|
|
iconColor: 'red',
|
|
},
|
|
name: 'layer1',
|
|
isEnabled: true,
|
|
isHidden: false,
|
|
}),
|
|
new DashboardAnnotationsDataLayer({
|
|
key: 'layer2',
|
|
query: {
|
|
datasource: {
|
|
type: 'prometheus',
|
|
uid: 'abcdef',
|
|
},
|
|
name: 'query2',
|
|
enable: true,
|
|
iconColor: 'blue',
|
|
},
|
|
name: 'layer2',
|
|
isEnabled: true,
|
|
isHidden: true,
|
|
}),
|
|
];
|
|
}
|
|
|
|
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
|
|
);
|
|
});
|
|
});
|
|
|