Fix v38 table panel overrides migration when defaults.custom is missing (#112430)

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.
This commit is contained in:
Dominik Prokop
2025-10-16 17:33:03 +02:00
committed by GitHub
parent 8449a2e4fb
commit 26d36ec7ff
2 changed files with 170 additions and 16 deletions
@@ -112,26 +112,22 @@ func processPanelsV38(panels []interface{}) {
continue
}
defaults, ok := fieldConfig["defaults"].(map[string]interface{})
if !ok {
continue
}
custom, ok := defaults["custom"].(map[string]interface{})
if !ok {
continue
}
// Migrate displayMode to cellOptions
if displayMode, exists := custom["displayMode"]; exists {
if displayModeStr, ok := displayMode.(string); ok {
custom["cellOptions"] = migrateTableDisplayModeToCellOptions(displayModeStr)
// 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")
}
}
// 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)
}
}
@@ -440,6 +440,164 @@ func TestV38(t *testing.T) {
},
},
},
{
name: "table with missing defaults.custom but overrides with custom.displayMode",
input: map[string]interface{}{
"title": "V38 Missing Defaults Custom Test",
"schemaVersion": 37,
"panels": []interface{}{
// Table with no custom in defaults but custom.displayMode in overrides
map[string]interface{}{
"type": "table",
"title": "Table with Missing Defaults Custom",
"id": 1,
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{}, // No custom object
"overrides": []interface{}{
map[string]interface{}{
"matcher": map[string]interface{}{
"id": "byName",
"options": "name",
},
"properties": []interface{}{
map[string]interface{}{
"id": "color",
"value": map[string]interface{}{
"fixedColor": "yellow",
"mode": "fixed",
},
},
map[string]interface{}{
"id": "custom.displayMode",
"value": "color-background",
},
},
},
},
},
},
// Table with empty custom in defaults but custom.displayMode in overrides
map[string]interface{}{
"type": "table",
"title": "Table with Empty Custom",
"id": 2,
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{}, // Empty custom object
},
"overrides": []interface{}{
map[string]interface{}{
"matcher": map[string]interface{}{
"id": "byName",
"options": "status",
},
"properties": []interface{}{
map[string]interface{}{
"id": "custom.displayMode",
"value": "gradient-gauge",
},
},
},
map[string]interface{}{
"matcher": map[string]interface{}{
"id": "byName",
"options": "value",
},
"properties": []interface{}{
map[string]interface{}{
"id": "custom.displayMode",
"value": "lcd-gauge",
},
},
},
},
},
},
},
},
expected: map[string]interface{}{
"title": "V38 Missing Defaults Custom Test",
"schemaVersion": 38,
"panels": []interface{}{
// Table with no custom in defaults but custom.displayMode in overrides (migrated)
map[string]interface{}{
"type": "table",
"title": "Table with Missing Defaults Custom",
"id": 1,
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{}, // Still no custom object
"overrides": []interface{}{
map[string]interface{}{
"matcher": map[string]interface{}{
"id": "byName",
"options": "name",
},
"properties": []interface{}{
map[string]interface{}{
"id": "color",
"value": map[string]interface{}{
"fixedColor": "yellow",
"mode": "fixed",
},
},
map[string]interface{}{
"id": "custom.cellOptions",
"value": map[string]interface{}{
"type": "color-background",
"mode": "gradient",
},
},
},
},
},
},
},
// Table with empty custom in defaults but custom.displayMode in overrides (migrated)
map[string]interface{}{
"type": "table",
"title": "Table with Empty Custom",
"id": 2,
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{}, // Empty custom object
},
"overrides": []interface{}{
map[string]interface{}{
"matcher": map[string]interface{}{
"id": "byName",
"options": "status",
},
"properties": []interface{}{
map[string]interface{}{
"id": "custom.cellOptions",
"value": map[string]interface{}{
"type": "gauge",
"mode": "gradient",
},
},
},
},
map[string]interface{}{
"matcher": map[string]interface{}{
"id": "byName",
"options": "value",
},
"properties": []interface{}{
map[string]interface{}{
"id": "custom.cellOptions",
"value": map[string]interface{}{
"type": "gauge",
"mode": "lcd",
},
},
},
},
},
},
},
},
},
},
}
runMigrationTests(t, tests, schemaversion.V38)
}