* descritpion and transformations * add fieldMinMax to schema * add nullValueMode to schema * convert actions * disabled prop in transformations * update * fix * gauge * remove index (deprecated) and decimals as strings * gofmt * codegen * lint * open api
3023 lines
100 KiB
Go
3023 lines
100 KiB
Go
package conversion
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"k8s.io/apimachinery/pkg/conversion"
|
|
"k8s.io/apiserver/pkg/endpoints/request"
|
|
|
|
"github.com/grafana/authlib/types"
|
|
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard"
|
|
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
|
dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
|
schemaversion "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
)
|
|
|
|
// getDefaultDatasourceType gets the default datasource type using the datasource provider
|
|
func getDefaultDatasourceType(ctx context.Context, provider schemaversion.DataSourceIndexProvider) string {
|
|
const defaultType = "grafana"
|
|
|
|
if provider == nil {
|
|
return defaultType
|
|
}
|
|
|
|
datasources := provider.Index(ctx)
|
|
if defaultDS := datasources.GetDefault(); defaultDS != nil {
|
|
return defaultDS.Type
|
|
}
|
|
|
|
return defaultType
|
|
}
|
|
|
|
// getDatasourceTypeByUID gets the datasource type by UID using the datasource provider
|
|
func getDatasourceTypeByUID(ctx context.Context, uid string, provider schemaversion.DataSourceIndexProvider) string {
|
|
if uid == "" {
|
|
return getDefaultDatasourceType(ctx, provider)
|
|
}
|
|
|
|
if provider == nil {
|
|
return "grafana"
|
|
}
|
|
|
|
dsIndex := provider.Index(ctx)
|
|
if ds := dsIndex.LookupByUID(uid); ds != nil {
|
|
return ds.Type
|
|
}
|
|
|
|
return getDefaultDatasourceType(ctx, provider)
|
|
}
|
|
|
|
// resolveGrafanaDatasourceUID resolves the Grafana datasource UID when type is "datasource" and UID is empty.
|
|
// The Grafana datasource has type "datasource" and UID "grafana". When a v1beta1 dashboard has
|
|
// datasource: { type: "datasource" } with no UID, it should resolve to uid: "grafana".
|
|
func resolveGrafanaDatasourceUID(dsType, dsUID string) string {
|
|
if dsType == "datasource" && dsUID == "" {
|
|
return dashboard.GrafanaDatasourceUID
|
|
}
|
|
return dsUID
|
|
}
|
|
|
|
// prepareV1beta1ConversionContext sets up the context with namespace and service identity
|
|
// for v1beta1 dashboard conversions. This context is needed to retrieve datasources for
|
|
// converting dashboard datasource references.
|
|
// A background service identity is used because the user who is reading the specific dashboard
|
|
// may not have access to all the datasources in the dashboard, but the conversion still needs to take place
|
|
// in order to be able to convert between k8s versions.
|
|
func prepareV1beta1ConversionContext(in *dashv1.Dashboard, dsIndexProvider schemaversion.DataSourceIndexProvider) (context.Context, *types.NamespaceInfo, error) {
|
|
if dsIndexProvider == nil {
|
|
return nil, nil, fmt.Errorf("datasource index provider not initialized")
|
|
}
|
|
|
|
namespace := in.GetNamespace()
|
|
if namespace == "" {
|
|
return context.Background(), nil, nil
|
|
}
|
|
|
|
// Try to parse namespace and set up tenant-aware context
|
|
ctx := request.WithNamespace(context.Background(), namespace)
|
|
nsInfo, err := types.ParseNamespace(namespace)
|
|
if err != nil {
|
|
return ctx, nil, nil
|
|
}
|
|
|
|
ctx, _ = identity.WithServiceIdentity(ctx, nsInfo.OrgID)
|
|
|
|
return ctx, &nsInfo, nil
|
|
}
|
|
|
|
func ConvertDashboard_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error {
|
|
out.ObjectMeta = in.ObjectMeta
|
|
out.APIVersion = dashv2alpha1.APIVERSION
|
|
out.Kind = in.Kind
|
|
|
|
// Prepare context with namespace and service identity
|
|
// The datasource provider is already wrapped with caching at registration time
|
|
ctx, _, err := prepareV1beta1ConversionContext(in, dsIndexProvider)
|
|
if err != nil {
|
|
// If context preparation fails, return error to be handled by wrapper
|
|
// The wrapper will set status and handle gracefully
|
|
return fmt.Errorf("failed to prepare conversion context: %w", err)
|
|
}
|
|
|
|
return convertDashboardSpec_V1beta1_to_V2alpha1(&in.Spec, &out.Spec, scope, ctx, dsIndexProvider, leIndexProvider)
|
|
}
|
|
|
|
func convertDashboardSpec_V1beta1_to_V2alpha1(in *dashv1.DashboardSpec, out *dashv2alpha1.DashboardSpec, scope conversion.Scope, ctx context.Context, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error {
|
|
// Parse the unstructured spec into a dashboard JSON structure
|
|
dashboardJSON, ok := in.Object["dashboard"]
|
|
if !ok {
|
|
// If no "dashboard" key, treat the entire object as the dashboard
|
|
dashboardJSON = in.Object
|
|
}
|
|
|
|
// Convert to bytes and back to handle complex nested structures
|
|
dashBytes, err := json.Marshal(dashboardJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal dashboard JSON: %w", err)
|
|
}
|
|
|
|
var dashboard map[string]interface{}
|
|
if err := json.Unmarshal(dashBytes, &dashboard); err != nil {
|
|
return fmt.Errorf("failed to unmarshal dashboard JSON: %w", err)
|
|
}
|
|
|
|
// Get defaults
|
|
timeSettingsDefaults := dashv2alpha1.NewDashboardTimeSettingsSpec()
|
|
dashboardDefaults := getDefaultDashboardV2Spec()
|
|
|
|
// Transform basic fields
|
|
out.Title = schemaversion.GetStringValue(dashboard, "title")
|
|
out.Description = getStringPtr(dashboard, "description")
|
|
out.Tags = getStringSlice(dashboard, "tags")
|
|
out.CursorSync = transformCursorSyncToEnum(getIntField(dashboard, "graphTooltip", 0))
|
|
out.Preload = getBoolField(dashboard, "preload", dashboardDefaults.Preload)
|
|
|
|
// Add frontend-style default values
|
|
// Set default editable: true to match frontend behavior
|
|
defaultEditable := true
|
|
out.Editable = &defaultEditable
|
|
|
|
// Set default liveNow: false to match frontend behavior
|
|
defaultLiveNow := false
|
|
out.LiveNow = &defaultLiveNow
|
|
|
|
// Override with input values if they exist
|
|
if liveNow, exists := dashboard["liveNow"]; exists {
|
|
if liveNowBool, ok := liveNow.(bool); ok {
|
|
out.LiveNow = &liveNowBool
|
|
}
|
|
}
|
|
if editable, exists := dashboard["editable"]; exists {
|
|
if editableBool, ok := editable.(bool); ok {
|
|
out.Editable = &editableBool
|
|
}
|
|
}
|
|
if revision, exists := dashboard["revision"]; exists {
|
|
if revisionInt, ok := revision.(float64); ok {
|
|
revisionUint := uint16(revisionInt)
|
|
out.Revision = &revisionUint
|
|
}
|
|
}
|
|
|
|
// Transform time settings
|
|
out.TimeSettings = transformTimeSettings(dashboard, timeSettingsDefaults)
|
|
|
|
// Transform links
|
|
out.Links = transformLinks(dashboard)
|
|
|
|
// Transform panels to elements and layout
|
|
elements, layout, err := transformPanelsToElementsAndLayout(ctx, dashboard, dsIndexProvider, leIndexProvider)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to transform panels: %w", err)
|
|
}
|
|
out.Elements = elements
|
|
out.Layout = layout
|
|
|
|
// Transform variables
|
|
variables, err := transformVariables(ctx, dashboard, dsIndexProvider)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to transform variables: %w", err)
|
|
}
|
|
out.Variables = variables
|
|
|
|
// Transform annotations
|
|
annotations, err := transformAnnotations(dashboard)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to transform annotations: %w", err)
|
|
}
|
|
out.Annotations = annotations
|
|
|
|
return nil
|
|
}
|
|
|
|
func getStringPtr(m map[string]interface{}, key string) *string {
|
|
if val, ok := m[key]; ok {
|
|
if str, ok := val.(string); ok && str != "" {
|
|
return &str
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getStringSlice(m map[string]interface{}, key string) []string {
|
|
if val, ok := m[key]; ok {
|
|
if slice, ok := val.([]interface{}); ok {
|
|
result := make([]string, 0, len(slice))
|
|
for _, item := range slice {
|
|
if str, ok := item.(string); ok {
|
|
result = append(result, str)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
return []string{}
|
|
}
|
|
|
|
func getBoolField(m map[string]interface{}, key string, defaultValue bool) bool {
|
|
if val, ok := m[key]; ok {
|
|
if b, ok := val.(bool); ok {
|
|
return b
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// stripBOM removes Byte Order Mark (BOM) characters from a string.
|
|
// BOMs (U+FEFF) can be introduced through copy/paste from certain editors
|
|
// and cause CUE validation errors ("illegal byte order mark").
|
|
func stripBOM(s string) string {
|
|
return strings.ReplaceAll(s, "\ufeff", "")
|
|
}
|
|
|
|
// stripBOMFromInterface recursively strips BOM characters from all strings
|
|
// in an interface{} value (map, slice, or string).
|
|
func stripBOMFromInterface(v interface{}) interface{} {
|
|
switch val := v.(type) {
|
|
case string:
|
|
return stripBOM(val)
|
|
case map[string]interface{}:
|
|
result := make(map[string]interface{}, len(val))
|
|
for k, v := range val {
|
|
result[k] = stripBOMFromInterface(v)
|
|
}
|
|
return result
|
|
case []interface{}:
|
|
result := make([]interface{}, len(val))
|
|
for i, item := range val {
|
|
result[i] = stripBOMFromInterface(item)
|
|
}
|
|
return result
|
|
default:
|
|
return v
|
|
}
|
|
}
|
|
|
|
func getUnionField[T ~string](m map[string]interface{}, key string) *T {
|
|
if val, ok := m[key]; ok {
|
|
if str, ok := val.(string); ok && str != "" {
|
|
result := T(str)
|
|
return &result
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Helper function to create int64 pointer
|
|
func int64Ptr(i int64) *int64 {
|
|
return &i
|
|
}
|
|
|
|
// isTemplateVariable checks if a string is a Grafana template variable reference
|
|
// Template variables can be in the form: $varname or ${varname}
|
|
func isTemplateVariable(s string) bool {
|
|
if s == "" {
|
|
return false
|
|
}
|
|
return strings.HasPrefix(s, "$") || strings.HasPrefix(s, "${")
|
|
}
|
|
|
|
func getIntField(m map[string]interface{}, key string, defaultValue int) int {
|
|
if val, ok := m[key]; ok {
|
|
switch v := val.(type) {
|
|
case int:
|
|
return v
|
|
case float64:
|
|
return int(v)
|
|
case string:
|
|
if i, err := strconv.Atoi(v); err == nil {
|
|
return i
|
|
}
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// Enum transformation functions
|
|
func transformCursorSyncToEnum(cursorSync int) dashv2alpha1.DashboardDashboardCursorSync {
|
|
switch cursorSync {
|
|
case 0:
|
|
return dashv2alpha1.DashboardDashboardCursorSyncOff
|
|
case 1:
|
|
return dashv2alpha1.DashboardDashboardCursorSyncCrosshair
|
|
case 2:
|
|
return dashv2alpha1.DashboardDashboardCursorSyncTooltip
|
|
default:
|
|
return dashv2alpha1.DashboardDashboardCursorSyncOff
|
|
}
|
|
}
|
|
|
|
func getDefaultDashboardV2Spec() dashv2alpha1.DashboardSpec {
|
|
return dashv2alpha1.DashboardSpec{
|
|
Preload: false,
|
|
}
|
|
}
|
|
|
|
// Transform time settings
|
|
func transformTimeSettings(dashboard map[string]interface{}, defaults *dashv2alpha1.DashboardTimeSettingsSpec) dashv2alpha1.DashboardTimeSettingsSpec {
|
|
timeSettings := dashv2alpha1.DashboardTimeSettingsSpec{
|
|
From: defaults.From,
|
|
To: defaults.To,
|
|
Timezone: defaults.Timezone,
|
|
AutoRefresh: defaults.AutoRefresh,
|
|
AutoRefreshIntervals: defaults.AutoRefreshIntervals,
|
|
FiscalYearStartMonth: defaults.FiscalYearStartMonth,
|
|
HideTimepicker: defaults.HideTimepicker,
|
|
// Don't initialize optional fields with defaults - only set them if present in input
|
|
}
|
|
|
|
// Extract time range
|
|
if timeRange, ok := dashboard["time"].(map[string]interface{}); ok {
|
|
if from := schemaversion.GetStringValue(timeRange, "from"); from != "" {
|
|
timeSettings.From = from
|
|
}
|
|
if to := schemaversion.GetStringValue(timeRange, "to"); to != "" {
|
|
timeSettings.To = to
|
|
}
|
|
}
|
|
|
|
// Extract other time-related fields
|
|
if timezone, exists := dashboard["timezone"]; exists {
|
|
if timezoneStr, ok := timezone.(string); ok {
|
|
timeSettings.Timezone = &timezoneStr
|
|
}
|
|
}
|
|
if refresh := schemaversion.GetStringValue(dashboard, "refresh"); refresh != "" {
|
|
timeSettings.AutoRefresh = refresh
|
|
}
|
|
|
|
timeSettings.FiscalYearStartMonth = int64(getIntField(dashboard, "fiscalYearStartMonth", int(defaults.FiscalYearStartMonth)))
|
|
|
|
if weekStart, exists := dashboard["weekStart"]; exists {
|
|
if weekStartStr, ok := weekStart.(string); ok && weekStartStr != "" {
|
|
weekStartEnum := dashv2alpha1.DashboardTimeSettingsSpecWeekStart(weekStartStr)
|
|
timeSettings.WeekStart = &weekStartEnum
|
|
}
|
|
}
|
|
|
|
// Extract timepicker settings
|
|
if timepicker, ok := dashboard["timepicker"].(map[string]interface{}); ok {
|
|
if intervals, ok := timepicker["refresh_intervals"].([]interface{}); ok {
|
|
refreshIntervals := make([]string, 0, len(intervals))
|
|
for _, interval := range intervals {
|
|
if str, ok := interval.(string); ok {
|
|
refreshIntervals = append(refreshIntervals, str)
|
|
}
|
|
}
|
|
if len(refreshIntervals) > 0 {
|
|
timeSettings.AutoRefreshIntervals = refreshIntervals
|
|
}
|
|
}
|
|
timeSettings.HideTimepicker = getBoolField(timepicker, "hidden", defaults.HideTimepicker)
|
|
if nowDelay, exists := timepicker["nowDelay"]; exists {
|
|
if nowDelayStr, ok := nowDelay.(string); ok && nowDelayStr != "" {
|
|
timeSettings.NowDelay = &nowDelayStr
|
|
}
|
|
}
|
|
|
|
// Handle quick_ranges
|
|
if quickRanges, ok := timepicker["quick_ranges"].([]interface{}); ok {
|
|
ranges := make([]dashv2alpha1.DashboardTimeRangeOption, 0, len(quickRanges))
|
|
for _, qr := range quickRanges {
|
|
if qrMap, ok := qr.(map[string]interface{}); ok {
|
|
quickRange := dashv2alpha1.DashboardTimeRangeOption{
|
|
Display: schemaversion.GetStringValue(qrMap, "display"),
|
|
From: schemaversion.GetStringValue(qrMap, "from"),
|
|
To: schemaversion.GetStringValue(qrMap, "to"),
|
|
}
|
|
ranges = append(ranges, quickRange)
|
|
}
|
|
}
|
|
timeSettings.QuickRanges = ranges
|
|
}
|
|
}
|
|
|
|
return timeSettings
|
|
}
|
|
|
|
// Transform links
|
|
func transformLinks(dashboard map[string]interface{}) []dashv2alpha1.DashboardDashboardLink {
|
|
if links, ok := dashboard["links"].([]interface{}); ok {
|
|
result := make([]dashv2alpha1.DashboardDashboardLink, 0, len(links))
|
|
for _, link := range links {
|
|
if linkMap, ok := link.(map[string]interface{}); ok {
|
|
// Required fields with defaults
|
|
dashLink := dashv2alpha1.DashboardDashboardLink{
|
|
Title: schemaversion.GetStringValue(linkMap, "title"),
|
|
Type: dashv2alpha1.DashboardDashboardLinkType(schemaversion.GetStringValue(linkMap, "type", "link")),
|
|
Icon: schemaversion.GetStringValue(linkMap, "icon"),
|
|
Tooltip: schemaversion.GetStringValue(linkMap, "tooltip"),
|
|
Tags: getStringSlice(linkMap, "tags"),
|
|
AsDropdown: getBoolField(linkMap, "asDropdown", false),
|
|
TargetBlank: getBoolField(linkMap, "targetBlank", false),
|
|
IncludeVars: getBoolField(linkMap, "includeVars", false),
|
|
KeepTime: getBoolField(linkMap, "keepTime", false),
|
|
}
|
|
|
|
// Optional field - only set if present
|
|
if url, exists := linkMap["url"]; exists {
|
|
if urlStr, ok := url.(string); ok {
|
|
cleanUrl := stripBOM(urlStr)
|
|
dashLink.Url = &cleanUrl
|
|
}
|
|
}
|
|
|
|
// Optional placement field - only set if present
|
|
if placement, exists := linkMap["placement"]; exists {
|
|
if placementStr, ok := placement.(string); ok {
|
|
dashLink.Placement = &placementStr
|
|
}
|
|
}
|
|
|
|
result = append(result, dashLink)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
return []dashv2alpha1.DashboardDashboardLink{}
|
|
}
|
|
|
|
// Panel transformation constants
|
|
const GRID_ROW_HEIGHT = 1
|
|
|
|
func transformPanelsToElementsAndLayout(ctx context.Context, dashboard map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) {
|
|
panels, ok := dashboard["panels"].([]interface{})
|
|
if !ok {
|
|
// Return empty elements and default grid layout
|
|
elements := make(map[string]dashv2alpha1.DashboardElement)
|
|
layout := dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{
|
|
GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{
|
|
Kind: "GridLayout",
|
|
Spec: dashv2alpha1.DashboardGridLayoutSpec{
|
|
Items: []dashv2alpha1.DashboardGridLayoutItemKind{},
|
|
},
|
|
},
|
|
}
|
|
return elements, layout, nil
|
|
}
|
|
|
|
// Check if any panels are row panels
|
|
hasRowPanels := false
|
|
for _, p := range panels {
|
|
if panelMap, ok := p.(map[string]interface{}); ok {
|
|
if schemaversion.GetStringValue(panelMap, "type") == "row" {
|
|
hasRowPanels = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if hasRowPanels {
|
|
return convertToRowsLayout(ctx, panels, dsIndexProvider, leIndexProvider)
|
|
}
|
|
|
|
return convertToGridLayout(ctx, panels, dsIndexProvider, leIndexProvider)
|
|
}
|
|
|
|
func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) {
|
|
elements := make(map[string]dashv2alpha1.DashboardElement)
|
|
items := make([]dashv2alpha1.DashboardGridLayoutItemKind, 0, len(panels))
|
|
|
|
for _, p := range panels {
|
|
panelMap, ok := p.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
element, elementName, err := buildElement(ctx, panelMap, dsIndexProvider)
|
|
if err != nil {
|
|
continue // Skip invalid panels
|
|
}
|
|
|
|
elements[elementName] = element
|
|
items = append(items, buildGridItemKind(ctx, panelMap, elementName, nil, leIndexProvider))
|
|
}
|
|
|
|
layout := dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{
|
|
GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{
|
|
Kind: "GridLayout",
|
|
Spec: dashv2alpha1.DashboardGridLayoutSpec{
|
|
Items: items,
|
|
},
|
|
},
|
|
}
|
|
|
|
return elements, layout, nil
|
|
}
|
|
|
|
func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) {
|
|
elements := make(map[string]dashv2alpha1.DashboardElement)
|
|
rows := make([]dashv2alpha1.DashboardRowsLayoutRowKind, 0)
|
|
|
|
var currentRow *dashv2alpha1.DashboardRowsLayoutRowKind
|
|
var legacyRowY int64
|
|
|
|
for _, p := range panels {
|
|
panelMap, ok := p.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if schemaversion.GetStringValue(panelMap, "type") == "row" {
|
|
// This is a row panel
|
|
if gridPos, ok := panelMap["gridPos"].(map[string]interface{}); ok {
|
|
legacyRowY = int64(getIntField(gridPos, "y", 0))
|
|
}
|
|
|
|
if currentRow != nil {
|
|
// If currentRow is a hidden-header row (panels before first explicit row),
|
|
// it should not be collapsed because it will disappear and be visible only in edit mode
|
|
if currentRow.Spec.HideHeader != nil && *currentRow.Spec.HideHeader {
|
|
currentRow.Spec.Collapse = &[]bool{false}[0]
|
|
}
|
|
// Flush current row to layout
|
|
rows = append(rows, *currentRow)
|
|
}
|
|
|
|
// Handle collapsed row panels
|
|
rowElements := make([]dashv2alpha1.DashboardGridLayoutItemKind, 0)
|
|
if collapsedPanels, ok := panelMap["panels"].([]interface{}); ok {
|
|
for _, panel := range collapsedPanels {
|
|
if collapsedPanelMap, ok := panel.(map[string]interface{}); ok {
|
|
element, name, err := buildElement(ctx, collapsedPanelMap, dsIndexProvider)
|
|
if err == nil {
|
|
elements[name] = element
|
|
rowElements = append(rowElements, buildGridItemKind(ctx, collapsedPanelMap, name, int64Ptr(yOffsetInRows(collapsedPanelMap, legacyRowY)), leIndexProvider))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
currentRow = buildRowKind(panelMap, rowElements)
|
|
} else {
|
|
// Regular panel
|
|
element, elementName, err := buildElement(ctx, panelMap, dsIndexProvider)
|
|
if err != nil {
|
|
continue // Skip invalid panels
|
|
}
|
|
|
|
elements[elementName] = element
|
|
|
|
if currentRow != nil {
|
|
// Add to current row
|
|
if currentRow.Spec.Layout.GridLayoutKind != nil {
|
|
currentRow.Spec.Layout.GridLayoutKind.Spec.Items = append(
|
|
currentRow.Spec.Layout.GridLayoutKind.Spec.Items,
|
|
buildGridItemKind(ctx, panelMap, elementName, int64Ptr(yOffsetInRows(panelMap, legacyRowY)), leIndexProvider),
|
|
)
|
|
}
|
|
} else {
|
|
// Create first row (hidden header)
|
|
// Since this row does not exist in V1, we simulate it being outside of the grid above the first panel
|
|
// The Y position does not matter for the rows layout, but it's used to calculate the position of the panels in the grid layout in the row.
|
|
legacyRowY = -1
|
|
gridItems := []dashv2alpha1.DashboardGridLayoutItemKind{
|
|
buildGridItemKind(ctx, panelMap, elementName, int64Ptr(0), leIndexProvider),
|
|
}
|
|
|
|
hideHeader := true
|
|
currentRow = &dashv2alpha1.DashboardRowsLayoutRowKind{
|
|
Kind: "RowsLayoutRow",
|
|
Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{
|
|
Collapse: &[]bool{false}[0],
|
|
Title: &[]string{""}[0],
|
|
HideHeader: &hideHeader,
|
|
Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{
|
|
GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{
|
|
Kind: "GridLayout",
|
|
Spec: dashv2alpha1.DashboardGridLayoutSpec{
|
|
Items: gridItems,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if currentRow != nil {
|
|
// Flush last row
|
|
rows = append(rows, *currentRow)
|
|
}
|
|
|
|
layout := dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{
|
|
RowsLayoutKind: &dashv2alpha1.DashboardRowsLayoutKind{
|
|
Kind: "RowsLayout",
|
|
Spec: dashv2alpha1.DashboardRowsLayoutSpec{
|
|
Rows: rows,
|
|
},
|
|
},
|
|
}
|
|
|
|
return elements, layout, nil
|
|
}
|
|
|
|
func buildElement(ctx context.Context, panelMap map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardElement, string, error) {
|
|
panelID := getIntField(panelMap, "id", 0)
|
|
elementName := fmt.Sprintf("panel-%d", panelID)
|
|
|
|
// Check if it's a library panel
|
|
if libraryPanel, ok := panelMap["libraryPanel"].(map[string]interface{}); ok {
|
|
libPanelKind := &dashv2alpha1.DashboardLibraryPanelKind{
|
|
Kind: "LibraryPanel",
|
|
Spec: dashv2alpha1.DashboardLibraryPanelKindSpec{
|
|
LibraryPanel: dashv2alpha1.DashboardLibraryPanelRef{
|
|
Uid: schemaversion.GetStringValue(libraryPanel, "uid"),
|
|
Name: schemaversion.GetStringValue(libraryPanel, "name"),
|
|
},
|
|
Id: float64(panelID),
|
|
Title: schemaversion.GetStringValue(panelMap, "title"),
|
|
},
|
|
}
|
|
|
|
element := dashv2alpha1.DashboardElement{
|
|
LibraryPanelKind: libPanelKind,
|
|
}
|
|
|
|
return element, elementName, nil
|
|
}
|
|
|
|
// Regular panel
|
|
panelKind, err := buildPanelKind(ctx, panelMap, dsIndexProvider)
|
|
if err != nil {
|
|
return dashv2alpha1.DashboardElement{}, "", err
|
|
}
|
|
|
|
element := dashv2alpha1.DashboardElement{
|
|
PanelKind: panelKind,
|
|
}
|
|
|
|
return element, elementName, nil
|
|
}
|
|
|
|
func buildPanelKind(ctx context.Context, panelMap map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) (*dashv2alpha1.DashboardPanelKind, error) {
|
|
panelID := float64(getIntField(panelMap, "id", 0))
|
|
|
|
// Transform queries
|
|
queries := transformPanelQueries(ctx, panelMap, dsIndexProvider)
|
|
|
|
// Transform transformations
|
|
transformations := transformPanelTransformations(panelMap)
|
|
|
|
// Build query options
|
|
queryOptions := buildQueryOptions(panelMap)
|
|
|
|
// Build data links
|
|
links := transformDataLinks(panelMap)
|
|
|
|
// Build viz config
|
|
vizConfig := buildVizConfig(panelMap)
|
|
|
|
panelKind := &dashv2alpha1.DashboardPanelKind{
|
|
Kind: "Panel",
|
|
Spec: dashv2alpha1.DashboardPanelSpec{
|
|
Id: panelID,
|
|
Title: schemaversion.GetStringValue(panelMap, "title"),
|
|
Description: schemaversion.GetStringValue(panelMap, "description"),
|
|
Links: links,
|
|
Data: dashv2alpha1.DashboardQueryGroupKind{
|
|
Kind: "QueryGroup",
|
|
Spec: dashv2alpha1.DashboardQueryGroupSpec{
|
|
Queries: queries,
|
|
Transformations: transformations,
|
|
QueryOptions: queryOptions,
|
|
},
|
|
},
|
|
VizConfig: vizConfig,
|
|
},
|
|
}
|
|
|
|
// Handle transparent panels
|
|
if transparent := getBoolField(panelMap, "transparent", false); transparent {
|
|
panelKind.Spec.Transparent = &transparent
|
|
}
|
|
|
|
return panelKind, nil
|
|
}
|
|
|
|
func buildGridItemKind(ctx context.Context, panelMap map[string]interface{}, elementName string, yOverride *int64, leIndexProvider schemaversion.LibraryElementIndexProvider) dashv2alpha1.DashboardGridLayoutItemKind {
|
|
// Default grid position (matches frontend PanelModel defaults: w=6, h=3)
|
|
x, y, width, height := int64(0), int64(0), int64(6), int64(3)
|
|
|
|
if gridPos, ok := panelMap["gridPos"].(map[string]interface{}); ok {
|
|
x = int64(getIntField(gridPos, "x", 0))
|
|
y = int64(getIntField(gridPos, "y", 0))
|
|
width = int64(getIntField(gridPos, "w", 6))
|
|
height = int64(getIntField(gridPos, "h", 3))
|
|
}
|
|
|
|
// Apply frontend-style grid position calculations
|
|
// Frontend recalculates positions based on row structure
|
|
if yOverride != nil {
|
|
y = *yOverride
|
|
}
|
|
|
|
item := dashv2alpha1.DashboardGridLayoutItemKind{
|
|
Kind: "GridLayoutItem",
|
|
Spec: dashv2alpha1.DashboardGridLayoutItemSpec{
|
|
X: x,
|
|
Y: y,
|
|
Width: width,
|
|
Height: height,
|
|
Element: dashv2alpha1.DashboardElementReference{
|
|
Kind: "ElementReference",
|
|
Name: elementName,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Handle repeat options
|
|
// First check if repeat options are set on the panel itself (dashboard instance level)
|
|
repeatOptions := getRepeatOptionsFromPanel(panelMap)
|
|
|
|
// If no repeat options on the panel and it's a library panel, try to get them from the library panel definition
|
|
if repeatOptions == nil {
|
|
if libraryPanel, ok := panelMap["libraryPanel"].(map[string]interface{}); ok {
|
|
libraryPanelUID := schemaversion.GetStringValue(libraryPanel, "uid")
|
|
if libraryPanelUID != "" && leIndexProvider != nil {
|
|
repeatOptions = getRepeatOptionsFromLibraryPanel(ctx, libraryPanelUID, leIndexProvider)
|
|
}
|
|
}
|
|
}
|
|
|
|
if repeatOptions != nil {
|
|
item.Spec.Repeat = repeatOptions
|
|
}
|
|
|
|
return item
|
|
}
|
|
|
|
// getRepeatOptionsFromPanel extracts repeat options from a panel map (dashboard instance level)
|
|
func getRepeatOptionsFromPanel(panelMap map[string]any) *dashv2alpha1.DashboardRepeatOptions {
|
|
repeat := schemaversion.GetStringValue(panelMap, "repeat")
|
|
if repeat == "" {
|
|
return nil
|
|
}
|
|
|
|
repeatOptions := &dashv2alpha1.DashboardRepeatOptions{
|
|
Mode: "variable",
|
|
Value: repeat,
|
|
}
|
|
|
|
if repeatDirection := schemaversion.GetStringValue(panelMap, "repeatDirection"); repeatDirection != "" {
|
|
switch repeatDirection {
|
|
case "h":
|
|
direction := dashv2alpha1.DashboardRepeatOptionsDirectionH
|
|
repeatOptions.Direction = &direction
|
|
case "v":
|
|
direction := dashv2alpha1.DashboardRepeatOptionsDirectionV
|
|
repeatOptions.Direction = &direction
|
|
}
|
|
}
|
|
|
|
if maxPerRow := getIntField(panelMap, "maxPerRow", 0); maxPerRow > 0 {
|
|
maxPerRowInt64 := int64(maxPerRow)
|
|
repeatOptions.MaxPerRow = &maxPerRowInt64
|
|
}
|
|
|
|
return repeatOptions
|
|
}
|
|
|
|
// getRepeatOptionsFromLibraryPanel retrieves repeat options from a library panel by UID
|
|
func getRepeatOptionsFromLibraryPanel(ctx context.Context, libraryPanelUID string, leIndexProvider schemaversion.LibraryElementIndexProvider) *dashv2alpha1.DashboardRepeatOptions {
|
|
libraryElements := leIndexProvider.GetLibraryElementInfo(ctx)
|
|
|
|
// Find the library panel by UID
|
|
var libraryPanelModel map[string]any
|
|
for _, elem := range libraryElements {
|
|
if elem.UID == libraryPanelUID {
|
|
libraryPanelModel = elem.Model.Object
|
|
break
|
|
}
|
|
}
|
|
|
|
if libraryPanelModel == nil {
|
|
return nil
|
|
}
|
|
|
|
// Extract repeat options from the library panel model
|
|
return getRepeatOptionsFromPanel(libraryPanelModel)
|
|
}
|
|
|
|
func buildRowKind(rowPanelMap map[string]interface{}, elements []dashv2alpha1.DashboardGridLayoutItemKind) *dashv2alpha1.DashboardRowsLayoutRowKind {
|
|
collapsed := getBoolField(rowPanelMap, "collapsed", false)
|
|
title := schemaversion.GetStringValue(rowPanelMap, "title")
|
|
|
|
row := &dashv2alpha1.DashboardRowsLayoutRowKind{
|
|
Kind: "RowsLayoutRow",
|
|
Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{
|
|
Collapse: &collapsed,
|
|
Title: &title,
|
|
Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{
|
|
GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{
|
|
Kind: "GridLayout",
|
|
Spec: dashv2alpha1.DashboardGridLayoutSpec{
|
|
Items: elements,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Handle repeat options for rows
|
|
if repeat := schemaversion.GetStringValue(rowPanelMap, "repeat"); repeat != "" {
|
|
row.Spec.Repeat = &dashv2alpha1.DashboardRowRepeatOptions{
|
|
Mode: "variable",
|
|
Value: repeat,
|
|
}
|
|
}
|
|
|
|
return row
|
|
}
|
|
|
|
func yOffsetInRows(panelMap map[string]interface{}, rowY int64) int64 {
|
|
if gridPos, ok := panelMap["gridPos"].(map[string]interface{}); ok {
|
|
panelY := int64(getIntField(gridPos, "y", 0))
|
|
return panelY - rowY - GRID_ROW_HEIGHT
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// LEGACY_STRING_VALUE_KEY is used for QueryVariableKind's query prop. Historically, queries
|
|
// could be defined as strings, but in schema V2 we've deprecated the string type and support
|
|
// only DataQuery objects. When a query is a simple string (legacy format), it's stored under
|
|
// this key in a map structure to preserve the value during migration from v1beta1 to v2alpha1.
|
|
const LEGACY_STRING_VALUE_KEY = "__legacyStringValue"
|
|
|
|
// Variable enum transformation functions
|
|
func transformVariableHideToEnum(hide interface{}) dashv2alpha1.DashboardVariableHide {
|
|
switch v := hide.(type) {
|
|
case int:
|
|
switch v {
|
|
case 0:
|
|
return dashv2alpha1.DashboardVariableHideDontHide
|
|
case 1:
|
|
return dashv2alpha1.DashboardVariableHideHideLabel
|
|
case 2:
|
|
return dashv2alpha1.DashboardVariableHideHideVariable
|
|
default:
|
|
return dashv2alpha1.DashboardVariableHideDontHide
|
|
}
|
|
case float64:
|
|
return transformVariableHideToEnum(int(v))
|
|
case string:
|
|
switch v {
|
|
case "dontHide", "":
|
|
return dashv2alpha1.DashboardVariableHideDontHide
|
|
case "hideLabel":
|
|
return dashv2alpha1.DashboardVariableHideHideLabel
|
|
case "hideVariable":
|
|
return dashv2alpha1.DashboardVariableHideHideVariable
|
|
default:
|
|
return dashv2alpha1.DashboardVariableHideDontHide
|
|
}
|
|
default:
|
|
return dashv2alpha1.DashboardVariableHideDontHide
|
|
}
|
|
}
|
|
|
|
func transformVariableRefreshToEnum(refresh interface{}) dashv2alpha1.DashboardVariableRefresh {
|
|
switch v := refresh.(type) {
|
|
case int:
|
|
switch v {
|
|
case 0:
|
|
return dashv2alpha1.DashboardVariableRefreshNever
|
|
case 1:
|
|
return dashv2alpha1.DashboardVariableRefreshOnDashboardLoad
|
|
case 2:
|
|
return dashv2alpha1.DashboardVariableRefreshOnTimeRangeChanged
|
|
default:
|
|
return dashv2alpha1.DashboardVariableRefreshNever
|
|
}
|
|
case float64:
|
|
return transformVariableRefreshToEnum(int(v))
|
|
case string:
|
|
switch v {
|
|
case "never", "":
|
|
return dashv2alpha1.DashboardVariableRefreshNever
|
|
case "onDashboardLoad":
|
|
return dashv2alpha1.DashboardVariableRefreshOnDashboardLoad
|
|
case "onTimeRangeChanged":
|
|
return dashv2alpha1.DashboardVariableRefreshOnTimeRangeChanged
|
|
default:
|
|
return dashv2alpha1.DashboardVariableRefreshNever
|
|
}
|
|
default:
|
|
return dashv2alpha1.DashboardVariableRefreshNever
|
|
}
|
|
}
|
|
|
|
func transformVariableSortToEnum(sort interface{}) dashv2alpha1.DashboardVariableSort {
|
|
switch v := sort.(type) {
|
|
case int:
|
|
switch v {
|
|
case 0:
|
|
return dashv2alpha1.DashboardVariableSortDisabled
|
|
case 1:
|
|
return dashv2alpha1.DashboardVariableSortAlphabeticalAsc
|
|
case 2:
|
|
return dashv2alpha1.DashboardVariableSortAlphabeticalDesc
|
|
case 3:
|
|
return dashv2alpha1.DashboardVariableSortNumericalAsc
|
|
case 4:
|
|
return dashv2alpha1.DashboardVariableSortNumericalDesc
|
|
default:
|
|
return dashv2alpha1.DashboardVariableSortDisabled
|
|
}
|
|
case float64:
|
|
return transformVariableSortToEnum(int(v))
|
|
case string:
|
|
switch v {
|
|
case "disabled", "":
|
|
return dashv2alpha1.DashboardVariableSortDisabled
|
|
case "alphabeticalAsc":
|
|
return dashv2alpha1.DashboardVariableSortAlphabeticalAsc
|
|
case "alphabeticalDesc":
|
|
return dashv2alpha1.DashboardVariableSortAlphabeticalDesc
|
|
case "numericalAsc":
|
|
return dashv2alpha1.DashboardVariableSortNumericalAsc
|
|
case "numericalDesc":
|
|
return dashv2alpha1.DashboardVariableSortNumericalDesc
|
|
case "alphabeticalCaseInsensitiveAsc":
|
|
return dashv2alpha1.DashboardVariableSortAlphabeticalCaseInsensitiveAsc
|
|
case "alphabeticalCaseInsensitiveDesc":
|
|
return dashv2alpha1.DashboardVariableSortAlphabeticalCaseInsensitiveDesc
|
|
case "naturalAsc":
|
|
return dashv2alpha1.DashboardVariableSortNaturalAsc
|
|
case "naturalDesc":
|
|
return dashv2alpha1.DashboardVariableSortNaturalDesc
|
|
default:
|
|
return dashv2alpha1.DashboardVariableSortDisabled
|
|
}
|
|
default:
|
|
return dashv2alpha1.DashboardVariableSortDisabled
|
|
}
|
|
}
|
|
|
|
func transformVariables(ctx context.Context, dashboard map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) ([]dashv2alpha1.DashboardVariableKind, error) {
|
|
templating, ok := dashboard["templating"].(map[string]interface{})
|
|
if !ok {
|
|
return []dashv2alpha1.DashboardVariableKind{}, nil
|
|
}
|
|
|
|
list, ok := templating["list"].([]interface{})
|
|
if !ok {
|
|
return []dashv2alpha1.DashboardVariableKind{}, nil
|
|
}
|
|
|
|
variables := make([]dashv2alpha1.DashboardVariableKind, 0, len(list))
|
|
|
|
for _, v := range list {
|
|
varMap, ok := v.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Extract common properties
|
|
commonProps := extractCommonVariableProperties(varMap)
|
|
varType := schemaversion.GetStringValue(varMap, "type")
|
|
|
|
switch varType {
|
|
case "query":
|
|
if queryVar, err := buildQueryVariable(ctx, varMap, commonProps, dsIndexProvider); err == nil {
|
|
variables = append(variables, queryVar)
|
|
}
|
|
case "datasource":
|
|
if dsVar, err := buildDatasourceVariable(ctx, varMap, commonProps, dsIndexProvider); err == nil {
|
|
variables = append(variables, dsVar)
|
|
}
|
|
case "custom":
|
|
if customVar, err := buildCustomVariable(varMap, commonProps); err == nil {
|
|
variables = append(variables, customVar)
|
|
}
|
|
case "adhoc":
|
|
if adhocVar, err := buildAdhocVariable(ctx, varMap, commonProps, dsIndexProvider); err == nil {
|
|
variables = append(variables, adhocVar)
|
|
}
|
|
case "constant":
|
|
if constantVar, err := buildConstantVariable(varMap, commonProps); err == nil {
|
|
variables = append(variables, constantVar)
|
|
}
|
|
case "interval":
|
|
if intervalVar, err := buildIntervalVariable(varMap, commonProps); err == nil {
|
|
variables = append(variables, intervalVar)
|
|
}
|
|
case "textbox":
|
|
if textVar, err := buildTextVariable(varMap, commonProps); err == nil {
|
|
variables = append(variables, textVar)
|
|
}
|
|
case "switch":
|
|
if switchVar, err := buildSwitchVariable(varMap, commonProps); err == nil {
|
|
variables = append(variables, switchVar)
|
|
}
|
|
case "groupby":
|
|
if groupByVar, err := buildGroupByVariable(ctx, varMap, commonProps, dsIndexProvider); err == nil {
|
|
variables = append(variables, groupByVar)
|
|
}
|
|
default:
|
|
// Skip unknown variable types
|
|
continue
|
|
}
|
|
}
|
|
|
|
return variables, nil
|
|
}
|
|
|
|
// Common variable properties extraction
|
|
type CommonVariableProperties struct {
|
|
Name string
|
|
Label *string
|
|
Description *string
|
|
Hide dashv2alpha1.DashboardVariableHide
|
|
SkipUrlSync bool
|
|
}
|
|
|
|
func extractCommonVariableProperties(varMap map[string]interface{}) CommonVariableProperties {
|
|
props := CommonVariableProperties{
|
|
Name: schemaversion.GetStringValue(varMap, "name"),
|
|
Hide: transformVariableHideToEnum(varMap["hide"]),
|
|
SkipUrlSync: getBoolField(varMap, "skipUrlSync", false),
|
|
}
|
|
|
|
if label := schemaversion.GetStringValue(varMap, "label"); label != "" {
|
|
props.Label = &label
|
|
}
|
|
|
|
if description := schemaversion.GetStringValue(varMap, "description"); description != "" {
|
|
props.Description = &description
|
|
}
|
|
|
|
return props
|
|
}
|
|
|
|
// Helper function to build variable options
|
|
func buildVariableOptions(options interface{}) []dashv2alpha1.DashboardVariableOption {
|
|
if options == nil {
|
|
return []dashv2alpha1.DashboardVariableOption{}
|
|
}
|
|
|
|
optionsSlice, ok := options.([]interface{})
|
|
if !ok {
|
|
return []dashv2alpha1.DashboardVariableOption{}
|
|
}
|
|
|
|
result := make([]dashv2alpha1.DashboardVariableOption, 0, len(optionsSlice))
|
|
for _, opt := range optionsSlice {
|
|
if optMap, ok := opt.(map[string]interface{}); ok {
|
|
selected := getBoolField(optMap, "selected", false)
|
|
option := dashv2alpha1.DashboardVariableOption{
|
|
Text: buildStringOrArrayOfString(optMap["text"]),
|
|
Value: buildStringOrArrayOfString(optMap["value"]),
|
|
Selected: &selected,
|
|
}
|
|
result = append(result, option)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Helper function to build DashboardStringOrArrayOfString
|
|
func buildStringOrArrayOfString(value interface{}) dashv2alpha1.DashboardStringOrArrayOfString {
|
|
switch v := value.(type) {
|
|
case string:
|
|
return dashv2alpha1.DashboardStringOrArrayOfString{
|
|
String: &v,
|
|
}
|
|
case []interface{}:
|
|
stringArray := make([]string, 0, len(v))
|
|
for _, item := range v {
|
|
if str, ok := item.(string); ok {
|
|
stringArray = append(stringArray, str)
|
|
}
|
|
}
|
|
return dashv2alpha1.DashboardStringOrArrayOfString{
|
|
ArrayOfString: stringArray,
|
|
}
|
|
case map[string]interface{}:
|
|
// Handle legacy string value format
|
|
if legacyValue, ok := v[LEGACY_STRING_VALUE_KEY].(string); ok {
|
|
return dashv2alpha1.DashboardStringOrArrayOfString{
|
|
String: &legacyValue,
|
|
}
|
|
}
|
|
// Fallback for other map types
|
|
empty := ""
|
|
return dashv2alpha1.DashboardStringOrArrayOfString{
|
|
String: &empty,
|
|
}
|
|
default:
|
|
empty := ""
|
|
return dashv2alpha1.DashboardStringOrArrayOfString{
|
|
String: &empty,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function to build variable current value
|
|
func buildVariableCurrent(current interface{}) dashv2alpha1.DashboardVariableOption {
|
|
if current == nil {
|
|
empty := ""
|
|
return dashv2alpha1.DashboardVariableOption{
|
|
Text: dashv2alpha1.DashboardStringOrArrayOfString{String: &empty},
|
|
Value: dashv2alpha1.DashboardStringOrArrayOfString{String: &empty},
|
|
}
|
|
}
|
|
|
|
if currentMap, ok := current.(map[string]interface{}); ok {
|
|
return dashv2alpha1.DashboardVariableOption{
|
|
Text: buildStringOrArrayOfString(currentMap["text"]),
|
|
Value: buildStringOrArrayOfString(currentMap["value"]),
|
|
}
|
|
}
|
|
|
|
// Fallback for simple values
|
|
empty := ""
|
|
return dashv2alpha1.DashboardVariableOption{
|
|
Text: dashv2alpha1.DashboardStringOrArrayOfString{String: &empty},
|
|
Value: dashv2alpha1.DashboardStringOrArrayOfString{String: &empty},
|
|
}
|
|
}
|
|
|
|
// Helper function to build DataQuery kind
|
|
func buildDataQueryKind(query interface{}, datasourceType string) dashv2alpha1.DashboardDataQueryKind {
|
|
var querySpec map[string]interface{}
|
|
|
|
switch q := query.(type) {
|
|
case string:
|
|
// Only include LEGACY_STRING_VALUE_KEY if query is a non-empty string
|
|
querySpec = make(map[string]interface{})
|
|
if q != "" {
|
|
querySpec[LEGACY_STRING_VALUE_KEY] = q
|
|
}
|
|
case map[string]interface{}:
|
|
// Use query directly to match frontend behavior
|
|
querySpec = q
|
|
default:
|
|
querySpec = make(map[string]interface{})
|
|
}
|
|
|
|
// Use datasourceType as the kind (datasource type, e.g., "prometheus", "elasticsearch")
|
|
kind := datasourceType
|
|
|
|
return dashv2alpha1.DashboardDataQueryKind{
|
|
Kind: kind,
|
|
Spec: querySpec,
|
|
}
|
|
}
|
|
|
|
// Helper function to build DataQuery kind for variables (filters out refId)
|
|
func buildDataQueryKindForVariable(query interface{}, datasourceType string) dashv2alpha1.DashboardDataQueryKind {
|
|
var querySpec map[string]interface{}
|
|
|
|
switch q := query.(type) {
|
|
case string:
|
|
// Only include LEGACY_STRING_VALUE_KEY if query is a non-empty string
|
|
querySpec = make(map[string]interface{})
|
|
if q != "" {
|
|
querySpec[LEGACY_STRING_VALUE_KEY] = q
|
|
}
|
|
case map[string]interface{}:
|
|
// Filter out refId for variables (refId should only be in panel queries)
|
|
querySpec = q
|
|
default:
|
|
querySpec = make(map[string]interface{})
|
|
}
|
|
|
|
// Use datasourceType as the kind (datasource type, e.g., "prometheus", "elasticsearch")
|
|
kind := datasourceType
|
|
|
|
return dashv2alpha1.DashboardDataQueryKind{
|
|
Kind: kind,
|
|
Spec: querySpec,
|
|
}
|
|
}
|
|
|
|
// Query Variable
|
|
func buildQueryVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardVariableKind, error) {
|
|
datasource := varMap["datasource"]
|
|
var datasourceType, datasourceUID string
|
|
|
|
if ds, ok := datasource.(map[string]interface{}); ok {
|
|
datasourceUID = schemaversion.GetStringValue(ds, "uid")
|
|
datasourceType = schemaversion.GetStringValue(ds, "type")
|
|
|
|
// If we have a UID, use it to get the correct type from the datasource service
|
|
// BUT: Don't try to resolve types for template variables
|
|
if datasourceUID != "" && datasourceType == "" && !isTemplateVariable(datasourceUID) {
|
|
datasourceType = getDatasourceTypeByUID(ctx, datasourceUID, dsIndexProvider)
|
|
} else if datasourceUID == "" && datasourceType == "" {
|
|
// If no UID and no type, use default
|
|
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
|
|
}
|
|
} else if dsStr, ok := datasource.(string); ok && isTemplateVariable(dsStr) {
|
|
// Handle datasource variable reference (e.g., "$datasource")
|
|
// Only process template variables - other string values are not supported in V2 format
|
|
datasourceUID = dsStr
|
|
} else {
|
|
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
|
|
}
|
|
|
|
queryVar := &dashv2alpha1.DashboardQueryVariableKind{
|
|
Kind: "QueryVariable",
|
|
Spec: dashv2alpha1.DashboardQueryVariableSpec{
|
|
Name: commonProps.Name,
|
|
Label: commonProps.Label,
|
|
Description: commonProps.Description,
|
|
Hide: commonProps.Hide,
|
|
SkipUrlSync: commonProps.SkipUrlSync,
|
|
Current: buildVariableCurrent(varMap["current"]),
|
|
Multi: getBoolField(varMap, "multi", false),
|
|
IncludeAll: getBoolField(varMap, "includeAll", false),
|
|
Refresh: transformVariableRefreshToEnum(varMap["refresh"]),
|
|
Sort: transformVariableSortToEnum(varMap["sort"]),
|
|
Regex: schemaversion.GetStringValue(varMap, "regex"),
|
|
RegexApplyTo: getUnionField[dashv2alpha1.DashboardVariableRegexApplyTo](varMap, "regexApplyTo"),
|
|
Query: buildDataQueryKindForVariable(varMap["query"], datasourceType),
|
|
AllowCustomValue: getBoolField(varMap, "allowCustomValue", true),
|
|
},
|
|
}
|
|
|
|
// Only set definition if it exists in the input (match frontend behavior)
|
|
if def, exists := varMap["definition"]; exists && def != nil {
|
|
if defStr, ok := def.(string); ok {
|
|
queryVar.Spec.Definition = &defStr
|
|
}
|
|
}
|
|
|
|
// Only include datasource if datasourceUID exists (matching frontend behavior)
|
|
if datasourceUID != "" {
|
|
dsRef := &dashv2alpha1.DashboardDataSourceRef{
|
|
Type: &datasourceType,
|
|
Uid: &datasourceUID,
|
|
}
|
|
queryVar.Spec.Datasource = dsRef
|
|
}
|
|
|
|
// Always set options (matching frontend behavior)
|
|
queryVar.Spec.Options = buildVariableOptions(varMap["options"])
|
|
|
|
if allValue := schemaversion.GetStringValue(varMap, "allValue"); allValue != "" {
|
|
queryVar.Spec.AllValue = &allValue
|
|
}
|
|
|
|
return dashv2alpha1.DashboardVariableKind{
|
|
QueryVariableKind: queryVar,
|
|
}, nil
|
|
}
|
|
|
|
// Datasource Variable
|
|
func buildDatasourceVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardVariableKind, error) {
|
|
pluginId := getDefaultDatasourceType(ctx, dsIndexProvider)
|
|
if query := varMap["query"]; query != nil {
|
|
if queryStr, ok := query.(string); ok {
|
|
pluginId = queryStr
|
|
}
|
|
}
|
|
|
|
dsVar := &dashv2alpha1.DashboardDatasourceVariableKind{
|
|
Kind: "DatasourceVariable",
|
|
Spec: dashv2alpha1.DashboardDatasourceVariableSpec{
|
|
Name: commonProps.Name,
|
|
Label: commonProps.Label,
|
|
Description: commonProps.Description,
|
|
Hide: commonProps.Hide,
|
|
SkipUrlSync: commonProps.SkipUrlSync,
|
|
PluginId: pluginId,
|
|
Current: buildVariableCurrent(varMap["current"]),
|
|
Multi: getBoolField(varMap, "multi", false),
|
|
IncludeAll: getBoolField(varMap, "includeAll", false),
|
|
Refresh: transformVariableRefreshToEnum(varMap["refresh"]),
|
|
Regex: schemaversion.GetStringValue(varMap, "regex"),
|
|
AllowCustomValue: getBoolField(varMap, "allowCustomValue", true),
|
|
},
|
|
}
|
|
|
|
// Always set options (matching frontend behavior)
|
|
dsVar.Spec.Options = buildVariableOptions(varMap["options"])
|
|
|
|
if allValue := schemaversion.GetStringValue(varMap, "allValue"); allValue != "" {
|
|
dsVar.Spec.AllValue = &allValue
|
|
}
|
|
|
|
return dashv2alpha1.DashboardVariableKind{
|
|
DatasourceVariableKind: dsVar,
|
|
}, nil
|
|
}
|
|
|
|
// Custom Variable
|
|
func buildCustomVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) {
|
|
customVar := &dashv2alpha1.DashboardCustomVariableKind{
|
|
Kind: "CustomVariable",
|
|
Spec: dashv2alpha1.DashboardCustomVariableSpec{
|
|
Name: commonProps.Name,
|
|
Label: commonProps.Label,
|
|
Description: commonProps.Description,
|
|
Hide: commonProps.Hide,
|
|
SkipUrlSync: commonProps.SkipUrlSync,
|
|
Query: schemaversion.GetStringValue(varMap, "query"),
|
|
Current: buildVariableCurrent(varMap["current"]),
|
|
Multi: getBoolField(varMap, "multi", false),
|
|
IncludeAll: getBoolField(varMap, "includeAll", false),
|
|
AllowCustomValue: getBoolField(varMap, "allowCustomValue", true),
|
|
},
|
|
}
|
|
|
|
// Always set options (matching frontend behavior)
|
|
customVar.Spec.Options = buildVariableOptions(varMap["options"])
|
|
|
|
if allValue := schemaversion.GetStringValue(varMap, "allValue"); allValue != "" {
|
|
customVar.Spec.AllValue = &allValue
|
|
}
|
|
|
|
if valuesFormat := schemaversion.GetStringValue(varMap, "valuesFormat"); valuesFormat != "" {
|
|
switch valuesFormat {
|
|
case string(dashv2alpha1.DashboardCustomVariableSpecValuesFormatJson):
|
|
format := dashv2alpha1.DashboardCustomVariableSpecValuesFormatJson
|
|
customVar.Spec.ValuesFormat = &format
|
|
case string(dashv2alpha1.DashboardCustomVariableSpecValuesFormatCsv):
|
|
format := dashv2alpha1.DashboardCustomVariableSpecValuesFormatCsv
|
|
customVar.Spec.ValuesFormat = &format
|
|
}
|
|
}
|
|
|
|
return dashv2alpha1.DashboardVariableKind{
|
|
CustomVariableKind: customVar,
|
|
}, nil
|
|
}
|
|
|
|
// Constant Variable
|
|
func buildConstantVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) {
|
|
// Get query value - if missing, it should be undefined (not empty string)
|
|
// Frontend uses variable.query for value, so if query is missing, value is undefined
|
|
var queryValue interface{}
|
|
if query, exists := varMap["query"]; exists {
|
|
if queryStr, ok := query.(string); ok {
|
|
queryValue = queryStr
|
|
} else {
|
|
queryValue = query
|
|
}
|
|
}
|
|
|
|
// Build current from query value (matching frontend: value: variable.query, text: String(variable.state.value))
|
|
// If query is undefined, frontend produces: text: "undefined", value: undefined (removed), query: undefined (removed)
|
|
var current dashv2alpha1.DashboardVariableOption
|
|
if currentVal, exists := varMap["current"]; exists && currentVal != nil {
|
|
// Use provided current if it exists
|
|
current = buildVariableCurrent(currentVal)
|
|
} else if queryValue != nil {
|
|
// Build current from query value (matching frontend behavior)
|
|
queryStr := ""
|
|
if str, ok := queryValue.(string); ok {
|
|
queryStr = str
|
|
}
|
|
current = dashv2alpha1.DashboardVariableOption{
|
|
Text: dashv2alpha1.DashboardStringOrArrayOfString{String: &queryStr},
|
|
Value: dashv2alpha1.DashboardStringOrArrayOfString{String: &queryStr},
|
|
}
|
|
} else {
|
|
// query is undefined - match frontend: value ? String(value) : '' (defaults to empty string, not "undefined")
|
|
emptyStr := ""
|
|
current = dashv2alpha1.DashboardVariableOption{
|
|
Text: dashv2alpha1.DashboardStringOrArrayOfString{String: &emptyStr},
|
|
Value: dashv2alpha1.DashboardStringOrArrayOfString{String: &emptyStr},
|
|
}
|
|
}
|
|
|
|
// Get query value - frontend sets query: variable.state.value which can be undefined
|
|
// Since Query is a required string field, we must set it, but when query is missing from v1,
|
|
// we set it to empty string (frontend would have undefined which gets removed by sortedDeepCloneWithoutNulls)
|
|
var query string
|
|
if queryValue != nil {
|
|
if queryStr, ok := queryValue.(string); ok {
|
|
query = queryStr
|
|
}
|
|
}
|
|
// If queryValue is nil, query remains empty string ""
|
|
|
|
constantVar := &dashv2alpha1.DashboardConstantVariableKind{
|
|
Kind: "ConstantVariable",
|
|
Spec: dashv2alpha1.DashboardConstantVariableSpec{
|
|
Name: commonProps.Name,
|
|
Label: commonProps.Label,
|
|
Description: commonProps.Description,
|
|
Hide: commonProps.Hide,
|
|
SkipUrlSync: commonProps.SkipUrlSync,
|
|
Query: query,
|
|
Current: current,
|
|
},
|
|
}
|
|
|
|
return dashv2alpha1.DashboardVariableKind{
|
|
ConstantVariableKind: constantVar,
|
|
}, nil
|
|
}
|
|
|
|
// Interval Variable
|
|
func buildIntervalVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) {
|
|
query := schemaversion.GetStringValue(varMap, "query")
|
|
|
|
// If query is missing/empty but options exist, generate query from options (matches frontend behavior)
|
|
if query == "" {
|
|
if options, ok := varMap["options"].([]interface{}); ok && len(options) > 0 {
|
|
queryValues := make([]string, 0, len(options))
|
|
for _, opt := range options {
|
|
if optMap, ok := opt.(map[string]interface{}); ok {
|
|
if value, exists := optMap["value"]; exists {
|
|
if valueStr, ok := value.(string); ok && valueStr != "" {
|
|
queryValues = append(queryValues, valueStr)
|
|
}
|
|
} else if text, exists := optMap["text"]; exists {
|
|
if textStr, ok := text.(string); ok && textStr != "" {
|
|
queryValues = append(queryValues, textStr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(queryValues) > 0 {
|
|
// Join interval values with comma, matching frontend getIntervalsQueryFromNewIntervalModel
|
|
query = strings.Join(queryValues, ",")
|
|
}
|
|
}
|
|
}
|
|
|
|
intervalVar := &dashv2alpha1.DashboardIntervalVariableKind{
|
|
Kind: "IntervalVariable",
|
|
Spec: dashv2alpha1.DashboardIntervalVariableSpec{
|
|
Name: commonProps.Name,
|
|
Label: commonProps.Label,
|
|
Description: commonProps.Description,
|
|
Hide: commonProps.Hide,
|
|
SkipUrlSync: commonProps.SkipUrlSync,
|
|
Query: query,
|
|
Current: buildVariableCurrent(varMap["current"]),
|
|
Refresh: dashv2alpha1.DashboardVariableRefreshOnTimeRangeChanged, // Always onTimeRangeChanged for interval
|
|
Auto: getBoolField(varMap, "auto", false),
|
|
AutoMin: schemaversion.GetStringValue(varMap, "auto_min"),
|
|
AutoCount: int64(getIntField(varMap, "auto_count", 0)),
|
|
},
|
|
}
|
|
|
|
// Always set options (matching frontend behavior)
|
|
intervalVar.Spec.Options = buildVariableOptions(varMap["options"])
|
|
|
|
return dashv2alpha1.DashboardVariableKind{
|
|
IntervalVariableKind: intervalVar,
|
|
}, nil
|
|
}
|
|
|
|
// Text Variable
|
|
func buildTextVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) {
|
|
textVar := &dashv2alpha1.DashboardTextVariableKind{
|
|
Kind: "TextVariable",
|
|
Spec: dashv2alpha1.DashboardTextVariableSpec{
|
|
Name: commonProps.Name,
|
|
Label: commonProps.Label,
|
|
Description: commonProps.Description,
|
|
Hide: commonProps.Hide,
|
|
SkipUrlSync: commonProps.SkipUrlSync,
|
|
Query: schemaversion.GetStringValue(varMap, "query"),
|
|
Current: buildVariableCurrent(varMap["current"]),
|
|
},
|
|
}
|
|
|
|
return dashv2alpha1.DashboardVariableKind{
|
|
TextVariableKind: textVar,
|
|
}, nil
|
|
}
|
|
|
|
// Helper function to extract string value from an option map (value or text field)
|
|
func getOptionValue(optMap map[string]interface{}) string {
|
|
if val, ok := optMap["value"].(string); ok && val != "" {
|
|
return val
|
|
}
|
|
if val, ok := optMap["text"].(string); ok && val != "" {
|
|
return val
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Switch Variable
|
|
func buildSwitchVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) {
|
|
current := ""
|
|
if currentVal, exists := varMap["current"]; exists {
|
|
if currentMap, ok := currentVal.(map[string]interface{}); ok {
|
|
current = getOptionValue(currentMap)
|
|
}
|
|
}
|
|
|
|
// In V1 the enabled value is the first value of the options array,
|
|
// while the disabled value is second one.
|
|
// (Falling back to "true" and "false" if options are not available)
|
|
enabledValue := "true"
|
|
disabledValue := "false"
|
|
|
|
if options, ok := varMap["options"].([]interface{}); ok {
|
|
// Get enabledValue from first option
|
|
if len(options) > 0 {
|
|
if opt1, ok := options[0].(map[string]interface{}); ok {
|
|
if val := getOptionValue(opt1); val != "" {
|
|
enabledValue = val
|
|
}
|
|
}
|
|
}
|
|
// Get disabledValue from second option
|
|
if len(options) > 1 {
|
|
if opt2, ok := options[1].(map[string]interface{}); ok {
|
|
if val := getOptionValue(opt2); val != "" {
|
|
disabledValue = val
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set current to disabledValue if not set
|
|
if current == "" {
|
|
current = disabledValue
|
|
}
|
|
|
|
switchVar := &dashv2alpha1.DashboardSwitchVariableKind{
|
|
Kind: "SwitchVariable",
|
|
Spec: dashv2alpha1.DashboardSwitchVariableSpec{
|
|
Name: commonProps.Name,
|
|
Current: current,
|
|
EnabledValue: enabledValue,
|
|
DisabledValue: disabledValue,
|
|
Label: commonProps.Label,
|
|
Description: commonProps.Description,
|
|
Hide: commonProps.Hide,
|
|
SkipUrlSync: commonProps.SkipUrlSync,
|
|
},
|
|
}
|
|
|
|
return dashv2alpha1.DashboardVariableKind{
|
|
SwitchVariableKind: switchVar,
|
|
}, nil
|
|
}
|
|
|
|
// Adhoc Variable
|
|
func buildAdhocVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardVariableKind, error) {
|
|
datasource := varMap["datasource"]
|
|
var datasourceType, datasourceUID string
|
|
|
|
if ds, ok := datasource.(map[string]interface{}); ok {
|
|
datasourceUID = schemaversion.GetStringValue(ds, "uid")
|
|
datasourceType = schemaversion.GetStringValue(ds, "type")
|
|
|
|
// If we have a UID, use it to get the correct type from the datasource service
|
|
// BUT: Don't try to resolve types for template variables
|
|
if datasourceUID != "" && datasourceType == "" && !isTemplateVariable(datasourceUID) {
|
|
datasourceType = getDatasourceTypeByUID(ctx, datasourceUID, dsIndexProvider)
|
|
} else if datasourceUID == "" && datasourceType == "" {
|
|
// If no UID and no type, use default
|
|
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
|
|
}
|
|
} else if dsStr, ok := datasource.(string); ok && isTemplateVariable(dsStr) {
|
|
// Handle datasource variable reference (e.g., "$datasource")
|
|
// Only process template variables - other string values are not supported in V2 format
|
|
datasourceUID = dsStr
|
|
} else {
|
|
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
|
|
}
|
|
|
|
adhocVar := &dashv2alpha1.DashboardAdhocVariableKind{
|
|
Kind: "AdhocVariable",
|
|
Spec: dashv2alpha1.DashboardAdhocVariableSpec{
|
|
Name: commonProps.Name,
|
|
Label: commonProps.Label,
|
|
Description: commonProps.Description,
|
|
Hide: commonProps.Hide,
|
|
SkipUrlSync: commonProps.SkipUrlSync,
|
|
AllowCustomValue: getBoolField(varMap, "allowCustomValue", true),
|
|
},
|
|
}
|
|
|
|
// Transform baseFilters if they exist, otherwise default to empty array
|
|
if baseFilters, exists := varMap["baseFilters"]; exists {
|
|
if baseFiltersArray, ok := baseFilters.([]interface{}); ok {
|
|
adhocVar.Spec.BaseFilters = transformAdHocFilters(baseFiltersArray)
|
|
}
|
|
}
|
|
// Ensure baseFilters is always set (default to empty array if not present or invalid)
|
|
if adhocVar.Spec.BaseFilters == nil {
|
|
adhocVar.Spec.BaseFilters = []dashv2alpha1.DashboardAdHocFilterWithLabels{}
|
|
}
|
|
|
|
// Transform filters if they exist, otherwise default to empty array
|
|
if filters, exists := varMap["filters"]; exists {
|
|
if filtersArray, ok := filters.([]interface{}); ok {
|
|
adhocVar.Spec.Filters = transformAdHocFilters(filtersArray)
|
|
}
|
|
}
|
|
// Ensure filters is always set (default to empty array if not present or invalid)
|
|
if adhocVar.Spec.Filters == nil {
|
|
adhocVar.Spec.Filters = []dashv2alpha1.DashboardAdHocFilterWithLabels{}
|
|
}
|
|
|
|
// Transform defaultKeys if they exist, otherwise default to empty array
|
|
if defaultKeys, exists := varMap["defaultKeys"]; exists {
|
|
if defaultKeysArray, ok := defaultKeys.([]interface{}); ok {
|
|
adhocVar.Spec.DefaultKeys = transformMetricFindValues(defaultKeysArray)
|
|
}
|
|
}
|
|
// Ensure defaultKeys is always set (default to empty array if not present or invalid)
|
|
if adhocVar.Spec.DefaultKeys == nil {
|
|
adhocVar.Spec.DefaultKeys = []dashv2alpha1.DashboardMetricFindValue{}
|
|
}
|
|
|
|
// Only include datasource if datasourceUID exists (matching frontend behavior)
|
|
if datasourceUID != "" {
|
|
dsRef := &dashv2alpha1.DashboardDataSourceRef{
|
|
Type: &datasourceType,
|
|
Uid: &datasourceUID,
|
|
}
|
|
adhocVar.Spec.Datasource = dsRef
|
|
}
|
|
|
|
return dashv2alpha1.DashboardVariableKind{
|
|
AdhocVariableKind: adhocVar,
|
|
}, nil
|
|
}
|
|
|
|
// Helper function to transform adhoc filters
|
|
func transformAdHocFilters(filters []interface{}) []dashv2alpha1.DashboardAdHocFilterWithLabels {
|
|
result := make([]dashv2alpha1.DashboardAdHocFilterWithLabels, 0, len(filters))
|
|
|
|
for _, filter := range filters {
|
|
if filterMap, ok := filter.(map[string]interface{}); ok {
|
|
// Only include filters that don't have an origin or have origin "dashboard"
|
|
// This matches the frontend validateFiltersOrigin logic
|
|
if origin, exists := filterMap["origin"]; !exists || origin == "dashboard" {
|
|
adhocFilter := dashv2alpha1.DashboardAdHocFilterWithLabels{
|
|
Key: schemaversion.GetStringValue(filterMap, "key"),
|
|
Operator: schemaversion.GetStringValue(filterMap, "operator", "="),
|
|
Value: schemaversion.GetStringValue(filterMap, "value"),
|
|
}
|
|
|
|
// Handle optional fields
|
|
if keyLabel := schemaversion.GetStringValue(filterMap, "keyLabel"); keyLabel != "" {
|
|
adhocFilter.KeyLabel = &keyLabel
|
|
}
|
|
if condition := schemaversion.GetStringValue(filterMap, "condition"); condition != "" {
|
|
adhocFilter.Condition = &condition
|
|
}
|
|
|
|
// Handle multi-value filters
|
|
if values, exists := filterMap["values"]; exists {
|
|
if valuesArray, ok := values.([]interface{}); ok {
|
|
stringValues := make([]string, 0, len(valuesArray))
|
|
for _, v := range valuesArray {
|
|
if str, ok := v.(string); ok {
|
|
stringValues = append(stringValues, str)
|
|
}
|
|
}
|
|
if len(stringValues) > 0 {
|
|
adhocFilter.Values = stringValues
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle value labels for multi-value filters
|
|
if valueLabels, exists := filterMap["valueLabels"]; exists {
|
|
if valueLabelsArray, ok := valueLabels.([]interface{}); ok {
|
|
stringValueLabels := make([]string, 0, len(valueLabelsArray))
|
|
for _, v := range valueLabelsArray {
|
|
if str, ok := v.(string); ok {
|
|
stringValueLabels = append(stringValueLabels, str)
|
|
}
|
|
}
|
|
if len(stringValueLabels) > 0 {
|
|
adhocFilter.ValueLabels = stringValueLabels
|
|
}
|
|
}
|
|
}
|
|
|
|
result = append(result, adhocFilter)
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Helper function to transform metric find values
|
|
func transformMetricFindValues(values []interface{}) []dashv2alpha1.DashboardMetricFindValue {
|
|
result := make([]dashv2alpha1.DashboardMetricFindValue, 0, len(values))
|
|
|
|
for _, value := range values {
|
|
if valueMap, ok := value.(map[string]interface{}); ok {
|
|
// Convert object format to string format to match frontend behavior
|
|
text := schemaversion.GetStringValue(valueMap, "text")
|
|
if text != "" {
|
|
// Use text as the string value to match frontend behavior
|
|
metricFindValue := dashv2alpha1.DashboardMetricFindValue{
|
|
Text: text,
|
|
Value: &dashv2alpha1.DashboardStringOrFloat64{
|
|
String: &text,
|
|
},
|
|
}
|
|
result = append(result, metricFindValue)
|
|
}
|
|
} else if str, ok := value.(string); ok {
|
|
// Handle simple string values - convert to object format with text and value
|
|
metricFindValue := dashv2alpha1.DashboardMetricFindValue{
|
|
Text: str,
|
|
Value: &dashv2alpha1.DashboardStringOrFloat64{
|
|
String: &str,
|
|
},
|
|
}
|
|
result = append(result, metricFindValue)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GroupBy Variable
|
|
func buildGroupByVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardVariableKind, error) {
|
|
datasource := varMap["datasource"]
|
|
var datasourceType, datasourceUID string
|
|
|
|
if ds, ok := datasource.(map[string]interface{}); ok {
|
|
datasourceUID = schemaversion.GetStringValue(ds, "uid")
|
|
datasourceType = schemaversion.GetStringValue(ds, "type")
|
|
|
|
// If we have a UID, use it to get the correct type from the datasource service
|
|
// BUT: Don't try to resolve types for template variables
|
|
if datasourceUID != "" && datasourceType == "" && !isTemplateVariable(datasourceUID) {
|
|
datasourceType = getDatasourceTypeByUID(ctx, datasourceUID, dsIndexProvider)
|
|
} else if datasourceUID == "" && datasourceType == "" {
|
|
// If no UID and no type, use default
|
|
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
|
|
}
|
|
|
|
// Resolve Grafana datasource UID when type is "datasource" and UID is empty
|
|
datasourceUID = resolveGrafanaDatasourceUID(datasourceType, datasourceUID)
|
|
} else if dsStr, ok := datasource.(string); ok && isTemplateVariable(dsStr) {
|
|
// Handle datasource variable reference (e.g., "$datasource")
|
|
// Only process template variables - other string values are not supported in V2 format
|
|
datasourceUID = dsStr
|
|
} else {
|
|
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
|
|
}
|
|
|
|
groupByVar := &dashv2alpha1.DashboardGroupByVariableKind{
|
|
Kind: "GroupByVariable",
|
|
Spec: dashv2alpha1.DashboardGroupByVariableSpec{
|
|
Name: commonProps.Name,
|
|
Label: commonProps.Label,
|
|
Description: commonProps.Description,
|
|
Hide: commonProps.Hide,
|
|
SkipUrlSync: commonProps.SkipUrlSync,
|
|
Current: buildVariableCurrent(varMap["current"]),
|
|
// We set it to true by default because GroupByVariable
|
|
// constructor defaults to multi: true
|
|
Multi: getBoolField(varMap, "multi", true),
|
|
},
|
|
}
|
|
|
|
// Only include datasource if datasourceUID exists
|
|
if datasourceUID != "" {
|
|
dsRef := &dashv2alpha1.DashboardDataSourceRef{
|
|
Type: &datasourceType,
|
|
Uid: &datasourceUID,
|
|
}
|
|
groupByVar.Spec.Datasource = dsRef
|
|
}
|
|
|
|
// Always set options (matching frontend behavior)
|
|
groupByVar.Spec.Options = buildVariableOptions(varMap["options"])
|
|
|
|
return dashv2alpha1.DashboardVariableKind{
|
|
GroupByVariableKind: groupByVar,
|
|
}, nil
|
|
}
|
|
|
|
func transformAnnotations(dashboard map[string]interface{}) ([]dashv2alpha1.DashboardAnnotationQueryKind, error) {
|
|
annotations, ok := dashboard["annotations"].(map[string]interface{})
|
|
if !ok {
|
|
// Return empty array to match frontend behavior
|
|
return []dashv2alpha1.DashboardAnnotationQueryKind{}, nil
|
|
}
|
|
|
|
list, ok := annotations["list"].([]interface{})
|
|
if !ok || len(list) == 0 {
|
|
// Return empty array to match frontend behavior
|
|
return []dashv2alpha1.DashboardAnnotationQueryKind{}, nil
|
|
}
|
|
|
|
result := make([]dashv2alpha1.DashboardAnnotationQueryKind, 0, len(list))
|
|
|
|
for _, a := range list {
|
|
annotationMap, ok := a.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if annotationQuery, err := buildAnnotationQuery(annotationMap); err == nil {
|
|
result = append(result, annotationQuery)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.DashboardAnnotationQueryKind, error) {
|
|
// Extract datasource information
|
|
var datasourceRef *dashv2alpha1.DashboardDataSourceRef
|
|
var datasourceType, datasourceUID string
|
|
|
|
if datasource, ok := annotationMap["datasource"].(map[string]interface{}); ok {
|
|
datasourceUID = schemaversion.GetStringValue(datasource, "uid")
|
|
datasourceType = schemaversion.GetStringValue(datasource, "type")
|
|
|
|
// Only create datasource ref if the annotation actually has a valid datasource type in the original input
|
|
// This matches frontend behavior: if (dsUID && annotation.datasource && annotation.datasource.type)
|
|
// We don't try to resolve datasources that are missing a type - they should be resolved at runtime
|
|
if datasourceUID != "" && datasourceType != "" {
|
|
datasourceRef = &dashv2alpha1.DashboardDataSourceRef{
|
|
Type: &datasourceType,
|
|
Uid: &datasourceUID,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build the query from target
|
|
var query *dashv2alpha1.DashboardDataQueryKind
|
|
// Use datasourceType as the kind (datasource type, e.g., "prometheus", "grafana")
|
|
kind := datasourceType
|
|
if target, ok := annotationMap["target"].(map[string]interface{}); ok && target != nil {
|
|
queryKind := dashv2alpha1.DashboardDataQueryKind{
|
|
Kind: kind,
|
|
Spec: target,
|
|
}
|
|
// Group field is not available in v2alpha1 DashboardDataQueryKind
|
|
query = &queryKind
|
|
} else {
|
|
// Always provide a query to match frontend behavior
|
|
queryKind := dashv2alpha1.DashboardDataQueryKind{
|
|
Kind: kind,
|
|
Spec: map[string]interface{}{},
|
|
}
|
|
// Group field is not available in v2alpha1 DashboardDataQueryKind
|
|
query = &queryKind
|
|
}
|
|
|
|
// Transform filter
|
|
var filter *dashv2alpha1.DashboardAnnotationPanelFilter
|
|
if filterMap, ok := annotationMap["filter"].(map[string]interface{}); ok && filterMap != nil {
|
|
filter = buildAnnotationFilter(filterMap)
|
|
}
|
|
|
|
// Transform mappings
|
|
var mappings map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping
|
|
if mappingsMap, ok := annotationMap["mappings"].(map[string]interface{}); ok && mappingsMap != nil {
|
|
mappings = convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
|
}
|
|
|
|
// Transform builtIn from float64 to bool
|
|
var builtInPtr *bool
|
|
if builtInVal, ok := annotationMap["builtIn"]; ok && builtInVal != nil {
|
|
switch v := builtInVal.(type) {
|
|
case float64:
|
|
val := v != 0
|
|
builtInPtr = &val
|
|
case int:
|
|
val := v != 0
|
|
builtInPtr = &val
|
|
case bool:
|
|
builtInPtr = &v
|
|
}
|
|
}
|
|
|
|
defaultAnnotationQuerySpec := dashv2alpha1.NewDashboardAnnotationQuerySpec()
|
|
spec := dashv2alpha1.DashboardAnnotationQuerySpec{
|
|
Name: schemaversion.GetStringValue(annotationMap, "name"),
|
|
Datasource: datasourceRef,
|
|
Query: query,
|
|
Enable: getBoolField(annotationMap, "enable", defaultAnnotationQuerySpec.Enable), // Default to false to match frontend: Boolean(annotation.enable) || Boolean(override?.enable)
|
|
Hide: getBoolField(annotationMap, "hide", defaultAnnotationQuerySpec.Hide),
|
|
IconColor: schemaversion.GetStringValue(annotationMap, "iconColor", defaultAnnotationQuerySpec.IconColor),
|
|
BuiltIn: builtInPtr,
|
|
Filter: filter,
|
|
Mappings: mappings,
|
|
}
|
|
|
|
// Handle any additional properties in LegacyOptions
|
|
legacyOptions := make(map[string]interface{})
|
|
if annotationType := schemaversion.GetStringValue(annotationMap, "type"); annotationType != "" {
|
|
legacyOptions["type"] = annotationType
|
|
}
|
|
|
|
// Add other legacy fields if they exist
|
|
for key, value := range annotationMap {
|
|
switch key {
|
|
case "name", "datasource", "enable", "hide", "iconColor", "filter", "target", "builtIn", "type", "mappings":
|
|
// Skip already handled fields
|
|
default:
|
|
legacyOptions[key] = value
|
|
}
|
|
}
|
|
|
|
if len(legacyOptions) > 0 {
|
|
spec.LegacyOptions = legacyOptions
|
|
}
|
|
|
|
return dashv2alpha1.DashboardAnnotationQueryKind{
|
|
Kind: "AnnotationQuery",
|
|
Spec: spec,
|
|
}, nil
|
|
}
|
|
|
|
func buildAnnotationFilter(filterMap map[string]interface{}) *dashv2alpha1.DashboardAnnotationPanelFilter {
|
|
filter := &dashv2alpha1.DashboardAnnotationPanelFilter{
|
|
Ids: []uint32{},
|
|
}
|
|
|
|
if exclude := getBoolField(filterMap, "exclude", false); exclude {
|
|
filter.Exclude = &exclude
|
|
}
|
|
|
|
if ids, ok := filterMap["ids"].([]interface{}); ok {
|
|
uintIds := make([]uint32, 0, len(ids))
|
|
for _, id := range ids {
|
|
switch v := id.(type) {
|
|
case float64:
|
|
if v >= 0 && v <= float64(^uint32(0)) {
|
|
uintIds = append(uintIds, uint32(v))
|
|
}
|
|
case int:
|
|
if v >= 0 && uint64(v) <= math.MaxUint32 {
|
|
uintIds = append(uintIds, uint32(v))
|
|
}
|
|
}
|
|
}
|
|
filter.Ids = uintIds
|
|
}
|
|
|
|
return filter
|
|
}
|
|
|
|
func convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap map[string]interface{}) map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping {
|
|
mappings := make(map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping)
|
|
|
|
for key, value := range mappingsMap {
|
|
mapping := dashv2alpha1.DashboardAnnotationEventFieldMapping{}
|
|
|
|
// Handle simple string format (v1beta1 legacy format: "fieldName": "targetFieldName")
|
|
if valueStr, ok := value.(string); ok && valueStr != "" {
|
|
// Simple string mapping: treat as field source with the value as the field name
|
|
defaultSource := "field"
|
|
mapping.Source = &defaultSource
|
|
mapping.Value = &valueStr
|
|
mappings[key] = mapping
|
|
continue
|
|
}
|
|
|
|
// Handle object format (v2alpha1 format: "fieldName": {"source": "field", "value": "...", "regex": "..."})
|
|
mappingMap, ok := value.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Extract source (defaults to "field" if not specified)
|
|
if source, ok := mappingMap["source"].(string); ok && source != "" {
|
|
mapping.Source = &source
|
|
} else {
|
|
defaultSource := "field"
|
|
mapping.Source = &defaultSource
|
|
}
|
|
|
|
// Extract value (optional)
|
|
if valueStr, ok := mappingMap["value"].(string); ok && valueStr != "" {
|
|
mapping.Value = &valueStr
|
|
}
|
|
|
|
// Extract regex (optional)
|
|
if regex, ok := mappingMap["regex"].(string); ok && regex != "" {
|
|
mapping.Regex = ®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)
|
|
}
|