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/api/ResponseTransformers.test.ts

1156 lines
39 KiB

import { AnnotationQuery, DataQuery, VariableModel, VariableRefresh, Panel } from '@grafana/schema';
import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2_examples';
import {
Spec as DashboardV2Spec,
GridLayoutItemKind,
GridLayoutKind,
PanelKind,
RowsLayoutKind,
RowsLayoutRowKind,
VariableKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import {
AnnoKeyCreatedBy,
AnnoKeyDashboardGnetId,
AnnoKeyFolder,
AnnoKeySlug,
AnnoKeyUpdatedBy,
AnnoKeyUpdatedTimestamp,
DeprecatedInternalId,
} from 'app/features/apiserver/types';
import { getDefaultDataSourceRef } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2';
import {
LEGACY_STRING_VALUE_KEY,
transformVariableHideToEnum,
transformVariableRefreshToEnum,
} from 'app/features/dashboard-scene/serialization/transformToV2TypesUtils';
import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard';
import {
getDefaultDatasource,
getPanelQueries,
ResponseTransformers,
transformMappingsToV1,
} from './ResponseTransformers';
import { DashboardWithAccessInfo } from './types';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
config: {
...jest.requireActual('@grafana/runtime').config,
bootData: {
...jest.requireActual('@grafana/runtime').config.bootData,
settings: {
...jest.requireActual('@grafana/runtime').config.bootData.settings,
datasources: {
PromTest: {
uid: 'xyz-abc',
name: 'PromTest',
id: 'prometheus',
meta: {
id: 'prometheus',
name: 'PromTest',
type: 'datasource',
},
isDefault: true,
apiVersion: 'v2',
},
'-- Grafana --': {
uid: 'grafana',
name: '-- Grafana --',
id: 'grafana',
meta: {
id: 'grafana',
name: '-- Grafana --',
type: 'datasource',
},
isDefault: false,
},
},
defaultDatasource: 'PromTest',
},
},
},
}));
describe('ResponseTransformers', () => {
describe('getDefaultDataSource', () => {
it('should return prometheus as default', () => {
expect(getDefaultDatasource()).toEqual({
apiVersion: 'v2',
uid: 'PromTest',
type: 'prometheus',
});
});
});
describe('getDefaultDataSourceRef', () => {
it('should return prometheus as default', () => {
expect(getDefaultDataSourceRef()).toEqual({
uid: 'PromTest',
type: 'prometheus',
});
});
});
describe('v1 -> v2 transformation', () => {
it('should transform DashboardDTO to DashboardWithAccessInfo<DashboardV2Spec>', () => {
const dashboardV1: DashboardDataDTO = {
uid: 'dashboard-uid',
id: 123,
title: 'Dashboard Title',
description: 'Dashboard Description',
tags: ['tag1', 'tag2'],
schemaVersion: 1,
graphTooltip: 0,
preload: true,
liveNow: false,
editable: true,
time: { from: 'now-6h', to: 'now' },
timezone: 'browser',
refresh: '5m',
timepicker: {
refresh_intervals: ['5s', '10s', '30s'],
hidden: false,
nowDelay: '1m',
quick_ranges: [
{
display: 'Last 6 hours',
from: 'now-6h',
to: 'now',
},
{
display: 'Last 7 days',
from: 'now-7d',
to: 'now',
},
],
},
fiscalYearStartMonth: 1,
weekStart: 'monday',
version: 1,
gnetId: 'something-like-a-uid',
revision: 225,
links: [
{
title: 'Link 1',
url: 'https://grafana.com',
asDropdown: false,
targetBlank: true,
includeVars: true,
keepTime: true,
tags: ['tag1', 'tag2'],
icon: 'external link',
type: 'link',
tooltip: 'Link 1 Tooltip',
},
],
annotations: {
list: [],
},
templating: {
list: [
{
type: 'query',
name: 'var1',
label: 'query var',
description: 'query var description',
skipUrlSync: false,
hide: 0,
multi: true,
includeAll: true,
current: { value: '1', text: '1' },
options: [
{ selected: true, text: '1', value: '1' },
{ selected: false, text: '2', value: '2' },
],
refresh: VariableRefresh.onTimeRangeChanged,
datasource: {
type: 'prometheus',
uid: 'abc',
},
regex: '.*',
sort: 1,
query: {
expr: 'sum(query)',
},
},
{
type: 'datasource',
name: 'var2',
label: 'datasource var',
description: 'datasource var description',
skipUrlSync: false,
hide: 0,
multi: true,
includeAll: true,
current: { value: 'PromTest', text: 'PromTest' },
options: [
{ selected: true, text: 'PromTest', value: 'PromTest' },
{ selected: false, text: 'Grafana', value: 'Grafana' },
],
refresh: VariableRefresh.onTimeRangeChanged,
regex: '.*',
sort: 1,
query: 'sum(query)',
},
{
type: 'custom',
name: 'var3',
label: 'custom var',
description: 'custom var description',
skipUrlSync: false,
hide: 0,
multi: true,
includeAll: true,
current: { value: '1', text: '1' },
query: '1,2,3',
options: [
{ selected: true, text: '1', value: '1' },
{ selected: false, text: '2', value: '2' },
],
allValue: '1,2,3',
},
{
type: 'adhoc',
name: 'var4',
label: 'adhoc var',
description: 'adhoc var description',
skipUrlSync: false,
hide: 0,
datasource: {
type: 'prometheus',
uid: 'abc',
},
// @ts-expect-error
baseFilters: [{ key: 'key1', operator: 'AND' }],
filters: [],
defaultKeys: [],
},
{
type: 'constant',
name: 'var5',
label: 'constant var',
description: 'constant var description',
skipUrlSync: false,
hide: 0,
current: { value: '1', text: '0' },
query: '1',
},
{
type: 'interval',
name: 'var6',
label: 'interval var',
description: 'interval var description',
skipUrlSync: false,
query: '1m,10m,30m,1h',
hide: 0,
current: {
value: 'auto',
text: 'auto',
},
refresh: VariableRefresh.onTimeRangeChanged,
options: [
{
selected: true,
text: '1m',
value: '1m',
},
{
selected: false,
text: '10m',
value: '10m',
},
{
selected: false,
text: '30m',
value: '30m',
},
{
selected: false,
text: '1h',
value: '1h',
},
],
// @ts-expect-error
auto: false,
auto_min: '1s',
auto_count: 1,
},
{
type: 'textbox',
name: 'var7',
label: 'textbox var',
description: 'textbox var description',
skipUrlSync: false,
hide: 0,
current: { value: '1', text: '1' },
query: '1',
},
{
type: 'groupby',
name: 'var8',
label: 'groupby var',
description: 'groupby var description',
skipUrlSync: false,
hide: 0,
datasource: {
type: 'prometheus',
uid: 'abc',
},
options: [
{ selected: true, text: '1', value: '1' },
{ selected: false, text: '2', value: '2' },
],
current: { value: ['1'], text: ['1'] },
},
// Query variable with minimal props and without current
{
datasource: { type: 'prometheus', uid: 'abc' },
name: 'org_id',
label: 'Org ID',
hide: 2,
type: 'query',
query: { refId: 'A', query: 'label_values(grafanacloud_org_info{org_slug="$org_slug"}, org_id)' },
},
],
},
panels: [
{
id: 1,
type: 'timeseries',
title: 'Panel Title',
gridPos: { x: 0, y: 0, w: 12, h: 8 },
targets: [
{
refId: 'A',
datasource: 'datasource1',
expr: 'test-query',
hide: false,
},
],
datasource: {
type: 'prometheus',
uid: 'datasource1',
},
fieldConfig: { defaults: {}, overrides: [] },
options: {},
transparent: false,
links: [],
transformations: [],
repeat: 'var1',
repeatDirection: 'h',
},
{
id: 2,
type: 'table',
title: 'Just a shared table',
libraryPanel: {
uid: 'library-panel-table',
name: 'Table Panel as Library Panel',
},
gridPos: { x: 0, y: 8, w: 12, h: 8 },
},
],
};
const dto: DashboardWithAccessInfo<DashboardDataDTO> = {
spec: dashboardV1,
access: {
slug: 'dashboard-slug',
url: '/d/dashboard-slug',
canAdmin: true,
canDelete: true,
canEdit: true,
canSave: true,
canShare: true,
canStar: true,
annotationsPermissions: {
dashboard: { canAdd: true, canEdit: true, canDelete: true },
organization: { canAdd: true, canEdit: true, canDelete: true },
},
},
apiVersion: 'v1',
kind: 'DashboardWithAccessInfo',
metadata: {
name: 'dashboard-uid',
resourceVersion: '1',
creationTimestamp: '2023-01-01T00:00:00Z',
annotations: {
[AnnoKeyCreatedBy]: 'user1',
[AnnoKeyUpdatedBy]: 'user2',
[AnnoKeyUpdatedTimestamp]: '2023-01-02T00:00:00Z',
[AnnoKeyFolder]: 'folder1',
[AnnoKeySlug]: 'dashboard-slug',
},
labels: {
[DeprecatedInternalId]: '123',
},
},
};
const transformed = ResponseTransformers.ensureV2Response(dto);
// Metadata
expect(transformed.apiVersion).toBe('v2alpha1');
expect(transformed.kind).toBe('DashboardWithAccessInfo');
expect(transformed.metadata.annotations?.[AnnoKeyCreatedBy]).toEqual('user1');
expect(transformed.metadata.annotations?.[AnnoKeyUpdatedBy]).toEqual('user2');
expect(transformed.metadata.annotations?.[AnnoKeyUpdatedTimestamp]).toEqual('2023-01-02T00:00:00Z');
expect(transformed.metadata.annotations?.[AnnoKeyFolder]).toEqual('folder1');
expect(transformed.metadata.annotations?.[AnnoKeySlug]).toEqual('dashboard-slug');
expect(transformed.metadata.annotations?.[AnnoKeyDashboardGnetId]).toBe('something-like-a-uid');
expect(transformed.metadata.labels?.[DeprecatedInternalId]).toBe('123');
// Spec
const spec = transformed.spec;
expect(spec.title).toBe(dashboardV1.title);
expect(spec.description).toBe(dashboardV1.description);
expect(spec.tags).toEqual(dashboardV1.tags);
expect(spec.cursorSync).toBe('Off'); // Assuming transformCursorSynctoEnum(0) returns 'Off'
expect(spec.preload).toBe(dashboardV1.preload);
expect(spec.liveNow).toBe(dashboardV1.liveNow);
expect(spec.editable).toBe(dashboardV1.editable);
expect(spec.revision).toBe(dashboardV1.revision);
expect(spec.timeSettings.from).toBe(dashboardV1.time?.from);
expect(spec.timeSettings.to).toBe(dashboardV1.time?.to);
expect(spec.timeSettings.timezone).toBe(dashboardV1.timezone);
expect(spec.timeSettings.autoRefresh).toBe(dashboardV1.refresh);
expect(spec.timeSettings.autoRefreshIntervals).toEqual(dashboardV1.timepicker?.refresh_intervals);
expect(spec.timeSettings.hideTimepicker).toBe(dashboardV1.timepicker?.hidden);
expect(spec.timeSettings.quickRanges).toEqual(dashboardV1.timepicker?.quick_ranges);
expect(spec.timeSettings.nowDelay).toBe(dashboardV1.timepicker?.nowDelay);
expect(spec.timeSettings.fiscalYearStartMonth).toBe(dashboardV1.fiscalYearStartMonth);
expect(spec.timeSettings.weekStart).toBe(dashboardV1.weekStart);
expect(spec.links).toEqual(dashboardV1.links);
expect(spec.annotations).toEqual([]);
// Panel
expect(spec.layout.kind).toBe('GridLayout');
const layout = spec.layout as GridLayoutKind;
expect(layout.spec.items).toHaveLength(2);
expect(layout.spec.items[0].spec).toEqual({
element: {
kind: 'ElementReference',
name: 'panel-1',
},
x: 0,
y: 0,
width: 12,
height: 8,
repeat: { value: 'var1', direction: 'h', mode: 'variable', maxPerRow: undefined },
});
expect(spec.elements['panel-1']).toEqual({
kind: 'Panel',
spec: {
title: 'Panel Title',
description: '',
id: 1,
links: [],
vizConfig: {
kind: 'timeseries',
spec: {
fieldConfig: {
defaults: {},
overrides: [],
},
options: {},
pluginVersion: undefined,
},
},
data: {
kind: 'QueryGroup',
spec: {
queries: [
{
kind: 'PanelQuery',
spec: {
datasource: 'datasource1',
hidden: false,
query: {
kind: 'prometheus',
spec: {
expr: 'test-query',
},
},
refId: 'A',
},
},
],
queryOptions: {
cacheTimeout: undefined,
hideTimeOverride: undefined,
interval: undefined,
maxDataPoints: undefined,
queryCachingTTL: undefined,
timeFrom: undefined,
timeShift: undefined,
},
transformations: [],
},
},
},
});
// Library Panel
expect(layout.spec.items[1].spec).toEqual({
element: {
kind: 'ElementReference',
name: 'panel-2',
},
x: 0,
y: 8,
width: 12,
height: 8,
});
expect(spec.elements['panel-2']).toEqual({
kind: 'LibraryPanel',
spec: {
libraryPanel: {
uid: 'library-panel-table',
name: 'Table Panel as Library Panel',
},
id: 2,
title: 'Just a shared table',
},
});
// Variables
validateVariablesV1ToV2(spec.variables[0], dashboardV1.templating?.list?.[0]);
validateVariablesV1ToV2(spec.variables[1], dashboardV1.templating?.list?.[1]);
validateVariablesV1ToV2(spec.variables[2], dashboardV1.templating?.list?.[2]);
validateVariablesV1ToV2(spec.variables[3], dashboardV1.templating?.list?.[3]);
validateVariablesV1ToV2(spec.variables[4], dashboardV1.templating?.list?.[4]);
validateVariablesV1ToV2(spec.variables[5], dashboardV1.templating?.list?.[5]);
validateVariablesV1ToV2(spec.variables[6], dashboardV1.templating?.list?.[6]);
validateVariablesV1ToV2(spec.variables[7], dashboardV1.templating?.list?.[7]);
validateVariablesV1ToV2(spec.variables[8], dashboardV1.templating?.list?.[8]);
});
});
describe('v1 -> v2 transformation with rows', () => {
it('should transform DashboardDTO to DashboardWithAccessInfo<DashboardV2Spec>', () => {
const dashboardV1: DashboardDataDTO = {
uid: 'dashboard-uid',
id: 123,
title: 'Dashboard Title',
description: 'Dashboard Description',
tags: ['tag1', 'tag2'],
schemaVersion: 1,
graphTooltip: 0,
preload: true,
liveNow: false,
editable: true,
time: { from: 'now-6h', to: 'now' },
timezone: 'browser',
refresh: '5m',
timepicker: {
refresh_intervals: ['5s', '10s', '30s'],
hidden: false,
nowDelay: '1m',
quick_ranges: [
{
display: 'Last 6 hours',
from: 'now-6h',
to: 'now',
},
{
display: 'Last 7 days',
from: 'now-7d',
to: 'now',
},
],
},
fiscalYearStartMonth: 1,
weekStart: 'monday',
version: 1,
gnetId: 'something-like-a-uid',
revision: 225,
links: [],
annotations: {
list: [],
},
templating: {
list: [],
},
panels: [
{
id: 1,
type: 'timeseries',
title: 'Panel Title',
gridPos: { x: 0, y: 0, w: 12, h: 8 },
targets: [
{
refId: 'A',
datasource: 'datasource1',
expr: 'test-query',
hide: false,
},
],
datasource: {
type: 'prometheus',
uid: 'datasource1',
},
fieldConfig: { defaults: {}, overrides: [] },
options: {},
transparent: false,
links: [],
transformations: [],
repeat: 'var1',
repeatDirection: 'h',
},
{
id: 2,
type: 'table',
title: 'Just a shared table',
libraryPanel: {
uid: 'library-panel-table',
name: 'Table Panel as Library Panel',
},
gridPos: { x: 0, y: 8, w: 12, h: 8 },
},
{
id: 3,
type: 'row',
title: 'Row test title',
gridPos: { x: 0, y: 16, w: 12, h: 1 },
repeat: 'var1',
repeatDirection: 'v',
panels: [],
collapsed: false,
},
{
id: 4,
type: 'timeseries',
title: 'Panel in row',
gridPos: { x: 0, y: 17, w: 16, h: 8 },
targets: [
{
refId: 'A',
datasource: 'datasource1',
expr: 'test-query',
hide: false,
},
],
datasource: {
type: 'prometheus',
uid: 'datasource1',
},
fieldConfig: { defaults: {}, overrides: [] },
options: {},
transparent: false,
links: [],
transformations: [],
},
{
id: 5,
type: 'row',
title: 'Collapsed row title',
gridPos: { x: 0, y: 25, w: 12, h: 1 },
panels: [
{
id: 6,
type: 'timeseries',
title: 'Panel in collapsed row',
gridPos: { x: 0, y: 26, w: 16, h: 8 },
targets: [
{
refId: 'A',
datasource: 'datasource1',
expr: 'test-query',
hide: false,
},
],
datasource: {
type: 'prometheus',
uid: 'datasource1',
},
fieldConfig: { defaults: {}, overrides: [] },
options: {},
transparent: false,
links: [],
transformations: [],
},
],
collapsed: true,
},
{
id: 7,
type: 'row',
title: 'collapsed row with no panel property',
gridPos: { x: 0, y: 26, w: 12, h: 1 },
collapsed: true,
},
{
id: 8,
type: 'row',
title: 'empty row',
gridPos: { x: 0, y: 27, w: 12, h: 1 },
collapsed: false,
},
],
};
const dto: DashboardWithAccessInfo<DashboardDataDTO> = {
spec: dashboardV1,
access: {
slug: 'dashboard-slug',
url: '/d/dashboard-slug',
canAdmin: true,
canDelete: true,
canEdit: true,
canSave: true,
canShare: true,
canStar: true,
annotationsPermissions: {
dashboard: { canAdd: true, canEdit: true, canDelete: true },
organization: { canAdd: true, canEdit: true, canDelete: true },
},
},
apiVersion: 'v1',
kind: 'DashboardWithAccessInfo',
metadata: {
name: 'dashboard-uid',
resourceVersion: '1',
creationTimestamp: '2023-01-01T00:00:00Z',
annotations: {
[AnnoKeyCreatedBy]: 'user1',
[AnnoKeyUpdatedBy]: 'user2',
[AnnoKeyUpdatedTimestamp]: '2023-01-02T00:00:00Z',
[AnnoKeyFolder]: 'folder1',
[AnnoKeySlug]: 'dashboard-slug',
},
labels: {
[DeprecatedInternalId]: '123',
},
},
};
const transformed = ResponseTransformers.ensureV2Response(dto);
const spec = transformed.spec;
// Panel
expect(spec.layout.kind).toBe('RowsLayout');
const layout = spec.layout as RowsLayoutKind;
expect(layout.spec.rows).toHaveLength(5);
const row0grid = layout.spec.rows[0].spec.layout as GridLayoutKind;
expect(row0grid.kind).toBe('GridLayout');
expect(row0grid.spec.items).toHaveLength(2);
expect(row0grid.spec.items[0].spec.element.name).toBe('panel-1');
expect(row0grid.spec.items[0].spec.y).toBe(0);
expect(row0grid.spec.items[1].spec.element.name).toBe('panel-2');
expect(row0grid.spec.items[1].spec.y).toBe(8);
const row1 = layout.spec.rows[1] as RowsLayoutRowKind;
expect(row1.kind).toBe('RowsLayoutRow');
expect(row1.spec.repeat?.value).toBe('var1');
expect(row1.spec.repeat?.mode).toBe('variable');
const row1grid = layout.spec.rows[1].spec.layout as GridLayoutKind;
expect(row1grid.kind).toBe('GridLayout');
expect(row1grid.spec.items).toHaveLength(1);
expect(row1grid.spec.items[0].spec.element.name).toBe('panel-4');
const row2grid = layout.spec.rows[2].spec.layout as GridLayoutKind;
expect(row2grid.kind).toBe('GridLayout');
expect(row2grid.spec.items).toHaveLength(1);
expect(row2grid.spec.items[0].spec.element.name).toBe('panel-6');
const row3 = layout.spec.rows[3] as RowsLayoutRowKind;
expect(row3.kind).toBe('RowsLayoutRow');
expect(row3.spec.collapse).toBe(true);
expect(row3.spec.layout.kind).toBe('GridLayout');
const row3grid = row3.spec.layout as GridLayoutKind;
expect(row3grid.kind).toBe('GridLayout');
expect(row3grid.spec.items).toHaveLength(0);
const row4 = layout.spec.rows[4] as RowsLayoutRowKind;
expect(row4.kind).toBe('RowsLayoutRow');
expect(row4.spec.collapse).toBe(false);
expect(row4.spec.layout.kind).toBe('GridLayout');
const row4grid = row4.spec.layout as GridLayoutKind;
expect(row4grid.kind).toBe('GridLayout');
expect(row4grid.spec.items).toHaveLength(0);
});
});
describe('v2 -> v1 transformation', () => {
it('should return the same object if it is already a DashboardDTO', () => {
const dashboard: DashboardDTO = {
dashboard: {
schemaVersion: 1,
title: 'Dashboard Title',
uid: 'dashboard1',
version: 1,
},
meta: {},
};
expect(ResponseTransformers.ensureV1Response(dashboard)).toBe(dashboard);
});
it('should transform DashboardWithAccessInfo<DashboardV2Spec> to DashboardDTO', () => {
const dashboardV2: DashboardWithAccessInfo<DashboardV2Spec> = {
apiVersion: 'v2alpha1',
kind: 'DashboardWithAccessInfo',
metadata: {
creationTimestamp: '2023-01-01T00:00:00Z',
name: 'dashboard1',
resourceVersion: '1',
annotations: {
'grafana.app/createdBy': 'user1',
'grafana.app/updatedBy': 'user2',
'grafana.app/updatedTimestamp': '2023-01-02T00:00:00Z',
'grafana.app/folder': 'folder1',
'grafana.app/slug': 'dashboard-slug',
'grafana.app/dashboard-gnet-id': 'something-like-a-uid',
},
},
spec: {
title: 'Dashboard Title',
description: 'Dashboard Description',
tags: ['tag1', 'tag2'],
cursorSync: 'Off',
preload: true,
liveNow: false,
editable: true,
revision: 225,
timeSettings: {
from: 'now-6h',
to: 'now',
timezone: 'browser',
autoRefresh: '5m',
autoRefreshIntervals: ['5s', '10s', '30s'],
hideTimepicker: false,
quickRanges: [
{
display: 'Last 6 hours',
from: 'now-6h',
to: 'now',
},
{
display: 'Last 7 days',
from: 'now-7d',
to: 'now',
},
],
nowDelay: '1m',
fiscalYearStartMonth: 1,
weekStart: 'monday',
},
links: [
{
title: 'Link 1',
url: 'https://grafana.com',
asDropdown: false,
targetBlank: true,
includeVars: true,
keepTime: true,
tags: ['tag1', 'tag2'],
icon: 'external link',
type: 'link',
tooltip: 'Link 1 Tooltip',
},
],
annotations: handyTestingSchema.annotations,
variables: handyTestingSchema.variables,
elements: handyTestingSchema.elements,
layout: handyTestingSchema.layout,
},
access: {
url: '/d/dashboard-slug',
canAdmin: true,
canDelete: true,
canEdit: true,
canSave: true,
canShare: true,
canStar: true,
slug: 'dashboard-slug',
annotationsPermissions: {
dashboard: { canAdd: true, canEdit: true, canDelete: true },
organization: { canAdd: true, canEdit: true, canDelete: true },
},
},
};
const transformed = ResponseTransformers.ensureV1Response(dashboardV2);
expect(transformed.meta.created).toBe(dashboardV2.metadata.creationTimestamp);
expect(transformed.meta.createdBy).toBe(dashboardV2.metadata.annotations?.['grafana.app/createdBy']);
expect(transformed.meta.updated).toBe(dashboardV2.metadata.annotations?.['grafana.app/updatedTimestamp']);
expect(transformed.meta.updatedBy).toBe(dashboardV2.metadata.annotations?.['grafana.app/updatedBy']);
expect(transformed.meta.folderUid).toBe(dashboardV2.metadata.annotations?.['grafana.app/folder']);
expect(transformed.meta.slug).toBe(dashboardV2.metadata.annotations?.['grafana.app/slug']);
expect(transformed.meta.url).toBe(dashboardV2.access.url);
expect(transformed.meta.canAdmin).toBe(dashboardV2.access.canAdmin);
expect(transformed.meta.canDelete).toBe(dashboardV2.access.canDelete);
expect(transformed.meta.canEdit).toBe(dashboardV2.access.canEdit);
expect(transformed.meta.canSave).toBe(dashboardV2.access.canSave);
expect(transformed.meta.canShare).toBe(dashboardV2.access.canShare);
expect(transformed.meta.canStar).toBe(dashboardV2.access.canStar);
expect(transformed.meta.annotationsPermissions).toEqual(dashboardV2.access.annotationsPermissions);
const dashboard = transformed.dashboard;
expect(dashboard.uid).toBe(dashboardV2.metadata.name);
expect(dashboard.title).toBe(dashboardV2.spec.title);
expect(dashboard.description).toBe(dashboardV2.spec.description);
expect(dashboard.tags).toEqual(dashboardV2.spec.tags);
expect(dashboard.schemaVersion).toBe(40);
// expect(dashboard.graphTooltip).toBe(0); // Assuming transformCursorSynctoEnum('Off') returns 0
expect(dashboard.preload).toBe(dashboardV2.spec.preload);
expect(dashboard.liveNow).toBe(dashboardV2.spec.liveNow);
expect(dashboard.editable).toBe(dashboardV2.spec.editable);
expect(dashboard.revision).toBe(225);
expect(dashboard.gnetId).toBe(dashboardV2.metadata.annotations?.['grafana.app/dashboard-gnet-id']);
expect(dashboard.time?.from).toBe(dashboardV2.spec.timeSettings.from);
expect(dashboard.time?.to).toBe(dashboardV2.spec.timeSettings.to);
expect(dashboard.timezone).toBe(dashboardV2.spec.timeSettings.timezone);
expect(dashboard.refresh).toBe(dashboardV2.spec.timeSettings.autoRefresh);
expect(dashboard.timepicker?.refresh_intervals).toEqual(dashboardV2.spec.timeSettings.autoRefreshIntervals);
expect(dashboard.timepicker?.hidden).toBe(dashboardV2.spec.timeSettings.hideTimepicker);
expect(dashboard.timepicker?.nowDelay).toBe(dashboardV2.spec.timeSettings.nowDelay);
expect(dashboard.fiscalYearStartMonth).toBe(dashboardV2.spec.timeSettings.fiscalYearStartMonth);
expect(dashboard.weekStart).toBe(dashboardV2.spec.timeSettings.weekStart);
expect(dashboard.links).toEqual(dashboardV2.spec.links);
// variables
validateVariablesV1ToV2(dashboardV2.spec.variables[0], dashboard.templating?.list?.[0]);
validateVariablesV1ToV2(dashboardV2.spec.variables[1], dashboard.templating?.list?.[1]);
validateVariablesV1ToV2(dashboardV2.spec.variables[2], dashboard.templating?.list?.[2]);
validateVariablesV1ToV2(dashboardV2.spec.variables[3], dashboard.templating?.list?.[3]);
validateVariablesV1ToV2(dashboardV2.spec.variables[4], dashboard.templating?.list?.[4]);
validateVariablesV1ToV2(dashboardV2.spec.variables[5], dashboard.templating?.list?.[5]);
validateVariablesV1ToV2(dashboardV2.spec.variables[6], dashboard.templating?.list?.[6]);
validateVariablesV1ToV2(dashboardV2.spec.variables[7], dashboard.templating?.list?.[7]);
// annotations
validateAnnotation(dashboard.annotations!.list![0], dashboardV2.spec.annotations[0]);
validateAnnotation(dashboard.annotations!.list![1], dashboardV2.spec.annotations[1]);
validateAnnotation(dashboard.annotations!.list![2], dashboardV2.spec.annotations[2]);
validateAnnotation(dashboard.annotations!.list![3], dashboardV2.spec.annotations[3]);
// panel
const panelKey = 'panel-1';
expect(dashboardV2.spec.elements[panelKey].kind).toBe('Panel');
const panelV2 = dashboardV2.spec.elements[panelKey] as PanelKind;
expect(panelV2.kind).toBe('Panel');
expect(dashboardV2.spec.layout.kind).toBe('GridLayout');
validatePanel(dashboard.panels![0], panelV2, dashboardV2.spec.layout as GridLayoutKind, panelKey);
// library panel
expect(dashboard.panels![1].libraryPanel).toEqual({
uid: 'uid-for-library-panel',
name: 'Library Panel',
});
});
describe('getPanelQueries', () => {
it('respects targets data source', () => {
const panelDs = {
type: 'theoretical-ds',
uid: 'theoretical-uid',
};
const targets: DataQuery[] = [
{
refId: 'A',
datasource: {
type: 'theoretical-ds',
uid: 'theoretical-uid',
},
},
{
refId: 'B',
datasource: {
type: 'theoretical-ds',
uid: 'theoretical-uid',
},
},
];
const result = getPanelQueries(targets, panelDs);
expect(result).toHaveLength(targets.length);
expect(result[0].spec.refId).toBe('A');
expect(result[1].spec.refId).toBe('B');
result.forEach((query) => {
expect(query.kind).toBe('PanelQuery');
expect(query.spec.datasource).toEqual({
type: 'theoretical-ds',
uid: 'theoretical-uid',
});
expect(query.spec.query.kind).toBe('theoretical-ds');
});
});
it('respects panel data source', () => {
const panelDs = {
type: 'theoretical-ds',
uid: 'theoretical-uid',
};
const targets: DataQuery[] = [
{
refId: 'A',
},
{
refId: 'B',
},
];
const result = getPanelQueries(targets, panelDs);
expect(result).toHaveLength(targets.length);
expect(result[0].spec.refId).toBe('A');
expect(result[1].spec.refId).toBe('B');
result.forEach((query) => {
expect(query.kind).toBe('PanelQuery');
expect(query.spec.datasource).toEqual({
type: 'theoretical-ds',
uid: 'theoretical-uid',
});
expect(query.spec.query.kind).toBe('theoretical-ds');
});
});
});
});
function validateAnnotation(v1: AnnotationQuery, v2: DashboardV2Spec['annotations'][0]) {
const { spec: v2Spec } = v2;
expect(v1.name).toBe(v2Spec.name);
expect(v1.datasource).toBe(v2Spec.datasource);
expect(v1.enable).toBe(v2Spec.enable);
expect(v1.hide).toBe(v2Spec.hide);
expect(v1.iconColor).toBe(v2Spec.iconColor);
expect(v1.builtIn).toBe(v2Spec.builtIn ? 1 : 0);
expect(v1.target).toBe(v2Spec.query?.spec);
expect(v1.filter).toEqual(v2Spec.filter);
}
function validatePanel(v1: Panel, v2: PanelKind, layoutV2: GridLayoutKind, panelKey: string) {
const { spec: v2Spec } = v2;
expect(v1.id).toBe(v2Spec.id);
expect(v1.id).toBe(v2Spec.id);
expect(v1.type).toBe(v2Spec.vizConfig.kind);
expect(v1.title).toBe(v2Spec.title);
expect(v1.description).toBe(v2Spec.description);
expect(v1.fieldConfig).toEqual(transformMappingsToV1(v2Spec.vizConfig.spec.fieldConfig));
expect(v1.options).toBe(v2Spec.vizConfig.spec.options);
expect(v1.pluginVersion).toBe(v2Spec.vizConfig.spec.pluginVersion);
expect(v1.links).toEqual(v2Spec.links);
expect(v1.targets).toEqual(
v2Spec.data.spec.queries.map((q) => {
return {
refId: q.spec.refId,
hide: q.spec.hidden,
datasource: q.spec.datasource,
...q.spec.query.spec,
};
})
);
expect(v1.transformations).toEqual(v2Spec.data.spec.transformations.map((t) => t.spec));
const layoutElement = layoutV2.spec.items.find(
(item) => item.kind === 'GridLayoutItem' && item.spec.element.name === panelKey
) as GridLayoutItemKind;
expect(v1.gridPos?.x).toEqual(layoutElement?.spec.x);
expect(v1.gridPos?.y).toEqual(layoutElement?.spec.y);
expect(v1.gridPos?.w).toEqual(layoutElement?.spec.width);
expect(v1.gridPos?.h).toEqual(layoutElement?.spec.height);
expect(v1.repeat).toEqual(layoutElement?.spec.repeat?.value);
expect(v1.repeatDirection).toEqual(layoutElement?.spec.repeat?.direction);
expect(v1.maxPerRow).toEqual(layoutElement?.spec.repeat?.maxPerRow);
expect(v1.cacheTimeout).toBe(v2Spec.data.spec.queryOptions.cacheTimeout);
expect(v1.maxDataPoints).toBe(v2Spec.data.spec.queryOptions.maxDataPoints);
expect(v1.interval).toBe(v2Spec.data.spec.queryOptions.interval);
expect(v1.hideTimeOverride).toBe(v2Spec.data.spec.queryOptions.hideTimeOverride);
expect(v1.queryCachingTTL).toBe(v2Spec.data.spec.queryOptions.queryCachingTTL);
expect(v1.timeFrom).toBe(v2Spec.data.spec.queryOptions.timeFrom);
expect(v1.timeShift).toBe(v2Spec.data.spec.queryOptions.timeShift);
expect(v1.transparent).toBe(v2Spec.transparent);
}
function validateVariablesV1ToV2(v2: VariableKind, v1: VariableModel | undefined) {
if (!v1) {
return expect(v1).toBeDefined();
}
const v1Common = {
name: v1.name,
label: v1.label,
description: v1.description,
hide: transformVariableHideToEnum(v1.hide),
skipUrlSync: Boolean(v1.skipUrlSync),
};
const v2Common = {
name: v2.spec.name,
label: v2.spec.label,
description: v2.spec.description,
hide: v2.spec.hide,
skipUrlSync: v2.spec.skipUrlSync,
};
expect(v2Common).toEqual(v1Common);
if (v2.kind === 'QueryVariable') {
expect(v2.spec.datasource).toEqual(v1.datasource);
if (typeof v1.query === 'string') {
expect(v2.spec.query.spec[LEGACY_STRING_VALUE_KEY]).toEqual(v1.query);
} else {
expect(v2.spec.query).toEqual({
kind: v1.datasource?.type,
spec: {
...(typeof v1.query === 'object' ? v1.query : {}),
},
});
}
}
if (v2.kind === 'DatasourceVariable') {
expect(v2.spec.pluginId).toBe(v1.query);
expect(v2.spec.refresh).toBe(transformVariableRefreshToEnum(v1.refresh));
}
if (v2.kind === 'CustomVariable') {
expect(v2.spec.query).toBe(v1.query);
expect(v2.spec.options).toEqual(v1.options);
}
if (v2.kind === 'AdhocVariable') {
expect(v2.spec.datasource).toEqual(v1.datasource);
// @ts-expect-error
expect(v2.spec.filters).toEqual(v1.filters);
// @ts-expect-error
expect(v2.spec.baseFilters).toEqual(v1.baseFilters);
}
if (v2.kind === 'ConstantVariable') {
expect(v2.spec.query).toBe(v1.query);
}
if (v2.kind === 'IntervalVariable') {
expect(v2.spec.query).toBe(v1.query);
expect(v2.spec.options).toEqual(v1.options);
expect(v2.spec.current).toEqual(v1.current);
// @ts-expect-error
expect(v2.spec.auto).toBe(v1.auto);
// @ts-expect-error
expect(v2.spec.auto_min).toBe(v1.auto_min);
// @ts-expect-error
expect(v2.spec.auto_count).toBe(v1.auto_count);
}
if (v2.kind === 'TextVariable') {
expect(v2.spec.query).toBe(v1.query);
expect(v2.spec.current).toEqual(v1.current);
}
if (v2.kind === 'GroupByVariable') {
expect(v2.spec.datasource).toEqual(v1.datasource);
expect(v2.spec.options).toEqual(v1.options);
}
}
});