26d36ec7ff
Dashboard Migration: Fix v38 table panel overrides migration when defaults.custom is missing The v38 migration was incorrectly skipping field config overrides processing when fieldConfig.defaults.custom didn't exist. This caused custom.displayMode properties in overrides to not be migrated to the new custom.cellOptions format. The fix ensures migrateOverrides is always called for table panels, regardless of whether defaults.custom exists. Added comprehensive unit test covering this edge case to prevent regression. Issue discovered through comprehensive migration testing infrastructure.
210 lines
5.9 KiB
Go
210 lines
5.9 KiB
Go
package schemaversion
|
|
|
|
import "context"
|
|
|
|
// V38 migrates table panel configuration from displayMode to the structured cellOptions format.
|
|
//
|
|
// This migration addresses limitations in the original table panel cell display configuration where
|
|
// the flat displayMode string property could not accommodate the growing complexity of cell rendering
|
|
// options. The original design forced all display settings into a single string value, making it
|
|
// difficult to add new customization parameters or provide mode-specific configuration options.
|
|
//
|
|
// The migration works by:
|
|
// 1. Locating table panels in the dashboard (including nested panels within rows)
|
|
// 2. Examining field configuration defaults and any field overrides for displayMode properties
|
|
// 3. Converting string displayMode values to structured cellOptions objects with type and mode
|
|
// 4. Updating both field defaults and field override references to use the new property path
|
|
// 5. Preserving all existing visual behavior while enabling future cell customization features
|
|
//
|
|
// This restructuring provides several key benefits:
|
|
// - Enables mode-specific configuration options (e.g., gauge thresholds, color schemes)
|
|
// - Supports future cell rendering types without breaking existing configurations
|
|
// - Provides clearer separation between cell type and rendering mode
|
|
// - Maintains backward compatibility while preparing for enhanced table functionality
|
|
//
|
|
// The migration handles special cases for legacy gauge modes and color background variants,
|
|
// ensuring all existing display behaviors are preserved exactly.
|
|
//
|
|
// Example transformations:
|
|
//
|
|
// Before migration (field defaults):
|
|
//
|
|
// fieldConfig: {
|
|
// defaults: {
|
|
// custom: {
|
|
// displayMode: "gradient-gauge"
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// After migration (field defaults):
|
|
//
|
|
// fieldConfig: {
|
|
// defaults: {
|
|
// custom: {
|
|
// cellOptions: {
|
|
// type: "gauge",
|
|
// mode: "gradient"
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// Before migration (field override):
|
|
//
|
|
// overrides: [{
|
|
// matcher: { id: "byName", options: "CPU" },
|
|
// properties: [{
|
|
// id: "custom.displayMode",
|
|
// value: "color-background-solid"
|
|
// }]
|
|
// }]
|
|
//
|
|
// After migration (field override):
|
|
//
|
|
// overrides: [{
|
|
// matcher: { id: "byName", options: "CPU" },
|
|
// properties: [{
|
|
// id: "custom.cellOptions",
|
|
// value: {
|
|
// type: "color-background",
|
|
// mode: "basic"
|
|
// }
|
|
// }]
|
|
// }]
|
|
func V38(_ context.Context, dashboard map[string]interface{}) error {
|
|
dashboard["schemaVersion"] = int(38)
|
|
|
|
panels, ok := dashboard["panels"].([]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
// Process all panels, including nested ones
|
|
processPanelsV38(panels)
|
|
|
|
return nil
|
|
}
|
|
|
|
// processPanelsV38 recursively processes panels, including nested panels within rows
|
|
func processPanelsV38(panels []interface{}) {
|
|
for _, panel := range panels {
|
|
p, ok := panel.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Process nested panels if this is a row panel
|
|
if p["type"] == "row" {
|
|
if nestedPanels, ok := p["panels"].([]interface{}); ok {
|
|
processPanelsV38(nestedPanels)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Only process table panels
|
|
if p["type"] != "table" {
|
|
continue
|
|
}
|
|
|
|
fieldConfig, ok := p["fieldConfig"].(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Process defaults.custom if it exists
|
|
if defaults, ok := fieldConfig["defaults"].(map[string]interface{}); ok {
|
|
if custom, ok := defaults["custom"].(map[string]interface{}); ok {
|
|
// Migrate displayMode to cellOptions in defaults
|
|
if displayMode, exists := custom["displayMode"]; exists {
|
|
if displayModeStr, ok := displayMode.(string); ok {
|
|
custom["cellOptions"] = migrateTableDisplayModeToCellOptions(displayModeStr)
|
|
}
|
|
// Delete the legacy field
|
|
delete(custom, "displayMode")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update any overrides referencing the cell display mode
|
|
// This must be called regardless of whether defaults.custom exists
|
|
migrateOverrides(fieldConfig)
|
|
}
|
|
}
|
|
|
|
// migrateOverrides updates the overrides configuration to use the new cellOptions format
|
|
func migrateOverrides(fieldConfig map[string]interface{}) {
|
|
overrides, ok := fieldConfig["overrides"].([]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
for _, override := range overrides {
|
|
o, ok := override.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
properties, ok := o["properties"].([]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, property := range properties {
|
|
prop, ok := property.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Update the id to cellOptions
|
|
if prop["id"] == "custom.displayMode" {
|
|
prop["id"] = "custom.cellOptions"
|
|
if value, ok := prop["value"]; ok {
|
|
if valueStr, ok := value.(string); ok {
|
|
prop["value"] = migrateTableDisplayModeToCellOptions(valueStr)
|
|
}
|
|
} else {
|
|
// If no value exists, add empty cellOptions object to match frontend behavior
|
|
// Frontend always assigns a value even when original displayMode had no value
|
|
// See: public/app/features/dashboard/state/DashboardMigrator.ts:880
|
|
// override.properties[j].value = migrateTableDisplayModeToCellOptions(overrideDisplayMode);
|
|
prop["value"] = map[string]interface{}{}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// migrateTableDisplayModeToCellOptions converts the old displayMode string to the new cellOptions format
|
|
func migrateTableDisplayModeToCellOptions(displayMode string) map[string]interface{} {
|
|
switch displayMode {
|
|
case "basic", "gradient-gauge", "lcd-gauge":
|
|
gaugeMode := "basic"
|
|
switch displayMode {
|
|
case "gradient-gauge":
|
|
gaugeMode = "gradient"
|
|
case "lcd-gauge":
|
|
gaugeMode = "lcd"
|
|
}
|
|
return map[string]interface{}{
|
|
"type": "gauge",
|
|
"mode": gaugeMode,
|
|
}
|
|
|
|
case "color-background", "color-background-solid":
|
|
mode := "basic"
|
|
if displayMode == "color-background" {
|
|
mode = "gradient"
|
|
}
|
|
return map[string]interface{}{
|
|
"type": "color-background",
|
|
"mode": mode,
|
|
}
|
|
|
|
default:
|
|
return map[string]interface{}{
|
|
"type": displayMode,
|
|
}
|
|
}
|
|
}
|