Files
grafana/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go
Haris Rozajac f5aa39cc27 Dashboard Conversion: Add missing dev dashboards when comparing v1 to v2 conversion (#115967)
* descritpion and transformations

* add fieldMinMax to schema

* add nullValueMode to schema

* convert actions

* disabled prop in transformations

* update

* fix

* gauge

* remove index (deprecated) and decimals as strings

* gofmt

* codegen

* lint

* open api
2026-01-13 07:38:35 -07:00

3023 lines
100 KiB
Go

package conversion
import (
"context"
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apiserver/pkg/endpoints/request"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard"
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
schemaversion "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
"github.com/grafana/grafana/pkg/apimachinery/identity"
)
// getDefaultDatasourceType gets the default datasource type using the datasource provider
func getDefaultDatasourceType(ctx context.Context, provider schemaversion.DataSourceIndexProvider) string {
const defaultType = "grafana"
if provider == nil {
return defaultType
}
datasources := provider.Index(ctx)
if defaultDS := datasources.GetDefault(); defaultDS != nil {
return defaultDS.Type
}
return defaultType
}
// getDatasourceTypeByUID gets the datasource type by UID using the datasource provider
func getDatasourceTypeByUID(ctx context.Context, uid string, provider schemaversion.DataSourceIndexProvider) string {
if uid == "" {
return getDefaultDatasourceType(ctx, provider)
}
if provider == nil {
return "grafana"
}
dsIndex := provider.Index(ctx)
if ds := dsIndex.LookupByUID(uid); ds != nil {
return ds.Type
}
return getDefaultDatasourceType(ctx, provider)
}
// resolveGrafanaDatasourceUID resolves the Grafana datasource UID when type is "datasource" and UID is empty.
// The Grafana datasource has type "datasource" and UID "grafana". When a v1beta1 dashboard has
// datasource: { type: "datasource" } with no UID, it should resolve to uid: "grafana".
func resolveGrafanaDatasourceUID(dsType, dsUID string) string {
if dsType == "datasource" && dsUID == "" {
return dashboard.GrafanaDatasourceUID
}
return dsUID
}
// prepareV1beta1ConversionContext sets up the context with namespace and service identity
// for v1beta1 dashboard conversions. This context is needed to retrieve datasources for
// converting dashboard datasource references.
// A background service identity is used because the user who is reading the specific dashboard
// may not have access to all the datasources in the dashboard, but the conversion still needs to take place
// in order to be able to convert between k8s versions.
func prepareV1beta1ConversionContext(in *dashv1.Dashboard, dsIndexProvider schemaversion.DataSourceIndexProvider) (context.Context, *types.NamespaceInfo, error) {
if dsIndexProvider == nil {
return nil, nil, fmt.Errorf("datasource index provider not initialized")
}
namespace := in.GetNamespace()
if namespace == "" {
return context.Background(), nil, nil
}
// Try to parse namespace and set up tenant-aware context
ctx := request.WithNamespace(context.Background(), namespace)
nsInfo, err := types.ParseNamespace(namespace)
if err != nil {
return ctx, nil, nil
}
ctx, _ = identity.WithServiceIdentity(ctx, nsInfo.OrgID)
return ctx, &nsInfo, nil
}
func ConvertDashboard_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error {
out.ObjectMeta = in.ObjectMeta
out.APIVersion = dashv2alpha1.APIVERSION
out.Kind = in.Kind
// Prepare context with namespace and service identity
// The datasource provider is already wrapped with caching at registration time
ctx, _, err := prepareV1beta1ConversionContext(in, dsIndexProvider)
if err != nil {
// If context preparation fails, return error to be handled by wrapper
// The wrapper will set status and handle gracefully
return fmt.Errorf("failed to prepare conversion context: %w", err)
}
return convertDashboardSpec_V1beta1_to_V2alpha1(&in.Spec, &out.Spec, scope, ctx, dsIndexProvider, leIndexProvider)
}
func convertDashboardSpec_V1beta1_to_V2alpha1(in *dashv1.DashboardSpec, out *dashv2alpha1.DashboardSpec, scope conversion.Scope, ctx context.Context, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error {
// Parse the unstructured spec into a dashboard JSON structure
dashboardJSON, ok := in.Object["dashboard"]
if !ok {
// If no "dashboard" key, treat the entire object as the dashboard
dashboardJSON = in.Object
}
// Convert to bytes and back to handle complex nested structures
dashBytes, err := json.Marshal(dashboardJSON)
if err != nil {
return fmt.Errorf("failed to marshal dashboard JSON: %w", err)
}
var dashboard map[string]interface{}
if err := json.Unmarshal(dashBytes, &dashboard); err != nil {
return fmt.Errorf("failed to unmarshal dashboard JSON: %w", err)
}
// Get defaults
timeSettingsDefaults := dashv2alpha1.NewDashboardTimeSettingsSpec()
dashboardDefaults := getDefaultDashboardV2Spec()
// Transform basic fields
out.Title = schemaversion.GetStringValue(dashboard, "title")
out.Description = getStringPtr(dashboard, "description")
out.Tags = getStringSlice(dashboard, "tags")
out.CursorSync = transformCursorSyncToEnum(getIntField(dashboard, "graphTooltip", 0))
out.Preload = getBoolField(dashboard, "preload", dashboardDefaults.Preload)
// Add frontend-style default values
// Set default editable: true to match frontend behavior
defaultEditable := true
out.Editable = &defaultEditable
// Set default liveNow: false to match frontend behavior
defaultLiveNow := false
out.LiveNow = &defaultLiveNow
// Override with input values if they exist
if liveNow, exists := dashboard["liveNow"]; exists {
if liveNowBool, ok := liveNow.(bool); ok {
out.LiveNow = &liveNowBool
}
}
if editable, exists := dashboard["editable"]; exists {
if editableBool, ok := editable.(bool); ok {
out.Editable = &editableBool
}
}
if revision, exists := dashboard["revision"]; exists {
if revisionInt, ok := revision.(float64); ok {
revisionUint := uint16(revisionInt)
out.Revision = &revisionUint
}
}
// Transform time settings
out.TimeSettings = transformTimeSettings(dashboard, timeSettingsDefaults)
// Transform links
out.Links = transformLinks(dashboard)
// Transform panels to elements and layout
elements, layout, err := transformPanelsToElementsAndLayout(ctx, dashboard, dsIndexProvider, leIndexProvider)
if err != nil {
return fmt.Errorf("failed to transform panels: %w", err)
}
out.Elements = elements
out.Layout = layout
// Transform variables
variables, err := transformVariables(ctx, dashboard, dsIndexProvider)
if err != nil {
return fmt.Errorf("failed to transform variables: %w", err)
}
out.Variables = variables
// Transform annotations
annotations, err := transformAnnotations(dashboard)
if err != nil {
return fmt.Errorf("failed to transform annotations: %w", err)
}
out.Annotations = annotations
return nil
}
func getStringPtr(m map[string]interface{}, key string) *string {
if val, ok := m[key]; ok {
if str, ok := val.(string); ok && str != "" {
return &str
}
}
return nil
}
func getStringSlice(m map[string]interface{}, key string) []string {
if val, ok := m[key]; ok {
if slice, ok := val.([]interface{}); ok {
result := make([]string, 0, len(slice))
for _, item := range slice {
if str, ok := item.(string); ok {
result = append(result, str)
}
}
return result
}
}
return []string{}
}
func getBoolField(m map[string]interface{}, key string, defaultValue bool) bool {
if val, ok := m[key]; ok {
if b, ok := val.(bool); ok {
return b
}
}
return defaultValue
}
// stripBOM removes Byte Order Mark (BOM) characters from a string.
// BOMs (U+FEFF) can be introduced through copy/paste from certain editors
// and cause CUE validation errors ("illegal byte order mark").
func stripBOM(s string) string {
return strings.ReplaceAll(s, "\ufeff", "")
}
// stripBOMFromInterface recursively strips BOM characters from all strings
// in an interface{} value (map, slice, or string).
func stripBOMFromInterface(v interface{}) interface{} {
switch val := v.(type) {
case string:
return stripBOM(val)
case map[string]interface{}:
result := make(map[string]interface{}, len(val))
for k, v := range val {
result[k] = stripBOMFromInterface(v)
}
return result
case []interface{}:
result := make([]interface{}, len(val))
for i, item := range val {
result[i] = stripBOMFromInterface(item)
}
return result
default:
return v
}
}
func getUnionField[T ~string](m map[string]interface{}, key string) *T {
if val, ok := m[key]; ok {
if str, ok := val.(string); ok && str != "" {
result := T(str)
return &result
}
}
return nil
}
// Helper function to create int64 pointer
func int64Ptr(i int64) *int64 {
return &i
}
// isTemplateVariable checks if a string is a Grafana template variable reference
// Template variables can be in the form: $varname or ${varname}
func isTemplateVariable(s string) bool {
if s == "" {
return false
}
return strings.HasPrefix(s, "$") || strings.HasPrefix(s, "${")
}
func getIntField(m map[string]interface{}, key string, defaultValue int) int {
if val, ok := m[key]; ok {
switch v := val.(type) {
case int:
return v
case float64:
return int(v)
case string:
if i, err := strconv.Atoi(v); err == nil {
return i
}
}
}
return defaultValue
}
// Enum transformation functions
func transformCursorSyncToEnum(cursorSync int) dashv2alpha1.DashboardDashboardCursorSync {
switch cursorSync {
case 0:
return dashv2alpha1.DashboardDashboardCursorSyncOff
case 1:
return dashv2alpha1.DashboardDashboardCursorSyncCrosshair
case 2:
return dashv2alpha1.DashboardDashboardCursorSyncTooltip
default:
return dashv2alpha1.DashboardDashboardCursorSyncOff
}
}
func getDefaultDashboardV2Spec() dashv2alpha1.DashboardSpec {
return dashv2alpha1.DashboardSpec{
Preload: false,
}
}
// Transform time settings
func transformTimeSettings(dashboard map[string]interface{}, defaults *dashv2alpha1.DashboardTimeSettingsSpec) dashv2alpha1.DashboardTimeSettingsSpec {
timeSettings := dashv2alpha1.DashboardTimeSettingsSpec{
From: defaults.From,
To: defaults.To,
Timezone: defaults.Timezone,
AutoRefresh: defaults.AutoRefresh,
AutoRefreshIntervals: defaults.AutoRefreshIntervals,
FiscalYearStartMonth: defaults.FiscalYearStartMonth,
HideTimepicker: defaults.HideTimepicker,
// Don't initialize optional fields with defaults - only set them if present in input
}
// Extract time range
if timeRange, ok := dashboard["time"].(map[string]interface{}); ok {
if from := schemaversion.GetStringValue(timeRange, "from"); from != "" {
timeSettings.From = from
}
if to := schemaversion.GetStringValue(timeRange, "to"); to != "" {
timeSettings.To = to
}
}
// Extract other time-related fields
if timezone, exists := dashboard["timezone"]; exists {
if timezoneStr, ok := timezone.(string); ok {
timeSettings.Timezone = &timezoneStr
}
}
if refresh := schemaversion.GetStringValue(dashboard, "refresh"); refresh != "" {
timeSettings.AutoRefresh = refresh
}
timeSettings.FiscalYearStartMonth = int64(getIntField(dashboard, "fiscalYearStartMonth", int(defaults.FiscalYearStartMonth)))
if weekStart, exists := dashboard["weekStart"]; exists {
if weekStartStr, ok := weekStart.(string); ok && weekStartStr != "" {
weekStartEnum := dashv2alpha1.DashboardTimeSettingsSpecWeekStart(weekStartStr)
timeSettings.WeekStart = &weekStartEnum
}
}
// Extract timepicker settings
if timepicker, ok := dashboard["timepicker"].(map[string]interface{}); ok {
if intervals, ok := timepicker["refresh_intervals"].([]interface{}); ok {
refreshIntervals := make([]string, 0, len(intervals))
for _, interval := range intervals {
if str, ok := interval.(string); ok {
refreshIntervals = append(refreshIntervals, str)
}
}
if len(refreshIntervals) > 0 {
timeSettings.AutoRefreshIntervals = refreshIntervals
}
}
timeSettings.HideTimepicker = getBoolField(timepicker, "hidden", defaults.HideTimepicker)
if nowDelay, exists := timepicker["nowDelay"]; exists {
if nowDelayStr, ok := nowDelay.(string); ok && nowDelayStr != "" {
timeSettings.NowDelay = &nowDelayStr
}
}
// Handle quick_ranges
if quickRanges, ok := timepicker["quick_ranges"].([]interface{}); ok {
ranges := make([]dashv2alpha1.DashboardTimeRangeOption, 0, len(quickRanges))
for _, qr := range quickRanges {
if qrMap, ok := qr.(map[string]interface{}); ok {
quickRange := dashv2alpha1.DashboardTimeRangeOption{
Display: schemaversion.GetStringValue(qrMap, "display"),
From: schemaversion.GetStringValue(qrMap, "from"),
To: schemaversion.GetStringValue(qrMap, "to"),
}
ranges = append(ranges, quickRange)
}
}
timeSettings.QuickRanges = ranges
}
}
return timeSettings
}
// Transform links
func transformLinks(dashboard map[string]interface{}) []dashv2alpha1.DashboardDashboardLink {
if links, ok := dashboard["links"].([]interface{}); ok {
result := make([]dashv2alpha1.DashboardDashboardLink, 0, len(links))
for _, link := range links {
if linkMap, ok := link.(map[string]interface{}); ok {
// Required fields with defaults
dashLink := dashv2alpha1.DashboardDashboardLink{
Title: schemaversion.GetStringValue(linkMap, "title"),
Type: dashv2alpha1.DashboardDashboardLinkType(schemaversion.GetStringValue(linkMap, "type", "link")),
Icon: schemaversion.GetStringValue(linkMap, "icon"),
Tooltip: schemaversion.GetStringValue(linkMap, "tooltip"),
Tags: getStringSlice(linkMap, "tags"),
AsDropdown: getBoolField(linkMap, "asDropdown", false),
TargetBlank: getBoolField(linkMap, "targetBlank", false),
IncludeVars: getBoolField(linkMap, "includeVars", false),
KeepTime: getBoolField(linkMap, "keepTime", false),
}
// Optional field - only set if present
if url, exists := linkMap["url"]; exists {
if urlStr, ok := url.(string); ok {
cleanUrl := stripBOM(urlStr)
dashLink.Url = &cleanUrl
}
}
// Optional placement field - only set if present
if placement, exists := linkMap["placement"]; exists {
if placementStr, ok := placement.(string); ok {
dashLink.Placement = &placementStr
}
}
result = append(result, dashLink)
}
}
return result
}
return []dashv2alpha1.DashboardDashboardLink{}
}
// Panel transformation constants
const GRID_ROW_HEIGHT = 1
func transformPanelsToElementsAndLayout(ctx context.Context, dashboard map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) {
panels, ok := dashboard["panels"].([]interface{})
if !ok {
// Return empty elements and default grid layout
elements := make(map[string]dashv2alpha1.DashboardElement)
layout := dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{
GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{
Kind: "GridLayout",
Spec: dashv2alpha1.DashboardGridLayoutSpec{
Items: []dashv2alpha1.DashboardGridLayoutItemKind{},
},
},
}
return elements, layout, nil
}
// Check if any panels are row panels
hasRowPanels := false
for _, p := range panels {
if panelMap, ok := p.(map[string]interface{}); ok {
if schemaversion.GetStringValue(panelMap, "type") == "row" {
hasRowPanels = true
break
}
}
}
if hasRowPanels {
return convertToRowsLayout(ctx, panels, dsIndexProvider, leIndexProvider)
}
return convertToGridLayout(ctx, panels, dsIndexProvider, leIndexProvider)
}
func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) {
elements := make(map[string]dashv2alpha1.DashboardElement)
items := make([]dashv2alpha1.DashboardGridLayoutItemKind, 0, len(panels))
for _, p := range panels {
panelMap, ok := p.(map[string]interface{})
if !ok {
continue
}
element, elementName, err := buildElement(ctx, panelMap, dsIndexProvider)
if err != nil {
continue // Skip invalid panels
}
elements[elementName] = element
items = append(items, buildGridItemKind(ctx, panelMap, elementName, nil, leIndexProvider))
}
layout := dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{
GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{
Kind: "GridLayout",
Spec: dashv2alpha1.DashboardGridLayoutSpec{
Items: items,
},
},
}
return elements, layout, nil
}
func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) {
elements := make(map[string]dashv2alpha1.DashboardElement)
rows := make([]dashv2alpha1.DashboardRowsLayoutRowKind, 0)
var currentRow *dashv2alpha1.DashboardRowsLayoutRowKind
var legacyRowY int64
for _, p := range panels {
panelMap, ok := p.(map[string]interface{})
if !ok {
continue
}
if schemaversion.GetStringValue(panelMap, "type") == "row" {
// This is a row panel
if gridPos, ok := panelMap["gridPos"].(map[string]interface{}); ok {
legacyRowY = int64(getIntField(gridPos, "y", 0))
}
if currentRow != nil {
// If currentRow is a hidden-header row (panels before first explicit row),
// it should not be collapsed because it will disappear and be visible only in edit mode
if currentRow.Spec.HideHeader != nil && *currentRow.Spec.HideHeader {
currentRow.Spec.Collapse = &[]bool{false}[0]
}
// Flush current row to layout
rows = append(rows, *currentRow)
}
// Handle collapsed row panels
rowElements := make([]dashv2alpha1.DashboardGridLayoutItemKind, 0)
if collapsedPanels, ok := panelMap["panels"].([]interface{}); ok {
for _, panel := range collapsedPanels {
if collapsedPanelMap, ok := panel.(map[string]interface{}); ok {
element, name, err := buildElement(ctx, collapsedPanelMap, dsIndexProvider)
if err == nil {
elements[name] = element
rowElements = append(rowElements, buildGridItemKind(ctx, collapsedPanelMap, name, int64Ptr(yOffsetInRows(collapsedPanelMap, legacyRowY)), leIndexProvider))
}
}
}
}
currentRow = buildRowKind(panelMap, rowElements)
} else {
// Regular panel
element, elementName, err := buildElement(ctx, panelMap, dsIndexProvider)
if err != nil {
continue // Skip invalid panels
}
elements[elementName] = element
if currentRow != nil {
// Add to current row
if currentRow.Spec.Layout.GridLayoutKind != nil {
currentRow.Spec.Layout.GridLayoutKind.Spec.Items = append(
currentRow.Spec.Layout.GridLayoutKind.Spec.Items,
buildGridItemKind(ctx, panelMap, elementName, int64Ptr(yOffsetInRows(panelMap, legacyRowY)), leIndexProvider),
)
}
} else {
// Create first row (hidden header)
// Since this row does not exist in V1, we simulate it being outside of the grid above the first panel
// The Y position does not matter for the rows layout, but it's used to calculate the position of the panels in the grid layout in the row.
legacyRowY = -1
gridItems := []dashv2alpha1.DashboardGridLayoutItemKind{
buildGridItemKind(ctx, panelMap, elementName, int64Ptr(0), leIndexProvider),
}
hideHeader := true
currentRow = &dashv2alpha1.DashboardRowsLayoutRowKind{
Kind: "RowsLayoutRow",
Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{
Collapse: &[]bool{false}[0],
Title: &[]string{""}[0],
HideHeader: &hideHeader,
Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{
GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{
Kind: "GridLayout",
Spec: dashv2alpha1.DashboardGridLayoutSpec{
Items: gridItems,
},
},
},
},
}
}
}
}
if currentRow != nil {
// Flush last row
rows = append(rows, *currentRow)
}
layout := dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{
RowsLayoutKind: &dashv2alpha1.DashboardRowsLayoutKind{
Kind: "RowsLayout",
Spec: dashv2alpha1.DashboardRowsLayoutSpec{
Rows: rows,
},
},
}
return elements, layout, nil
}
func buildElement(ctx context.Context, panelMap map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardElement, string, error) {
panelID := getIntField(panelMap, "id", 0)
elementName := fmt.Sprintf("panel-%d", panelID)
// Check if it's a library panel
if libraryPanel, ok := panelMap["libraryPanel"].(map[string]interface{}); ok {
libPanelKind := &dashv2alpha1.DashboardLibraryPanelKind{
Kind: "LibraryPanel",
Spec: dashv2alpha1.DashboardLibraryPanelKindSpec{
LibraryPanel: dashv2alpha1.DashboardLibraryPanelRef{
Uid: schemaversion.GetStringValue(libraryPanel, "uid"),
Name: schemaversion.GetStringValue(libraryPanel, "name"),
},
Id: float64(panelID),
Title: schemaversion.GetStringValue(panelMap, "title"),
},
}
element := dashv2alpha1.DashboardElement{
LibraryPanelKind: libPanelKind,
}
return element, elementName, nil
}
// Regular panel
panelKind, err := buildPanelKind(ctx, panelMap, dsIndexProvider)
if err != nil {
return dashv2alpha1.DashboardElement{}, "", err
}
element := dashv2alpha1.DashboardElement{
PanelKind: panelKind,
}
return element, elementName, nil
}
func buildPanelKind(ctx context.Context, panelMap map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) (*dashv2alpha1.DashboardPanelKind, error) {
panelID := float64(getIntField(panelMap, "id", 0))
// Transform queries
queries := transformPanelQueries(ctx, panelMap, dsIndexProvider)
// Transform transformations
transformations := transformPanelTransformations(panelMap)
// Build query options
queryOptions := buildQueryOptions(panelMap)
// Build data links
links := transformDataLinks(panelMap)
// Build viz config
vizConfig := buildVizConfig(panelMap)
panelKind := &dashv2alpha1.DashboardPanelKind{
Kind: "Panel",
Spec: dashv2alpha1.DashboardPanelSpec{
Id: panelID,
Title: schemaversion.GetStringValue(panelMap, "title"),
Description: schemaversion.GetStringValue(panelMap, "description"),
Links: links,
Data: dashv2alpha1.DashboardQueryGroupKind{
Kind: "QueryGroup",
Spec: dashv2alpha1.DashboardQueryGroupSpec{
Queries: queries,
Transformations: transformations,
QueryOptions: queryOptions,
},
},
VizConfig: vizConfig,
},
}
// Handle transparent panels
if transparent := getBoolField(panelMap, "transparent", false); transparent {
panelKind.Spec.Transparent = &transparent
}
return panelKind, nil
}
func buildGridItemKind(ctx context.Context, panelMap map[string]interface{}, elementName string, yOverride *int64, leIndexProvider schemaversion.LibraryElementIndexProvider) dashv2alpha1.DashboardGridLayoutItemKind {
// Default grid position (matches frontend PanelModel defaults: w=6, h=3)
x, y, width, height := int64(0), int64(0), int64(6), int64(3)
if gridPos, ok := panelMap["gridPos"].(map[string]interface{}); ok {
x = int64(getIntField(gridPos, "x", 0))
y = int64(getIntField(gridPos, "y", 0))
width = int64(getIntField(gridPos, "w", 6))
height = int64(getIntField(gridPos, "h", 3))
}
// Apply frontend-style grid position calculations
// Frontend recalculates positions based on row structure
if yOverride != nil {
y = *yOverride
}
item := dashv2alpha1.DashboardGridLayoutItemKind{
Kind: "GridLayoutItem",
Spec: dashv2alpha1.DashboardGridLayoutItemSpec{
X: x,
Y: y,
Width: width,
Height: height,
Element: dashv2alpha1.DashboardElementReference{
Kind: "ElementReference",
Name: elementName,
},
},
}
// Handle repeat options
// First check if repeat options are set on the panel itself (dashboard instance level)
repeatOptions := getRepeatOptionsFromPanel(panelMap)
// If no repeat options on the panel and it's a library panel, try to get them from the library panel definition
if repeatOptions == nil {
if libraryPanel, ok := panelMap["libraryPanel"].(map[string]interface{}); ok {
libraryPanelUID := schemaversion.GetStringValue(libraryPanel, "uid")
if libraryPanelUID != "" && leIndexProvider != nil {
repeatOptions = getRepeatOptionsFromLibraryPanel(ctx, libraryPanelUID, leIndexProvider)
}
}
}
if repeatOptions != nil {
item.Spec.Repeat = repeatOptions
}
return item
}
// getRepeatOptionsFromPanel extracts repeat options from a panel map (dashboard instance level)
func getRepeatOptionsFromPanel(panelMap map[string]any) *dashv2alpha1.DashboardRepeatOptions {
repeat := schemaversion.GetStringValue(panelMap, "repeat")
if repeat == "" {
return nil
}
repeatOptions := &dashv2alpha1.DashboardRepeatOptions{
Mode: "variable",
Value: repeat,
}
if repeatDirection := schemaversion.GetStringValue(panelMap, "repeatDirection"); repeatDirection != "" {
switch repeatDirection {
case "h":
direction := dashv2alpha1.DashboardRepeatOptionsDirectionH
repeatOptions.Direction = &direction
case "v":
direction := dashv2alpha1.DashboardRepeatOptionsDirectionV
repeatOptions.Direction = &direction
}
}
if maxPerRow := getIntField(panelMap, "maxPerRow", 0); maxPerRow > 0 {
maxPerRowInt64 := int64(maxPerRow)
repeatOptions.MaxPerRow = &maxPerRowInt64
}
return repeatOptions
}
// getRepeatOptionsFromLibraryPanel retrieves repeat options from a library panel by UID
func getRepeatOptionsFromLibraryPanel(ctx context.Context, libraryPanelUID string, leIndexProvider schemaversion.LibraryElementIndexProvider) *dashv2alpha1.DashboardRepeatOptions {
libraryElements := leIndexProvider.GetLibraryElementInfo(ctx)
// Find the library panel by UID
var libraryPanelModel map[string]any
for _, elem := range libraryElements {
if elem.UID == libraryPanelUID {
libraryPanelModel = elem.Model.Object
break
}
}
if libraryPanelModel == nil {
return nil
}
// Extract repeat options from the library panel model
return getRepeatOptionsFromPanel(libraryPanelModel)
}
func buildRowKind(rowPanelMap map[string]interface{}, elements []dashv2alpha1.DashboardGridLayoutItemKind) *dashv2alpha1.DashboardRowsLayoutRowKind {
collapsed := getBoolField(rowPanelMap, "collapsed", false)
title := schemaversion.GetStringValue(rowPanelMap, "title")
row := &dashv2alpha1.DashboardRowsLayoutRowKind{
Kind: "RowsLayoutRow",
Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{
Collapse: &collapsed,
Title: &title,
Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{
GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{
Kind: "GridLayout",
Spec: dashv2alpha1.DashboardGridLayoutSpec{
Items: elements,
},
},
},
},
}
// Handle repeat options for rows
if repeat := schemaversion.GetStringValue(rowPanelMap, "repeat"); repeat != "" {
row.Spec.Repeat = &dashv2alpha1.DashboardRowRepeatOptions{
Mode: "variable",
Value: repeat,
}
}
return row
}
func yOffsetInRows(panelMap map[string]interface{}, rowY int64) int64 {
if gridPos, ok := panelMap["gridPos"].(map[string]interface{}); ok {
panelY := int64(getIntField(gridPos, "y", 0))
return panelY - rowY - GRID_ROW_HEIGHT
}
return 0
}
// LEGACY_STRING_VALUE_KEY is used for QueryVariableKind's query prop. Historically, queries
// could be defined as strings, but in schema V2 we've deprecated the string type and support
// only DataQuery objects. When a query is a simple string (legacy format), it's stored under
// this key in a map structure to preserve the value during migration from v1beta1 to v2alpha1.
const LEGACY_STRING_VALUE_KEY = "__legacyStringValue"
// Variable enum transformation functions
func transformVariableHideToEnum(hide interface{}) dashv2alpha1.DashboardVariableHide {
switch v := hide.(type) {
case int:
switch v {
case 0:
return dashv2alpha1.DashboardVariableHideDontHide
case 1:
return dashv2alpha1.DashboardVariableHideHideLabel
case 2:
return dashv2alpha1.DashboardVariableHideHideVariable
default:
return dashv2alpha1.DashboardVariableHideDontHide
}
case float64:
return transformVariableHideToEnum(int(v))
case string:
switch v {
case "dontHide", "":
return dashv2alpha1.DashboardVariableHideDontHide
case "hideLabel":
return dashv2alpha1.DashboardVariableHideHideLabel
case "hideVariable":
return dashv2alpha1.DashboardVariableHideHideVariable
default:
return dashv2alpha1.DashboardVariableHideDontHide
}
default:
return dashv2alpha1.DashboardVariableHideDontHide
}
}
func transformVariableRefreshToEnum(refresh interface{}) dashv2alpha1.DashboardVariableRefresh {
switch v := refresh.(type) {
case int:
switch v {
case 0:
return dashv2alpha1.DashboardVariableRefreshNever
case 1:
return dashv2alpha1.DashboardVariableRefreshOnDashboardLoad
case 2:
return dashv2alpha1.DashboardVariableRefreshOnTimeRangeChanged
default:
return dashv2alpha1.DashboardVariableRefreshNever
}
case float64:
return transformVariableRefreshToEnum(int(v))
case string:
switch v {
case "never", "":
return dashv2alpha1.DashboardVariableRefreshNever
case "onDashboardLoad":
return dashv2alpha1.DashboardVariableRefreshOnDashboardLoad
case "onTimeRangeChanged":
return dashv2alpha1.DashboardVariableRefreshOnTimeRangeChanged
default:
return dashv2alpha1.DashboardVariableRefreshNever
}
default:
return dashv2alpha1.DashboardVariableRefreshNever
}
}
func transformVariableSortToEnum(sort interface{}) dashv2alpha1.DashboardVariableSort {
switch v := sort.(type) {
case int:
switch v {
case 0:
return dashv2alpha1.DashboardVariableSortDisabled
case 1:
return dashv2alpha1.DashboardVariableSortAlphabeticalAsc
case 2:
return dashv2alpha1.DashboardVariableSortAlphabeticalDesc
case 3:
return dashv2alpha1.DashboardVariableSortNumericalAsc
case 4:
return dashv2alpha1.DashboardVariableSortNumericalDesc
default:
return dashv2alpha1.DashboardVariableSortDisabled
}
case float64:
return transformVariableSortToEnum(int(v))
case string:
switch v {
case "disabled", "":
return dashv2alpha1.DashboardVariableSortDisabled
case "alphabeticalAsc":
return dashv2alpha1.DashboardVariableSortAlphabeticalAsc
case "alphabeticalDesc":
return dashv2alpha1.DashboardVariableSortAlphabeticalDesc
case "numericalAsc":
return dashv2alpha1.DashboardVariableSortNumericalAsc
case "numericalDesc":
return dashv2alpha1.DashboardVariableSortNumericalDesc
case "alphabeticalCaseInsensitiveAsc":
return dashv2alpha1.DashboardVariableSortAlphabeticalCaseInsensitiveAsc
case "alphabeticalCaseInsensitiveDesc":
return dashv2alpha1.DashboardVariableSortAlphabeticalCaseInsensitiveDesc
case "naturalAsc":
return dashv2alpha1.DashboardVariableSortNaturalAsc
case "naturalDesc":
return dashv2alpha1.DashboardVariableSortNaturalDesc
default:
return dashv2alpha1.DashboardVariableSortDisabled
}
default:
return dashv2alpha1.DashboardVariableSortDisabled
}
}
func transformVariables(ctx context.Context, dashboard map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) ([]dashv2alpha1.DashboardVariableKind, error) {
templating, ok := dashboard["templating"].(map[string]interface{})
if !ok {
return []dashv2alpha1.DashboardVariableKind{}, nil
}
list, ok := templating["list"].([]interface{})
if !ok {
return []dashv2alpha1.DashboardVariableKind{}, nil
}
variables := make([]dashv2alpha1.DashboardVariableKind, 0, len(list))
for _, v := range list {
varMap, ok := v.(map[string]interface{})
if !ok {
continue
}
// Extract common properties
commonProps := extractCommonVariableProperties(varMap)
varType := schemaversion.GetStringValue(varMap, "type")
switch varType {
case "query":
if queryVar, err := buildQueryVariable(ctx, varMap, commonProps, dsIndexProvider); err == nil {
variables = append(variables, queryVar)
}
case "datasource":
if dsVar, err := buildDatasourceVariable(ctx, varMap, commonProps, dsIndexProvider); err == nil {
variables = append(variables, dsVar)
}
case "custom":
if customVar, err := buildCustomVariable(varMap, commonProps); err == nil {
variables = append(variables, customVar)
}
case "adhoc":
if adhocVar, err := buildAdhocVariable(ctx, varMap, commonProps, dsIndexProvider); err == nil {
variables = append(variables, adhocVar)
}
case "constant":
if constantVar, err := buildConstantVariable(varMap, commonProps); err == nil {
variables = append(variables, constantVar)
}
case "interval":
if intervalVar, err := buildIntervalVariable(varMap, commonProps); err == nil {
variables = append(variables, intervalVar)
}
case "textbox":
if textVar, err := buildTextVariable(varMap, commonProps); err == nil {
variables = append(variables, textVar)
}
case "switch":
if switchVar, err := buildSwitchVariable(varMap, commonProps); err == nil {
variables = append(variables, switchVar)
}
case "groupby":
if groupByVar, err := buildGroupByVariable(ctx, varMap, commonProps, dsIndexProvider); err == nil {
variables = append(variables, groupByVar)
}
default:
// Skip unknown variable types
continue
}
}
return variables, nil
}
// Common variable properties extraction
type CommonVariableProperties struct {
Name string
Label *string
Description *string
Hide dashv2alpha1.DashboardVariableHide
SkipUrlSync bool
}
func extractCommonVariableProperties(varMap map[string]interface{}) CommonVariableProperties {
props := CommonVariableProperties{
Name: schemaversion.GetStringValue(varMap, "name"),
Hide: transformVariableHideToEnum(varMap["hide"]),
SkipUrlSync: getBoolField(varMap, "skipUrlSync", false),
}
if label := schemaversion.GetStringValue(varMap, "label"); label != "" {
props.Label = &label
}
if description := schemaversion.GetStringValue(varMap, "description"); description != "" {
props.Description = &description
}
return props
}
// Helper function to build variable options
func buildVariableOptions(options interface{}) []dashv2alpha1.DashboardVariableOption {
if options == nil {
return []dashv2alpha1.DashboardVariableOption{}
}
optionsSlice, ok := options.([]interface{})
if !ok {
return []dashv2alpha1.DashboardVariableOption{}
}
result := make([]dashv2alpha1.DashboardVariableOption, 0, len(optionsSlice))
for _, opt := range optionsSlice {
if optMap, ok := opt.(map[string]interface{}); ok {
selected := getBoolField(optMap, "selected", false)
option := dashv2alpha1.DashboardVariableOption{
Text: buildStringOrArrayOfString(optMap["text"]),
Value: buildStringOrArrayOfString(optMap["value"]),
Selected: &selected,
}
result = append(result, option)
}
}
return result
}
// Helper function to build DashboardStringOrArrayOfString
func buildStringOrArrayOfString(value interface{}) dashv2alpha1.DashboardStringOrArrayOfString {
switch v := value.(type) {
case string:
return dashv2alpha1.DashboardStringOrArrayOfString{
String: &v,
}
case []interface{}:
stringArray := make([]string, 0, len(v))
for _, item := range v {
if str, ok := item.(string); ok {
stringArray = append(stringArray, str)
}
}
return dashv2alpha1.DashboardStringOrArrayOfString{
ArrayOfString: stringArray,
}
case map[string]interface{}:
// Handle legacy string value format
if legacyValue, ok := v[LEGACY_STRING_VALUE_KEY].(string); ok {
return dashv2alpha1.DashboardStringOrArrayOfString{
String: &legacyValue,
}
}
// Fallback for other map types
empty := ""
return dashv2alpha1.DashboardStringOrArrayOfString{
String: &empty,
}
default:
empty := ""
return dashv2alpha1.DashboardStringOrArrayOfString{
String: &empty,
}
}
}
// Helper function to build variable current value
func buildVariableCurrent(current interface{}) dashv2alpha1.DashboardVariableOption {
if current == nil {
empty := ""
return dashv2alpha1.DashboardVariableOption{
Text: dashv2alpha1.DashboardStringOrArrayOfString{String: &empty},
Value: dashv2alpha1.DashboardStringOrArrayOfString{String: &empty},
}
}
if currentMap, ok := current.(map[string]interface{}); ok {
return dashv2alpha1.DashboardVariableOption{
Text: buildStringOrArrayOfString(currentMap["text"]),
Value: buildStringOrArrayOfString(currentMap["value"]),
}
}
// Fallback for simple values
empty := ""
return dashv2alpha1.DashboardVariableOption{
Text: dashv2alpha1.DashboardStringOrArrayOfString{String: &empty},
Value: dashv2alpha1.DashboardStringOrArrayOfString{String: &empty},
}
}
// Helper function to build DataQuery kind
func buildDataQueryKind(query interface{}, datasourceType string) dashv2alpha1.DashboardDataQueryKind {
var querySpec map[string]interface{}
switch q := query.(type) {
case string:
// Only include LEGACY_STRING_VALUE_KEY if query is a non-empty string
querySpec = make(map[string]interface{})
if q != "" {
querySpec[LEGACY_STRING_VALUE_KEY] = q
}
case map[string]interface{}:
// Use query directly to match frontend behavior
querySpec = q
default:
querySpec = make(map[string]interface{})
}
// Use datasourceType as the kind (datasource type, e.g., "prometheus", "elasticsearch")
kind := datasourceType
return dashv2alpha1.DashboardDataQueryKind{
Kind: kind,
Spec: querySpec,
}
}
// Helper function to build DataQuery kind for variables (filters out refId)
func buildDataQueryKindForVariable(query interface{}, datasourceType string) dashv2alpha1.DashboardDataQueryKind {
var querySpec map[string]interface{}
switch q := query.(type) {
case string:
// Only include LEGACY_STRING_VALUE_KEY if query is a non-empty string
querySpec = make(map[string]interface{})
if q != "" {
querySpec[LEGACY_STRING_VALUE_KEY] = q
}
case map[string]interface{}:
// Filter out refId for variables (refId should only be in panel queries)
querySpec = q
default:
querySpec = make(map[string]interface{})
}
// Use datasourceType as the kind (datasource type, e.g., "prometheus", "elasticsearch")
kind := datasourceType
return dashv2alpha1.DashboardDataQueryKind{
Kind: kind,
Spec: querySpec,
}
}
// Query Variable
func buildQueryVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardVariableKind, error) {
datasource := varMap["datasource"]
var datasourceType, datasourceUID string
if ds, ok := datasource.(map[string]interface{}); ok {
datasourceUID = schemaversion.GetStringValue(ds, "uid")
datasourceType = schemaversion.GetStringValue(ds, "type")
// If we have a UID, use it to get the correct type from the datasource service
// BUT: Don't try to resolve types for template variables
if datasourceUID != "" && datasourceType == "" && !isTemplateVariable(datasourceUID) {
datasourceType = getDatasourceTypeByUID(ctx, datasourceUID, dsIndexProvider)
} else if datasourceUID == "" && datasourceType == "" {
// If no UID and no type, use default
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
}
} else if dsStr, ok := datasource.(string); ok && isTemplateVariable(dsStr) {
// Handle datasource variable reference (e.g., "$datasource")
// Only process template variables - other string values are not supported in V2 format
datasourceUID = dsStr
} else {
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
}
queryVar := &dashv2alpha1.DashboardQueryVariableKind{
Kind: "QueryVariable",
Spec: dashv2alpha1.DashboardQueryVariableSpec{
Name: commonProps.Name,
Label: commonProps.Label,
Description: commonProps.Description,
Hide: commonProps.Hide,
SkipUrlSync: commonProps.SkipUrlSync,
Current: buildVariableCurrent(varMap["current"]),
Multi: getBoolField(varMap, "multi", false),
IncludeAll: getBoolField(varMap, "includeAll", false),
Refresh: transformVariableRefreshToEnum(varMap["refresh"]),
Sort: transformVariableSortToEnum(varMap["sort"]),
Regex: schemaversion.GetStringValue(varMap, "regex"),
RegexApplyTo: getUnionField[dashv2alpha1.DashboardVariableRegexApplyTo](varMap, "regexApplyTo"),
Query: buildDataQueryKindForVariable(varMap["query"], datasourceType),
AllowCustomValue: getBoolField(varMap, "allowCustomValue", true),
},
}
// Only set definition if it exists in the input (match frontend behavior)
if def, exists := varMap["definition"]; exists && def != nil {
if defStr, ok := def.(string); ok {
queryVar.Spec.Definition = &defStr
}
}
// Only include datasource if datasourceUID exists (matching frontend behavior)
if datasourceUID != "" {
dsRef := &dashv2alpha1.DashboardDataSourceRef{
Type: &datasourceType,
Uid: &datasourceUID,
}
queryVar.Spec.Datasource = dsRef
}
// Always set options (matching frontend behavior)
queryVar.Spec.Options = buildVariableOptions(varMap["options"])
if allValue := schemaversion.GetStringValue(varMap, "allValue"); allValue != "" {
queryVar.Spec.AllValue = &allValue
}
return dashv2alpha1.DashboardVariableKind{
QueryVariableKind: queryVar,
}, nil
}
// Datasource Variable
func buildDatasourceVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardVariableKind, error) {
pluginId := getDefaultDatasourceType(ctx, dsIndexProvider)
if query := varMap["query"]; query != nil {
if queryStr, ok := query.(string); ok {
pluginId = queryStr
}
}
dsVar := &dashv2alpha1.DashboardDatasourceVariableKind{
Kind: "DatasourceVariable",
Spec: dashv2alpha1.DashboardDatasourceVariableSpec{
Name: commonProps.Name,
Label: commonProps.Label,
Description: commonProps.Description,
Hide: commonProps.Hide,
SkipUrlSync: commonProps.SkipUrlSync,
PluginId: pluginId,
Current: buildVariableCurrent(varMap["current"]),
Multi: getBoolField(varMap, "multi", false),
IncludeAll: getBoolField(varMap, "includeAll", false),
Refresh: transformVariableRefreshToEnum(varMap["refresh"]),
Regex: schemaversion.GetStringValue(varMap, "regex"),
AllowCustomValue: getBoolField(varMap, "allowCustomValue", true),
},
}
// Always set options (matching frontend behavior)
dsVar.Spec.Options = buildVariableOptions(varMap["options"])
if allValue := schemaversion.GetStringValue(varMap, "allValue"); allValue != "" {
dsVar.Spec.AllValue = &allValue
}
return dashv2alpha1.DashboardVariableKind{
DatasourceVariableKind: dsVar,
}, nil
}
// Custom Variable
func buildCustomVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) {
customVar := &dashv2alpha1.DashboardCustomVariableKind{
Kind: "CustomVariable",
Spec: dashv2alpha1.DashboardCustomVariableSpec{
Name: commonProps.Name,
Label: commonProps.Label,
Description: commonProps.Description,
Hide: commonProps.Hide,
SkipUrlSync: commonProps.SkipUrlSync,
Query: schemaversion.GetStringValue(varMap, "query"),
Current: buildVariableCurrent(varMap["current"]),
Multi: getBoolField(varMap, "multi", false),
IncludeAll: getBoolField(varMap, "includeAll", false),
AllowCustomValue: getBoolField(varMap, "allowCustomValue", true),
},
}
// Always set options (matching frontend behavior)
customVar.Spec.Options = buildVariableOptions(varMap["options"])
if allValue := schemaversion.GetStringValue(varMap, "allValue"); allValue != "" {
customVar.Spec.AllValue = &allValue
}
if valuesFormat := schemaversion.GetStringValue(varMap, "valuesFormat"); valuesFormat != "" {
switch valuesFormat {
case string(dashv2alpha1.DashboardCustomVariableSpecValuesFormatJson):
format := dashv2alpha1.DashboardCustomVariableSpecValuesFormatJson
customVar.Spec.ValuesFormat = &format
case string(dashv2alpha1.DashboardCustomVariableSpecValuesFormatCsv):
format := dashv2alpha1.DashboardCustomVariableSpecValuesFormatCsv
customVar.Spec.ValuesFormat = &format
}
}
return dashv2alpha1.DashboardVariableKind{
CustomVariableKind: customVar,
}, nil
}
// Constant Variable
func buildConstantVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) {
// Get query value - if missing, it should be undefined (not empty string)
// Frontend uses variable.query for value, so if query is missing, value is undefined
var queryValue interface{}
if query, exists := varMap["query"]; exists {
if queryStr, ok := query.(string); ok {
queryValue = queryStr
} else {
queryValue = query
}
}
// Build current from query value (matching frontend: value: variable.query, text: String(variable.state.value))
// If query is undefined, frontend produces: text: "undefined", value: undefined (removed), query: undefined (removed)
var current dashv2alpha1.DashboardVariableOption
if currentVal, exists := varMap["current"]; exists && currentVal != nil {
// Use provided current if it exists
current = buildVariableCurrent(currentVal)
} else if queryValue != nil {
// Build current from query value (matching frontend behavior)
queryStr := ""
if str, ok := queryValue.(string); ok {
queryStr = str
}
current = dashv2alpha1.DashboardVariableOption{
Text: dashv2alpha1.DashboardStringOrArrayOfString{String: &queryStr},
Value: dashv2alpha1.DashboardStringOrArrayOfString{String: &queryStr},
}
} else {
// query is undefined - match frontend: value ? String(value) : '' (defaults to empty string, not "undefined")
emptyStr := ""
current = dashv2alpha1.DashboardVariableOption{
Text: dashv2alpha1.DashboardStringOrArrayOfString{String: &emptyStr},
Value: dashv2alpha1.DashboardStringOrArrayOfString{String: &emptyStr},
}
}
// Get query value - frontend sets query: variable.state.value which can be undefined
// Since Query is a required string field, we must set it, but when query is missing from v1,
// we set it to empty string (frontend would have undefined which gets removed by sortedDeepCloneWithoutNulls)
var query string
if queryValue != nil {
if queryStr, ok := queryValue.(string); ok {
query = queryStr
}
}
// If queryValue is nil, query remains empty string ""
constantVar := &dashv2alpha1.DashboardConstantVariableKind{
Kind: "ConstantVariable",
Spec: dashv2alpha1.DashboardConstantVariableSpec{
Name: commonProps.Name,
Label: commonProps.Label,
Description: commonProps.Description,
Hide: commonProps.Hide,
SkipUrlSync: commonProps.SkipUrlSync,
Query: query,
Current: current,
},
}
return dashv2alpha1.DashboardVariableKind{
ConstantVariableKind: constantVar,
}, nil
}
// Interval Variable
func buildIntervalVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) {
query := schemaversion.GetStringValue(varMap, "query")
// If query is missing/empty but options exist, generate query from options (matches frontend behavior)
if query == "" {
if options, ok := varMap["options"].([]interface{}); ok && len(options) > 0 {
queryValues := make([]string, 0, len(options))
for _, opt := range options {
if optMap, ok := opt.(map[string]interface{}); ok {
if value, exists := optMap["value"]; exists {
if valueStr, ok := value.(string); ok && valueStr != "" {
queryValues = append(queryValues, valueStr)
}
} else if text, exists := optMap["text"]; exists {
if textStr, ok := text.(string); ok && textStr != "" {
queryValues = append(queryValues, textStr)
}
}
}
}
if len(queryValues) > 0 {
// Join interval values with comma, matching frontend getIntervalsQueryFromNewIntervalModel
query = strings.Join(queryValues, ",")
}
}
}
intervalVar := &dashv2alpha1.DashboardIntervalVariableKind{
Kind: "IntervalVariable",
Spec: dashv2alpha1.DashboardIntervalVariableSpec{
Name: commonProps.Name,
Label: commonProps.Label,
Description: commonProps.Description,
Hide: commonProps.Hide,
SkipUrlSync: commonProps.SkipUrlSync,
Query: query,
Current: buildVariableCurrent(varMap["current"]),
Refresh: dashv2alpha1.DashboardVariableRefreshOnTimeRangeChanged, // Always onTimeRangeChanged for interval
Auto: getBoolField(varMap, "auto", false),
AutoMin: schemaversion.GetStringValue(varMap, "auto_min"),
AutoCount: int64(getIntField(varMap, "auto_count", 0)),
},
}
// Always set options (matching frontend behavior)
intervalVar.Spec.Options = buildVariableOptions(varMap["options"])
return dashv2alpha1.DashboardVariableKind{
IntervalVariableKind: intervalVar,
}, nil
}
// Text Variable
func buildTextVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) {
textVar := &dashv2alpha1.DashboardTextVariableKind{
Kind: "TextVariable",
Spec: dashv2alpha1.DashboardTextVariableSpec{
Name: commonProps.Name,
Label: commonProps.Label,
Description: commonProps.Description,
Hide: commonProps.Hide,
SkipUrlSync: commonProps.SkipUrlSync,
Query: schemaversion.GetStringValue(varMap, "query"),
Current: buildVariableCurrent(varMap["current"]),
},
}
return dashv2alpha1.DashboardVariableKind{
TextVariableKind: textVar,
}, nil
}
// Helper function to extract string value from an option map (value or text field)
func getOptionValue(optMap map[string]interface{}) string {
if val, ok := optMap["value"].(string); ok && val != "" {
return val
}
if val, ok := optMap["text"].(string); ok && val != "" {
return val
}
return ""
}
// Switch Variable
func buildSwitchVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) {
current := ""
if currentVal, exists := varMap["current"]; exists {
if currentMap, ok := currentVal.(map[string]interface{}); ok {
current = getOptionValue(currentMap)
}
}
// In V1 the enabled value is the first value of the options array,
// while the disabled value is second one.
// (Falling back to "true" and "false" if options are not available)
enabledValue := "true"
disabledValue := "false"
if options, ok := varMap["options"].([]interface{}); ok {
// Get enabledValue from first option
if len(options) > 0 {
if opt1, ok := options[0].(map[string]interface{}); ok {
if val := getOptionValue(opt1); val != "" {
enabledValue = val
}
}
}
// Get disabledValue from second option
if len(options) > 1 {
if opt2, ok := options[1].(map[string]interface{}); ok {
if val := getOptionValue(opt2); val != "" {
disabledValue = val
}
}
}
}
// Set current to disabledValue if not set
if current == "" {
current = disabledValue
}
switchVar := &dashv2alpha1.DashboardSwitchVariableKind{
Kind: "SwitchVariable",
Spec: dashv2alpha1.DashboardSwitchVariableSpec{
Name: commonProps.Name,
Current: current,
EnabledValue: enabledValue,
DisabledValue: disabledValue,
Label: commonProps.Label,
Description: commonProps.Description,
Hide: commonProps.Hide,
SkipUrlSync: commonProps.SkipUrlSync,
},
}
return dashv2alpha1.DashboardVariableKind{
SwitchVariableKind: switchVar,
}, nil
}
// Adhoc Variable
func buildAdhocVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardVariableKind, error) {
datasource := varMap["datasource"]
var datasourceType, datasourceUID string
if ds, ok := datasource.(map[string]interface{}); ok {
datasourceUID = schemaversion.GetStringValue(ds, "uid")
datasourceType = schemaversion.GetStringValue(ds, "type")
// If we have a UID, use it to get the correct type from the datasource service
// BUT: Don't try to resolve types for template variables
if datasourceUID != "" && datasourceType == "" && !isTemplateVariable(datasourceUID) {
datasourceType = getDatasourceTypeByUID(ctx, datasourceUID, dsIndexProvider)
} else if datasourceUID == "" && datasourceType == "" {
// If no UID and no type, use default
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
}
} else if dsStr, ok := datasource.(string); ok && isTemplateVariable(dsStr) {
// Handle datasource variable reference (e.g., "$datasource")
// Only process template variables - other string values are not supported in V2 format
datasourceUID = dsStr
} else {
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
}
adhocVar := &dashv2alpha1.DashboardAdhocVariableKind{
Kind: "AdhocVariable",
Spec: dashv2alpha1.DashboardAdhocVariableSpec{
Name: commonProps.Name,
Label: commonProps.Label,
Description: commonProps.Description,
Hide: commonProps.Hide,
SkipUrlSync: commonProps.SkipUrlSync,
AllowCustomValue: getBoolField(varMap, "allowCustomValue", true),
},
}
// Transform baseFilters if they exist, otherwise default to empty array
if baseFilters, exists := varMap["baseFilters"]; exists {
if baseFiltersArray, ok := baseFilters.([]interface{}); ok {
adhocVar.Spec.BaseFilters = transformAdHocFilters(baseFiltersArray)
}
}
// Ensure baseFilters is always set (default to empty array if not present or invalid)
if adhocVar.Spec.BaseFilters == nil {
adhocVar.Spec.BaseFilters = []dashv2alpha1.DashboardAdHocFilterWithLabels{}
}
// Transform filters if they exist, otherwise default to empty array
if filters, exists := varMap["filters"]; exists {
if filtersArray, ok := filters.([]interface{}); ok {
adhocVar.Spec.Filters = transformAdHocFilters(filtersArray)
}
}
// Ensure filters is always set (default to empty array if not present or invalid)
if adhocVar.Spec.Filters == nil {
adhocVar.Spec.Filters = []dashv2alpha1.DashboardAdHocFilterWithLabels{}
}
// Transform defaultKeys if they exist, otherwise default to empty array
if defaultKeys, exists := varMap["defaultKeys"]; exists {
if defaultKeysArray, ok := defaultKeys.([]interface{}); ok {
adhocVar.Spec.DefaultKeys = transformMetricFindValues(defaultKeysArray)
}
}
// Ensure defaultKeys is always set (default to empty array if not present or invalid)
if adhocVar.Spec.DefaultKeys == nil {
adhocVar.Spec.DefaultKeys = []dashv2alpha1.DashboardMetricFindValue{}
}
// Only include datasource if datasourceUID exists (matching frontend behavior)
if datasourceUID != "" {
dsRef := &dashv2alpha1.DashboardDataSourceRef{
Type: &datasourceType,
Uid: &datasourceUID,
}
adhocVar.Spec.Datasource = dsRef
}
return dashv2alpha1.DashboardVariableKind{
AdhocVariableKind: adhocVar,
}, nil
}
// Helper function to transform adhoc filters
func transformAdHocFilters(filters []interface{}) []dashv2alpha1.DashboardAdHocFilterWithLabels {
result := make([]dashv2alpha1.DashboardAdHocFilterWithLabels, 0, len(filters))
for _, filter := range filters {
if filterMap, ok := filter.(map[string]interface{}); ok {
// Only include filters that don't have an origin or have origin "dashboard"
// This matches the frontend validateFiltersOrigin logic
if origin, exists := filterMap["origin"]; !exists || origin == "dashboard" {
adhocFilter := dashv2alpha1.DashboardAdHocFilterWithLabels{
Key: schemaversion.GetStringValue(filterMap, "key"),
Operator: schemaversion.GetStringValue(filterMap, "operator", "="),
Value: schemaversion.GetStringValue(filterMap, "value"),
}
// Handle optional fields
if keyLabel := schemaversion.GetStringValue(filterMap, "keyLabel"); keyLabel != "" {
adhocFilter.KeyLabel = &keyLabel
}
if condition := schemaversion.GetStringValue(filterMap, "condition"); condition != "" {
adhocFilter.Condition = &condition
}
// Handle multi-value filters
if values, exists := filterMap["values"]; exists {
if valuesArray, ok := values.([]interface{}); ok {
stringValues := make([]string, 0, len(valuesArray))
for _, v := range valuesArray {
if str, ok := v.(string); ok {
stringValues = append(stringValues, str)
}
}
if len(stringValues) > 0 {
adhocFilter.Values = stringValues
}
}
}
// Handle value labels for multi-value filters
if valueLabels, exists := filterMap["valueLabels"]; exists {
if valueLabelsArray, ok := valueLabels.([]interface{}); ok {
stringValueLabels := make([]string, 0, len(valueLabelsArray))
for _, v := range valueLabelsArray {
if str, ok := v.(string); ok {
stringValueLabels = append(stringValueLabels, str)
}
}
if len(stringValueLabels) > 0 {
adhocFilter.ValueLabels = stringValueLabels
}
}
}
result = append(result, adhocFilter)
}
}
}
return result
}
// Helper function to transform metric find values
func transformMetricFindValues(values []interface{}) []dashv2alpha1.DashboardMetricFindValue {
result := make([]dashv2alpha1.DashboardMetricFindValue, 0, len(values))
for _, value := range values {
if valueMap, ok := value.(map[string]interface{}); ok {
// Convert object format to string format to match frontend behavior
text := schemaversion.GetStringValue(valueMap, "text")
if text != "" {
// Use text as the string value to match frontend behavior
metricFindValue := dashv2alpha1.DashboardMetricFindValue{
Text: text,
Value: &dashv2alpha1.DashboardStringOrFloat64{
String: &text,
},
}
result = append(result, metricFindValue)
}
} else if str, ok := value.(string); ok {
// Handle simple string values - convert to object format with text and value
metricFindValue := dashv2alpha1.DashboardMetricFindValue{
Text: str,
Value: &dashv2alpha1.DashboardStringOrFloat64{
String: &str,
},
}
result = append(result, metricFindValue)
}
}
return result
}
// GroupBy Variable
func buildGroupByVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardVariableKind, error) {
datasource := varMap["datasource"]
var datasourceType, datasourceUID string
if ds, ok := datasource.(map[string]interface{}); ok {
datasourceUID = schemaversion.GetStringValue(ds, "uid")
datasourceType = schemaversion.GetStringValue(ds, "type")
// If we have a UID, use it to get the correct type from the datasource service
// BUT: Don't try to resolve types for template variables
if datasourceUID != "" && datasourceType == "" && !isTemplateVariable(datasourceUID) {
datasourceType = getDatasourceTypeByUID(ctx, datasourceUID, dsIndexProvider)
} else if datasourceUID == "" && datasourceType == "" {
// If no UID and no type, use default
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
}
// Resolve Grafana datasource UID when type is "datasource" and UID is empty
datasourceUID = resolveGrafanaDatasourceUID(datasourceType, datasourceUID)
} else if dsStr, ok := datasource.(string); ok && isTemplateVariable(dsStr) {
// Handle datasource variable reference (e.g., "$datasource")
// Only process template variables - other string values are not supported in V2 format
datasourceUID = dsStr
} else {
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
}
groupByVar := &dashv2alpha1.DashboardGroupByVariableKind{
Kind: "GroupByVariable",
Spec: dashv2alpha1.DashboardGroupByVariableSpec{
Name: commonProps.Name,
Label: commonProps.Label,
Description: commonProps.Description,
Hide: commonProps.Hide,
SkipUrlSync: commonProps.SkipUrlSync,
Current: buildVariableCurrent(varMap["current"]),
// We set it to true by default because GroupByVariable
// constructor defaults to multi: true
Multi: getBoolField(varMap, "multi", true),
},
}
// Only include datasource if datasourceUID exists
if datasourceUID != "" {
dsRef := &dashv2alpha1.DashboardDataSourceRef{
Type: &datasourceType,
Uid: &datasourceUID,
}
groupByVar.Spec.Datasource = dsRef
}
// Always set options (matching frontend behavior)
groupByVar.Spec.Options = buildVariableOptions(varMap["options"])
return dashv2alpha1.DashboardVariableKind{
GroupByVariableKind: groupByVar,
}, nil
}
func transformAnnotations(dashboard map[string]interface{}) ([]dashv2alpha1.DashboardAnnotationQueryKind, error) {
annotations, ok := dashboard["annotations"].(map[string]interface{})
if !ok {
// Return empty array to match frontend behavior
return []dashv2alpha1.DashboardAnnotationQueryKind{}, nil
}
list, ok := annotations["list"].([]interface{})
if !ok || len(list) == 0 {
// Return empty array to match frontend behavior
return []dashv2alpha1.DashboardAnnotationQueryKind{}, nil
}
result := make([]dashv2alpha1.DashboardAnnotationQueryKind, 0, len(list))
for _, a := range list {
annotationMap, ok := a.(map[string]interface{})
if !ok {
continue
}
if annotationQuery, err := buildAnnotationQuery(annotationMap); err == nil {
result = append(result, annotationQuery)
}
}
return result, nil
}
func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.DashboardAnnotationQueryKind, error) {
// Extract datasource information
var datasourceRef *dashv2alpha1.DashboardDataSourceRef
var datasourceType, datasourceUID string
if datasource, ok := annotationMap["datasource"].(map[string]interface{}); ok {
datasourceUID = schemaversion.GetStringValue(datasource, "uid")
datasourceType = schemaversion.GetStringValue(datasource, "type")
// Only create datasource ref if the annotation actually has a valid datasource type in the original input
// This matches frontend behavior: if (dsUID && annotation.datasource && annotation.datasource.type)
// We don't try to resolve datasources that are missing a type - they should be resolved at runtime
if datasourceUID != "" && datasourceType != "" {
datasourceRef = &dashv2alpha1.DashboardDataSourceRef{
Type: &datasourceType,
Uid: &datasourceUID,
}
}
}
// Build the query from target
var query *dashv2alpha1.DashboardDataQueryKind
// Use datasourceType as the kind (datasource type, e.g., "prometheus", "grafana")
kind := datasourceType
if target, ok := annotationMap["target"].(map[string]interface{}); ok && target != nil {
queryKind := dashv2alpha1.DashboardDataQueryKind{
Kind: kind,
Spec: target,
}
// Group field is not available in v2alpha1 DashboardDataQueryKind
query = &queryKind
} else {
// Always provide a query to match frontend behavior
queryKind := dashv2alpha1.DashboardDataQueryKind{
Kind: kind,
Spec: map[string]interface{}{},
}
// Group field is not available in v2alpha1 DashboardDataQueryKind
query = &queryKind
}
// Transform filter
var filter *dashv2alpha1.DashboardAnnotationPanelFilter
if filterMap, ok := annotationMap["filter"].(map[string]interface{}); ok && filterMap != nil {
filter = buildAnnotationFilter(filterMap)
}
// Transform mappings
var mappings map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping
if mappingsMap, ok := annotationMap["mappings"].(map[string]interface{}); ok && mappingsMap != nil {
mappings = convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
}
// Transform builtIn from float64 to bool
var builtInPtr *bool
if builtInVal, ok := annotationMap["builtIn"]; ok && builtInVal != nil {
switch v := builtInVal.(type) {
case float64:
val := v != 0
builtInPtr = &val
case int:
val := v != 0
builtInPtr = &val
case bool:
builtInPtr = &v
}
}
defaultAnnotationQuerySpec := dashv2alpha1.NewDashboardAnnotationQuerySpec()
spec := dashv2alpha1.DashboardAnnotationQuerySpec{
Name: schemaversion.GetStringValue(annotationMap, "name"),
Datasource: datasourceRef,
Query: query,
Enable: getBoolField(annotationMap, "enable", defaultAnnotationQuerySpec.Enable), // Default to false to match frontend: Boolean(annotation.enable) || Boolean(override?.enable)
Hide: getBoolField(annotationMap, "hide", defaultAnnotationQuerySpec.Hide),
IconColor: schemaversion.GetStringValue(annotationMap, "iconColor", defaultAnnotationQuerySpec.IconColor),
BuiltIn: builtInPtr,
Filter: filter,
Mappings: mappings,
}
// Handle any additional properties in LegacyOptions
legacyOptions := make(map[string]interface{})
if annotationType := schemaversion.GetStringValue(annotationMap, "type"); annotationType != "" {
legacyOptions["type"] = annotationType
}
// Add other legacy fields if they exist
for key, value := range annotationMap {
switch key {
case "name", "datasource", "enable", "hide", "iconColor", "filter", "target", "builtIn", "type", "mappings":
// Skip already handled fields
default:
legacyOptions[key] = value
}
}
if len(legacyOptions) > 0 {
spec.LegacyOptions = legacyOptions
}
return dashv2alpha1.DashboardAnnotationQueryKind{
Kind: "AnnotationQuery",
Spec: spec,
}, nil
}
func buildAnnotationFilter(filterMap map[string]interface{}) *dashv2alpha1.DashboardAnnotationPanelFilter {
filter := &dashv2alpha1.DashboardAnnotationPanelFilter{
Ids: []uint32{},
}
if exclude := getBoolField(filterMap, "exclude", false); exclude {
filter.Exclude = &exclude
}
if ids, ok := filterMap["ids"].([]interface{}); ok {
uintIds := make([]uint32, 0, len(ids))
for _, id := range ids {
switch v := id.(type) {
case float64:
if v >= 0 && v <= float64(^uint32(0)) {
uintIds = append(uintIds, uint32(v))
}
case int:
if v >= 0 && uint64(v) <= math.MaxUint32 {
uintIds = append(uintIds, uint32(v))
}
}
}
filter.Ids = uintIds
}
return filter
}
func convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap map[string]interface{}) map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping {
mappings := make(map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping)
for key, value := range mappingsMap {
mapping := dashv2alpha1.DashboardAnnotationEventFieldMapping{}
// Handle simple string format (v1beta1 legacy format: "fieldName": "targetFieldName")
if valueStr, ok := value.(string); ok && valueStr != "" {
// Simple string mapping: treat as field source with the value as the field name
defaultSource := "field"
mapping.Source = &defaultSource
mapping.Value = &valueStr
mappings[key] = mapping
continue
}
// Handle object format (v2alpha1 format: "fieldName": {"source": "field", "value": "...", "regex": "..."})
mappingMap, ok := value.(map[string]interface{})
if !ok {
continue
}
// Extract source (defaults to "field" if not specified)
if source, ok := mappingMap["source"].(string); ok && source != "" {
mapping.Source = &source
} else {
defaultSource := "field"
mapping.Source = &defaultSource
}
// Extract value (optional)
if valueStr, ok := mappingMap["value"].(string); ok && valueStr != "" {
mapping.Value = &valueStr
}
// Extract regex (optional)
if regex, ok := mappingMap["regex"].(string); ok && regex != "" {
mapping.Regex = &regex
}
mappings[key] = mapping
}
return mappings
}
// Panel helper functions
func transformPanelQueries(ctx context.Context, panelMap map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) []dashv2alpha1.DashboardPanelQueryKind {
targets, ok := panelMap["targets"].([]interface{})
if !ok {
return []dashv2alpha1.DashboardPanelQueryKind{}
}
// Get panel datasource
var panelDatasource *dashv2alpha1.DashboardDataSourceRef
ds, dsExists := panelMap["datasource"]
if dsExists && ds != nil {
if dsMap, ok := ds.(map[string]interface{}); ok {
// Handle panel datasource as object
dsUID := schemaversion.GetStringValue(dsMap, "uid")
dsType := schemaversion.GetStringValue(dsMap, "type")
// Check if datasource object is effectively empty (no uid and no type)
// Empty objects {} should be preserved as empty, not converted to defaults
isEmpty := dsUID == "" && dsType == ""
// If we have a UID, use it to get the correct type from the datasource service
// BUT: Don't try to resolve types for template variables
if dsUID != "" && dsType == "" && !isTemplateVariable(dsUID) {
dsType = getDatasourceTypeByUID(ctx, dsUID, dsIndexProvider)
} else if !isEmpty && dsUID == "" && dsType == "" {
// Only set default if datasource is missing (not empty object)
// Empty objects {} should remain empty
dsType = getDefaultDatasourceType(ctx, dsIndexProvider)
}
// Resolve Grafana datasource UID when type is "datasource" and UID is empty
// Only resolve if we have a type (not for empty objects)
if !isEmpty {
dsUID = resolveGrafanaDatasourceUID(dsType, dsUID)
}
// Only create panelDatasource if it's not empty after resolution
// Empty objects {} should result in nil panelDatasource
// After resolution, check if we have a type or UID (not just the original isEmpty)
// This ensures that type: "datasource" with empty UID gets resolved to uid: "grafana"
// and panelDatasource is created
if dsType != "" || dsUID != "" {
panelDatasource = &dashv2alpha1.DashboardDataSourceRef{
Type: &dsType,
Uid: &dsUID,
}
}
} else if dsStr, ok := ds.(string); ok && isTemplateVariable(dsStr) {
// Handle legacy panel datasource as string (template variable reference e.g., "$datasource")
// Only process template variables - other string values are not supported in V2 format
panelDatasource = &dashv2alpha1.DashboardDataSourceRef{
Uid: &dsStr,
}
}
}
// Ensure each target has a non-empty refId. We only fill missing refIds;
existingRefIds := make(map[string]bool)
for _, target := range targets {
if targetMap, ok := target.(map[string]interface{}); ok {
if refId := schemaversion.GetStringValue(targetMap, "refId"); refId != "" {
existingRefIds[refId] = true
}
}
}
for _, target := range targets {
targetMap, ok := target.(map[string]interface{})
if !ok {
continue
}
refId := schemaversion.GetStringValue(targetMap, "refId")
if refId == "" {
refId = nextAvailableRefId(existingRefIds)
targetMap["refId"] = refId
existingRefIds[refId] = true
}
}
queries := make([]dashv2alpha1.DashboardPanelQueryKind, 0, len(targets))
for _, target := range targets {
if targetMap, ok := target.(map[string]interface{}); ok {
query := transformSingleQuery(ctx, targetMap, panelDatasource, dsIndexProvider)
queries = append(queries, query)
}
}
return queries
}
// nextAvailableRefId returns the next unused refId using the same sequence as the
// frontend helper (A, B, ..., Z, AA, AB, ...).
func nextAvailableRefId(existing map[string]bool) string {
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
var refIdFromIndex func(num int) string
refIdFromIndex = func(num int) string {
if num < len(letters) {
return string(letters[num])
}
return refIdFromIndex(num/len(letters)-1) + string(letters[num%len(letters)])
}
for i := 0; ; i++ {
refId := refIdFromIndex(i)
if !existing[refId] {
return refId
}
}
}
func transformSingleQuery(ctx context.Context, targetMap map[string]interface{}, panelDatasource *dashv2alpha1.DashboardDataSourceRef, dsIndexProvider schemaversion.DataSourceIndexProvider) dashv2alpha1.DashboardPanelQueryKind {
refId := schemaversion.GetStringValue(targetMap, "refId", "A")
if refId == "" {
refId = "A"
}
hidden := getBoolField(targetMap, "hide", false)
// Extract datasource from query or use panel datasource
var queryDatasourceType string
var queryDatasourceUID string
if ds, ok := targetMap["datasource"].(map[string]interface{}); ok {
queryDatasourceUID = schemaversion.GetStringValue(ds, "uid")
queryDatasourceType = schemaversion.GetStringValue(ds, "type")
// If target datasource is empty object {} (no uid and no type), treat it as missing
// and fall through to use panel datasource (matches frontend behavior in v36 migration)
if queryDatasourceUID == "" && queryDatasourceType == "" {
// Empty datasource object - will use panel datasource below
} else {
// If we have a UID, use it to get the correct type from the datasource service
// BUT: Don't try to resolve types for template variables
if queryDatasourceUID != "" && queryDatasourceType == "" && !isTemplateVariable(queryDatasourceUID) {
queryDatasourceType = getDatasourceTypeByUID(ctx, queryDatasourceUID, dsIndexProvider)
}
// Resolve Grafana datasource UID when type is "datasource" and UID is empty
queryDatasourceUID = resolveGrafanaDatasourceUID(queryDatasourceType, queryDatasourceUID)
}
} else if dsStr, ok := targetMap["datasource"].(string); ok && isTemplateVariable(dsStr) {
// Handle legacy target datasource as string (template variable reference e.g., "$datasource")
// Only process template variables - other string values are not supported in V2 format
queryDatasourceUID = dsStr
}
// Use panel datasource if target datasource is missing or empty
if queryDatasourceUID == "" && queryDatasourceType == "" && panelDatasource != nil {
// Only use panel datasource if it's not a mixed datasource
// Mixed datasources should not be propagated to individual queries
if panelDatasource.Uid != nil && *panelDatasource.Uid != "-- Mixed --" {
if panelDatasource.Type != nil {
queryDatasourceType = *panelDatasource.Type
}
queryDatasourceUID = *panelDatasource.Uid
} else if panelDatasource.Type != nil && *panelDatasource.Type == "datasource" {
// Handle case where panel datasource has type "datasource" but no UID
queryDatasourceType = *panelDatasource.Type
queryDatasourceUID = resolveGrafanaDatasourceUID(*panelDatasource.Type, "")
}
}
// Build query spec by excluding known fields
querySpec := make(map[string]interface{})
for key, value := range targetMap {
switch key {
case "refId", "hide", "datasource":
// Skip these fields as they're handled separately
default:
querySpec[key] = value
}
}
// Create the panel query spec
panelQuerySpec := dashv2alpha1.DashboardPanelQuerySpec{
RefId: refId,
Hidden: hidden,
Query: buildDataQueryKind(querySpec, queryDatasourceType),
}
// Only include datasource reference if UID is provided
if queryDatasourceUID != "" {
panelQuerySpec.Datasource = &dashv2alpha1.DashboardDataSourceRef{
Type: &queryDatasourceType,
Uid: &queryDatasourceUID,
}
}
return dashv2alpha1.DashboardPanelQueryKind{
Kind: "PanelQuery",
Spec: panelQuerySpec,
}
}
func transformPanelTransformations(panelMap map[string]interface{}) []dashv2alpha1.DashboardTransformationKind {
transformations, ok := panelMap["transformations"].([]interface{})
if !ok {
return []dashv2alpha1.DashboardTransformationKind{}
}
result := make([]dashv2alpha1.DashboardTransformationKind, 0, len(transformations))
for _, t := range transformations {
if tMap, ok := t.(map[string]interface{}); ok {
// Extract the transformation ID
transformationId := schemaversion.GetStringValue(tMap, "id")
// Extract the options field specifically
var options interface{}
if opts, exists := tMap["options"]; exists {
options = opts
}
transformationKind := dashv2alpha1.DashboardTransformationKind{
Kind: transformationId,
Spec: dashv2alpha1.DashboardDataTransformerConfig{
Id: transformationId,
Options: options,
},
}
// Extract disabled if present (optional, transformations are enabled by default)
if disabled, ok := tMap["disabled"].(bool); ok && disabled {
transformationKind.Spec.Disabled = &disabled
}
// Extract filter if present (optional frame matcher for transformations)
if filterMap, ok := tMap["filter"].(map[string]interface{}); ok {
transformationKind.Spec.Filter = &dashv2alpha1.DashboardMatcherConfig{
Id: schemaversion.GetStringValue(filterMap, "id"),
Options: filterMap["options"],
}
}
result = append(result, transformationKind)
}
}
return result
}
func buildQueryOptions(panelMap map[string]interface{}) dashv2alpha1.DashboardQueryOptionsSpec {
queryOptions := dashv2alpha1.DashboardQueryOptionsSpec{}
if cacheTimeout := schemaversion.GetStringValue(panelMap, "cacheTimeout"); cacheTimeout != "" {
queryOptions.CacheTimeout = &cacheTimeout
}
if maxDataPoints := getIntField(panelMap, "maxDataPoints", 0); maxDataPoints > 0 {
maxDP := int64(maxDataPoints)
queryOptions.MaxDataPoints = &maxDP
}
if interval := schemaversion.GetStringValue(panelMap, "interval"); interval != "" {
queryOptions.Interval = &interval
}
if hideTimeOverride := getBoolField(panelMap, "hideTimeOverride", false); hideTimeOverride {
queryOptions.HideTimeOverride = &hideTimeOverride
}
// Handle queryCachingTTL as string or number
if val, exists := panelMap["queryCachingTTL"]; exists {
switch v := val.(type) {
case string:
if v != "" {
if ttl, err := strconv.ParseInt(v, 10, 64); err == nil {
queryOptions.QueryCachingTTL = &ttl
}
}
case float64:
ttl := int64(v)
queryOptions.QueryCachingTTL = &ttl
case int:
ttl := int64(v)
queryOptions.QueryCachingTTL = &ttl
}
}
if timeFrom := schemaversion.GetStringValue(panelMap, "timeFrom"); timeFrom != "" {
queryOptions.TimeFrom = &timeFrom
}
if timeShift := schemaversion.GetStringValue(panelMap, "timeShift"); timeShift != "" {
queryOptions.TimeShift = &timeShift
}
return queryOptions
}
func transformDataLinks(panelMap map[string]interface{}) []dashv2alpha1.DashboardDataLink {
links, ok := panelMap["links"].([]interface{})
if !ok {
return []dashv2alpha1.DashboardDataLink{}
}
result := make([]dashv2alpha1.DashboardDataLink, 0, len(links))
for _, link := range links {
if linkMap, ok := link.(map[string]interface{}); ok {
dataLink := dashv2alpha1.DashboardDataLink{
Title: schemaversion.GetStringValue(linkMap, "title"),
Url: stripBOM(schemaversion.GetStringValue(linkMap, "url")),
}
if _, exists := linkMap["targetBlank"]; exists {
targetBlank := getBoolField(linkMap, "targetBlank", false)
dataLink.TargetBlank = &targetBlank
}
result = append(result, dataLink)
}
}
return result
}
// knownPanelProperties lists all properties defined in the Panel schema (dashboard_kind.cue)
// that should NOT be passed to Angular migration handlers. Only "unknown" Angular-specific
// properties should be passed to migration handlers.
var knownPanelProperties = map[string]bool{
"type": true, "id": true, "pluginVersion": true, "targets": true,
"title": true, "description": true, "transparent": true, "datasource": true,
"gridPos": true, "links": true, "repeat": true, "repeatDirection": true,
"maxPerRow": true, "maxDataPoints": true, "transformations": true,
"interval": true, "timeFrom": true, "timeShift": true, "hideTimeOverride": true,
"timeCompare": true, "libraryPanel": true, "cacheTimeout": true,
"queryCachingTTL": true, "options": true, "fieldConfig": true, "autoMigrateFrom": true,
}
// extractAngularOptions extracts only the Angular-specific options from a panel map,
// filtering out all known Panel schema properties. This is used to pass just the
// Angular options to migration handlers (e.g., sparkline, valueName, format for singlestat).
func extractAngularOptions(panelMap map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for key, value := range panelMap {
if !knownPanelProperties[key] {
result[key] = value
}
}
return result
}
func buildVizConfig(panelMap map[string]interface{}) dashv2alpha1.DashboardVizConfigKind {
panelType := schemaversion.GetStringValue(panelMap, "type", "timeseries")
pluginVersion := schemaversion.GetStringValue(panelMap, "pluginVersion")
// Extract field config and options
fieldConfig := make(map[string]interface{})
if fc, ok := panelMap["fieldConfig"].(map[string]interface{}); ok {
fieldConfig = fc
}
options := make(map[string]interface{})
if opts, ok := panelMap["options"].(map[string]interface{}); ok {
// Deep copy options to avoid modifying the original
for k, v := range opts {
options[k] = v
}
}
// Handle Angular panel migrations
// This replicates the v0→v1 migration logic for panels that weren't migrated yet.
// We check two cases:
// 1. Panel already has autoMigrateFrom set (from v0→v1 migration) - panel type already converted
// 2. Panel type is a known Angular panel - need to convert type AND set autoMigrateFrom
// 3. Panel has original options - need to set autoMigrateFrom and originalOptions
autoMigrateFrom, hasAutoMigrateFrom := panelMap["autoMigrateFrom"].(string)
originalOptions := extractAngularOptions(panelMap)
if !hasAutoMigrateFrom || autoMigrateFrom == "" {
// Check if panel type is an Angular type that needs migration
if newType := getAngularPanelMigration(panelType, panelMap); newType != "" {
autoMigrateFrom = panelType // Original Angular type
panelType = newType // New modern type
} else if len(originalOptions) > 0 {
autoMigrateFrom = panelType
}
}
if autoMigrateFrom != "" {
options["__angularMigration"] = map[string]interface{}{
"autoMigrateFrom": autoMigrateFrom,
"originalOptions": originalOptions,
}
}
// Strip BOMs from options (may contain dataLinks with URLs that have BOMs)
cleanedOptions := stripBOMFromInterface(options)
if cleanedMap, ok := cleanedOptions.(map[string]interface{}); ok {
options = cleanedMap
}
// Build field config by mapping each field individually
fieldConfigSource := extractFieldConfigSource(fieldConfig)
return dashv2alpha1.DashboardVizConfigKind{
Kind: panelType, // Use panelType as Kind (plugin ID) - may be converted from Angular type
Spec: dashv2alpha1.DashboardVizConfigSpec{
PluginVersion: pluginVersion,
FieldConfig: fieldConfigSource,
Options: options,
},
}
}
func extractFieldConfigSource(fieldConfig map[string]interface{}) dashv2alpha1.DashboardFieldConfigSource {
// Always initialize with empty defaults to match frontend behavior
fieldConfigSource := dashv2alpha1.DashboardFieldConfigSource{
Defaults: dashv2alpha1.DashboardFieldConfig{},
Overrides: []dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides{},
}
if defaults, ok := fieldConfig["defaults"].(map[string]interface{}); ok {
fieldConfigDefaults := extractFieldConfigDefaults(defaults)
fieldConfigSource.Defaults = fieldConfigDefaults
}
// Handle overrides
fieldConfigSource.Overrides = extractFieldConfigOverrides(fieldConfig)
return fieldConfigSource
}
// Helper functions for extracting field config values
func extractFloat64Field(defaults map[string]interface{}, key string) (*float64, bool) {
if val, exists := defaults[key]; exists {
if floatVal, ok := val.(float64); ok {
return &floatVal, true
}
}
return nil, false
}
func extractStringField(defaults map[string]interface{}, key string) (*string, bool) {
if val, exists := defaults[key]; exists {
if strVal, ok := val.(string); ok {
return &strVal, true
}
}
return nil, false
}
func extractBoolField(defaults map[string]interface{}, key string) (*bool, bool) {
if val, exists := defaults[key]; exists {
if boolVal, ok := val.(bool); ok {
return &boolVal, true
}
}
return nil, false
}
func extractArrayField(defaults map[string]interface{}, key string) ([]interface{}, bool) {
if val, exists := defaults[key]; exists {
if arrayVal, ok := val.([]interface{}); ok {
return arrayVal, true
}
}
return nil, false
}
func extractMapField(defaults map[string]interface{}, key string) (map[string]interface{}, bool) {
if val, exists := defaults[key]; exists {
if mapVal, ok := val.(map[string]interface{}); ok {
return mapVal, true
}
}
return nil, false
}
func extractFieldConfigDefaults(defaults map[string]interface{}) dashv2alpha1.DashboardFieldConfig {
fieldConfigDefaults := dashv2alpha1.DashboardFieldConfig{}
hasDefaults := false
// Extract color
if colorMap, ok := extractMapField(defaults, "color"); ok {
fieldConfigDefaults.Color = buildFieldColor(colorMap)
hasDefaults = true
}
// Extract custom
if customMap, ok := extractMapField(defaults, "custom"); ok {
fieldConfigDefaults.Custom = customMap
hasDefaults = true
}
// Extract numeric fields
if val, ok := extractFloat64Field(defaults, "decimals"); ok {
fieldConfigDefaults.Decimals = val
hasDefaults = true
}
if val, ok := extractFloat64Field(defaults, "max"); ok {
fieldConfigDefaults.Max = val
hasDefaults = true
}
if val, ok := extractFloat64Field(defaults, "min"); ok {
fieldConfigDefaults.Min = val
hasDefaults = true
}
// Extract string fields
if val, ok := extractStringField(defaults, "description"); ok {
fieldConfigDefaults.Description = val
hasDefaults = true
}
if val, ok := extractStringField(defaults, "displayName"); ok {
fieldConfigDefaults.DisplayName = val
hasDefaults = true
}
if val, ok := extractStringField(defaults, "displayNameFromDS"); ok {
fieldConfigDefaults.DisplayNameFromDS = val
hasDefaults = true
}
if val, ok := extractStringField(defaults, "noValue"); ok {
fieldConfigDefaults.NoValue = val
hasDefaults = true
}
if val, ok := extractStringField(defaults, "path"); ok {
fieldConfigDefaults.Path = val
hasDefaults = true
}
if val, ok := extractStringField(defaults, "unit"); ok {
fieldConfigDefaults.Unit = val
hasDefaults = true
}
// Extract bool fields
if val, ok := extractBoolField(defaults, "filterable"); ok {
fieldConfigDefaults.Filterable = val
hasDefaults = true
}
if val, ok := extractBoolField(defaults, "writeable"); ok {
fieldConfigDefaults.Writeable = val
hasDefaults = true
}
if val, ok := extractBoolField(defaults, "fieldMinMax"); ok {
fieldConfigDefaults.FieldMinMax = val
hasDefaults = true
}
if val, ok := defaults["nullValueMode"].(string); ok {
nullValueMode := dashv2alpha1.DashboardNullValueMode(val)
fieldConfigDefaults.NullValueMode = &nullValueMode
hasDefaults = true
}
// Extract array field - strip BOMs from link URLs
if linksArray, ok := extractArrayField(defaults, "links"); ok {
cleanedLinks := stripBOMFromInterface(linksArray)
if cleanedArray, ok := cleanedLinks.([]interface{}); ok {
fieldConfigDefaults.Links = cleanedArray
} else {
fieldConfigDefaults.Links = linksArray
}
hasDefaults = true
}
// Extract actions array
if actionsArray, ok := extractArrayField(defaults, "actions"); ok {
fieldConfigDefaults.Actions = convertActionsToV2(actionsArray)
hasDefaults = true
}
// Extract mappings
if mappings, exists := defaults["mappings"]; exists {
resultMappings := buildValueMappings(mappings)
fieldConfigDefaults.Mappings = resultMappings
hasDefaults = true
}
// Extract thresholds
if thresholdsMap, ok := extractMapField(defaults, "thresholds"); ok {
thresholdsConfig := buildThresholdsConfig(thresholdsMap)
fieldConfigDefaults.Thresholds = thresholdsConfig
hasDefaults = true
}
// Add frontend-style default custom field to match frontend behavior
if !hasDefaults {
fieldConfigDefaults.Custom = map[string]interface{}{}
}
return fieldConfigDefaults
}
func buildFieldColor(colorMap map[string]interface{}) *dashv2alpha1.DashboardFieldColor {
var fieldColor dashv2alpha1.DashboardFieldColor
// mode (required)
if mode, ok := colorMap["mode"].(string); ok {
fieldColor.Mode = dashv2alpha1.DashboardFieldColorModeId(mode)
}
// fixedColor (optional)
if fixedColor, ok := colorMap["fixedColor"].(string); ok {
fieldColor.FixedColor = &fixedColor
}
// seriesBy (optional)
if seriesBy, ok := colorMap["seriesBy"].(string); ok {
sb := dashv2alpha1.DashboardFieldColorSeriesByMode(seriesBy)
fieldColor.SeriesBy = &sb
}
return &fieldColor
}
func buildValueMappings(mappings interface{}) []dashv2alpha1.DashboardValueMapping {
var resultMappings []dashv2alpha1.DashboardValueMapping
mappingsArr, ok := mappings.([]interface{})
if !ok || len(mappingsArr) == 0 {
return resultMappings
}
for _, mapping := range mappingsArr {
mappingMap, ok := mapping.(map[string]interface{})
if !ok {
continue
}
typ, ok := mappingMap["type"].(string)
if !ok {
continue
}
switch typ {
case "value":
if valueMapping := buildValueMap(mappingMap); valueMapping != nil {
resultMappings = append(resultMappings, dashv2alpha1.DashboardValueMapping{
ValueMap: valueMapping,
})
}
case "range":
if rangeMapping := buildRangeMap(mappingMap); rangeMapping != nil {
resultMappings = append(resultMappings, dashv2alpha1.DashboardValueMapping{
RangeMap: rangeMapping,
})
}
case "regex":
if regexMapping := buildRegexMap(mappingMap); regexMapping != nil {
resultMappings = append(resultMappings, dashv2alpha1.DashboardValueMapping{
RegexMap: regexMapping,
})
}
case "special":
if specialMapping := buildSpecialValueMap(mappingMap); specialMapping != nil {
resultMappings = append(resultMappings, dashv2alpha1.DashboardValueMapping{
SpecialValueMap: specialMapping,
})
}
}
}
return resultMappings
}
func buildValueMap(mappingMap map[string]interface{}) *dashv2alpha1.DashboardValueMap {
valMap := &dashv2alpha1.DashboardValueMap{}
valMap.Type = dashv2alpha1.DashboardMappingTypeValue
opts, ok := mappingMap["options"].(map[string]interface{})
if !ok {
return valMap
}
valMap.Options = make(map[string]dashv2alpha1.DashboardValueMappingResult)
for k, v := range opts {
resMap, ok := v.(map[string]interface{})
if !ok {
continue
}
res := buildValueMappingResult(resMap)
valMap.Options[k] = res
}
return valMap
}
func buildRangeMap(mappingMap map[string]interface{}) *dashv2alpha1.DashboardRangeMap {
rangeMap := &dashv2alpha1.DashboardRangeMap{}
rangeMap.Type = dashv2alpha1.DashboardMappingTypeRange
opts, ok := mappingMap["options"].(map[string]interface{})
if !ok || len(opts) == 0 {
return nil
}
// Return nil only if both "from" and "to" are missing
if opts["from"] == nil && opts["to"] == nil {
return nil
}
r := dashv2alpha1.DashboardV2alpha1RangeMapOptions{}
if from, ok := opts["from"].(float64); ok {
r.From = &from
}
if to, ok := opts["to"].(float64); ok {
r.To = &to
}
// Result is a DashboardValueMappingResult
if resMap, ok := opts["result"].(map[string]interface{}); ok {
r.Result = buildValueMappingResult(resMap)
}
rangeMap.Options = r
return rangeMap
}
func buildRegexMap(mappingMap map[string]interface{}) *dashv2alpha1.DashboardRegexMap {
regexMap := &dashv2alpha1.DashboardRegexMap{}
regexMap.Type = dashv2alpha1.DashboardMappingTypeRegex
optMap, ok := mappingMap["options"].(map[string]interface{})
if !ok {
return nil
}
r := dashv2alpha1.DashboardV2alpha1RegexMapOptions{}
if pattern, ok := optMap["pattern"].(string); ok {
r.Pattern = pattern
}
if resMap, ok := optMap["result"].(map[string]interface{}); ok {
r.Result = buildValueMappingResult(resMap)
}
regexMap.Options = r
return regexMap
}
func buildSpecialValueMap(mappingMap map[string]interface{}) *dashv2alpha1.DashboardSpecialValueMap {
specialMap := &dashv2alpha1.DashboardSpecialValueMap{}
specialMap.Type = dashv2alpha1.DashboardMappingTypeSpecial
opts, ok := mappingMap["options"].(map[string]interface{})
if !ok || len(opts) == 0 {
return nil
}
r := dashv2alpha1.DashboardV2alpha1SpecialValueMapOptions{}
if match, ok := opts["match"].(string); ok {
r.Match = dashv2alpha1.DashboardSpecialValueMatch(match)
}
// Result is a DashboardValueMappingResult
if resMap, ok := opts["result"].(map[string]interface{}); ok {
r.Result = buildValueMappingResult(resMap)
}
specialMap.Options = r
return specialMap
}
func buildValueMappingResult(resMap map[string]interface{}) dashv2alpha1.DashboardValueMappingResult {
res := dashv2alpha1.DashboardValueMappingResult{}
if text, ok := resMap["text"].(string); ok {
res.Text = &text
}
if color, ok := resMap["color"].(string); ok {
res.Color = &color
}
if icon, ok := resMap["icon"].(string); ok {
res.Icon = &icon
}
if idx, ok := resMap["index"].(float64); ok {
idx32 := int32(idx)
res.Index = &idx32
}
return res
}
func buildThresholdsConfig(thresholdsMap map[string]interface{}) *dashv2alpha1.DashboardThresholdsConfig {
thresholdsConfig := &dashv2alpha1.DashboardThresholdsConfig{}
// Convert mode
if mode, ok := thresholdsMap["mode"].(string); ok {
thresholdsConfig.Mode = dashv2alpha1.DashboardThresholdsMode(mode)
}
// Convert steps
if steps, ok := thresholdsMap["steps"].([]interface{}); ok {
thresholdsConfig.Steps = make([]dashv2alpha1.DashboardThreshold, 0, len(steps))
for _, step := range steps {
if stepMap, ok := step.(map[string]interface{}); ok {
threshold := dashv2alpha1.DashboardThreshold{}
// Handle threshold value: null means -Infinity
if value, exists := stepMap["value"]; exists {
if value == nil {
// null means -Infinity
threshold.Value = nil
} else if floatVal, ok := value.(float64); ok {
threshold.Value = &floatVal
}
} else {
// Value not present, leave as nil (represents -Infinity)
threshold.Value = nil
}
if color, ok := stepMap["color"].(string); ok {
threshold.Color = color
}
thresholdsConfig.Steps = append(thresholdsConfig.Steps, threshold)
}
}
}
return thresholdsConfig
}
func extractFieldConfigOverrides(fieldConfig map[string]interface{}) []dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides {
overrides, ok := fieldConfig["overrides"].([]interface{})
if !ok || len(overrides) == 0 {
// Use empty array to match frontend behavior
return []dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides{}
}
result := make([]dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides, 0, len(overrides))
for _, override := range overrides {
overrideMap, ok := override.(map[string]interface{})
if !ok {
continue
}
fieldOverride := dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides{}
if systemRef, ok := overrideMap["__systemRef"].(string); ok {
fieldOverride.SystemRef = &systemRef
}
// Map override fields individually
if matcher, exists := overrideMap["matcher"]; exists {
if matcherMap, ok := matcher.(map[string]interface{}); ok {
fieldOverride.Matcher = dashv2alpha1.DashboardMatcherConfig{
Id: schemaversion.GetStringValue(matcherMap, "id"),
Options: matcherMap["options"],
}
}
}
if properties, exists := overrideMap["properties"]; exists {
if propertiesArray, ok := properties.([]interface{}); ok {
fieldOverride.Properties = make([]dashv2alpha1.DashboardDynamicConfigValue, 0, len(propertiesArray))
for _, property := range propertiesArray {
if propertyMap, ok := property.(map[string]interface{}); ok {
// Strip BOMs from property values (may contain links with URLs)
cleanedValue := stripBOMFromInterface(propertyMap["value"])
fieldOverride.Properties = append(fieldOverride.Properties, dashv2alpha1.DashboardDynamicConfigValue{
Id: schemaversion.GetStringValue(propertyMap, "id"),
Value: cleanedValue,
})
}
}
}
}
result = append(result, fieldOverride)
}
return result
}
// convertActionsToV2 converts an array of V1 action objects to V2 DashboardAction structs.
func convertActionsToV2(actionsArray []interface{}) []dashv2alpha1.DashboardAction {
if len(actionsArray) == 0 {
return nil
}
result := make([]dashv2alpha1.DashboardAction, 0, len(actionsArray))
for _, action := range actionsArray {
actionMap, ok := action.(map[string]interface{})
if !ok {
continue
}
dashAction := dashv2alpha1.DashboardAction{
Type: dashv2alpha1.DashboardActionType(schemaversion.GetStringValue(actionMap, "type")),
Title: schemaversion.GetStringValue(actionMap, "title"),
}
// Convert confirmation
if confirmation, ok := actionMap["confirmation"].(string); ok && confirmation != "" {
dashAction.Confirmation = &confirmation
}
// Convert oneClick
if oneClick, ok := actionMap["oneClick"].(bool); ok {
dashAction.OneClick = &oneClick
}
// Convert fetch options
if fetchMap, ok := actionMap["fetch"].(map[string]interface{}); ok {
dashAction.Fetch = convertFetchOptionsToV2(fetchMap)
}
// Convert infinity options
if infinityMap, ok := actionMap["infinity"].(map[string]interface{}); ok {
dashAction.Infinity = convertInfinityOptionsToV2(infinityMap)
}
// Convert variables
if variablesArray, ok := actionMap["variables"].([]interface{}); ok {
dashAction.Variables = convertActionVariablesToV2(variablesArray)
}
// Convert style
if styleMap, ok := actionMap["style"].(map[string]interface{}); ok {
dashAction.Style = convertActionStyleToV2(styleMap)
}
result = append(result, dashAction)
}
return result
}
func convertFetchOptionsToV2(fetchMap map[string]interface{}) *dashv2alpha1.DashboardFetchOptions {
fetchOptions := &dashv2alpha1.DashboardFetchOptions{
Method: dashv2alpha1.DashboardHttpRequestMethod(schemaversion.GetStringValue(fetchMap, "method")),
Url: schemaversion.GetStringValue(fetchMap, "url"),
}
if body, ok := fetchMap["body"].(string); ok {
fetchOptions.Body = &body
}
// Convert queryParams (2D array of strings) - preserve empty arrays
if queryParams, ok := fetchMap["queryParams"].([]interface{}); ok {
fetchOptions.QueryParams = convert2DStringArrayPreserveEmpty(queryParams)
}
// Convert headers (2D array of strings) - preserve empty arrays
if headers, ok := fetchMap["headers"].([]interface{}); ok {
fetchOptions.Headers = convert2DStringArrayPreserveEmpty(headers)
}
return fetchOptions
}
func convertInfinityOptionsToV2(infinityMap map[string]interface{}) *dashv2alpha1.DashboardInfinityOptions {
infinityOptions := &dashv2alpha1.DashboardInfinityOptions{
Method: dashv2alpha1.DashboardHttpRequestMethod(schemaversion.GetStringValue(infinityMap, "method")),
Url: schemaversion.GetStringValue(infinityMap, "url"),
DatasourceUid: schemaversion.GetStringValue(infinityMap, "datasourceUid"),
}
if body, ok := infinityMap["body"].(string); ok {
infinityOptions.Body = &body
}
if queryParams, ok := infinityMap["queryParams"].([]interface{}); ok {
infinityOptions.QueryParams = convert2DStringArrayPreserveEmpty(queryParams)
}
if headers, ok := infinityMap["headers"].([]interface{}); ok {
infinityOptions.Headers = convert2DStringArrayPreserveEmpty(headers)
}
return infinityOptions
}
func convertActionVariablesToV2(variablesArray []interface{}) []dashv2alpha1.DashboardActionVariable {
if len(variablesArray) == 0 {
return nil
}
result := make([]dashv2alpha1.DashboardActionVariable, 0, len(variablesArray))
for _, variable := range variablesArray {
variableMap, ok := variable.(map[string]interface{})
if !ok {
continue
}
result = append(result, dashv2alpha1.DashboardActionVariable{
Key: schemaversion.GetStringValue(variableMap, "key"),
Name: schemaversion.GetStringValue(variableMap, "name"),
Type: schemaversion.GetStringValue(variableMap, "type"),
})
}
return result
}
func convertActionStyleToV2(styleMap map[string]interface{}) *dashv2alpha1.DashboardV2alpha1ActionStyle {
style := &dashv2alpha1.DashboardV2alpha1ActionStyle{}
if backgroundColor, ok := styleMap["backgroundColor"].(string); ok {
style.BackgroundColor = &backgroundColor
}
return style
}
// convert2DStringArrayPreserveEmpty is like convert2DStringArray but returns
// an empty slice (not nil) when input is empty, to ensure JSON marshals as []
func convert2DStringArrayPreserveEmpty(arr []interface{}) [][]string {
// Return empty slice (not nil) to preserve [] in JSON output
result := make([][]string, 0, len(arr))
for _, item := range arr {
if innerArr, ok := item.([]interface{}); ok {
stringArr := make([]string, 0, len(innerArr))
for _, s := range innerArr {
if str, ok := s.(string); ok {
stringArr = append(stringArr, str)
}
}
result = append(result, stringArr)
}
}
return result
}
// getAngularPanelMigration is a convenience wrapper around schemaversion.GetAngularPanelMigration.
// It checks if a panel type is an Angular panel and returns the new type to migrate to.
// Returns the new panel type if migration is needed, empty string otherwise.
func getAngularPanelMigration(panelType string, panelMap map[string]interface{}) string {
return schemaversion.GetAngularPanelMigration(panelType, panelMap)
}