The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
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.
grafana/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2T...

793 lines
30 KiB

import { cloneDeep } from 'lodash';
import { config } from '@grafana/runtime';
import {
behaviors,
ConstantVariable,
CustomVariable,
DataSourceVariable,
IntervalVariable,
QueryVariable,
TextBoxVariable,
sceneGraph,
GroupByVariable,
AdHocFiltersVariable,
SceneDataTransformer,
SceneGridItem,
} from '@grafana/scenes';
import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2_examples';
import {
AdhocVariableKind,
ConstantVariableKind,
CustomVariableKind,
Spec as DashboardV2Spec,
DatasourceVariableKind,
GridLayoutItemSpec,
GridLayoutSpec,
GroupByVariableKind,
IntervalVariableKind,
QueryVariableKind,
TextVariableKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { AnnoKeyDashboardIsSnapshot } from 'app/features/apiserver/types';
Dashboard API versions handling (#96666) * structure apic * API versioning proposal * Make api service independent from version * Update v2 * Fix public dashboards page test * Uncomment reload dashboard feature code * Revert * Betterer * Fix imports * useV2DashboardsAPI feature toggle * POC/v2 schema: Add v1<-> v2 transformers (#97058) * Make dshboard access interface more precise * Add first pass for schema v1<->v2 transformers * Update response transformer test * Import fixes * Manage dashboards validation: Handle v2 schema * Handle new dashboard with v2 * Fix tests * Move dashboard is folder error handling to legacy API implementation * Add tests for dashboard api client * betterer * Use dashboard DTO when capturing dashbaord impression * prettier * Dashboard API: resolve folder metadata * Add tests for resolving folder metadata * Fix DashboardPicker * Renames and nits * POC Alternative Suggestion for Dashboard API versions handling (#97789) * Add transitional_dashboard_api, reset components that are not ready for v2 schema, and start working on migrating DashboardPicker to use v2 schema * reset DashboardScenePageStateManager * Improve logic in transitional api, also remove isDashboardResource checks from components * REmove transitional_dashboard_api and apply PR feedback * Apply PR feedback, use 'v2' as a parameter and remove unnecesary if * Fix tests * Adding missing comments from original PR and also changing order to improve diffing in github :) * update betterer * fix prettier * Add tests for DashboardPicker * Do not use unified alerting mocks * Fix unused type in dashboard test * Improve comments in DahboardPicker * Update folder validation fn * Validation update * Update legacy api test * Lint --------- Co-authored-by: alexandra vargas <alexa1866@gmail.com> Co-authored-by: Alexa V <239999+axelavargas@users.noreply.github.com> Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
7 months ago
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem';
import { AutoGridLayoutManager } from '../scene/layout-auto-grid/AutoGridLayoutManager';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager';
import { TabsLayoutManager } from '../scene/layout-tabs/TabsLayoutManager';
import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getQueryRunnerFor } from '../utils/utils';
import { validateVariable, validateVizPanel } from '../v2schema/test-helpers';
import { SnapshotVariable } from './custom-variables/SnapshotVariable';
import {
getLibraryPanelElement,
getPanelElement,
transformSaveModelSchemaV2ToScene,
} from './transformSaveModelSchemaV2ToScene';
import { transformCursorSynctoEnum } from './transformToV2TypesUtils';
export const defaultDashboard: DashboardWithAccessInfo<DashboardV2Spec> = {
kind: 'DashboardWithAccessInfo',
metadata: {
name: 'dashboard-uid',
namespace: 'default',
labels: {},
generation: 123,
resourceVersion: '123',
creationTimestamp: 'creationTs',
annotations: {
'grafana.app/createdBy': 'user:createBy',
'grafana.app/folder': 'folder-uid',
'grafana.app/updatedBy': 'user:updatedBy',
'grafana.app/updatedTimestamp': 'updatedTs',
},
},
spec: handyTestingSchema,
access: {
url: '/d/abc',
slug: 'what-a-dashboard',
},
apiVersion: 'v2',
};
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: () => ({
getInstanceSettings: jest.fn(),
}),
}));
describe('transformSaveModelSchemaV2ToScene', () => {
beforeAll(() => {
config.featureToggles.groupByVariable = true;
});
afterAll(() => {
config.featureToggles.groupByVariable = false;
});
it('should initialize the DashboardScene with the model state', () => {
const scene = transformSaveModelSchemaV2ToScene(defaultDashboard);
const dashboardControls = scene.state.controls!;
const dash = defaultDashboard.spec;
expect(scene.state.uid).toEqual(defaultDashboard.metadata.name);
expect(scene.state.title).toEqual(dash.title);
expect(scene.state.description).toEqual(dash.description);
expect(scene.state.editable).toEqual(dash.editable);
expect(scene.state.preload).toEqual(false);
expect(scene.state.version).toEqual(123);
expect(scene.state.tags).toEqual(dash.tags);
const liveNow = scene.state.$behaviors?.find((b) => b instanceof behaviors.LiveNowTimer);
expect(liveNow?.state.enabled).toEqual(dash.liveNow);
const cursorSync = scene.state.$behaviors?.find((b) => b instanceof behaviors.CursorSync);
expect(transformCursorSynctoEnum(cursorSync?.state.sync)).toEqual(dash.cursorSync);
// Dashboard links
expect(scene.state.links).toHaveLength(dash.links.length);
expect(scene.state.links![0].title).toBe(dash.links[0].title);
// Time settings
const time = dash.timeSettings;
const refreshPicker = dashboardSceneGraph.getRefreshPicker(scene)!;
const timeRange = sceneGraph.getTimeRange(scene)!;
// Time settings
expect(refreshPicker.state.refresh).toEqual(time.autoRefresh);
expect(refreshPicker.state.intervals).toEqual(time.autoRefreshIntervals);
expect(timeRange?.state.fiscalYearStartMonth).toEqual(dash.timeSettings.fiscalYearStartMonth);
expect(timeRange?.state.value.raw).toEqual({ from: dash.timeSettings.from, to: dash.timeSettings.to });
expect(dashboardControls.state.hideTimeControls).toEqual(dash.timeSettings.hideTimepicker);
expect(timeRange?.state.UNSAFE_nowDelay).toEqual(dash.timeSettings.nowDelay);
expect(timeRange?.state.timeZone).toEqual(dash.timeSettings.timezone);
expect(timeRange?.state.weekStart).toEqual(dash.timeSettings.weekStart);
expect(dashboardControls).toBeDefined();
expect(dashboardControls.state.refreshPicker.state.intervals).toEqual(time.autoRefreshIntervals);
expect(dashboardControls.state.hideTimeControls).toBe(time.hideTimepicker);
expect(dashboardControls.state.timePicker.state.quickRanges).toEqual(dash.timeSettings.quickRanges);
// Variables
const variables = scene.state?.$variables;
expect(variables?.state.variables).toHaveLength(dash.variables.length);
validateVariable({
sceneVariable: variables?.state.variables[0],
variableKind: dash.variables[0] as QueryVariableKind,
scene: scene,
dashSpec: dash,
sceneVariableClass: QueryVariable,
index: 0,
});
validateVariable({
sceneVariable: variables?.state.variables[1],
variableKind: dash.variables[1] as CustomVariableKind,
scene: scene,
dashSpec: dash,
sceneVariableClass: CustomVariable,
index: 1,
});
validateVariable({
sceneVariable: variables?.state.variables[2],
variableKind: dash.variables[2] as DatasourceVariableKind,
scene: scene,
dashSpec: dash,
sceneVariableClass: DataSourceVariable,
index: 2,
});
validateVariable({
sceneVariable: variables?.state.variables[3],
variableKind: dash.variables[3] as ConstantVariableKind,
scene: scene,
dashSpec: dash,
sceneVariableClass: ConstantVariable,
index: 3,
});
validateVariable({
sceneVariable: variables?.state.variables[4],
variableKind: dash.variables[4] as IntervalVariableKind,
scene: scene,
dashSpec: dash,
sceneVariableClass: IntervalVariable,
index: 4,
});
validateVariable({
sceneVariable: variables?.state.variables[5],
variableKind: dash.variables[5] as TextVariableKind,
scene: scene,
dashSpec: dash,
sceneVariableClass: TextBoxVariable,
index: 5,
});
validateVariable({
sceneVariable: variables?.state.variables[6],
variableKind: dash.variables[6] as GroupByVariableKind,
scene: scene,
dashSpec: dash,
sceneVariableClass: GroupByVariable,
index: 6,
});
validateVariable({
sceneVariable: variables?.state.variables[7],
variableKind: dash.variables[7] as AdhocVariableKind,
scene: scene,
dashSpec: dash,
sceneVariableClass: AdHocFiltersVariable,
index: 7,
});
// Annotations
expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet);
const dataLayers = scene.state.$data as DashboardDataLayerSet;
// we should get two annotations, Grafana built-in and the custom ones
expect(dataLayers.state.annotationLayers).toHaveLength(dash.annotations.length);
expect(dataLayers.state.annotationLayers).toHaveLength(5);
// Built-in
const builtInAnnotation = dataLayers.state.annotationLayers[0] as unknown as DashboardAnnotationsDataLayer;
expect(builtInAnnotation.state.name).toBe('Annotations & Alerts');
expect(builtInAnnotation.state.isEnabled).toBe(true);
expect(builtInAnnotation.state.isHidden).toBe(true);
expect(builtInAnnotation.state?.query.builtIn).toBe(1);
// Enabled
expect(dataLayers.state.annotationLayers[1].state.name).toBe(dash.annotations[1].spec.name);
expect(dataLayers.state.annotationLayers[1].state.isEnabled).toBe(dash.annotations[1].spec.enable);
expect(dataLayers.state.annotationLayers[1].state.isHidden).toBe(dash.annotations[1].spec.hide);
expect(dataLayers.state.annotationLayers[2].state.name).toBe(dash.annotations[2].spec.name);
expect(dataLayers.state.annotationLayers[2].state.isEnabled).toBe(dash.annotations[2].spec.enable);
expect(dataLayers.state.annotationLayers[2].state.isHidden).toBe(dash.annotations[2].spec.hide);
// Disabled
expect(dataLayers.state.annotationLayers[3].state.name).toBe(dash.annotations[3].spec.name);
expect(dataLayers.state.annotationLayers[3].state.isEnabled).toBe(dash.annotations[3].spec.enable);
expect(dataLayers.state.annotationLayers[3].state.isHidden).toBe(dash.annotations[3].spec.hide);
// Hidden
expect(dataLayers.state.annotationLayers[4].state.name).toBe(dash.annotations[4].spec.name);
expect(dataLayers.state.annotationLayers[4].state.isEnabled).toBe(dash.annotations[4].spec.enable);
expect(dataLayers.state.annotationLayers[4].state.isHidden).toBe(dash.annotations[4].spec.hide);
// VizPanel
const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels();
expect(vizPanels).toHaveLength(2);
// Layout
const layout = scene.state.body as DefaultGridLayoutManager;
// Panel
const panel = getPanelElement(dash, 'panel-1')!;
expect(layout.state.grid.state.children.length).toBe(2);
expect(layout.state.grid.state.children[0].state.key).toBe(`grid-item-${panel.spec.id}`);
const gridLayoutItemSpec = (dash.layout.spec as GridLayoutSpec).items[0].spec as GridLayoutItemSpec;
expect(layout.state.grid.state.children[0].state.width).toBe(gridLayoutItemSpec.width);
expect(layout.state.grid.state.children[0].state.height).toBe(gridLayoutItemSpec.height);
expect(layout.state.grid.state.children[0].state.x).toBe(gridLayoutItemSpec.x);
expect(layout.state.grid.state.children[0].state.y).toBe(gridLayoutItemSpec.y);
const vizPanel = vizPanels.find((p) => p.state.key === 'panel-1')!;
validateVizPanel(vizPanel, dash);
// Library Panel
const libraryPanel = getLibraryPanelElement(dash, 'panel-2')!;
expect(layout.state.grid.state.children[1].state.key).toBe(`grid-item-${libraryPanel.spec.id}`);
const libraryGridLayoutItemSpec = (dash.layout.spec as GridLayoutSpec).items[1].spec as GridLayoutItemSpec;
expect(layout.state.grid.state.children[1].state.width).toBe(libraryGridLayoutItemSpec.width);
expect(layout.state.grid.state.children[1].state.height).toBe(libraryGridLayoutItemSpec.height);
expect(layout.state.grid.state.children[1].state.x).toBe(libraryGridLayoutItemSpec.x);
expect(layout.state.grid.state.children[1].state.y).toBe(libraryGridLayoutItemSpec.y);
const vizLibraryPanel = vizPanels.find((p) => p.state.key === 'panel-2')!;
validateVizPanel(vizLibraryPanel, dash);
// Transformations
const panelWithTransformations = vizPanels.find((p) => p.state.key === 'panel-1')!;
expect((panelWithTransformations.state.$data as SceneDataTransformer)?.state.transformations[0]).toEqual(
getPanelElement(dash, 'panel-1')!.spec.data.spec.transformations[0].spec
);
});
it('should set panel ds if it is mixed DS', () => {
const dashboard = cloneDeep(defaultDashboard);
getPanelElement(dashboard.spec, 'panel-1')?.spec.data.spec.queries.push({
kind: 'PanelQuery',
spec: {
refId: 'A',
datasource: {
type: 'graphite',
uid: 'datasource1',
},
hidden: false,
query: {
kind: 'prometheus',
spec: {
expr: 'test-query',
},
},
},
});
const scene = transformSaveModelSchemaV2ToScene(dashboard);
const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels();
expect(vizPanels.length).toBe(2);
expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.type).toBe('mixed');
expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.uid).toBe(MIXED_DATASOURCE_NAME);
});
it('should set ds if it is not mixed DS', () => {
const dashboard = cloneDeep(defaultDashboard);
getPanelElement(dashboard.spec, 'panel-1')?.spec.data.spec.queries.push({
kind: 'PanelQuery',
spec: {
refId: 'A',
datasource: {
type: 'prometheus',
uid: 'datasource1',
},
hidden: false,
query: {
kind: 'prometheus',
spec: {
expr: 'test-query',
},
},
},
});
const scene = transformSaveModelSchemaV2ToScene(dashboard);
const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels();
expect(vizPanels.length).toBe(2);
expect(getQueryRunnerFor(vizPanels[0])?.state.queries[0].datasource).toEqual({
type: 'prometheus',
uid: 'datasource1',
});
});
it('should set panel ds as mixed if no panels have ds defined', () => {
const dashboard = cloneDeep(defaultDashboard);
getPanelElement(dashboard.spec, 'panel-1')?.spec.data.spec.queries.push({
kind: 'PanelQuery',
spec: {
refId: 'A',
hidden: false,
query: {
kind: 'prometheus',
spec: {
expr: 'test-query',
},
},
},
});
const scene = transformSaveModelSchemaV2ToScene(dashboard);
const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels();
expect(vizPanels.length).toBe(2);
expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.type).toBe('mixed');
expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.uid).toBe(MIXED_DATASOURCE_NAME);
});
describe('When creating a snapshot dashboard scene', () => {
it('should initialize a dashboard scene with SnapshotVariables', () => {
const snapshot: DashboardWithAccessInfo<DashboardV2Spec> = {
...defaultDashboard,
metadata: {
...defaultDashboard.metadata,
annotations: {
...defaultDashboard.metadata.annotations,
[AnnoKeyDashboardIsSnapshot]: 'true',
},
},
};
const scene = transformSaveModelSchemaV2ToScene(snapshot);
// check variables were converted to snapshot variables
expect(scene.state.$variables?.state.variables).toHaveLength(8);
expect(scene.state.$variables?.getByName('customVar')).toBeInstanceOf(SnapshotVariable);
expect(scene.state.$variables?.getByName('adhocVar')).toBeInstanceOf(AdHocFiltersVariable);
expect(scene.state.$variables?.getByName('intervalVar')).toBeInstanceOf(SnapshotVariable);
// custom snapshot
const customSnapshot = scene.state.$variables?.getByName('customVar') as SnapshotVariable;
expect(customSnapshot.state.value).toBe('option1');
expect(customSnapshot.state.text).toBe('option1');
expect(customSnapshot.state.isReadOnly).toBe(true);
// adhoc snapshot
const adhocSnapshot = scene.state.$variables?.getByName('adhocVar') as AdHocFiltersVariable;
const adhocVariable = snapshot.spec.variables[7] as AdhocVariableKind;
expect(adhocSnapshot.state.filters).toEqual(adhocVariable.spec.filters);
expect(adhocSnapshot.state.readOnly).toBe(true);
// interval snapshot
const intervalSnapshot = scene.state.$variables?.getByName('intervalVar') as SnapshotVariable;
expect(intervalSnapshot.state.value).toBe('1m');
expect(intervalSnapshot.state.text).toBe('1m');
expect(intervalSnapshot.state.isReadOnly).toBe(true);
});
});
describe('meta', () => {
describe('initializes meta based on k8s resource', () => {
it('handles undefined access values', () => {
const scene = transformSaveModelSchemaV2ToScene(defaultDashboard);
// when access metadata undefined
expect(scene.state.meta.canShare).toBe(true);
expect(scene.state.meta.canSave).toBe(true);
expect(scene.state.meta.canStar).toBe(true);
expect(scene.state.meta.canEdit).toBe(true);
expect(scene.state.meta.canDelete).toBe(true);
expect(scene.state.meta.canAdmin).toBe(true);
expect(scene.state.meta.annotationsPermissions).toBe(undefined);
expect(scene.state.meta.url).toBe('/d/abc');
expect(scene.state.meta.slug).toBe('what-a-dashboard');
expect(scene.state.meta.created).toBe('creationTs');
expect(scene.state.meta.createdBy).toBe('user:createBy');
expect(scene.state.meta.updated).toBe('updatedTs');
expect(scene.state.meta.updatedBy).toBe('user:updatedBy');
expect(scene.state.meta.folderUid).toBe('folder-uid');
expect(scene.state.meta.version).toBe(123);
});
it('handles access metadata values', () => {
const dashboard: DashboardWithAccessInfo<DashboardV2Spec> = {
...defaultDashboard,
access: {
canSave: false,
canEdit: false,
canDelete: false,
canShare: false,
canStar: false,
canAdmin: false,
annotationsPermissions: {
dashboard: {
canAdd: false,
canEdit: false,
canDelete: false,
},
organization: {
canAdd: false,
canEdit: false,
canDelete: false,
},
},
},
};
const scene = transformSaveModelSchemaV2ToScene(dashboard);
expect(scene.state.meta.canShare).toBe(false);
expect(scene.state.meta.canSave).toBe(false);
expect(scene.state.meta.canStar).toBe(false);
expect(scene.state.meta.canEdit).toBe(false);
expect(scene.state.meta.canDelete).toBe(false);
expect(scene.state.meta.canAdmin).toBe(false);
expect(scene.state.meta.annotationsPermissions).toEqual(dashboard.access.annotationsPermissions);
expect(scene.state.meta.version).toBe(123);
});
});
describe('Editable false dashboard', () => {
let dashboard: DashboardWithAccessInfo<DashboardV2Spec>;
beforeEach(() => {
dashboard = {
...cloneDeep(defaultDashboard),
spec: {
...defaultDashboard.spec,
editable: false,
},
};
});
it('Should set meta canEdit and canSave to false', () => {
const scene = transformSaveModelSchemaV2ToScene(dashboard);
expect(scene.state.meta.canMakeEditable).toBe(true);
expect(scene.state.meta.canSave).toBe(false);
expect(scene.state.meta.canEdit).toBe(false);
expect(scene.state.meta.canDelete).toBe(false);
});
describe('when does not have save permissions', () => {
it('Should set meta correct meta', () => {
dashboard.access.canSave = false;
const scene = transformSaveModelSchemaV2ToScene(dashboard);
expect(scene.state.meta.canMakeEditable).toBe(false);
expect(scene.state.meta.canSave).toBe(false);
expect(scene.state.meta.canEdit).toBe(false);
expect(scene.state.meta.canDelete).toBe(false);
});
});
});
describe('Editable true dashboard', () => {
let dashboard: DashboardWithAccessInfo<DashboardV2Spec>;
beforeEach(() => {
dashboard = {
...cloneDeep(defaultDashboard),
spec: {
...defaultDashboard.spec,
editable: true,
},
};
});
it('Should set meta canEdit and canSave to false', () => {
const scene = transformSaveModelSchemaV2ToScene(dashboard);
expect(scene.state.meta.canMakeEditable).toBe(false);
expect(scene.state.meta.canSave).toBe(true);
expect(scene.state.meta.canEdit).toBe(true);
expect(scene.state.meta.canDelete).toBe(true);
});
});
describe('dynamic dashboard layouts', () => {
it('should build a dashboard scene with a auto grid layout', () => {
const dashboard = cloneDeep(defaultDashboard);
dashboard.spec.layout = {
kind: 'AutoGridLayout',
spec: {
maxColumnCount: 4,
columnWidthMode: 'custom',
columnWidth: 100,
rowHeightMode: 'standard',
items: [
{
kind: 'AutoGridLayoutItem',
spec: {
element: {
kind: 'ElementReference',
name: 'panel-1',
},
},
},
],
},
};
const scene = transformSaveModelSchemaV2ToScene(dashboard);
const layoutManager = scene.state.body as AutoGridLayoutManager;
expect(layoutManager.descriptor.id).toBe('AutoGridLayout');
expect(layoutManager.state.maxColumnCount).toBe(4);
expect(layoutManager.state.columnWidth).toBe(100);
expect(layoutManager.state.rowHeight).toBe('standard');
expect(layoutManager.state.layout.state.children.length).toBe(1);
const gridItem = layoutManager.state.layout.state.children[0] as AutoGridItem;
expect(gridItem.state.body.state.key).toBe('panel-1');
});
it('should build a dashboard scene with a tabs layout', () => {
const dashboard = cloneDeep(defaultDashboard);
dashboard.spec.layout = {
kind: 'TabsLayout',
spec: {
tabs: [
{
kind: 'TabsLayoutTab',
spec: {
title: 'tab1',
layout: {
kind: 'AutoGridLayout',
spec: {
maxColumnCount: 4,
columnWidthMode: 'standard',
rowHeightMode: 'standard',
items: [
{
kind: 'AutoGridLayoutItem',
spec: {
element: {
kind: 'ElementReference',
name: 'panel-1',
},
},
},
],
},
},
},
},
],
},
};
const scene = transformSaveModelSchemaV2ToScene(dashboard);
const layoutManager = scene.state.body as TabsLayoutManager;
expect(layoutManager.descriptor.id).toBe('TabsLayout');
expect(layoutManager.state.tabs.length).toBe(1);
expect(layoutManager.state.tabs[0].state.title).toBe('tab1');
const gridLayoutManager = layoutManager.state.tabs[0].state.layout as AutoGridLayoutManager;
expect(gridLayoutManager.state.maxColumnCount).toBe(4);
expect(gridLayoutManager.state.columnWidth).toBe('standard');
expect(gridLayoutManager.state.rowHeight).toBe('standard');
expect(gridLayoutManager.state.layout.state.children.length).toBe(1);
const gridItem = gridLayoutManager.state.layout.state.children[0] as AutoGridItem;
expect(gridItem.state.body.state.key).toBe('panel-1');
});
it('should build a dashboard scene with rows layout', () => {
const dashboard = cloneDeep(defaultDashboard);
dashboard.spec.layout = {
kind: 'RowsLayout',
spec: {
rows: [
{
kind: 'RowsLayoutRow',
spec: {
title: 'row1',
collapse: false,
layout: {
kind: 'AutoGridLayout',
spec: {
maxColumnCount: 4,
columnWidthMode: 'standard',
rowHeightMode: 'standard',
items: [
{
kind: 'AutoGridLayoutItem',
spec: {
element: {
kind: 'ElementReference',
name: 'panel-1',
},
},
},
],
},
},
},
},
{
kind: 'RowsLayoutRow',
spec: {
title: 'row2',
collapse: true,
layout: {
kind: 'GridLayout',
spec: {
items: [
{
kind: 'GridLayoutItem',
spec: {
y: 0,
x: 0,
height: 10,
width: 10,
element: {
kind: 'ElementReference',
name: 'panel-2',
},
},
},
],
},
},
},
},
],
},
};
const scene = transformSaveModelSchemaV2ToScene(dashboard);
const layoutManager = scene.state.body as RowsLayoutManager;
expect(layoutManager.descriptor.id).toBe('RowsLayout');
expect(layoutManager.state.rows.length).toBe(2);
const row1Manager = layoutManager.state.rows[0].state.layout as AutoGridLayoutManager;
expect(row1Manager.descriptor.id).toBe('AutoGridLayout');
expect(row1Manager.state.maxColumnCount).toBe(4);
expect(row1Manager.state.columnWidth).toBe('standard');
expect(row1Manager.state.rowHeight).toBe('standard');
const row1GridItem = row1Manager.state.layout.state.children[0] as AutoGridItem;
expect(row1GridItem.state.body.state.key).toBe('panel-1');
const row2Manager = layoutManager.state.rows[1].state.layout as DefaultGridLayoutManager;
expect(row2Manager.descriptor.id).toBe('GridLayout');
const row2GridItem = row2Manager.state.grid.state.children[0] as SceneGridItem;
expect(row2GridItem.state.body!.state.key).toBe('panel-2');
});
});
});
describe('annotations', () => {
it('should transform annotation with legacyOptions field', () => {
// Create a dashboard with an annotation that has options
const dashboardWithAnnotationOptions: DashboardWithAccessInfo<DashboardV2Spec> = {
kind: 'DashboardWithAccessInfo',
apiVersion: 'v2alpha1',
metadata: {
name: 'test-dashboard',
namespace: 'default',
creationTimestamp: new Date().toISOString(),
labels: {},
annotations: {},
generation: 1,
resourceVersion: '1',
},
spec: {
title: 'Dashboard with annotation options',
editable: true,
preload: false,
liveNow: false,
cursorSync: 'Off',
links: [],
tags: [],
timeSettings: {
from: 'now-6h',
to: 'now',
timezone: 'browser',
hideTimepicker: false,
autoRefresh: '5s',
autoRefreshIntervals: ['5s', '10s', '30s'],
fiscalYearStartMonth: 0,
weekStart: 'monday',
},
variables: [],
elements: {},
layout: {
kind: 'GridLayout',
spec: { items: [] },
},
annotations: [
{
kind: 'AnnotationQuery',
spec: {
name: 'Annotation with legacy options',
builtIn: false,
enable: true,
hide: false,
iconColor: 'purple',
datasource: {
type: 'prometheus',
uid: 'abc123',
},
legacyOptions: {
expr: 'rate(http_requests_total[5m])',
queryType: 'range',
legendFormat: '{{method}} {{endpoint}}',
useValueAsTime: true,
step: '1m',
},
},
},
],
},
access: {
canSave: true,
canEdit: true,
canDelete: true,
canAdmin: true,
canStar: true,
canShare: true,
annotationsPermissions: {
dashboard: {
canAdd: true,
canEdit: true,
canDelete: true,
},
organization: {
canAdd: true,
canEdit: true,
canDelete: true,
},
},
},
};
const scene = transformSaveModelSchemaV2ToScene(dashboardWithAnnotationOptions);
// Get the annotation layers
const dataLayerSet = scene.state.$data as DashboardDataLayerSet;
expect(dataLayerSet).toBeDefined();
// it should have two annotation layers, built-in and custom
expect(dataLayerSet.state.annotationLayers.length).toBe(2);
const annotationLayer = dataLayerSet.state.annotationLayers[1] as DashboardAnnotationsDataLayer;
// Verify that the legacyOptions have been merged into the query object
expect(annotationLayer.state.query).toMatchObject({
name: 'Annotation with legacy options',
expr: 'rate(http_requests_total[5m])',
queryType: 'range',
legendFormat: '{{method}} {{endpoint}}',
useValueAsTime: true,
step: '1m',
});
// Verify the original legacyOptions object is also preserved
expect(annotationLayer.state.query.legacyOptions).toMatchObject({
expr: 'rate(http_requests_total[5m])',
queryType: 'range',
legendFormat: '{{method}} {{endpoint}}',
useValueAsTime: true,
step: '1m',
});
});
});
});