* preserve repeat property * fix test * preserve repeat when converting panels in tabs or rows with autogrid layout * fix v1 serialization of autogrid
2320 lines
72 KiB
Go
2320 lines
72 KiB
Go
package conversion
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
|
dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
|
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
|
"k8s.io/apimachinery/pkg/conversion"
|
|
)
|
|
|
|
// ConvertDashboard_V2alpha1_to_V1beta1 converts a v2alpha1 dashboard to v1beta1 format.
|
|
// The v1beta1 format uses an unstructured JSON structure, so we build a map[string]interface{}
|
|
// that represents the v1 dashboard JSON format.
|
|
// The dsIndexProvider is used to resolve default datasources when queries/variables/annotations
|
|
// don't have explicit datasource references.
|
|
func ConvertDashboard_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error {
|
|
out.ObjectMeta = in.ObjectMeta
|
|
out.APIVersion = dashv1.APIVERSION
|
|
out.Kind = in.Kind // Preserve the Kind from input (should be "Dashboard")
|
|
|
|
// Convert the spec to v1beta1 unstructured format
|
|
dashboardJSON, err := convertDashboardSpec_V2alpha1_to_V1beta1(&in.Spec)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to convert dashboard spec: %w", err)
|
|
}
|
|
|
|
// Set the dashboard JSON directly at the Spec.Object level
|
|
out.Spec.Object = dashboardJSON
|
|
|
|
return nil
|
|
}
|
|
|
|
func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec) (map[string]interface{}, error) {
|
|
dashboard := make(map[string]interface{})
|
|
|
|
// Convert basic fields
|
|
dashboard["title"] = in.Title
|
|
if in.Description != nil {
|
|
dashboard["description"] = *in.Description
|
|
}
|
|
if len(in.Tags) > 0 {
|
|
dashboard["tags"] = in.Tags
|
|
}
|
|
dashboard["graphTooltip"] = transformCursorSyncFromEnum(in.CursorSync)
|
|
dashboard["schemaVersion"] = schemaversion.LATEST_VERSION
|
|
dashboard["preload"] = in.Preload
|
|
// Default editable to true if not explicitly set, matching frontend DashboardModel behavior
|
|
if in.Editable != nil {
|
|
dashboard["editable"] = *in.Editable
|
|
} else {
|
|
dashboard["editable"] = true
|
|
}
|
|
if in.LiveNow != nil {
|
|
dashboard["liveNow"] = *in.LiveNow
|
|
}
|
|
if in.Revision != nil {
|
|
dashboard["revision"] = *in.Revision
|
|
}
|
|
|
|
// Convert time settings
|
|
convertTimeSettingsToV1(&in.TimeSettings, dashboard)
|
|
|
|
// Convert links
|
|
if len(in.Links) > 0 {
|
|
dashboard["links"] = convertLinksToV1(in.Links)
|
|
}
|
|
|
|
// Convert panels from elements and layout
|
|
panels, err := convertPanelsFromElementsAndLayout(in.Elements, in.Layout)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert panels: %w", err)
|
|
}
|
|
|
|
if len(panels) > 0 {
|
|
dashboard["panels"] = panels
|
|
}
|
|
|
|
// Convert variables
|
|
variables := convertVariablesToV1(in.Variables)
|
|
if len(variables) > 0 {
|
|
dashboard["templating"] = map[string]interface{}{
|
|
"list": variables,
|
|
}
|
|
}
|
|
|
|
// Convert annotations - always include even if empty to prevent DashboardModel from adding built-in
|
|
annotations := convertAnnotationsToV1(in.Annotations)
|
|
dashboard["annotations"] = map[string]interface{}{
|
|
"list": annotations,
|
|
}
|
|
|
|
return dashboard, nil
|
|
}
|
|
|
|
// transformCursorSyncFromEnum converts v2alpha1 CursorSync enum to v1 graphTooltip integer
|
|
func transformCursorSyncFromEnum(cursorSync dashv2alpha1.DashboardDashboardCursorSync) int {
|
|
switch cursorSync {
|
|
case dashv2alpha1.DashboardDashboardCursorSyncOff:
|
|
return 0
|
|
case dashv2alpha1.DashboardDashboardCursorSyncCrosshair:
|
|
return 1
|
|
case dashv2alpha1.DashboardDashboardCursorSyncTooltip:
|
|
return 2
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func convertTimeSettingsToV1(timeSettings *dashv2alpha1.DashboardTimeSettingsSpec, dashboard map[string]interface{}) {
|
|
// Convert time range - use defaults when empty to match DashboardModel behavior
|
|
from := timeSettings.From
|
|
to := timeSettings.To
|
|
if from == "" {
|
|
from = "now-6h"
|
|
}
|
|
if to == "" {
|
|
to = "now"
|
|
}
|
|
dashboard["time"] = map[string]interface{}{
|
|
"from": from,
|
|
"to": to,
|
|
}
|
|
|
|
if timeSettings.Timezone != nil {
|
|
dashboard["timezone"] = *timeSettings.Timezone
|
|
}
|
|
|
|
dashboard["refresh"] = timeSettings.AutoRefresh
|
|
|
|
dashboard["fiscalYearStartMonth"] = timeSettings.FiscalYearStartMonth
|
|
|
|
if timeSettings.WeekStart != nil {
|
|
dashboard["weekStart"] = string(*timeSettings.WeekStart)
|
|
}
|
|
|
|
timepicker := make(map[string]interface{})
|
|
hasTimepicker := false
|
|
|
|
if len(timeSettings.AutoRefreshIntervals) > 0 {
|
|
timepicker["refresh_intervals"] = timeSettings.AutoRefreshIntervals
|
|
hasTimepicker = true
|
|
}
|
|
if timeSettings.HideTimepicker {
|
|
timepicker["hidden"] = timeSettings.HideTimepicker
|
|
hasTimepicker = true
|
|
}
|
|
if timeSettings.NowDelay != nil {
|
|
timepicker["nowDelay"] = *timeSettings.NowDelay
|
|
hasTimepicker = true
|
|
}
|
|
if len(timeSettings.QuickRanges) > 0 {
|
|
quickRanges := make([]map[string]interface{}, 0, len(timeSettings.QuickRanges))
|
|
for _, qr := range timeSettings.QuickRanges {
|
|
quickRange := map[string]interface{}{
|
|
"display": qr.Display,
|
|
"from": qr.From,
|
|
"to": qr.To,
|
|
}
|
|
quickRanges = append(quickRanges, quickRange)
|
|
}
|
|
timepicker["quick_ranges"] = quickRanges
|
|
hasTimepicker = true
|
|
}
|
|
|
|
if hasTimepicker {
|
|
dashboard["timepicker"] = timepicker
|
|
}
|
|
}
|
|
|
|
func convertLinksToV1(links []dashv2alpha1.DashboardDashboardLink) []map[string]interface{} {
|
|
result := make([]map[string]interface{}, 0, len(links))
|
|
for _, link := range links {
|
|
linkMap := map[string]interface{}{
|
|
"title": link.Title,
|
|
"type": string(link.Type),
|
|
"icon": link.Icon,
|
|
"tooltip": link.Tooltip,
|
|
"tags": link.Tags,
|
|
"asDropdown": link.AsDropdown,
|
|
"targetBlank": link.TargetBlank,
|
|
"includeVars": link.IncludeVars,
|
|
"keepTime": link.KeepTime,
|
|
}
|
|
if link.Url != nil {
|
|
linkMap["url"] = *link.Url
|
|
}
|
|
if link.Placement != nil {
|
|
linkMap["placement"] = *link.Placement
|
|
}
|
|
result = append(result, linkMap)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// convertPanelsFromElementsAndLayout converts V2 layout structures to V1 panel arrays.
|
|
// V1 only supports a flat array of panels with row panels for grouping.
|
|
// This function dispatches to the appropriate converter based on layout type:
|
|
// - GridLayout: Direct 1:1 mapping to V1 panels with gridPos
|
|
// - RowsLayout: Rows become row panels; nested structures are flattened
|
|
// - AutoGridLayout: Calculates gridPos based on column count and row height
|
|
// - TabsLayout: Tabs become expanded row panels; content is flattened
|
|
func convertPanelsFromElementsAndLayout(elements map[string]dashv2alpha1.DashboardElement, layout dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind) ([]interface{}, error) {
|
|
if layout.GridLayoutKind != nil {
|
|
return convertGridLayoutToPanels(elements, layout.GridLayoutKind)
|
|
}
|
|
|
|
if layout.RowsLayoutKind != nil {
|
|
return convertRowsLayoutToPanels(elements, layout.RowsLayoutKind)
|
|
}
|
|
|
|
if layout.AutoGridLayoutKind != nil {
|
|
return convertAutoGridLayoutToPanels(elements, layout.AutoGridLayoutKind)
|
|
}
|
|
|
|
if layout.TabsLayoutKind != nil {
|
|
return convertTabsLayoutToPanels(elements, layout.TabsLayoutKind)
|
|
}
|
|
|
|
// No layout specified, return empty panels
|
|
return []interface{}{}, nil
|
|
}
|
|
|
|
func convertGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, gridLayout *dashv2alpha1.DashboardGridLayoutKind) ([]interface{}, error) {
|
|
panels := make([]interface{}, 0, len(gridLayout.Spec.Items))
|
|
|
|
for _, item := range gridLayout.Spec.Items {
|
|
element, ok := elements[item.Spec.Element.Name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("panel with uid %s not found in the dashboard elements", item.Spec.Element.Name)
|
|
}
|
|
|
|
panel, err := convertPanelFromElement(&element, &item)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
|
|
}
|
|
panels = append(panels, panel)
|
|
}
|
|
|
|
return panels, nil
|
|
}
|
|
|
|
// convertRowsLayoutToPanels converts a RowsLayout to V1 panels.
|
|
// All nested structures (rows within rows, tabs within rows) are flattened to the root level.
|
|
// Each row becomes a row panel, and nested content is added sequentially after it.
|
|
func convertRowsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind) ([]interface{}, error) {
|
|
return convertNestedLayoutToPanels(elements, rowsLayout, nil, 0)
|
|
}
|
|
|
|
// convertNestedLayoutToPanels handles arbitrary nesting of RowsLayout and TabsLayout.
|
|
// It processes each row/tab in order, tracking Y position to ensure panels don't overlap.
|
|
// The function recursively flattens nested structures to produce a flat V1 panel array.
|
|
func convertNestedLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind, yOffset int64) ([]interface{}, error) {
|
|
panels := make([]interface{}, 0)
|
|
currentY := yOffset
|
|
|
|
// Process RowsLayout
|
|
if rowsLayout != nil {
|
|
for _, row := range rowsLayout.Spec.Rows {
|
|
rowPanels, newY, err := processRowItem(elements, &row, currentY)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
panels = append(panels, rowPanels...)
|
|
currentY = newY
|
|
}
|
|
}
|
|
|
|
// Process TabsLayout (tabs are converted to rows)
|
|
if tabsLayout != nil {
|
|
for _, tab := range tabsLayout.Spec.Tabs {
|
|
tabPanels, newY, err := processTabItem(elements, &tab, currentY)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
panels = append(panels, tabPanels...)
|
|
currentY = newY
|
|
}
|
|
}
|
|
|
|
return panels, nil
|
|
}
|
|
|
|
// processRowItem converts a single V2 row to V1 panels.
|
|
// Behavior depends on row configuration:
|
|
// - Hidden header (hideHeader=true): No row panel created; panels added directly
|
|
// - Explicit row (hideHeader=false): Row panel created at currentY; content follows
|
|
// - Collapsed row: Panels stored inside row.panels with absolute Y positions
|
|
// - Expanded row: Panels added to top level after the row panel
|
|
// - Nested layouts: Parent row is preserved; nested content is flattened after it
|
|
func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dashv2alpha1.DashboardRowsLayoutRowKind, startY int64) ([]interface{}, int64, error) {
|
|
panels := make([]interface{}, 0)
|
|
currentY := startY
|
|
|
|
isHiddenHeader := row.Spec.HideHeader != nil && *row.Spec.HideHeader
|
|
|
|
// Handle nested RowsLayout - keep parent row, then flatten nested rows
|
|
if row.Spec.Layout.RowsLayoutKind != nil {
|
|
// Create parent row panel first (if not hidden header)
|
|
if !isHiddenHeader {
|
|
rowPanel := map[string]interface{}{
|
|
"type": "row",
|
|
"id": -1,
|
|
"gridPos": map[string]interface{}{
|
|
"x": 0,
|
|
"y": currentY,
|
|
"w": 24,
|
|
"h": 1,
|
|
},
|
|
}
|
|
if row.Spec.Title != nil {
|
|
rowPanel["title"] = *row.Spec.Title
|
|
}
|
|
rowPanel["collapsed"] = false
|
|
rowPanel["panels"] = []interface{}{}
|
|
panels = append(panels, rowPanel)
|
|
currentY++
|
|
}
|
|
|
|
// Then process nested rows
|
|
nestedPanels, err := convertNestedLayoutToPanels(elements, row.Spec.Layout.RowsLayoutKind, nil, currentY)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
panels = append(panels, nestedPanels...)
|
|
currentY = getMaxYFromPanels(nestedPanels, currentY)
|
|
return panels, currentY, nil
|
|
}
|
|
|
|
// Handle nested TabsLayout - keep parent row, then flatten tabs
|
|
if row.Spec.Layout.TabsLayoutKind != nil {
|
|
// Create parent row panel first (if not hidden header)
|
|
if !isHiddenHeader {
|
|
rowPanel := map[string]interface{}{
|
|
"type": "row",
|
|
"id": -1,
|
|
"gridPos": map[string]interface{}{
|
|
"x": 0,
|
|
"y": currentY,
|
|
"w": 24,
|
|
"h": 1,
|
|
},
|
|
}
|
|
if row.Spec.Title != nil {
|
|
rowPanel["title"] = *row.Spec.Title
|
|
}
|
|
rowPanel["collapsed"] = false
|
|
rowPanel["panels"] = []interface{}{}
|
|
panels = append(panels, rowPanel)
|
|
currentY++
|
|
}
|
|
|
|
// Then process nested tabs
|
|
nestedPanels, err := convertNestedLayoutToPanels(elements, nil, row.Spec.Layout.TabsLayoutKind, currentY)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
panels = append(panels, nestedPanels...)
|
|
currentY = getMaxYFromPanels(nestedPanels, currentY)
|
|
return panels, currentY, nil
|
|
}
|
|
|
|
// Create row panel for explicit rows (not hidden header)
|
|
isCollapsed := row.Spec.Collapse != nil && *row.Spec.Collapse
|
|
|
|
if !isHiddenHeader {
|
|
rowPanel := map[string]interface{}{
|
|
"type": "row",
|
|
"id": -1,
|
|
}
|
|
|
|
if row.Spec.Title != nil {
|
|
rowPanel["title"] = *row.Spec.Title
|
|
}
|
|
if row.Spec.Collapse != nil {
|
|
rowPanel["collapsed"] = *row.Spec.Collapse
|
|
}
|
|
if row.Spec.Repeat != nil && row.Spec.Repeat.Value != "" {
|
|
rowPanel["repeat"] = row.Spec.Repeat.Value
|
|
}
|
|
|
|
// Set row gridPos - always set for collapsed rows, otherwise only if has content
|
|
hasContent := (row.Spec.Layout.GridLayoutKind != nil && len(row.Spec.Layout.GridLayoutKind.Spec.Items) > 0) ||
|
|
row.Spec.Layout.AutoGridLayoutKind != nil
|
|
if hasContent || isCollapsed {
|
|
rowPanel["gridPos"] = map[string]interface{}{
|
|
"x": 0,
|
|
"y": currentY,
|
|
"w": 24,
|
|
"h": 1,
|
|
}
|
|
}
|
|
|
|
// Add collapsed panels if row is collapsed (panels use absolute Y positions)
|
|
if isCollapsed {
|
|
collapsedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, currentY+1)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if len(collapsedPanels) > 0 {
|
|
rowPanel["panels"] = collapsedPanels
|
|
}
|
|
}
|
|
|
|
panels = append(panels, rowPanel)
|
|
currentY++ // Row panel takes 1 grid unit
|
|
}
|
|
|
|
// Add panels from row layout (only for expanded rows or hidden header rows)
|
|
if !isCollapsed || isHiddenHeader {
|
|
rowPanels, newY, err := extractExpandedPanels(elements, &row.Spec.Layout, currentY, isHiddenHeader, startY)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
panels = append(panels, rowPanels...)
|
|
currentY = newY
|
|
}
|
|
|
|
return panels, currentY, nil
|
|
}
|
|
|
|
// processTabItem converts a V2 tab to V1 panels.
|
|
// Each tab becomes an expanded row panel (collapsed=false) with an empty panels array.
|
|
// The tab's content is flattened and added to the top level after the row panel.
|
|
// Nested layouts within the tab are recursively processed.
|
|
func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dashv2alpha1.DashboardTabsLayoutTabKind, startY int64) ([]interface{}, int64, error) {
|
|
panels := make([]interface{}, 0)
|
|
currentY := startY
|
|
|
|
// Create a row panel for this tab (tabs become expanded rows)
|
|
rowPanel := map[string]interface{}{
|
|
"type": "row",
|
|
"id": -1,
|
|
"collapsed": false,
|
|
"panels": []interface{}{},
|
|
}
|
|
|
|
if tab.Spec.Title != nil {
|
|
rowPanel["title"] = *tab.Spec.Title
|
|
}
|
|
|
|
if tab.Spec.Repeat != nil && tab.Spec.Repeat.Value != "" {
|
|
// We only use value here as V1 doesn't support mode
|
|
rowPanel["repeat"] = tab.Spec.Repeat.Value
|
|
}
|
|
|
|
rowPanel["gridPos"] = map[string]interface{}{
|
|
"x": 0,
|
|
"y": currentY,
|
|
"w": 24,
|
|
"h": 1,
|
|
}
|
|
panels = append(panels, rowPanel)
|
|
currentY++
|
|
|
|
// Handle nested layouts inside the tab
|
|
if tab.Spec.Layout.RowsLayoutKind != nil {
|
|
// Nested RowsLayout inside tab
|
|
nestedPanels, err := convertNestedLayoutToPanels(elements, tab.Spec.Layout.RowsLayoutKind, nil, currentY)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
panels = append(panels, nestedPanels...)
|
|
currentY = getMaxYFromPanels(nestedPanels, currentY)
|
|
} else if tab.Spec.Layout.TabsLayoutKind != nil {
|
|
// Nested TabsLayout inside tab
|
|
nestedPanels, err := convertNestedLayoutToPanels(elements, nil, tab.Spec.Layout.TabsLayoutKind, currentY)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
panels = append(panels, nestedPanels...)
|
|
currentY = getMaxYFromPanels(nestedPanels, currentY)
|
|
} else if tab.Spec.Layout.GridLayoutKind != nil {
|
|
// GridLayout inside tab
|
|
baseY := currentY
|
|
maxY := currentY
|
|
|
|
for _, item := range tab.Spec.Layout.GridLayoutKind.Spec.Items {
|
|
element, ok := elements[item.Spec.Element.Name]
|
|
if !ok {
|
|
return nil, 0, fmt.Errorf("panel with uid %s not found in the dashboard elements", item.Spec.Element.Name)
|
|
}
|
|
|
|
adjustedItem := item
|
|
adjustedItem.Spec.Y = item.Spec.Y + baseY
|
|
|
|
panel, err := convertPanelFromElement(&element, &adjustedItem)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
|
|
}
|
|
panels = append(panels, panel)
|
|
|
|
panelEndY := adjustedItem.Spec.Y + item.Spec.Height
|
|
if panelEndY > maxY {
|
|
maxY = panelEndY
|
|
}
|
|
}
|
|
|
|
currentY = maxY
|
|
} else if tab.Spec.Layout.AutoGridLayoutKind != nil {
|
|
// AutoGridLayout inside tab - convert with Y offset
|
|
autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, tab.Spec.Layout.AutoGridLayoutKind, currentY)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
panels = append(panels, autoGridPanels...)
|
|
currentY = getMaxYFromPanels(autoGridPanels, currentY)
|
|
}
|
|
|
|
return panels, currentY, nil
|
|
}
|
|
|
|
// extractCollapsedPanelsWithAbsoluteY extracts panels for a collapsed row.
|
|
// Panels are positioned with absolute Y coordinates (baseY + relative Y).
|
|
// This matches V1 behavior where collapsed row panels store their children
|
|
// with Y positions as if the row were expanded at that location.
|
|
func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, baseY int64) ([]interface{}, error) {
|
|
panels := make([]interface{}, 0)
|
|
|
|
if layout.GridLayoutKind != nil {
|
|
for _, item := range layout.GridLayoutKind.Spec.Items {
|
|
element, ok := elements[item.Spec.Element.Name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("panel with uid %s not found in the dashboard elements", item.Spec.Element.Name)
|
|
}
|
|
// Create a copy with adjusted Y position
|
|
adjustedItem := item
|
|
adjustedItem.Spec.Y = item.Spec.Y + baseY
|
|
panel, err := convertPanelFromElement(&element, &adjustedItem)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
|
|
}
|
|
panels = append(panels, panel)
|
|
}
|
|
}
|
|
// Handle AutoGridLayout for collapsed rows with Y offset
|
|
if layout.AutoGridLayoutKind != nil {
|
|
autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, baseY)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
panels = append(panels, autoGridPanels...)
|
|
}
|
|
// For nested rows/tabs in collapsed state, recursively extract all panels
|
|
if layout.RowsLayoutKind != nil {
|
|
currentY := baseY
|
|
for _, row := range layout.RowsLayoutKind.Spec.Rows {
|
|
nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, currentY)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
panels = append(panels, nestedPanels...)
|
|
currentY += getLayoutHeight(&row.Spec.Layout)
|
|
}
|
|
}
|
|
if layout.TabsLayoutKind != nil {
|
|
currentY := baseY
|
|
for _, tab := range layout.TabsLayoutKind.Spec.Tabs {
|
|
nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, currentY)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
panels = append(panels, nestedPanels...)
|
|
currentY += getLayoutHeightFromTab(&tab.Spec.Layout)
|
|
}
|
|
}
|
|
|
|
return panels, nil
|
|
}
|
|
|
|
// extractCollapsedPanelsFromTabLayoutWithAbsoluteY extracts panels from a tab layout with absolute Y.
|
|
// Similar to extractCollapsedPanelsWithAbsoluteY but handles the tab-specific layout type.
|
|
func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, baseY int64) ([]interface{}, error) {
|
|
panels := make([]interface{}, 0)
|
|
|
|
if layout.GridLayoutKind != nil {
|
|
for _, item := range layout.GridLayoutKind.Spec.Items {
|
|
element, ok := elements[item.Spec.Element.Name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("panel with uid %s not found in the dashboard elements", item.Spec.Element.Name)
|
|
}
|
|
adjustedItem := item
|
|
adjustedItem.Spec.Y = item.Spec.Y + baseY
|
|
panel, err := convertPanelFromElement(&element, &adjustedItem)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
|
|
}
|
|
panels = append(panels, panel)
|
|
}
|
|
}
|
|
if layout.AutoGridLayoutKind != nil {
|
|
autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, baseY)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
panels = append(panels, autoGridPanels...)
|
|
}
|
|
if layout.RowsLayoutKind != nil {
|
|
currentY := baseY
|
|
for _, row := range layout.RowsLayoutKind.Spec.Rows {
|
|
nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, currentY)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
panels = append(panels, nestedPanels...)
|
|
currentY += getLayoutHeight(&row.Spec.Layout)
|
|
}
|
|
}
|
|
if layout.TabsLayoutKind != nil {
|
|
currentY := baseY
|
|
for _, tab := range layout.TabsLayoutKind.Spec.Tabs {
|
|
nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, currentY)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
panels = append(panels, nestedPanels...)
|
|
currentY += getLayoutHeightFromTab(&tab.Spec.Layout)
|
|
}
|
|
}
|
|
|
|
return panels, nil
|
|
}
|
|
|
|
// getLayoutHeightFromTab calculates the height of a tab's content.
|
|
// Similar to getLayoutHeight but handles the tab-specific layout type.
|
|
func getLayoutHeightFromTab(layout *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind) int64 {
|
|
var maxY int64 = 0
|
|
|
|
if layout.GridLayoutKind != nil {
|
|
for _, item := range layout.GridLayoutKind.Spec.Items {
|
|
if item.Spec.Y+item.Spec.Height > maxY {
|
|
maxY = item.Spec.Y + item.Spec.Height
|
|
}
|
|
}
|
|
}
|
|
if layout.AutoGridLayoutKind != nil {
|
|
// Calculate based on item count and default row height
|
|
itemCount := len(layout.AutoGridLayoutKind.Spec.Items)
|
|
maxCols := 3
|
|
if layout.AutoGridLayoutKind.Spec.MaxColumnCount != nil {
|
|
maxCols = int(*layout.AutoGridLayoutKind.Spec.MaxColumnCount)
|
|
}
|
|
rowCount := (itemCount + maxCols - 1) / maxCols
|
|
maxY = int64(rowCount * 9) // default standard height
|
|
}
|
|
|
|
return maxY
|
|
}
|
|
|
|
// extractExpandedPanels extracts panels for an expanded row, adding them to the top level.
|
|
// Y position handling:
|
|
// - Hidden header: Keep original relative Y (no adjustment)
|
|
// - Explicit row: Add (currentY - 1) to relative Y for absolute positioning
|
|
//
|
|
// Returns the panels and the new Y position for the next row.
|
|
func extractExpandedPanels(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, currentY int64, isHiddenHeader bool, startY int64) ([]interface{}, int64, error) {
|
|
panels := make([]interface{}, 0)
|
|
// For hidden headers, don't track Y changes (matches original behavior)
|
|
maxY := startY
|
|
|
|
if layout.GridLayoutKind != nil {
|
|
var localMaxY int64 = 0
|
|
for _, item := range layout.GridLayoutKind.Spec.Items {
|
|
element, ok := elements[item.Spec.Element.Name]
|
|
if !ok {
|
|
return nil, 0, fmt.Errorf("panel with uid %s not found in the dashboard elements", item.Spec.Element.Name)
|
|
}
|
|
|
|
adjustedItem := item
|
|
if !isHiddenHeader {
|
|
// For explicit rows: Y = item.Spec.Y + currentY - 1
|
|
// currentY has been incremented after row panel, so currentY - 1 gives offset
|
|
adjustedItem.Spec.Y = item.Spec.Y + currentY - 1
|
|
}
|
|
// For hidden headers: don't adjust Y, keep item.Spec.Y as-is
|
|
|
|
panel, err := convertPanelFromElement(&element, &adjustedItem)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
|
|
}
|
|
panels = append(panels, panel)
|
|
|
|
// Track max extent within this row's panels using ABSOLUTE Y positions
|
|
panelEndY := adjustedItem.Spec.Y + item.Spec.Height
|
|
if panelEndY > localMaxY {
|
|
localMaxY = panelEndY
|
|
}
|
|
}
|
|
|
|
// Return maxY for next row position (applies to both explicit rows and hidden headers)
|
|
maxY = localMaxY
|
|
}
|
|
|
|
// Handle AutoGridLayout
|
|
if layout.AutoGridLayoutKind != nil {
|
|
// Calculate Y offset for panels
|
|
yOffset := startY
|
|
if !isHiddenHeader {
|
|
yOffset = currentY - 1
|
|
}
|
|
|
|
autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, yOffset)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
panels = append(panels, autoGridPanels...)
|
|
|
|
// Update maxY based on panels added (applies to both explicit rows and hidden headers)
|
|
maxY = getMaxYFromPanels(autoGridPanels, yOffset)
|
|
}
|
|
|
|
return panels, maxY, nil
|
|
}
|
|
|
|
// getMaxYFromPanels finds the maximum Y extent (y + h) from a list of panels.
|
|
// Used to determine where the next row should start to avoid overlap.
|
|
func getMaxYFromPanels(panels []interface{}, currentY int64) int64 {
|
|
maxY := currentY
|
|
for _, p := range panels {
|
|
if pm, ok := p.(map[string]interface{}); ok {
|
|
if gridPos, ok := pm["gridPos"].(map[string]interface{}); ok {
|
|
var y, h int64
|
|
// Handle both int and int64 types for Y
|
|
switch yVal := gridPos["y"].(type) {
|
|
case int64:
|
|
y = yVal
|
|
case int:
|
|
y = int64(yVal)
|
|
}
|
|
// Handle both int and int64 types for H
|
|
switch hVal := gridPos["h"].(type) {
|
|
case int64:
|
|
h = hVal
|
|
case int:
|
|
h = int64(hVal)
|
|
}
|
|
if y+h > maxY {
|
|
maxY = y + h
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return maxY
|
|
}
|
|
|
|
// getLayoutHeight calculates the total height of a layout's content.
|
|
// Used for Y position tracking when processing collapsed rows with nested content.
|
|
func getLayoutHeight(layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind) int64 {
|
|
var maxY int64 = 0
|
|
|
|
if layout.GridLayoutKind != nil {
|
|
for _, item := range layout.GridLayoutKind.Spec.Items {
|
|
if item.Spec.Y+item.Spec.Height > maxY {
|
|
maxY = item.Spec.Y + item.Spec.Height
|
|
}
|
|
}
|
|
}
|
|
|
|
return maxY
|
|
}
|
|
|
|
// convertAutoGridLayoutToPanelsWithOffset converts AutoGridLayout with a Y offset.
|
|
// Same as convertAutoGridLayoutToPanels but starts at yOffset instead of 0.
|
|
// Used when AutoGridLayout appears inside rows or tabs.
|
|
func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind, yOffset int64) ([]interface{}, error) {
|
|
panels := make([]interface{}, 0, len(autoGridLayout.Spec.Items))
|
|
|
|
const (
|
|
gridTotalColumns int64 = 24
|
|
gridCellHeightPx float64 = 30
|
|
gridCellVerticalMargin float64 = 8
|
|
defaultMaxColumnCount float64 = 3
|
|
)
|
|
|
|
pixelsToGridUnits := func(pixels float64) int64 {
|
|
gridUnitSize := gridCellHeightPx + gridCellVerticalMargin
|
|
return int64((pixels + gridUnitSize - 1) / gridUnitSize)
|
|
}
|
|
|
|
maxColumnCount := defaultMaxColumnCount
|
|
if autoGridLayout.Spec.MaxColumnCount != nil && *autoGridLayout.Spec.MaxColumnCount > 0 {
|
|
maxColumnCount = *autoGridLayout.Spec.MaxColumnCount
|
|
}
|
|
panelWidth := int64(float64(gridTotalColumns) / maxColumnCount)
|
|
|
|
var panelHeight int64
|
|
switch autoGridLayout.Spec.RowHeightMode {
|
|
case dashv2alpha1.DashboardAutoGridLayoutSpecRowHeightModeShort:
|
|
panelHeight = 5
|
|
case dashv2alpha1.DashboardAutoGridLayoutSpecRowHeightModeStandard:
|
|
panelHeight = 9
|
|
case dashv2alpha1.DashboardAutoGridLayoutSpecRowHeightModeTall:
|
|
panelHeight = 14
|
|
case dashv2alpha1.DashboardAutoGridLayoutSpecRowHeightModeCustom:
|
|
if autoGridLayout.Spec.RowHeight != nil && *autoGridLayout.Spec.RowHeight > 0 {
|
|
panelHeight = pixelsToGridUnits(*autoGridLayout.Spec.RowHeight)
|
|
} else {
|
|
panelHeight = 9
|
|
}
|
|
default:
|
|
panelHeight = 9
|
|
}
|
|
|
|
currentY := yOffset
|
|
var currentX int64 = 0
|
|
|
|
for _, item := range autoGridLayout.Spec.Items {
|
|
element, ok := elements[item.Spec.Element.Name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("panel with uid %s not found in the dashboard elements", item.Spec.Element.Name)
|
|
}
|
|
|
|
gridItem := dashv2alpha1.DashboardGridLayoutItemKind{
|
|
Kind: "GridLayoutItem",
|
|
Spec: dashv2alpha1.DashboardGridLayoutItemSpec{
|
|
X: currentX,
|
|
Y: currentY,
|
|
Width: panelWidth,
|
|
Height: panelHeight,
|
|
Element: dashv2alpha1.DashboardElementReference{
|
|
Kind: item.Spec.Element.Kind,
|
|
Name: item.Spec.Element.Name,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Convert AutoGridRepeatOptions to RepeatOptions if present
|
|
// AutoGridRepeatOptions only has mode and value; infer direction and maxPerRow from AutoGrid settings:
|
|
// - direction: always "h" (AutoGrid flows horizontally, left-to-right then wraps)
|
|
// - maxPerRow: from AutoGrid's maxColumnCount
|
|
if item.Spec.Repeat != nil {
|
|
directionH := dashv2alpha1.DashboardRepeatOptionsDirectionH
|
|
maxPerRow := int64(maxColumnCount)
|
|
gridItem.Spec.Repeat = &dashv2alpha1.DashboardRepeatOptions{
|
|
Mode: item.Spec.Repeat.Mode,
|
|
Value: item.Spec.Repeat.Value,
|
|
Direction: &directionH,
|
|
MaxPerRow: &maxPerRow,
|
|
}
|
|
}
|
|
|
|
panel, err := convertPanelFromElement(&element, &gridItem)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
|
|
}
|
|
panels = append(panels, panel)
|
|
|
|
currentX += panelWidth
|
|
if currentX >= gridTotalColumns {
|
|
currentX = 0
|
|
currentY += panelHeight
|
|
}
|
|
}
|
|
|
|
return panels, nil
|
|
}
|
|
|
|
// convertAutoGridLayoutToPanels converts V2 AutoGridLayout to V1 panels with calculated gridPos.
|
|
// AutoGridLayout arranges panels automatically; V1 requires explicit x/y/w/h coordinates.
|
|
//
|
|
// Grid System:
|
|
// - V1 uses a 24-column grid where each cell is 30px tall with 8px vertical margin
|
|
// - To convert pixels to grid units: ceil(pixels / 38)
|
|
// - Panels flow left-to-right, wrapping to next row when column limit reached
|
|
//
|
|
// Width: 24 / maxColumnCount (default 3 columns = 8 units wide)
|
|
// Height: Predefined grid units per mode (see pixelsToGridUnits for custom)
|
|
func convertAutoGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind) ([]interface{}, error) {
|
|
panels := make([]interface{}, 0, len(autoGridLayout.Spec.Items))
|
|
|
|
const (
|
|
gridTotalColumns int64 = 24 // V1 grid is 24 columns wide
|
|
gridCellHeightPx float64 = 30 // GRID_CELL_HEIGHT: height of one grid cell in pixels
|
|
gridCellVerticalMargin float64 = 8 // GRID_CELL_VMARGIN: margin between grid cells in pixels
|
|
defaultMaxColumnCount float64 = 3 // Default number of columns in AutoGrid
|
|
)
|
|
|
|
// pixelsToGridUnits converts a pixel height to grid units.
|
|
// Formula: ceil(pixels / (cellHeight + margin)) = ceil(pixels / 38)
|
|
pixelsToGridUnits := func(pixels float64) int64 {
|
|
gridUnitSize := gridCellHeightPx + gridCellVerticalMargin // 38px per grid unit
|
|
return int64((pixels + gridUnitSize - 1) / gridUnitSize) // Ceiling division
|
|
}
|
|
|
|
// Calculate panel width: divide 24-column grid by number of columns
|
|
maxColumnCount := defaultMaxColumnCount
|
|
if autoGridLayout.Spec.MaxColumnCount != nil && *autoGridLayout.Spec.MaxColumnCount > 0 {
|
|
maxColumnCount = *autoGridLayout.Spec.MaxColumnCount
|
|
}
|
|
panelWidth := int64(float64(gridTotalColumns) / maxColumnCount)
|
|
|
|
// Calculate panel height based on rowHeightMode
|
|
// Each mode has a target pixel height that maps to a specific grid unit count:
|
|
// short: ~168px -> 5 grid units
|
|
// standard: ~320px -> 9 grid units (default)
|
|
// tall: ~512px -> 14 grid units
|
|
// custom: user-specified pixels -> calculated grid units
|
|
var panelHeight int64
|
|
switch autoGridLayout.Spec.RowHeightMode {
|
|
case dashv2alpha1.DashboardAutoGridLayoutSpecRowHeightModeShort:
|
|
panelHeight = 5
|
|
case dashv2alpha1.DashboardAutoGridLayoutSpecRowHeightModeStandard:
|
|
panelHeight = 9
|
|
case dashv2alpha1.DashboardAutoGridLayoutSpecRowHeightModeTall:
|
|
panelHeight = 14
|
|
case dashv2alpha1.DashboardAutoGridLayoutSpecRowHeightModeCustom:
|
|
if autoGridLayout.Spec.RowHeight != nil && *autoGridLayout.Spec.RowHeight > 0 {
|
|
panelHeight = pixelsToGridUnits(*autoGridLayout.Spec.RowHeight)
|
|
} else {
|
|
panelHeight = 9 // Fall back to standard
|
|
}
|
|
default:
|
|
panelHeight = 9 // Default to standard
|
|
}
|
|
|
|
var currentY int64 = 0
|
|
var currentX int64 = 0
|
|
|
|
for _, item := range autoGridLayout.Spec.Items {
|
|
element, ok := elements[item.Spec.Element.Name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("panel with uid %s not found in the dashboard elements", item.Spec.Element.Name)
|
|
}
|
|
|
|
// Create a GridLayoutItem from the AutoGridLayoutItem for conversion
|
|
gridItem := dashv2alpha1.DashboardGridLayoutItemKind{
|
|
Kind: "GridLayoutItem",
|
|
Spec: dashv2alpha1.DashboardGridLayoutItemSpec{
|
|
X: currentX,
|
|
Y: currentY,
|
|
Width: panelWidth,
|
|
Height: panelHeight,
|
|
Element: dashv2alpha1.DashboardElementReference{
|
|
Kind: item.Spec.Element.Kind,
|
|
Name: item.Spec.Element.Name,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Convert AutoGridRepeatOptions to RepeatOptions if present
|
|
// AutoGridRepeatOptions only has mode and value; infer direction and maxPerRow from AutoGrid settings:
|
|
// - direction: always "h" (AutoGrid flows horizontally, left-to-right then wraps)
|
|
// - maxPerRow: from AutoGrid's maxColumnCount
|
|
if item.Spec.Repeat != nil {
|
|
directionH := dashv2alpha1.DashboardRepeatOptionsDirectionH
|
|
maxPerRow := int64(maxColumnCount)
|
|
gridItem.Spec.Repeat = &dashv2alpha1.DashboardRepeatOptions{
|
|
Mode: item.Spec.Repeat.Mode,
|
|
Value: item.Spec.Repeat.Value,
|
|
Direction: &directionH,
|
|
MaxPerRow: &maxPerRow,
|
|
}
|
|
}
|
|
|
|
panel, err := convertPanelFromElement(&element, &gridItem)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
|
|
}
|
|
panels = append(panels, panel)
|
|
|
|
// Move to next position: wrap to next row when exceeding maxColumnCount
|
|
currentX += panelWidth
|
|
if currentX >= gridTotalColumns {
|
|
currentX = 0
|
|
currentY += panelHeight
|
|
}
|
|
}
|
|
|
|
return panels, nil
|
|
}
|
|
|
|
// convertTabsLayoutToPanels converts V2 TabsLayout to V1 row panels.
|
|
// V1 has no native tab concept, so tabs are converted to expanded row panels.
|
|
// Each tab becomes a row panel (collapsed=false, panels=[]) with its content
|
|
// flattened to the top level. Tab order is preserved in the output.
|
|
func convertTabsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind) ([]interface{}, error) {
|
|
return convertNestedLayoutToPanels(elements, nil, tabsLayout, 0)
|
|
}
|
|
|
|
func convertPanelFromElement(element *dashv2alpha1.DashboardElement, layoutItem *dashv2alpha1.DashboardGridLayoutItemKind) (map[string]interface{}, error) {
|
|
panel := make(map[string]interface{})
|
|
|
|
// Set grid position
|
|
gridPos := map[string]interface{}{
|
|
"x": layoutItem.Spec.X,
|
|
"y": layoutItem.Spec.Y,
|
|
"w": layoutItem.Spec.Width,
|
|
"h": layoutItem.Spec.Height,
|
|
}
|
|
panel["gridPos"] = gridPos
|
|
|
|
// Handle repeat options
|
|
if layoutItem.Spec.Repeat != nil {
|
|
panel["repeat"] = layoutItem.Spec.Repeat.Value
|
|
if layoutItem.Spec.Repeat.Direction != nil {
|
|
switch *layoutItem.Spec.Repeat.Direction {
|
|
case dashv2alpha1.DashboardRepeatOptionsDirectionH:
|
|
panel["repeatDirection"] = "h"
|
|
case dashv2alpha1.DashboardRepeatOptionsDirectionV:
|
|
panel["repeatDirection"] = "v"
|
|
}
|
|
}
|
|
if layoutItem.Spec.Repeat.MaxPerRow != nil {
|
|
panel["maxPerRow"] = *layoutItem.Spec.Repeat.MaxPerRow
|
|
}
|
|
}
|
|
|
|
if element.PanelKind != nil {
|
|
return convertPanelKindToV1(element.PanelKind, panel)
|
|
}
|
|
|
|
if element.LibraryPanelKind != nil {
|
|
return convertLibraryPanelKindToV1(element.LibraryPanelKind, panel)
|
|
}
|
|
|
|
return nil, fmt.Errorf("element has neither PanelKind nor LibraryPanelKind")
|
|
}
|
|
|
|
func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[string]interface{}) (map[string]interface{}, error) {
|
|
spec := panelKind.Spec
|
|
|
|
panel["id"] = int(spec.Id)
|
|
panel["title"] = spec.Title
|
|
panel["pluginVersion"] = spec.VizConfig.Spec.PluginVersion
|
|
|
|
if spec.Description != "" {
|
|
panel["description"] = spec.Description
|
|
}
|
|
|
|
// Convert vizConfig - use the plugin ID from VizConfig.Kind, not PanelKind.Kind
|
|
panel["type"] = spec.VizConfig.Kind // panel type from vizConfig kind (plugin ID)
|
|
if spec.VizConfig.Spec.Options != nil {
|
|
panel["options"] = spec.VizConfig.Spec.Options
|
|
}
|
|
|
|
// Convert field config
|
|
fieldConfig := convertFieldConfigSourceToV1(&spec.VizConfig.Spec.FieldConfig)
|
|
if fieldConfig != nil {
|
|
panel["fieldConfig"] = fieldConfig
|
|
}
|
|
|
|
// Convert data links
|
|
if len(spec.Links) > 0 {
|
|
links := make([]map[string]interface{}, 0, len(spec.Links))
|
|
for _, link := range spec.Links {
|
|
linkMap := map[string]interface{}{
|
|
"title": link.Title,
|
|
"url": link.Url,
|
|
}
|
|
if link.TargetBlank != nil {
|
|
linkMap["targetBlank"] = *link.TargetBlank
|
|
}
|
|
links = append(links, linkMap)
|
|
}
|
|
panel["links"] = links
|
|
}
|
|
|
|
// Convert queries (targets)
|
|
// Use []interface{} for consistency with JSON unmarshaling and other code paths
|
|
targets := make([]interface{}, 0, len(spec.Data.Spec.Queries))
|
|
for _, query := range spec.Data.Spec.Queries {
|
|
target := convertPanelQueryToV1(&query)
|
|
targets = append(targets, target)
|
|
}
|
|
panel["targets"] = targets
|
|
|
|
// Set panel-level datasource from queries.
|
|
// - If queries use different datasources, set to "mixed"
|
|
// - If all queries use the same datasource, set to that datasource
|
|
// This is required because the frontend's legacy PanelModel.PanelQueryRunner.run uses panel.datasource
|
|
// and some components like CSVExportPage rely on it to resolve the datasource. If undefined, it falls back to the default datasource
|
|
// which would overwrite the query's datasource.
|
|
if panelDS := getPanelDatasource(spec.Data.Spec.Queries); panelDS != nil {
|
|
panel["datasource"] = panelDS
|
|
}
|
|
|
|
// Convert transformations
|
|
if len(spec.Data.Spec.Transformations) > 0 {
|
|
transformations := make([]map[string]interface{}, 0, len(spec.Data.Spec.Transformations))
|
|
for _, t := range spec.Data.Spec.Transformations {
|
|
transformation := map[string]interface{}{
|
|
"id": t.Spec.Id,
|
|
"options": t.Spec.Options,
|
|
}
|
|
// Add disabled if set
|
|
if t.Spec.Disabled != nil {
|
|
transformation["disabled"] = *t.Spec.Disabled
|
|
}
|
|
// Add filter if set
|
|
if t.Spec.Filter != nil {
|
|
transformation["filter"] = map[string]interface{}{
|
|
"id": t.Spec.Filter.Id,
|
|
"options": t.Spec.Filter.Options,
|
|
}
|
|
}
|
|
transformations = append(transformations, transformation)
|
|
}
|
|
panel["transformations"] = transformations
|
|
}
|
|
|
|
// Convert query options
|
|
queryOptions := spec.Data.Spec.QueryOptions
|
|
if queryOptions.CacheTimeout != nil {
|
|
panel["cacheTimeout"] = *queryOptions.CacheTimeout
|
|
}
|
|
if queryOptions.MaxDataPoints != nil {
|
|
panel["maxDataPoints"] = *queryOptions.MaxDataPoints
|
|
}
|
|
if queryOptions.Interval != nil {
|
|
panel["interval"] = *queryOptions.Interval
|
|
}
|
|
if queryOptions.HideTimeOverride != nil {
|
|
panel["hideTimeOverride"] = *queryOptions.HideTimeOverride
|
|
}
|
|
if queryOptions.QueryCachingTTL != nil {
|
|
panel["queryCachingTTL"] = *queryOptions.QueryCachingTTL
|
|
}
|
|
if queryOptions.TimeFrom != nil {
|
|
panel["timeFrom"] = *queryOptions.TimeFrom
|
|
}
|
|
if queryOptions.TimeShift != nil {
|
|
panel["timeShift"] = *queryOptions.TimeShift
|
|
}
|
|
|
|
// Convert transparent
|
|
if spec.Transparent != nil {
|
|
panel["transparent"] = *spec.Transparent
|
|
}
|
|
|
|
return panel, nil
|
|
}
|
|
|
|
func convertPanelQueryToV1(query *dashv2alpha1.DashboardPanelQueryKind) map[string]interface{} {
|
|
target := make(map[string]interface{})
|
|
|
|
// Copy query spec (excluding refId, hide, datasource which are handled separately)
|
|
querySpec := query.Spec.Query.Spec
|
|
for key, value := range querySpec {
|
|
// Skip LEGACY_STRING_VALUE_KEY - handle it specially
|
|
if key == LEGACY_STRING_VALUE_KEY {
|
|
if strValue, ok := value.(string); ok {
|
|
target["query"] = strValue
|
|
}
|
|
continue
|
|
}
|
|
target[key] = value
|
|
}
|
|
|
|
// Add refId
|
|
target["refId"] = query.Spec.RefId
|
|
|
|
// Only include hide when true (hidden). This matches frontend behavior which omits falsy values.
|
|
if query.Spec.Hidden {
|
|
target["hide"] = true
|
|
}
|
|
|
|
// Resolve datasource based on V2 input (reuse shared function)
|
|
datasource := getDataSourceForQuery(query.Spec.Datasource, query.Spec.Query.Kind)
|
|
if datasource != nil {
|
|
target["datasource"] = datasource
|
|
}
|
|
|
|
return target
|
|
}
|
|
|
|
// getDataSourceForQuery converts V2 datasource info to V1 format.
|
|
// This preserves exactly what V2 has without runtime resolution:
|
|
// - If explicit UID provided → return {uid, type}
|
|
// - Else if queryKind (type) is non-empty → return {type} only
|
|
// - Else → return nil (no datasource)
|
|
// Used for variables and annotations. Panel queries use convertPanelQueryToV1Target.
|
|
func getDataSourceForQuery(explicitDS *dashv2alpha1.DashboardDataSourceRef, queryKind string) map[string]interface{} {
|
|
// Case 1: Explicit datasource with UID provided
|
|
if explicitDS != nil && explicitDS.Uid != nil && *explicitDS.Uid != "" {
|
|
datasource := map[string]interface{}{
|
|
"uid": *explicitDS.Uid,
|
|
}
|
|
if explicitDS.Type != nil {
|
|
datasource["type"] = *explicitDS.Type
|
|
} else if queryKind != "" {
|
|
// Use query kind as type if explicit type not provided
|
|
datasource["type"] = queryKind
|
|
}
|
|
return datasource
|
|
}
|
|
|
|
// Case 2: No explicit UID but query kind (type) exists - include only type
|
|
if queryKind != "" {
|
|
return map[string]interface{}{
|
|
"type": queryKind,
|
|
}
|
|
}
|
|
|
|
// Case 3: No UID and no query kind - no datasource
|
|
return nil
|
|
}
|
|
|
|
// getPanelDatasource determines the panel-level datasource for V1.
|
|
// Returns:
|
|
// - Mixed datasource reference if queries use different datasources
|
|
// - Mixed datasource reference if multiple queries use Dashboard datasource (they fetch from different panels)
|
|
// - Dashboard datasource reference if a single query uses Dashboard datasource
|
|
// - First query's datasource if all queries use the same datasource
|
|
// - nil if no queries exist
|
|
// Compares based on V2 input without runtime resolution:
|
|
// - If query has explicit datasource.uid → use that UID and type
|
|
// - Else → use query.Kind as type (empty UID)
|
|
func getPanelDatasource(queries []dashv2alpha1.DashboardPanelQueryKind) map[string]interface{} {
|
|
const sharedDashboardQuery = "-- Dashboard --"
|
|
|
|
if len(queries) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Count how many queries use Dashboard datasource
|
|
// Multiple dashboard queries need mixed mode because they fetch from different panels
|
|
// which may have different underlying datasources
|
|
dashboardDsQueryCount := 0
|
|
for _, query := range queries {
|
|
if query.Spec.Datasource != nil && query.Spec.Datasource.Uid != nil && *query.Spec.Datasource.Uid == sharedDashboardQuery {
|
|
dashboardDsQueryCount++
|
|
}
|
|
}
|
|
if dashboardDsQueryCount > 1 {
|
|
return map[string]interface{}{
|
|
"type": "mixed",
|
|
"uid": "-- Mixed --",
|
|
}
|
|
}
|
|
|
|
var firstUID, firstType string
|
|
var hasFirst bool
|
|
|
|
for _, query := range queries {
|
|
var queryUID, queryType string
|
|
|
|
// Get datasource from query based on V2 input (no runtime resolution)
|
|
if query.Spec.Datasource != nil && query.Spec.Datasource.Uid != nil && *query.Spec.Datasource.Uid != "" {
|
|
// Explicit datasource reference
|
|
queryUID = *query.Spec.Datasource.Uid
|
|
if query.Spec.Datasource.Type != nil {
|
|
queryType = *query.Spec.Datasource.Type
|
|
} else {
|
|
queryType = query.Spec.Query.Kind
|
|
}
|
|
} else {
|
|
// No explicit datasource - use query kind as type
|
|
queryUID = ""
|
|
queryType = query.Spec.Query.Kind
|
|
}
|
|
|
|
if !hasFirst {
|
|
firstUID = queryUID
|
|
firstType = queryType
|
|
hasFirst = true
|
|
} else if firstUID != queryUID || firstType != queryType {
|
|
// Different datasource found - this is a mixed panel
|
|
return map[string]interface{}{
|
|
"type": "mixed",
|
|
"uid": "-- Mixed --",
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle case when a single query uses Dashboard datasource.
|
|
// This is needed for the frontend to properly activate and fetch data from source panels.
|
|
// See DashboardDatasourceBehaviour.tsx for more details.
|
|
if firstUID == sharedDashboardQuery {
|
|
return map[string]interface{}{
|
|
"type": "datasource",
|
|
"uid": sharedDashboardQuery,
|
|
}
|
|
}
|
|
|
|
// Not mixed - return the first query's datasource so the panel has a datasource set.
|
|
// This is required because the frontend's legacy PanelModel.PanelQueryRunner.run uses panel.datasource
|
|
// to resolve the datasource, and if undefined, it falls back to the default datasource
|
|
// which then overwrites the query's datasource.
|
|
if firstType == "" && firstUID == "" {
|
|
return nil
|
|
}
|
|
result := make(map[string]interface{})
|
|
if firstType != "" {
|
|
result["type"] = firstType
|
|
}
|
|
if firstUID != "" {
|
|
result["uid"] = firstUID
|
|
}
|
|
return result
|
|
}
|
|
|
|
func convertLibraryPanelKindToV1(libPanelKind *dashv2alpha1.DashboardLibraryPanelKind, panel map[string]interface{}) (map[string]interface{}, error) {
|
|
spec := libPanelKind.Spec
|
|
|
|
panel["id"] = int(spec.Id)
|
|
panel["title"] = spec.Title
|
|
panel["type"] = "library-panel-ref"
|
|
|
|
libraryPanel := map[string]interface{}{
|
|
"uid": spec.LibraryPanel.Uid,
|
|
"name": spec.LibraryPanel.Name,
|
|
}
|
|
panel["libraryPanel"] = libraryPanel
|
|
|
|
return panel, nil
|
|
}
|
|
|
|
func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind) []map[string]interface{} {
|
|
result := make([]map[string]interface{}, 0, len(variables))
|
|
|
|
for _, variable := range variables {
|
|
var varMap map[string]interface{}
|
|
var err error
|
|
|
|
if variable.QueryVariableKind != nil {
|
|
varMap, err = convertQueryVariableToV1(variable.QueryVariableKind)
|
|
} else if variable.DatasourceVariableKind != nil {
|
|
varMap, err = convertDatasourceVariableToV1(variable.DatasourceVariableKind)
|
|
} else if variable.CustomVariableKind != nil {
|
|
varMap, err = convertCustomVariableToV1(variable.CustomVariableKind)
|
|
} else if variable.ConstantVariableKind != nil {
|
|
varMap, err = convertConstantVariableToV1(variable.ConstantVariableKind)
|
|
} else if variable.IntervalVariableKind != nil {
|
|
varMap, err = convertIntervalVariableToV1(variable.IntervalVariableKind)
|
|
} else if variable.TextVariableKind != nil {
|
|
varMap, err = convertTextVariableToV1(variable.TextVariableKind)
|
|
} else if variable.GroupByVariableKind != nil {
|
|
varMap, err = convertGroupByVariableToV1(variable.GroupByVariableKind)
|
|
} else if variable.AdhocVariableKind != nil {
|
|
varMap, err = convertAdhocVariableToV1(variable.AdhocVariableKind)
|
|
} else if variable.SwitchVariableKind != nil {
|
|
varMap, err = convertSwitchVariableToV1(variable.SwitchVariableKind)
|
|
}
|
|
|
|
if err == nil && varMap != nil {
|
|
result = append(result, varMap)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind) (map[string]interface{}, error) {
|
|
spec := variable.Spec
|
|
varMap := map[string]interface{}{
|
|
"name": spec.Name,
|
|
"type": "query",
|
|
"hide": transformVariableHideFromEnum(spec.Hide),
|
|
"skipUrlSync": spec.SkipUrlSync,
|
|
"multi": spec.Multi,
|
|
"includeAll": spec.IncludeAll,
|
|
"refresh": transformVariableRefreshFromEnum(spec.Refresh),
|
|
"sort": transformVariableSortFromEnum(spec.Sort),
|
|
"regex": spec.Regex,
|
|
"current": map[string]interface{}{
|
|
"text": convertStringOrArrayOfStringToV1(spec.Current.Text),
|
|
"value": convertStringOrArrayOfStringToV1(spec.Current.Value),
|
|
},
|
|
"options": convertVariableOptionsToV1(spec.Options),
|
|
}
|
|
|
|
if spec.Label != nil {
|
|
varMap["label"] = *spec.Label
|
|
}
|
|
if spec.Description != nil {
|
|
varMap["description"] = *spec.Description
|
|
}
|
|
if spec.AllValue != nil {
|
|
varMap["allValue"] = *spec.AllValue
|
|
}
|
|
if spec.Definition != nil {
|
|
varMap["definition"] = *spec.Definition
|
|
}
|
|
if spec.RegexApplyTo != nil {
|
|
varMap["regexApplyTo"] = string(*spec.RegexApplyTo)
|
|
}
|
|
varMap["allowCustomValue"] = spec.AllowCustomValue
|
|
|
|
// Convert query - handle LEGACY_STRING_VALUE_KEY
|
|
querySpec := spec.Query.Spec
|
|
if legacyValue, ok := querySpec[LEGACY_STRING_VALUE_KEY].(string); ok {
|
|
varMap["query"] = legacyValue
|
|
} else {
|
|
// Copy query spec as-is
|
|
queryCopy := make(map[string]interface{})
|
|
for k, v := range querySpec {
|
|
queryCopy[k] = v
|
|
}
|
|
varMap["query"] = queryCopy
|
|
}
|
|
|
|
// Resolve datasource - use explicit datasource or resolve from query kind (datasource type)/default
|
|
datasource := getDataSourceForQuery(spec.Datasource, spec.Query.Kind)
|
|
if datasource != nil {
|
|
varMap["datasource"] = datasource
|
|
}
|
|
|
|
return varMap, nil
|
|
}
|
|
|
|
func convertDatasourceVariableToV1(variable *dashv2alpha1.DashboardDatasourceVariableKind) (map[string]interface{}, error) {
|
|
spec := variable.Spec
|
|
varMap := map[string]interface{}{
|
|
"name": spec.Name,
|
|
"type": "datasource",
|
|
"hide": transformVariableHideFromEnum(spec.Hide),
|
|
"skipUrlSync": spec.SkipUrlSync,
|
|
"multi": spec.Multi,
|
|
"includeAll": spec.IncludeAll,
|
|
"refresh": transformVariableRefreshFromEnum(spec.Refresh),
|
|
"regex": spec.Regex,
|
|
"query": spec.PluginId,
|
|
"current": map[string]interface{}{
|
|
"text": convertStringOrArrayOfStringToV1(spec.Current.Text),
|
|
"value": convertStringOrArrayOfStringToV1(spec.Current.Value),
|
|
},
|
|
"options": convertVariableOptionsToV1(spec.Options),
|
|
}
|
|
|
|
if spec.Label != nil {
|
|
varMap["label"] = *spec.Label
|
|
}
|
|
if spec.Description != nil {
|
|
varMap["description"] = *spec.Description
|
|
}
|
|
if spec.AllValue != nil {
|
|
varMap["allValue"] = *spec.AllValue
|
|
}
|
|
varMap["allowCustomValue"] = spec.AllowCustomValue
|
|
|
|
return varMap, nil
|
|
}
|
|
|
|
func convertCustomVariableToV1(variable *dashv2alpha1.DashboardCustomVariableKind) (map[string]interface{}, error) {
|
|
spec := variable.Spec
|
|
varMap := map[string]interface{}{
|
|
"name": spec.Name,
|
|
"type": "custom",
|
|
"hide": transformVariableHideFromEnum(spec.Hide),
|
|
"skipUrlSync": spec.SkipUrlSync,
|
|
"multi": spec.Multi,
|
|
"includeAll": spec.IncludeAll,
|
|
"query": spec.Query,
|
|
"current": map[string]interface{}{
|
|
"text": convertStringOrArrayOfStringToV1(spec.Current.Text),
|
|
"value": convertStringOrArrayOfStringToV1(spec.Current.Value),
|
|
},
|
|
"options": convertVariableOptionsToV1(spec.Options),
|
|
}
|
|
|
|
if spec.Label != nil {
|
|
varMap["label"] = *spec.Label
|
|
}
|
|
if spec.Description != nil {
|
|
varMap["description"] = *spec.Description
|
|
}
|
|
if spec.AllValue != nil {
|
|
varMap["allValue"] = *spec.AllValue
|
|
}
|
|
varMap["allowCustomValue"] = spec.AllowCustomValue
|
|
|
|
return varMap, nil
|
|
}
|
|
|
|
func convertConstantVariableToV1(variable *dashv2alpha1.DashboardConstantVariableKind) (map[string]interface{}, error) {
|
|
spec := variable.Spec
|
|
// Constant variables in v1beta1 must always be hidden (hide: 2),
|
|
// otherwise DashboardMigrator will convert them to textbox variables.
|
|
varMap := map[string]interface{}{
|
|
"name": spec.Name,
|
|
"type": "constant",
|
|
"hide": 2, // hideVariable - constant variables must always be hidden in v1beta1
|
|
"skipUrlSync": spec.SkipUrlSync,
|
|
"query": spec.Query,
|
|
"current": map[string]interface{}{
|
|
"text": convertStringOrArrayOfStringToV1(spec.Current.Text),
|
|
"value": convertStringOrArrayOfStringToV1(spec.Current.Value),
|
|
},
|
|
}
|
|
|
|
if spec.Label != nil {
|
|
varMap["label"] = *spec.Label
|
|
}
|
|
if spec.Description != nil {
|
|
varMap["description"] = *spec.Description
|
|
}
|
|
|
|
return varMap, nil
|
|
}
|
|
|
|
func convertIntervalVariableToV1(variable *dashv2alpha1.DashboardIntervalVariableKind) (map[string]interface{}, error) {
|
|
spec := variable.Spec
|
|
varMap := map[string]interface{}{
|
|
"name": spec.Name,
|
|
"type": "interval",
|
|
"hide": transformVariableHideFromEnum(spec.Hide),
|
|
"skipUrlSync": spec.SkipUrlSync,
|
|
"query": spec.Query,
|
|
"auto": spec.Auto,
|
|
"auto_min": spec.AutoMin,
|
|
"auto_count": spec.AutoCount,
|
|
"current": map[string]interface{}{
|
|
"text": convertStringOrArrayOfStringToV1(spec.Current.Text),
|
|
"value": convertStringOrArrayOfStringToV1(spec.Current.Value),
|
|
},
|
|
"options": convertVariableOptionsToV1(spec.Options),
|
|
}
|
|
|
|
if spec.Label != nil {
|
|
varMap["label"] = *spec.Label
|
|
}
|
|
if spec.Description != nil {
|
|
varMap["description"] = *spec.Description
|
|
}
|
|
|
|
return varMap, nil
|
|
}
|
|
|
|
func convertTextVariableToV1(variable *dashv2alpha1.DashboardTextVariableKind) (map[string]interface{}, error) {
|
|
spec := variable.Spec
|
|
varMap := map[string]interface{}{
|
|
"name": spec.Name,
|
|
"type": "textbox",
|
|
"hide": transformVariableHideFromEnum(spec.Hide),
|
|
"skipUrlSync": spec.SkipUrlSync,
|
|
"query": spec.Query,
|
|
"current": map[string]interface{}{
|
|
"text": convertStringOrArrayOfStringToV1(spec.Current.Text),
|
|
"value": convertStringOrArrayOfStringToV1(spec.Current.Value),
|
|
},
|
|
}
|
|
|
|
if spec.Label != nil {
|
|
varMap["label"] = *spec.Label
|
|
}
|
|
if spec.Description != nil {
|
|
varMap["description"] = *spec.Description
|
|
}
|
|
|
|
return varMap, nil
|
|
}
|
|
|
|
func convertGroupByVariableToV1(variable *dashv2alpha1.DashboardGroupByVariableKind) (map[string]interface{}, error) {
|
|
spec := variable.Spec
|
|
varMap := map[string]interface{}{
|
|
"name": spec.Name,
|
|
"type": "groupby",
|
|
"hide": transformVariableHideFromEnum(spec.Hide),
|
|
"skipUrlSync": spec.SkipUrlSync,
|
|
"multi": spec.Multi,
|
|
"current": map[string]interface{}{
|
|
"text": convertStringOrArrayOfStringToV1(spec.Current.Text),
|
|
"value": convertStringOrArrayOfStringToV1(spec.Current.Value),
|
|
},
|
|
"options": convertVariableOptionsToV1(spec.Options),
|
|
}
|
|
|
|
if spec.Label != nil {
|
|
varMap["label"] = *spec.Label
|
|
}
|
|
if spec.Description != nil {
|
|
varMap["description"] = *spec.Description
|
|
}
|
|
|
|
// Resolve datasource - GroupBy variables don't have a query kind, so use empty string (will fall back to default)
|
|
datasource := getDataSourceForQuery(spec.Datasource, "")
|
|
if datasource != nil {
|
|
varMap["datasource"] = datasource
|
|
}
|
|
|
|
return varMap, nil
|
|
}
|
|
|
|
func convertAdhocVariableToV1(variable *dashv2alpha1.DashboardAdhocVariableKind) (map[string]interface{}, error) {
|
|
spec := variable.Spec
|
|
varMap := map[string]interface{}{
|
|
"name": spec.Name,
|
|
"type": "adhoc",
|
|
"hide": transformVariableHideFromEnum(spec.Hide),
|
|
"skipUrlSync": spec.SkipUrlSync,
|
|
}
|
|
|
|
if spec.Label != nil {
|
|
varMap["label"] = *spec.Label
|
|
}
|
|
if spec.Description != nil {
|
|
varMap["description"] = *spec.Description
|
|
}
|
|
// Always include allowCustomValue for adhoc variables, including false values
|
|
varMap["allowCustomValue"] = spec.AllowCustomValue
|
|
|
|
// Resolve datasource - Adhoc variables don't have a query kind, so use empty string (will fall back to default)
|
|
datasource := getDataSourceForQuery(spec.Datasource, "")
|
|
if datasource != nil {
|
|
varMap["datasource"] = datasource
|
|
}
|
|
|
|
// Convert filters
|
|
if len(spec.Filters) > 0 {
|
|
filters := make([]map[string]interface{}, 0, len(spec.Filters))
|
|
for _, filter := range spec.Filters {
|
|
filterMap := map[string]interface{}{
|
|
"key": filter.Key,
|
|
"operator": filter.Operator,
|
|
"value": filter.Value,
|
|
}
|
|
if filter.KeyLabel != nil {
|
|
filterMap["keyLabel"] = *filter.KeyLabel
|
|
}
|
|
if filter.Condition != nil {
|
|
filterMap["condition"] = *filter.Condition
|
|
}
|
|
if len(filter.Values) > 0 {
|
|
filterMap["values"] = filter.Values
|
|
}
|
|
if len(filter.ValueLabels) > 0 {
|
|
filterMap["valueLabels"] = filter.ValueLabels
|
|
}
|
|
filters = append(filters, filterMap)
|
|
}
|
|
varMap["filters"] = filters
|
|
}
|
|
|
|
// Convert baseFilters
|
|
if len(spec.BaseFilters) > 0 {
|
|
baseFilters := make([]map[string]interface{}, 0, len(spec.BaseFilters))
|
|
for _, filter := range spec.BaseFilters {
|
|
filterMap := map[string]interface{}{
|
|
"key": filter.Key,
|
|
"operator": filter.Operator,
|
|
"value": filter.Value,
|
|
}
|
|
if filter.KeyLabel != nil {
|
|
filterMap["keyLabel"] = *filter.KeyLabel
|
|
}
|
|
if filter.Condition != nil {
|
|
filterMap["condition"] = *filter.Condition
|
|
}
|
|
if len(filter.Values) > 0 {
|
|
filterMap["values"] = filter.Values
|
|
}
|
|
if len(filter.ValueLabels) > 0 {
|
|
filterMap["valueLabels"] = filter.ValueLabels
|
|
}
|
|
baseFilters = append(baseFilters, filterMap)
|
|
}
|
|
varMap["baseFilters"] = baseFilters
|
|
}
|
|
|
|
// Convert defaultKeys
|
|
if len(spec.DefaultKeys) > 0 {
|
|
defaultKeys := make([]interface{}, 0, len(spec.DefaultKeys))
|
|
for _, key := range spec.DefaultKeys {
|
|
keyMap := map[string]interface{}{
|
|
"text": key.Text,
|
|
}
|
|
if key.Value != nil {
|
|
if key.Value.String != nil {
|
|
keyMap["value"] = *key.Value.String
|
|
} else if key.Value.Float64 != nil {
|
|
keyMap["value"] = *key.Value.Float64
|
|
}
|
|
}
|
|
// Preserve optional fields if present in input
|
|
if key.Group != nil {
|
|
keyMap["group"] = *key.Group
|
|
}
|
|
if key.Expandable != nil {
|
|
keyMap["expandable"] = *key.Expandable
|
|
}
|
|
defaultKeys = append(defaultKeys, keyMap)
|
|
}
|
|
varMap["defaultKeys"] = defaultKeys
|
|
}
|
|
|
|
return varMap, nil
|
|
}
|
|
|
|
func convertSwitchVariableToV1(variable *dashv2alpha1.DashboardSwitchVariableKind) (map[string]interface{}, error) {
|
|
spec := variable.Spec
|
|
|
|
// Determine which value is selected based on current
|
|
enabledSelected := spec.Current == spec.EnabledValue
|
|
disabledSelected := spec.Current == spec.DisabledValue
|
|
|
|
varMap := map[string]interface{}{
|
|
"name": spec.Name,
|
|
"type": "switch",
|
|
"hide": transformVariableHideFromEnum(spec.Hide),
|
|
"skipUrlSync": spec.SkipUrlSync,
|
|
"query": "", // Switch variables have empty query in V1
|
|
"current": map[string]interface{}{
|
|
"text": spec.Current,
|
|
"value": spec.Current,
|
|
},
|
|
"options": []map[string]interface{}{
|
|
{
|
|
"text": spec.EnabledValue,
|
|
"value": spec.EnabledValue,
|
|
"selected": enabledSelected,
|
|
},
|
|
{
|
|
"text": spec.DisabledValue,
|
|
"value": spec.DisabledValue,
|
|
"selected": disabledSelected,
|
|
},
|
|
},
|
|
}
|
|
|
|
if spec.Label != nil {
|
|
varMap["label"] = *spec.Label
|
|
}
|
|
if spec.Description != nil {
|
|
varMap["description"] = *spec.Description
|
|
}
|
|
|
|
return varMap, nil
|
|
}
|
|
|
|
func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryKind) []map[string]interface{} {
|
|
result := make([]map[string]interface{}, 0, len(annotations))
|
|
|
|
for _, annotation := range annotations {
|
|
annotationMap := map[string]interface{}{
|
|
"name": annotation.Spec.Name,
|
|
"enable": annotation.Spec.Enable,
|
|
"hide": annotation.Spec.Hide,
|
|
"iconColor": annotation.Spec.IconColor,
|
|
}
|
|
|
|
// Convert builtIn boolean to integer (v1beta1 uses 1 for true, and omits for false)
|
|
// Also add type: "dashboard" for built-in annotations
|
|
if annotation.Spec.BuiltIn != nil && *annotation.Spec.BuiltIn {
|
|
annotationMap["builtIn"] = 1
|
|
annotationMap["type"] = "dashboard"
|
|
}
|
|
|
|
// Resolve datasource - use explicit datasource or resolve from query kind (datasource type)/default
|
|
var queryKind string
|
|
if annotation.Spec.Query != nil {
|
|
queryKind = annotation.Spec.Query.Kind
|
|
}
|
|
datasource := getDataSourceForQuery(annotation.Spec.Datasource, queryKind)
|
|
if datasource != nil {
|
|
annotationMap["datasource"] = datasource
|
|
}
|
|
|
|
// Convert query to target
|
|
if annotation.Spec.Query != nil {
|
|
// Copy query spec as target
|
|
target := make(map[string]interface{})
|
|
for k, v := range annotation.Spec.Query.Spec {
|
|
target[k] = v
|
|
}
|
|
if len(target) > 0 {
|
|
annotationMap["target"] = target
|
|
}
|
|
}
|
|
|
|
// Convert filter
|
|
if annotation.Spec.Filter != nil {
|
|
filter := make(map[string]interface{})
|
|
if annotation.Spec.Filter.Exclude != nil {
|
|
filter["exclude"] = *annotation.Spec.Filter.Exclude
|
|
}
|
|
if len(annotation.Spec.Filter.Ids) > 0 {
|
|
ids := make([]interface{}, 0, len(annotation.Spec.Filter.Ids))
|
|
for _, id := range annotation.Spec.Filter.Ids {
|
|
ids = append(ids, id)
|
|
}
|
|
filter["ids"] = ids
|
|
}
|
|
if len(filter) > 0 {
|
|
annotationMap["filter"] = filter
|
|
}
|
|
}
|
|
|
|
// Convert mappings from v2alpha1 format back to v1beta1 format
|
|
if len(annotation.Spec.Mappings) > 0 {
|
|
mappings := convertAnnotationMappings_V2alpha1_to_V1beta1(annotation.Spec.Mappings)
|
|
if len(mappings) > 0 {
|
|
annotationMap["mappings"] = mappings
|
|
}
|
|
}
|
|
|
|
// Copy legacy options
|
|
// This is used to copy any unknown properties from the v1 at the root of the annotations that were not handled by the conversion.
|
|
// When they are converted into V2 they are moved to legacyOptions. Now we move them back to the root of the annotation.
|
|
if annotation.Spec.LegacyOptions != nil {
|
|
for k, v := range annotation.Spec.LegacyOptions {
|
|
// Skip fields already handled
|
|
if k != "name" && k != "enable" && k != "hide" && k != "iconColor" &&
|
|
k != "datasource" && k != "target" && k != "filter" && k != "builtIn" && k != "placement" && k != "mappings" {
|
|
annotationMap[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
result = append(result, annotationMap)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// convertAnnotationMappings_V2alpha1_to_V1beta1 converts mappings from v2alpha1 structured format
|
|
// back to v1beta1 format. v1beta1 supports both simple string format and structured format with source/value/regex.
|
|
// v2alpha1 format: map[string]DashboardAnnotationEventFieldMapping with Source, Value, Regex
|
|
// v1beta1 format: map[string]interface{} where values can be either:
|
|
// - string (legacy simple format: "fieldName": "targetFieldName")
|
|
// - object (structured format: "fieldName": {"source": "field", "value": "...", "regex": "..."})
|
|
func convertAnnotationMappings_V2alpha1_to_V1beta1(mappings map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping) map[string]interface{} {
|
|
result := make(map[string]interface{})
|
|
|
|
for key, mapping := range mappings {
|
|
// Always convert to structured format with source and value fields
|
|
mappingMap := make(map[string]interface{})
|
|
|
|
// Source defaults to "field" if not specified
|
|
source := "field"
|
|
if mapping.Source != nil {
|
|
source = *mapping.Source
|
|
}
|
|
mappingMap["source"] = source
|
|
|
|
// Value is optional (required for "field" and "text" sources, but "skip" doesn't need it)
|
|
if mapping.Value != nil && *mapping.Value != "" {
|
|
mappingMap["value"] = *mapping.Value
|
|
}
|
|
|
|
// Regex is optional
|
|
if mapping.Regex != nil && *mapping.Regex != "" {
|
|
mappingMap["regex"] = *mapping.Regex
|
|
}
|
|
|
|
// Include the mapping if it has source (and value for non-skip sources)
|
|
// Skip source doesn't require a value
|
|
if source == "skip" || (mapping.Value != nil && *mapping.Value != "") {
|
|
result[key] = mappingMap
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Enum transformation functions (reverse of v1→v2)
|
|
func transformVariableHideFromEnum(hide dashv2alpha1.DashboardVariableHide) interface{} {
|
|
switch hide {
|
|
case dashv2alpha1.DashboardVariableHideDontHide:
|
|
return 0
|
|
case dashv2alpha1.DashboardVariableHideHideLabel:
|
|
return 1
|
|
case dashv2alpha1.DashboardVariableHideHideVariable:
|
|
return 2
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func transformVariableRefreshFromEnum(refresh dashv2alpha1.DashboardVariableRefresh) interface{} {
|
|
switch refresh {
|
|
case dashv2alpha1.DashboardVariableRefreshNever:
|
|
return 0
|
|
case dashv2alpha1.DashboardVariableRefreshOnDashboardLoad:
|
|
return 1
|
|
case dashv2alpha1.DashboardVariableRefreshOnTimeRangeChanged:
|
|
return 2
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func transformVariableSortFromEnum(sort dashv2alpha1.DashboardVariableSort) interface{} {
|
|
switch sort {
|
|
case dashv2alpha1.DashboardVariableSortDisabled:
|
|
return 0
|
|
case dashv2alpha1.DashboardVariableSortAlphabeticalAsc:
|
|
return 1
|
|
case dashv2alpha1.DashboardVariableSortAlphabeticalDesc:
|
|
return 2
|
|
case dashv2alpha1.DashboardVariableSortNumericalAsc:
|
|
return 3
|
|
case dashv2alpha1.DashboardVariableSortNumericalDesc:
|
|
return 4
|
|
case dashv2alpha1.DashboardVariableSortAlphabeticalCaseInsensitiveAsc:
|
|
return 5
|
|
case dashv2alpha1.DashboardVariableSortAlphabeticalCaseInsensitiveDesc:
|
|
return 6
|
|
case dashv2alpha1.DashboardVariableSortNaturalAsc:
|
|
return 7
|
|
case dashv2alpha1.DashboardVariableSortNaturalDesc:
|
|
return 8
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func convertStringOrArrayOfStringToV1(value dashv2alpha1.DashboardStringOrArrayOfString) interface{} {
|
|
if value.String != nil {
|
|
return *value.String
|
|
}
|
|
if len(value.ArrayOfString) > 0 {
|
|
return value.ArrayOfString
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func convertVariableOptionsToV1(options []dashv2alpha1.DashboardVariableOption) []map[string]interface{} {
|
|
result := make([]map[string]interface{}, 0, len(options))
|
|
for _, option := range options {
|
|
optionMap := map[string]interface{}{
|
|
"text": convertStringOrArrayOfStringToV1(option.Text),
|
|
"value": convertStringOrArrayOfStringToV1(option.Value),
|
|
}
|
|
if option.Selected != nil {
|
|
optionMap["selected"] = *option.Selected
|
|
}
|
|
result = append(result, optionMap)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func convertFieldConfigSourceToV1(fieldConfig *dashv2alpha1.DashboardFieldConfigSource) map[string]interface{} {
|
|
if fieldConfig == nil {
|
|
return nil
|
|
}
|
|
|
|
result := make(map[string]interface{})
|
|
|
|
// Convert defaults
|
|
defaults := convertFieldConfigDefaultsToV1(&fieldConfig.Defaults)
|
|
if defaults != nil {
|
|
result["defaults"] = defaults
|
|
}
|
|
|
|
// Convert overrides
|
|
overrides := convertFieldConfigOverridesToV1(fieldConfig.Overrides)
|
|
if overrides != nil {
|
|
result["overrides"] = overrides
|
|
}
|
|
|
|
if len(result) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func convertFieldConfigDefaultsToV1(defaults *dashv2alpha1.DashboardFieldConfig) map[string]interface{} {
|
|
if defaults == nil {
|
|
return nil
|
|
}
|
|
|
|
result := make(map[string]interface{})
|
|
|
|
if defaults.Custom != nil {
|
|
result["custom"] = defaults.Custom
|
|
}
|
|
if defaults.Decimals != nil {
|
|
result["decimals"] = *defaults.Decimals
|
|
}
|
|
if defaults.Max != nil {
|
|
result["max"] = *defaults.Max
|
|
}
|
|
if defaults.Min != nil {
|
|
result["min"] = *defaults.Min
|
|
}
|
|
if defaults.Description != nil {
|
|
result["description"] = *defaults.Description
|
|
}
|
|
if defaults.DisplayName != nil {
|
|
result["displayName"] = *defaults.DisplayName
|
|
}
|
|
if defaults.DisplayNameFromDS != nil {
|
|
result["displayNameFromDS"] = *defaults.DisplayNameFromDS
|
|
}
|
|
if defaults.NoValue != nil {
|
|
result["noValue"] = *defaults.NoValue
|
|
}
|
|
if defaults.Path != nil {
|
|
result["path"] = *defaults.Path
|
|
}
|
|
if defaults.Unit != nil {
|
|
result["unit"] = *defaults.Unit
|
|
}
|
|
if defaults.Filterable != nil {
|
|
result["filterable"] = *defaults.Filterable
|
|
}
|
|
if defaults.Writeable != nil {
|
|
result["writeable"] = *defaults.Writeable
|
|
}
|
|
if defaults.FieldMinMax != nil {
|
|
result["fieldMinMax"] = *defaults.FieldMinMax
|
|
}
|
|
if defaults.NullValueMode != nil {
|
|
result["nullValueMode"] = string(*defaults.NullValueMode)
|
|
}
|
|
if defaults.Links != nil {
|
|
result["links"] = defaults.Links
|
|
}
|
|
if len(defaults.Actions) > 0 {
|
|
result["actions"] = convertActionsToV1(defaults.Actions)
|
|
}
|
|
if defaults.Color != nil {
|
|
result["color"] = convertFieldColorToV1(defaults.Color)
|
|
}
|
|
if defaults.Mappings != nil {
|
|
result["mappings"] = convertMappingsToV1(defaults.Mappings)
|
|
}
|
|
if defaults.Thresholds != nil {
|
|
result["thresholds"] = convertThresholdsToV1(defaults.Thresholds)
|
|
}
|
|
|
|
if len(result) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func convertFieldConfigOverridesToV1(overrides []dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides) []map[string]interface{} {
|
|
if len(overrides) == 0 {
|
|
return nil
|
|
}
|
|
|
|
result := make([]map[string]interface{}, 0, len(overrides))
|
|
for _, override := range overrides {
|
|
overrideMap := make(map[string]interface{})
|
|
|
|
if override.SystemRef != nil {
|
|
overrideMap["__systemRef"] = *override.SystemRef
|
|
}
|
|
|
|
overrideMap["matcher"] = map[string]interface{}{
|
|
"id": override.Matcher.Id,
|
|
"options": override.Matcher.Options,
|
|
}
|
|
|
|
properties := make([]map[string]interface{}, 0, len(override.Properties))
|
|
if len(override.Properties) > 0 {
|
|
for _, prop := range override.Properties {
|
|
properties = append(properties, map[string]interface{}{
|
|
"id": prop.Id,
|
|
"value": prop.Value,
|
|
})
|
|
}
|
|
}
|
|
overrideMap["properties"] = properties
|
|
|
|
result = append(result, overrideMap)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func convertFieldColorToV1(color *dashv2alpha1.DashboardFieldColor) map[string]interface{} {
|
|
if color == nil {
|
|
return nil
|
|
}
|
|
|
|
colorMap := map[string]interface{}{
|
|
"mode": string(color.Mode),
|
|
}
|
|
|
|
if color.FixedColor != nil {
|
|
colorMap["fixedColor"] = *color.FixedColor
|
|
}
|
|
if color.SeriesBy != nil {
|
|
colorMap["seriesBy"] = string(*color.SeriesBy)
|
|
}
|
|
|
|
return colorMap
|
|
}
|
|
|
|
func convertMappingsToV1(mappings []dashv2alpha1.DashboardValueMapping) []map[string]interface{} {
|
|
result := make([]map[string]interface{}, 0, len(mappings))
|
|
|
|
for _, mapping := range mappings {
|
|
var mappingMap map[string]interface{}
|
|
|
|
if mapping.ValueMap != nil {
|
|
mappingMap = convertValueMapToV1(mapping.ValueMap)
|
|
} else if mapping.RangeMap != nil {
|
|
mappingMap = convertRangeMapToV1(mapping.RangeMap)
|
|
} else if mapping.RegexMap != nil {
|
|
mappingMap = convertRegexMapToV1(mapping.RegexMap)
|
|
} else if mapping.SpecialValueMap != nil {
|
|
mappingMap = convertSpecialValueMapToV1(mapping.SpecialValueMap)
|
|
}
|
|
|
|
if mappingMap != nil {
|
|
result = append(result, mappingMap)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func convertValueMapToV1(valueMap *dashv2alpha1.DashboardValueMap) map[string]interface{} {
|
|
if valueMap == nil {
|
|
return nil
|
|
}
|
|
|
|
options := make(map[string]interface{})
|
|
for k, v := range valueMap.Options {
|
|
options[k] = convertValueMappingResultToV1(v)
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"type": "value",
|
|
"options": options,
|
|
}
|
|
}
|
|
|
|
func convertRangeMapToV1(rangeMap *dashv2alpha1.DashboardRangeMap) map[string]interface{} {
|
|
if rangeMap == nil {
|
|
return nil
|
|
}
|
|
|
|
options := map[string]interface{}{
|
|
"result": convertValueMappingResultToV1(rangeMap.Options.Result),
|
|
}
|
|
if rangeMap.Options.From != nil {
|
|
options["from"] = *rangeMap.Options.From
|
|
}
|
|
if rangeMap.Options.To != nil {
|
|
options["to"] = *rangeMap.Options.To
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"type": "range",
|
|
"options": options,
|
|
}
|
|
}
|
|
|
|
func convertRegexMapToV1(regexMap *dashv2alpha1.DashboardRegexMap) map[string]interface{} {
|
|
if regexMap == nil {
|
|
return nil
|
|
}
|
|
|
|
options := map[string]interface{}{
|
|
"pattern": regexMap.Options.Pattern,
|
|
"result": convertValueMappingResultToV1(regexMap.Options.Result),
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"type": "regex",
|
|
"options": options,
|
|
}
|
|
}
|
|
|
|
func convertSpecialValueMapToV1(specialMap *dashv2alpha1.DashboardSpecialValueMap) map[string]interface{} {
|
|
if specialMap == nil {
|
|
return nil
|
|
}
|
|
|
|
options := map[string]interface{}{
|
|
"match": string(specialMap.Options.Match),
|
|
"result": convertValueMappingResultToV1(specialMap.Options.Result),
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"type": "special",
|
|
"options": options,
|
|
}
|
|
}
|
|
|
|
func convertValueMappingResultToV1(result dashv2alpha1.DashboardValueMappingResult) map[string]interface{} {
|
|
resultMap := make(map[string]interface{})
|
|
if result.Text != nil {
|
|
resultMap["text"] = *result.Text
|
|
}
|
|
if result.Color != nil {
|
|
resultMap["color"] = *result.Color
|
|
}
|
|
if result.Icon != nil {
|
|
resultMap["icon"] = *result.Icon
|
|
}
|
|
if result.Index != nil {
|
|
resultMap["index"] = *result.Index
|
|
}
|
|
return resultMap
|
|
}
|
|
|
|
func convertThresholdsToV1(thresholds *dashv2alpha1.DashboardThresholdsConfig) map[string]interface{} {
|
|
if thresholds == nil {
|
|
return nil
|
|
}
|
|
|
|
thresholdsMap := map[string]interface{}{
|
|
"mode": string(thresholds.Mode),
|
|
"steps": make([]map[string]interface{}, 0, len(thresholds.Steps)),
|
|
}
|
|
|
|
for _, step := range thresholds.Steps {
|
|
stepMap := map[string]interface{}{
|
|
"color": step.Color,
|
|
}
|
|
// Preserve null values (representing -Infinity)
|
|
if step.Value != nil {
|
|
stepMap["value"] = *step.Value
|
|
} else {
|
|
stepMap["value"] = nil
|
|
}
|
|
thresholdsMap["steps"] = append(thresholdsMap["steps"].([]map[string]interface{}), stepMap)
|
|
}
|
|
|
|
return thresholdsMap
|
|
}
|
|
|
|
func convertActionsToV1(actions []dashv2alpha1.DashboardAction) []map[string]interface{} {
|
|
result := make([]map[string]interface{}, 0, len(actions))
|
|
|
|
for _, action := range actions {
|
|
actionMap := map[string]interface{}{
|
|
"type": string(action.Type),
|
|
"title": action.Title,
|
|
}
|
|
|
|
if action.Confirmation != nil {
|
|
actionMap["confirmation"] = *action.Confirmation
|
|
}
|
|
|
|
if action.OneClick != nil {
|
|
actionMap["oneClick"] = *action.OneClick
|
|
}
|
|
|
|
if action.Fetch != nil {
|
|
actionMap["fetch"] = convertFetchOptionsToV1(action.Fetch)
|
|
}
|
|
|
|
if action.Infinity != nil {
|
|
actionMap["infinity"] = convertInfinityOptionsToV1(action.Infinity)
|
|
}
|
|
|
|
if len(action.Variables) > 0 {
|
|
actionMap["variables"] = convertActionVariablesToV1(action.Variables)
|
|
}
|
|
|
|
if action.Style != nil {
|
|
styleMap := map[string]interface{}{}
|
|
if action.Style.BackgroundColor != nil {
|
|
styleMap["backgroundColor"] = *action.Style.BackgroundColor
|
|
}
|
|
if len(styleMap) > 0 {
|
|
actionMap["style"] = styleMap
|
|
}
|
|
}
|
|
|
|
result = append(result, actionMap)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func convertFetchOptionsToV1(fetch *dashv2alpha1.DashboardFetchOptions) map[string]interface{} {
|
|
result := map[string]interface{}{
|
|
"method": string(fetch.Method),
|
|
"url": fetch.Url,
|
|
}
|
|
|
|
if fetch.Body != nil {
|
|
result["body"] = *fetch.Body
|
|
}
|
|
|
|
if len(fetch.QueryParams) > 0 {
|
|
result["queryParams"] = convert2DStringArrayToInterface(fetch.QueryParams)
|
|
}
|
|
|
|
if len(fetch.Headers) > 0 {
|
|
result["headers"] = convert2DStringArrayToInterface(fetch.Headers)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func convertInfinityOptionsToV1(infinity *dashv2alpha1.DashboardInfinityOptions) map[string]interface{} {
|
|
result := map[string]interface{}{
|
|
"method": string(infinity.Method),
|
|
"url": infinity.Url,
|
|
"datasourceUid": infinity.DatasourceUid,
|
|
}
|
|
|
|
if infinity.Body != nil {
|
|
result["body"] = *infinity.Body
|
|
}
|
|
|
|
if len(infinity.QueryParams) > 0 {
|
|
result["queryParams"] = convert2DStringArrayToInterface(infinity.QueryParams)
|
|
}
|
|
|
|
if len(infinity.Headers) > 0 {
|
|
result["headers"] = convert2DStringArrayToInterface(infinity.Headers)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func convertActionVariablesToV1(variables []dashv2alpha1.DashboardActionVariable) []map[string]interface{} {
|
|
result := make([]map[string]interface{}, 0, len(variables))
|
|
for _, v := range variables {
|
|
result = append(result, map[string]interface{}{
|
|
"key": v.Key,
|
|
"name": v.Name,
|
|
"type": v.Type,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func convert2DStringArrayToInterface(arr [][]string) []interface{} {
|
|
result := make([]interface{}, 0, len(arr))
|
|
for _, innerArr := range arr {
|
|
interfaceArr := make([]interface{}, 0, len(innerArr))
|
|
for _, s := range innerArr {
|
|
interfaceArr = append(interfaceArr, s)
|
|
}
|
|
result = append(result, interfaceArr)
|
|
}
|
|
return result
|
|
}
|