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 = ®ex } 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) }