diff --git a/.betterer.results b/.betterer.results index fc5e3c214fe..9dc56a3748e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3775,7 +3775,8 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "2"], [0, 0, 0, "Do not use any type assertions.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"] + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"] ], "public/app/features/dashboard/api/v0.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts index eb35c143333..bb9c410c7ec 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -175,8 +175,6 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio } else if (sceneUtils.isAdHocVariable(variable)) { variables.push({ ...commonProperties, - name: variable.state.name, - type: 'adhoc', datasource: variable.state.datasource, allowCustomValue: variable.state.allowCustomValue, // @ts-expect-error diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index a0ec37d375c..8100e0aa5b4 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -1,5 +1,6 @@ -import { DataQuery, VariableModel, VariableRefresh } from '@grafana/schema'; -import { DashboardV2Spec, VariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; +import { AnnotationQuery, DataQuery, VariableModel, VariableRefresh, Panel } from '@grafana/schema'; +import { DashboardV2Spec, PanelKind, VariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; +import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/examples'; import { AnnoKeyCreatedBy, AnnoKeyDashboardGnetId, @@ -16,7 +17,12 @@ import { } from 'app/features/dashboard-scene/serialization/transformToV2TypesUtils'; import { DashboardDataDTO, DashboardDTO } from 'app/types'; -import { getDefaultDatasource, getPanelQueries, ResponseTransformers } from './ResponseTransformers'; +import { + getDefaultDatasource, + getPanelQueries, + ResponseTransformers, + transformMappingsToV1, +} from './ResponseTransformers'; import { DashboardWithAccessInfo } from './types'; jest.mock('@grafana/runtime', () => ({ @@ -546,15 +552,10 @@ describe('ResponseTransformers', () => { tooltip: 'Link 1 Tooltip', }, ], - annotations: [], - variables: [], - elements: {}, - layout: { - kind: 'GridLayout', - spec: { - items: [], - }, - }, + annotations: handyTestingSchema.annotations, + variables: handyTestingSchema.variables, + elements: handyTestingSchema.elements, + layout: handyTestingSchema.layout, }, access: { url: '/d/dashboard-slug', @@ -612,153 +613,237 @@ describe('ResponseTransformers', () => { expect(dashboard.fiscalYearStartMonth).toBe(dashboardV2.spec.timeSettings.fiscalYearStartMonth); expect(dashboard.weekStart).toBe(dashboardV2.spec.timeSettings.weekStart); expect(dashboard.links).toEqual(dashboardV2.spec.links); - expect(dashboard.annotations).toEqual({ list: [] }); + // 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'; + const panelV2 = dashboardV2.spec.elements[panelKey] as PanelKind; + expect(panelV2.kind).toBe('Panel'); + validatePanel(dashboard.panels![0], panelV2, dashboardV2.spec.layout, panelKey); + // library panel + expect(dashboard.panels![1].libraryPanel).toEqual(dashboardV2.spec.elements['library-panel-1'].spec); }); - }); - 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', + 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', + { + refId: 'B', + datasource: { + type: 'theoretical-ds', + uid: 'theoretical-uid', + }, }, - }, - ]; + ]; - const result = getPanelQueries(targets, panelDs); + const result = getPanelQueries(targets, panelDs); - expect(result).toHaveLength(targets.length); - expect(result[0].spec.refId).toBe('A'); - expect(result[1].spec.refId).toBe('B'); + 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', + 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'); }); - 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', - }, - ]; + 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); + const result = getPanelQueries(targets, panelDs); - expect(result).toHaveLength(targets.length); - expect(result[0].spec.refId).toBe('A'); - expect(result[1].spec.refId).toBe('B'); + 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', + 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'); }); - expect(query.spec.query.kind).toBe('theoretical-ds'); }); }); }); -}); - -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: 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); - 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); - expect(v2.spec.filters).toEqual([]); - // @ts-expect-error - expect(v2.spec.baseFilters).toEqual(v1.baseFilters); + 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); } - if (v2.kind === 'ConstantVariable') { - expect(v2.spec.query).toBe(v1.query); + function validatePanel(v1: Panel, v2: PanelKind, layoutV2: DashboardV2Spec['layout'], 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 + ); + 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); } - 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); + 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: 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).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); + } } -} +}); diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 50fd9939fe9..071004812a5 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -1,6 +1,18 @@ import { TypedVariableModel } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { AnnotationQuery, DataQuery, DataSourceRef, Panel } from '@grafana/schema'; +import { + AnnotationQuery, + DataQuery, + DataSourceRef, + Panel, + VariableModel, + VariableType, + FieldConfigSource as FieldConfigSourceV1, + FieldColorModeId as FieldColorModeIdV1, + ThresholdsMode as ThresholdsModeV1, + MappingType as MappingTypeV1, + SpecialValueMatch as SpecialValueMatchV1, +} from '@grafana/schema'; import { AnnotationQueryKind, DashboardV2Spec, @@ -12,6 +24,10 @@ import { PanelQueryKind, QueryVariableKind, TransformationKind, + FieldColorModeId, + FieldConfigSource, + ThresholdsMode, + SpecialValueMatch, AdhocVariableKind, CustomVariableKind, ConstantVariableKind, @@ -19,7 +35,7 @@ import { TextVariableKind, GroupByVariableKind, } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; -import { DataTransformerConfig } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen'; +import { DashboardLink, DataTransformerConfig } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen'; import { AnnoKeyCreatedBy, AnnoKeyDashboardGnetId, @@ -31,8 +47,14 @@ import { AnnoKeyUpdatedBy, AnnoKeyUpdatedTimestamp, } from 'app/features/apiserver/types'; +import { TypedVariableModelV2 } from 'app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene'; import { getDefaultDataSourceRef } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2'; -import { transformCursorSyncV2ToV1 } from 'app/features/dashboard-scene/serialization/transformToV1TypesUtils'; +import { + transformCursorSyncV2ToV1, + transformSortVariableToEnumV1, + transformVariableHideToEnumV1, + transformVariableRefreshToEnumV1, +} from 'app/features/dashboard-scene/serialization/transformToV1TypesUtils'; import { transformCursorSynctoEnum, transformDataTopic, @@ -179,6 +201,9 @@ export function ensureV1Response( }; } else { // if dashboard is on v2 schema convert to v1 schema + const annotations = getAnnotationsV1(spec.annotations); + const variables = getVariablesV1(spec.variables); + const panels = getPanelsV1(spec.elements, spec.layout); return { meta: { created: dashboard.metadata.creationTimestamp, @@ -224,9 +249,9 @@ export function ensureV1Response( weekStart: spec.timeSettings.weekStart, version: parseInt(dashboard.metadata.resourceVersion, 10), links: spec.links, - annotations: { list: [] }, // TODO - panels: [], // TODO - templating: { list: [] }, // TODO + annotations: { list: annotations }, + panels, + templating: { list: variables }, }, }; } @@ -593,3 +618,372 @@ function getAnnotations(annotations: AnnotationQuery[]): DashboardV2Spec['annota return aq; }); } + +function getVariablesV1(vars: DashboardV2Spec['variables']): VariableModel[] { + const variables: VariableModel[] = []; + + for (const v of vars) { + const commonProperties = { + name: v.spec.name, + label: v.spec.label, + ...(v.spec.description && { description: v.spec.description }), + skipUrlSync: v.spec.skipUrlSync, + hide: transformVariableHideToEnumV1(v.spec.hide), + type: transformToV1VariableTypes(v), + }; + + switch (v.kind) { + case 'QueryVariable': + const qv: VariableModel = { + ...commonProperties, + current: v.spec.current, + options: v.spec.options, + query: typeof v.spec.query === 'string' ? v.spec.query : v.spec.query.spec, + datasource: v.spec.datasource, + sort: transformSortVariableToEnumV1(v.spec.sort), + refresh: transformVariableRefreshToEnumV1(v.spec.refresh), + regex: v.spec.regex, + allValue: v.spec.allValue, + includeAll: v.spec.includeAll, + multi: v.spec.multi, + // @ts-expect-error - definition is not part of v1 VariableModel + definition: v.spec.definition, + }; + variables.push(qv); + break; + case 'DatasourceVariable': + const dv: VariableModel = { + ...commonProperties, + current: v.spec.current, + options: [], + regex: v.spec.regex, + refresh: transformVariableRefreshToEnumV1(v.spec.refresh), + query: v.spec.pluginId, + multi: v.spec.multi, + allValue: v.spec.allValue, + includeAll: v.spec.includeAll, + }; + variables.push(dv); + break; + case 'CustomVariable': + const cv: VariableModel = { + ...commonProperties, + current: { + text: v.spec.current.value, + value: v.spec.current.value, + }, + options: v.spec.options, + query: v.spec.query, + multi: v.spec.multi, + allValue: v.spec.allValue, + includeAll: v.spec.includeAll, + }; + variables.push(cv); + break; + case 'ConstantVariable': + const constant: VariableModel = { + ...commonProperties, + current: { + text: v.spec.current.value, + value: v.spec.current.value, + }, + hide: transformVariableHideToEnumV1(v.spec.hide), + // @ts-expect-error + query: v.spec.current.value, + }; + variables.push(constant); + break; + case 'IntervalVariable': + const iv: VariableModel = { + ...commonProperties, + current: { + text: v.spec.current.value, + value: v.spec.current.value, + }, + hide: transformVariableHideToEnumV1(v.spec.hide), + query: v.spec.query, + refresh: transformVariableRefreshToEnumV1(v.spec.refresh), + options: v.spec.options, + // @ts-expect-error + auto: v.spec.auto, + auto_min: v.spec.auto_min, + auto_count: v.spec.auto_count, + }; + variables.push(iv); + break; + case 'TextVariable': + const current = { + text: v.spec.current.value, + value: v.spec.current.value, + }; + + const tv: VariableModel = { + ...commonProperties, + current: { + text: v.spec.current.value, + value: v.spec.current.value, + }, + options: [{ ...current, selected: true }], + query: v.spec.query, + }; + variables.push(tv); + break; + case 'GroupByVariable': + const gv: VariableModel = { + ...commonProperties, + datasource: v.spec.datasource, + current: v.spec.current, + options: v.spec.options, + }; + variables.push(gv); + break; + case 'AdhocVariable': + const av: VariableModel = { + ...commonProperties, + datasource: v.spec.datasource, + // @ts-expect-error + baseFilters: v.spec.baseFilters, + filters: v.spec.filters, + defaultKeys: v.spec.defaultKeys, + }; + variables.push(av); + break; + default: + // do not throw error, just log it + console.error(`Variable transformation not implemented: ${v}`); + } + } + return variables; +} + +function getAnnotationsV1(annotations: DashboardV2Spec['annotations']): AnnotationQuery[] { + // @ts-expect-error - target v2 query is not compatible with v1 target + return annotations.map((a) => { + return { + name: a.spec.name, + datasource: a.spec.datasource, + enable: a.spec.enable, + hide: a.spec.hide, + iconColor: a.spec.iconColor, + builtIn: a.spec.builtIn ? 1 : 0, + target: a.spec.query?.spec, + filter: a.spec.filter, + }; + }); +} + +interface LibraryPanelDTO extends Pick {} + +function getPanelsV1( + panels: DashboardV2Spec['elements'], + layout: DashboardV2Spec['layout'] +): Array { + return Object.entries(panels).map(([key, p]) => { + const layoutElement = layout.spec.items.find( + (item) => item.kind === 'GridLayoutItem' && item.spec.element.name === key + ); + const { x, y, width, height, repeat } = layoutElement?.spec || { x: 0, y: 0, width: 0, height: 0 }; + const gridPos = { x, y, w: width, h: height }; + if (p.kind === 'Panel') { + const panel = p.spec; + return { + id: panel.id, + type: panel.vizConfig.kind, + title: panel.title, + description: panel.description, + fieldConfig: transformMappingsToV1(panel.vizConfig.spec.fieldConfig), + options: panel.vizConfig.spec.options, + pluginVersion: panel.vizConfig.spec.pluginVersion, + links: + // @ts-expect-error - Panel link is wrongly typed as DashboardLink + panel.links?.map((l) => ({ + title: l.title, + url: l.url, + ...(l.targetBlank && { targetBlank: l.targetBlank }), + })) || [], + targets: panel.data.spec.queries.map((q) => { + return { + refId: q.spec.refId, + hide: q.spec.hidden, + datasource: q.spec.datasource, + ...q.spec.query.spec, + }; + }), + transformations: panel.data.spec.transformations.map((t) => t.spec), + gridPos, + cacheTimeout: panel.data.spec.queryOptions.cacheTimeout, + maxDataPoints: panel.data.spec.queryOptions.maxDataPoints, + interval: panel.data.spec.queryOptions.interval, + hideTimeOverride: panel.data.spec.queryOptions.hideTimeOverride, + queryCachingTTL: panel.data.spec.queryOptions.queryCachingTTL, + timeFrom: panel.data.spec.queryOptions.timeFrom, + timeShift: panel.data.spec.queryOptions.timeShift, + transparent: panel.transparent, + ...(repeat?.value && { repeat: repeat.value }), + ...(repeat?.direction && { repeatDirection: repeat.direction }), + ...(repeat?.maxPerRow && { maxPerRow: repeat.maxPerRow }), + }; + } else if (p.kind === 'LibraryPanel') { + const panel = p.spec; + return { + id: 0, //TODO: LibraryPanelSpec.id will be available after https://github.com/grafana/grafana/pull/99281/ is merged + title: panel.name, + gridPos, + libraryPanel: { + uid: panel.uid, + name: panel.name, + }, + }; + } else { + throw new Error(`Unknown element kind: ${p}`); + } + }); +} + +export function transformMappingsToV1(fieldConfig: FieldConfigSource): FieldConfigSourceV1 { + const getThresholdsMode = (mode: ThresholdsMode): ThresholdsModeV1 => { + switch (mode) { + case 'absolute': + return ThresholdsModeV1.Absolute; + case 'percentage': + return ThresholdsModeV1.Percentage; + default: + return ThresholdsModeV1.Absolute; + } + }; + + const transformedDefaults: any = { + ...fieldConfig.defaults, + }; + + if (fieldConfig.defaults.mappings) { + transformedDefaults.mappings = fieldConfig.defaults.mappings.map((mapping) => { + switch (mapping.type) { + case 'value': + return { + ...mapping, + type: MappingTypeV1.ValueToText, + }; + case 'range': + return { + ...mapping, + type: MappingTypeV1.RangeToText, + }; + case 'regex': + return { + ...mapping, + type: MappingTypeV1.RegexToText, + }; + case 'special': + return { + ...mapping, + options: { + ...mapping.options, + match: transformSpecialValueMatchToV1(mapping.options.match), + }, + type: MappingTypeV1.SpecialValue, + }; + default: + return mapping; + } + }); + } + + if (fieldConfig.defaults.thresholds) { + transformedDefaults.thresholds = { + ...fieldConfig.defaults.thresholds, + mode: getThresholdsMode(fieldConfig.defaults.thresholds.mode), + }; + } + + if (fieldConfig.defaults.color?.mode) { + transformedDefaults.color = { + ...fieldConfig.defaults.color, + mode: colorIdToEnumv1(fieldConfig.defaults.color.mode), + }; + } + + return { + ...fieldConfig, + defaults: transformedDefaults, + }; +} + +function colorIdToEnumv1(colorId: FieldColorModeId): FieldColorModeIdV1 { + switch (colorId) { + case 'thresholds': + return FieldColorModeIdV1.Thresholds; + case 'palette-classic': + return FieldColorModeIdV1.PaletteClassic; + case 'palette-classic-by-name': + return FieldColorModeIdV1.PaletteClassicByName; + case 'continuous-GrYlRd': + return FieldColorModeIdV1.ContinuousGrYlRd; + case 'continuous-RdYlGr': + return FieldColorModeIdV1.ContinuousRdYlGr; + case 'continuous-BlYlRd': + return FieldColorModeIdV1.ContinuousBlYlRd; + case 'continuous-YlRd': + return FieldColorModeIdV1.ContinuousYlRd; + case 'continuous-BlPu': + return FieldColorModeIdV1.ContinuousBlPu; + case 'continuous-YlBl': + return FieldColorModeIdV1.ContinuousYlBl; + case 'continuous-blues': + return FieldColorModeIdV1.ContinuousBlues; + case 'continuous-reds': + return FieldColorModeIdV1.ContinuousReds; + case 'continuous-greens': + return FieldColorModeIdV1.ContinuousGreens; + case 'continuous-purples': + return FieldColorModeIdV1.ContinuousPurples; + case 'fixed': + return FieldColorModeIdV1.Fixed; + case 'shades': + return FieldColorModeIdV1.Shades; + default: + return FieldColorModeIdV1.Thresholds; + } +} + +function transformSpecialValueMatchToV1(match: SpecialValueMatch): SpecialValueMatchV1 { + switch (match) { + case 'true': + return SpecialValueMatchV1.True; + case 'false': + return SpecialValueMatchV1.False; + case 'null': + return SpecialValueMatchV1.Null; + case 'nan': + return SpecialValueMatchV1.NaN; + case 'null+nan': + return SpecialValueMatchV1.NullAndNan; + case 'empty': + return SpecialValueMatchV1.Empty; + default: + throw new Error(`Unknown match type: ${match}`); + } +} + +function transformToV1VariableTypes(variable: TypedVariableModelV2): VariableType { + switch (variable.kind) { + case 'QueryVariable': + return 'query'; + case 'DatasourceVariable': + return 'datasource'; + case 'CustomVariable': + return 'custom'; + case 'ConstantVariable': + return 'constant'; + case 'IntervalVariable': + return 'interval'; + case 'TextVariable': + return 'textbox'; + case 'GroupByVariable': + return 'groupby'; + case 'AdhocVariable': + return 'adhoc'; + default: + throw new Error(`Unknown variable type: ${variable}`); + } +}