import { TypedVariableModel } from '@grafana/data'; import { config } from '@grafana/runtime'; import { AnnotationQuery, DataQuery, DataSourceRef, Panel, RowPanel, VariableModel, VariableType, FieldConfigSource as FieldConfigSourceV1, FieldColorModeId as FieldColorModeIdV1, ThresholdsMode as ThresholdsModeV1, MappingType as MappingTypeV1, SpecialValueMatch as SpecialValueMatchV1, } from '@grafana/schema'; import { AnnotationQueryKind, Spec as DashboardV2Spec, DataLink, DatasourceVariableKind, defaultSpec as defaultDashboardV2Spec, defaultFieldConfigSource, defaultTimeSettingsSpec, PanelQueryKind, QueryVariableKind, TransformationKind, FieldColorModeId, FieldConfigSource, ThresholdsMode, SpecialValueMatch, AdhocVariableKind, CustomVariableKind, ConstantVariableKind, IntervalVariableKind, TextVariableKind, GroupByVariableKind, LibraryPanelKind, PanelKind, GridLayoutRowKind, GridLayoutItemKind, } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { DashboardLink, DataTransformerConfig } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen'; import { isWeekStart, WeekStart } from '@grafana/ui'; import { AnnoKeyCreatedBy, AnnoKeyDashboardGnetId, AnnoKeyDashboardIsSnapshot, AnnoKeyDashboardSnapshotOriginalUrl, AnnoKeyFolder, AnnoKeySlug, AnnoKeyUpdatedBy, AnnoKeyUpdatedTimestamp, DeprecatedInternalId, ObjectMeta, } from 'app/features/apiserver/types'; import { GRID_ROW_HEIGHT } from 'app/features/dashboard-scene/serialization/const'; import { TypedVariableModelV2 } from 'app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene'; import { getDefaultDataSourceRef } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2'; import { transformCursorSyncV2ToV1, transformSortVariableToEnumV1, transformVariableHideToEnumV1, transformVariableRefreshToEnumV1, } from 'app/features/dashboard-scene/serialization/transformToV1TypesUtils'; import { LEGACY_STRING_VALUE_KEY, transformCursorSynctoEnum, transformDataTopic, transformSortVariableToEnum, transformVariableHideToEnum, transformVariableRefreshToEnum, } from 'app/features/dashboard-scene/serialization/transformToV2TypesUtils'; import { DashboardDataDTO, DashboardDTO } from 'app/types'; import { DashboardWithAccessInfo } from './types'; import { isDashboardResource, isDashboardV0Spec, isDashboardV2Resource } from './utils'; export function ensureV2Response( dto: DashboardDTO | DashboardWithAccessInfo | DashboardWithAccessInfo ): DashboardWithAccessInfo { if (isDashboardV2Resource(dto)) { return dto; } let dashboard: DashboardDataDTO; if (isDashboardResource(dto)) { dashboard = dto.spec; } else { dashboard = dto.dashboard; } const timeSettingsDefaults = defaultTimeSettingsSpec(); const dashboardDefaults = defaultDashboardV2Spec(); const [elements, layout] = getElementsFromPanels(dashboard.panels || []); // @ts-expect-error - dashboard.templating.list is VariableModel[] and we need TypedVariableModel[] here // that would allow accessing unique properties for each variable type that the API returns const variables = getVariables(dashboard.templating?.list || []); const annotations = getAnnotations(dashboard.annotations?.list || []); let accessMeta: DashboardWithAccessInfo['access']; let annotationsMeta: DashboardWithAccessInfo['metadata']['annotations']; let labelsMeta: DashboardWithAccessInfo['metadata']['labels']; let creationTimestamp; if (isDashboardResource(dto)) { accessMeta = dto.access; annotationsMeta = { ...dto.metadata.annotations, [AnnoKeyDashboardGnetId]: dashboard.gnetId ?? undefined, }; creationTimestamp = dto.metadata.creationTimestamp; labelsMeta = { [DeprecatedInternalId]: dto.metadata.labels?.[DeprecatedInternalId], }; } else { accessMeta = { url: dto.meta.url, slug: dto.meta.slug, canSave: dto.meta.canSave, canEdit: dto.meta.canEdit, canDelete: dto.meta.canDelete, canShare: dto.meta.canShare, canStar: dto.meta.canStar, canAdmin: dto.meta.canAdmin, annotationsPermissions: dto.meta.annotationsPermissions, }; annotationsMeta = { [AnnoKeyCreatedBy]: dto.meta.createdBy, [AnnoKeyUpdatedBy]: dto.meta.updatedBy, [AnnoKeyUpdatedTimestamp]: dto.meta.updated, [AnnoKeyFolder]: dto.meta.folderUid, [AnnoKeySlug]: dto.meta.slug, }; if (dashboard.gnetId) { annotationsMeta[AnnoKeyDashboardGnetId] = dashboard.gnetId; } if (dto.meta.isSnapshot) { // FIXME -- lets not put non-annotation data in annotations! annotationsMeta[AnnoKeyDashboardIsSnapshot] = 'true'; } creationTimestamp = dto.meta.created; labelsMeta = { [DeprecatedInternalId]: dashboard.id?.toString() ?? undefined, }; } if (annotationsMeta?.[AnnoKeyDashboardIsSnapshot]) { annotationsMeta[AnnoKeyDashboardSnapshotOriginalUrl] = dashboard.snapshot?.originalUrl; } const spec: DashboardV2Spec = { title: dashboard.title, description: dashboard.description, tags: dashboard.tags ?? [], cursorSync: transformCursorSynctoEnum(dashboard.graphTooltip), preload: dashboard.preload || dashboardDefaults.preload, liveNow: dashboard.liveNow, editable: dashboard.editable, revision: dashboard.revision, timeSettings: { from: dashboard.time?.from || timeSettingsDefaults.from, to: dashboard.time?.to || timeSettingsDefaults.to, timezone: dashboard.timezone || timeSettingsDefaults.timezone, autoRefresh: dashboard.refresh || timeSettingsDefaults.autoRefresh, autoRefreshIntervals: dashboard.timepicker?.refresh_intervals || timeSettingsDefaults.autoRefreshIntervals, fiscalYearStartMonth: dashboard.fiscalYearStartMonth || timeSettingsDefaults.fiscalYearStartMonth, hideTimepicker: dashboard.timepicker?.hidden || timeSettingsDefaults.hideTimepicker, quickRanges: dashboard.timepicker?.quick_ranges, weekStart: getWeekStart(dashboard.weekStart, timeSettingsDefaults.weekStart), nowDelay: dashboard.timepicker?.nowDelay || timeSettingsDefaults.nowDelay, }, links: dashboard.links || [], annotations, variables, elements, layout, }; return { apiVersion: 'v2alpha1', kind: 'DashboardWithAccessInfo', metadata: { creationTimestamp: creationTimestamp || '', // TODO verify this empty string is valid name: dashboard.uid, resourceVersion: dashboard.version?.toString() || '0', annotations: annotationsMeta, labels: labelsMeta, }, spec, access: accessMeta, }; } export function ensureV1Response( dashboard: DashboardDTO | DashboardWithAccessInfo | DashboardWithAccessInfo ): DashboardDTO { // if dashboard is not on v1 schema or v2 schema, return as is if (!isDashboardResource(dashboard)) { return dashboard; } const spec = dashboard.spec; // if dashboard is on v1 schema if (isDashboardV0Spec(spec)) { return { meta: { ...dashboard.access, isNew: false, isFolder: false, uid: dashboard.metadata.name, k8s: dashboard.metadata, version: dashboard.metadata.generation, }, dashboard: spec, }; } else { // if dashboard is on v2 schema convert to v1 schema return { meta: { created: dashboard.metadata.creationTimestamp, createdBy: dashboard.metadata.annotations?.[AnnoKeyCreatedBy] ?? '', updated: dashboard.metadata.annotations?.[AnnoKeyUpdatedTimestamp], updatedBy: dashboard.metadata.annotations?.[AnnoKeyUpdatedBy], folderUid: dashboard.metadata.annotations?.[AnnoKeyFolder], slug: dashboard.metadata.annotations?.[AnnoKeySlug], url: dashboard.access.url, canAdmin: dashboard.access.canAdmin, canDelete: dashboard.access.canDelete, canEdit: dashboard.access.canEdit, canSave: dashboard.access.canSave, canShare: dashboard.access.canShare, canStar: dashboard.access.canStar, annotationsPermissions: dashboard.access.annotationsPermissions, }, dashboard: transformDashboardV2SpecToV1(spec, dashboard.metadata), }; } } export const ResponseTransformers = { ensureV2Response, ensureV1Response, }; function getElementsFromPanels( panels: Array ): [DashboardV2Spec['elements'], DashboardV2Spec['layout']] { const elements: DashboardV2Spec['elements'] = {}; const layout: DashboardV2Spec['layout'] = { kind: 'GridLayout', spec: { items: [], }, }; if (!panels) { return [elements, layout]; } let currentRow: GridLayoutRowKind | null = null; // iterate over panels for (const p of panels) { if (isRowPanel(p)) { if (currentRow) { // Flush current row to layout before we create a new one layout.spec.items.push(currentRow); } const rowElements = []; for (const panel of p.panels || []) { const [element, name] = buildElement(panel); elements[name] = element; rowElements.push(buildGridItemKind(panel, name, yOffsetInRows(panel, p.gridPos!.y))); } currentRow = buildRowKind(p, rowElements); } else { const [element, elementName] = buildElement(p); elements[elementName] = element; if (currentRow) { // Collect panels to current layout row currentRow.spec.elements.push(buildGridItemKind(p, elementName, yOffsetInRows(p, currentRow.spec.y))); } else { layout.spec.items.push(buildGridItemKind(p, elementName)); } } } if (currentRow) { // Flush last row to layout layout.spec.items.push(currentRow); } return [elements, layout]; } function isRowPanel(panel: Panel | RowPanel): panel is RowPanel { return panel.type === 'row'; } function getWeekStart(weekStart?: string, defaultWeekStart?: WeekStart): WeekStart | undefined { if (!weekStart || !isWeekStart(weekStart)) { return defaultWeekStart; } return weekStart; } function buildRowKind(p: RowPanel, elements: GridLayoutItemKind[]): GridLayoutRowKind { return { kind: 'GridLayoutRow', spec: { collapsed: p.collapsed, title: p.title ?? '', repeat: p.repeat ? { value: p.repeat, mode: 'variable' } : undefined, y: p.gridPos?.y ?? 0, elements, }, }; } function buildGridItemKind(p: Panel, elementName: string, yOverride?: number): GridLayoutItemKind { return { kind: 'GridLayoutItem', spec: { x: p.gridPos!.x, y: yOverride ?? p.gridPos!.y, width: p.gridPos!.w, height: p.gridPos!.h, repeat: p.repeat ? { value: p.repeat, mode: 'variable', direction: p.repeatDirection, maxPerRow: p.maxPerRow } : undefined, element: { kind: 'ElementReference', name: elementName!, }, }, }; } function yOffsetInRows(p: Panel, rowY: number): number { return p.gridPos!.y - rowY - GRID_ROW_HEIGHT; } function buildElement(p: Panel): [PanelKind | LibraryPanelKind, string] { const element_identifier = `panel-${p.id}`; if (p.libraryPanel) { // LibraryPanelKind const panelKind: LibraryPanelKind = { kind: 'LibraryPanel', spec: { libraryPanel: { uid: p.libraryPanel.uid, name: p.libraryPanel.name, }, id: p.id!, title: p.title ?? '', }, }; return [panelKind, element_identifier]; } else { // PanelKind const queries = getPanelQueries( (p.targets as unknown as DataQuery[]) || [], p.datasource || getDefaultDatasource() ); const transformations = getPanelTransformations(p.transformations || []); const panelKind: PanelKind = { kind: 'Panel', spec: { title: p.title || '', description: p.description || '', vizConfig: { kind: p.type, spec: { fieldConfig: (p.fieldConfig as any) || defaultFieldConfigSource(), options: p.options as any, pluginVersion: p.pluginVersion!, }, }, links: p.links?.map((l) => ({ title: l.title, url: l.url || '', targetBlank: l.targetBlank, })) || [], id: p.id!, data: { kind: 'QueryGroup', spec: { queries, transformations, queryOptions: { cacheTimeout: p.cacheTimeout, maxDataPoints: p.maxDataPoints, interval: p.interval, hideTimeOverride: p.hideTimeOverride, queryCachingTTL: p.queryCachingTTL, timeFrom: p.timeFrom, timeShift: p.timeShift, }, }, }, }, }; return [panelKind, element_identifier]; } } function getDefaultDatasourceType() { // if there is no default datasource, return 'grafana' as default return getDefaultDataSourceRef()?.type ?? 'grafana'; } export function getDefaultDatasource(): DataSourceRef { const configDefaultDS = getDefaultDataSourceRef() ?? { type: 'grafana', uid: '-- Grafana --' }; if (configDefaultDS.uid && !configDefaultDS.apiVersion) { // get api version from config const dsInstance = config.bootData.settings.datasources[configDefaultDS.uid]; configDefaultDS.apiVersion = dsInstance.apiVersion ?? undefined; } return { apiVersion: configDefaultDS.apiVersion, type: configDefaultDS.type, uid: configDefaultDS.uid, }; } export function getPanelQueries(targets: DataQuery[], panelDatasource: DataSourceRef): PanelQueryKind[] { return targets.map((t) => { const { refId, hide, datasource, ...query } = t; const q: PanelQueryKind = { kind: 'PanelQuery', spec: { refId: t.refId, hidden: t.hide ?? false, datasource: t.datasource ? t.datasource : panelDatasource, query: { kind: t.datasource?.type || panelDatasource.type!, spec: { ...query, }, }, }, }; return q; }); } function getPanelTransformations(transformations: DataTransformerConfig[]): TransformationKind[] { return transformations.map((t) => { return { kind: t.id, spec: { ...t, topic: transformDataTopic(t.topic), }, }; }); } function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables'] { const variables: DashboardV2Spec['variables'] = []; for (const v of vars) { const commonProperties = { name: v.name, label: v.label, ...(v.description && { description: v.description }), skipUrlSync: Boolean(v.skipUrlSync), hide: transformVariableHideToEnum(v.hide), }; switch (v.type) { case 'query': let query = v.query || {}; if (typeof query === 'string') { console.warn( 'Query variable query is a string which is deprecated in the schema v2. It should extend DataQuery' ); query = { [LEGACY_STRING_VALUE_KEY]: query, }; } const qv: QueryVariableKind = { kind: 'QueryVariable', spec: { ...commonProperties, multi: Boolean(v.multi), includeAll: Boolean(v.includeAll), ...(v.allValue && { allValue: v.allValue }), current: { value: v.current?.value, text: v.current?.text, }, options: v.options || [], refresh: transformVariableRefreshToEnum(v.refresh), ...(v.datasource && { datasource: v.datasource }), regex: v.regex || '', sort: transformSortVariableToEnum(v.sort), query: { kind: v.datasource?.type || getDefaultDatasourceType(), spec: query, }, }, }; variables.push(qv); break; case 'datasource': let pluginId = getDefaultDatasourceType(); if (v.query && typeof v.query === 'string') { pluginId = v.query; } const dv: DatasourceVariableKind = { kind: 'DatasourceVariable', spec: { ...commonProperties, multi: Boolean(v.multi), includeAll: Boolean(v.includeAll), ...(v.allValue && { allValue: v.allValue }), current: { value: v.current.value, text: v.current.text, }, options: v.options || [], refresh: transformVariableRefreshToEnum(v.refresh), pluginId, regex: v.regex || '', }, }; variables.push(dv); break; case 'custom': const cv: CustomVariableKind = { kind: 'CustomVariable', spec: { ...commonProperties, query: v.query, current: { value: v.current.value, text: v.current.text, }, options: v.options, multi: v.multi, includeAll: v.includeAll, ...(v.allValue && { allValue: v.allValue }), }, }; variables.push(cv); break; case 'adhoc': const av: AdhocVariableKind = { kind: 'AdhocVariable', spec: { ...commonProperties, datasource: v.datasource || getDefaultDatasource(), baseFilters: v.baseFilters || [], filters: v.filters || [], defaultKeys: v.defaultKeys || [], }, }; variables.push(av); break; case 'constant': const cnts: ConstantVariableKind = { kind: 'ConstantVariable', spec: { ...commonProperties, current: { value: v.current.value, // Constant variable doesn't use text state text: v.current.value, }, query: v.query, }, }; variables.push(cnts); break; case 'interval': const intrv: IntervalVariableKind = { kind: 'IntervalVariable', spec: { ...commonProperties, current: { value: v.current.value, // Interval variable doesn't use text state text: v.current.value, }, query: v.query, refresh: 'onTimeRangeChanged', options: v.options, auto: v.auto, auto_min: v.auto_min, auto_count: v.auto_count, }, }; variables.push(intrv); break; case 'textbox': const tx: TextVariableKind = { kind: 'TextVariable', spec: { ...commonProperties, current: { value: v.current.value, // Text variable doesn't use text state text: v.current.value, }, query: v.query, }, }; variables.push(tx); break; case 'groupby': const gb: GroupByVariableKind = { kind: 'GroupByVariable', spec: { ...commonProperties, datasource: v.datasource || getDefaultDatasource(), options: v.options, current: { value: v.current.value, text: v.current.text, }, multi: v.multi, }, }; variables.push(gb); break; default: // do not throw error, just log it console.error(`Variable transformation not implemented: ${v.type}`); } } return variables; } function getAnnotations(annotations: AnnotationQuery[]): DashboardV2Spec['annotations'] { return annotations.map((a) => { const aq: AnnotationQueryKind = { kind: 'AnnotationQuery', spec: { name: a.name, ...(a.datasource && { datasource: a.datasource }), enable: a.enable, hide: Boolean(a.hide), iconColor: a.iconColor, builtIn: Boolean(a.builtIn), query: { kind: a.datasource?.type || getDefaultDatasourceType(), spec: { ...a.target, }, }, filter: a.filter, }, }; 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: LEGACY_STRING_VALUE_KEY in v.spec.query.spec ? v.spec.query.spec[LEGACY_STRING_VALUE_KEY] : 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 { const panelsV1: Array = []; let maxPanelId = 0; if (layout.kind !== 'GridLayout') { throw new Error('Cannot convert non-GridLayout layout to v1'); } for (const item of layout.spec.items) { if (item.kind === 'GridLayoutItem') { const panel = panels[item.spec.element.name]; const v1Panel = transformV2PanelToV1Panel(panel, item); panelsV1.push(v1Panel); if (v1Panel.id ?? 0 > maxPanelId) { maxPanelId = v1Panel.id ?? 0; } } else if (item.kind === 'GridLayoutRow') { const row: RowPanel = { id: -1, // Temporarily set to -1, updated later to be unique type: 'row', title: item.spec.title, collapsed: item.spec.collapsed, repeat: item.spec.repeat ? item.spec.repeat.value : undefined, gridPos: { x: 0, y: item.spec.y, w: 24, h: GRID_ROW_HEIGHT, }, panels: [], }; const rowPanels = []; for (const panel of item.spec.elements) { const panelElement = panels[panel.spec.element.name]; const v1Panel = transformV2PanelToV1Panel(panelElement, panel, item.spec.y + GRID_ROW_HEIGHT + panel.spec.y); rowPanels.push(v1Panel); if (v1Panel.id ?? 0 > maxPanelId) { maxPanelId = v1Panel.id ?? 0; } } if (item.spec.collapsed) { // When a row is collapsed, panels inside it are stored in the panels property. row.panels = rowPanels; panelsV1.push(row); } else { panelsV1.push(row); panelsV1.push(...rowPanels); } } } // Update row panel ids to be unique for (const panel of panelsV1) { if (panel.type === 'row' && panel.id === -1) { panel.id = ++maxPanelId; } } return panelsV1; } function transformV2PanelToV1Panel( p: PanelKind | LibraryPanelKind, layoutElement: GridLayoutItemKind, yOverride?: number ): Panel | LibraryPanelDTO { const { x, y, width, height, repeat } = layoutElement?.spec || { x: 0, y: 0, width: 0, height: 0 }; const gridPos = { x, y: yOverride ?? 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: panel.id, title: panel.title, gridPos, libraryPanel: { uid: panel.libraryPanel.uid, name: panel.libraryPanel.name, }, type: 'library-panel-ref', }; } 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}`); } } export function transformDashboardV2SpecToV1(spec: DashboardV2Spec, metadata: ObjectMeta): DashboardDataDTO { const annotations = getAnnotationsV1(spec.annotations); const variables = getVariablesV1(spec.variables); const panels = getPanelsV1(spec.elements, spec.layout); return { uid: metadata.name, title: spec.title, description: spec.description, tags: spec.tags, schemaVersion: 40, graphTooltip: transformCursorSyncV2ToV1(spec.cursorSync), preload: spec.preload, liveNow: spec.liveNow, editable: spec.editable, gnetId: metadata.annotations?.[AnnoKeyDashboardGnetId], revision: spec.revision, time: { from: spec.timeSettings.from, to: spec.timeSettings.to, }, timezone: spec.timeSettings.timezone, refresh: spec.timeSettings.autoRefresh, timepicker: { refresh_intervals: spec.timeSettings.autoRefreshIntervals, hidden: spec.timeSettings.hideTimepicker, quick_ranges: spec.timeSettings.quickRanges, nowDelay: spec.timeSettings.nowDelay, }, fiscalYearStartMonth: spec.timeSettings.fiscalYearStartMonth, weekStart: spec.timeSettings.weekStart, version: metadata.generation, links: spec.links, annotations: { list: annotations }, panels, templating: { list: variables }, }; }