Fix dashboard migration discrepancies between backend and frontend implementations (use toEqual) (#110268)
**Highlights**
* **Single-version migrations**: add `targetVersion` to migrator & model, separate outputs, enforce exact version.
* **Datasource fixes**: include `apiVersion` in tests, empty-string → `{}`, preserve `{}` refs, drop unwanted defaults.
* **Panel defaults & nesting**: only top-level panels get defaults; preserve empty `transformations` context-aware; filter repeated panels.
* **Migration parity**
* V16: collapsed rows, grid height parsing (`px`).
* V17: omit `maxPerRow` when `minSpan=1`.
* V19–V20: cleanup defaults (`targetBlank`, style).
* V23–V24: template vars + table panel consistency.
* V28: full singlestat/stat parity, mappings & color.
* V30–V36: threshold logic, empty refs, nested targets.
* **Save-model cleanup**: replicate frontend defaults/filtering, drop null IDs, metadata, unused props.
* **Testing**: unified suites, dev dashboards (v42), full unit coverage for major migrations.
Co-authored-by: Ivan Ortega [ivanortegaalba@gmail.com](mailto:ivanortegaalba@gmail.com)
Co-authored-by: Dominik Prokop [dominik.prokop@grafana.com](mailto:dominik.prokop@grafana.com)
This commit is contained in:
co-authored by
Ivan Ortega [ivanortegaalba@gmail.com](mailto:ivanortegaalba@gmail.com)
Dominik Prokop [dominik.prokop@grafana.com](mailto:dominik.prokop@grafana.com)
parent
98fd3e8fe9
commit
a72e02f88a
@@ -236,21 +236,19 @@ func tablePanelChangedHandler(panel map[string]interface{}) error {
|
||||
overrides = []interface{}{}
|
||||
}
|
||||
|
||||
panel["transformations"] = transformations
|
||||
// Only add transformations if they're not empty - frontend omits empty arrays
|
||||
if len(transformations) > 0 {
|
||||
panel["transformations"] = transformations
|
||||
}
|
||||
panel["fieldConfig"] = map[string]interface{}{
|
||||
"defaults": defaults,
|
||||
"overrides": overrides,
|
||||
}
|
||||
|
||||
// Add default table panel options to match frontend behavior
|
||||
// Add minimal table panel options to match frontend behavior
|
||||
// Frontend doesn't add default footer options, so we don't either
|
||||
panel["options"] = map[string]interface{}{
|
||||
"cellHeight": "sm",
|
||||
"footer": map[string]interface{}{
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": []interface{}{"sum"},
|
||||
"show": false,
|
||||
},
|
||||
"showHeader": true,
|
||||
}
|
||||
|
||||
@@ -259,6 +257,9 @@ func tablePanelChangedHandler(panel map[string]interface{}) error {
|
||||
delete(panel, "transform")
|
||||
delete(panel, "columns")
|
||||
|
||||
// Remove legend property - frontend table panel migration doesn't preserve it
|
||||
delete(panel, "legend")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -394,12 +395,7 @@ func migrateTableStyleToOverride(style map[string]interface{}) map[string]interf
|
||||
}
|
||||
|
||||
// Add decimals
|
||||
if decimals, ok := style["decimals"].(float64); ok {
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "decimals",
|
||||
"value": int(decimals),
|
||||
})
|
||||
} else if decimals, ok := style["decimals"].(int); ok {
|
||||
if decimals := GetIntValue(style, "decimals", -1); decimals != -1 {
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "decimals",
|
||||
"value": decimals,
|
||||
@@ -456,9 +452,11 @@ func migrateTableStyleToOverride(style map[string]interface{}) map[string]interf
|
||||
|
||||
// Handle alignment
|
||||
if align, ok := style["align"].(string); ok && align != "" {
|
||||
alignValue := align
|
||||
var alignValue interface{}
|
||||
if align == "auto" {
|
||||
alignValue = ""
|
||||
alignValue = nil // Frontend sets to null and filters it out
|
||||
} else {
|
||||
alignValue = align
|
||||
}
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "custom.align",
|
||||
@@ -514,7 +512,7 @@ func migrateDefaults(prevDefaults map[string]interface{}) map[string]interface{}
|
||||
defaults["thresholds"] = map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{"color": "green"},
|
||||
map[string]interface{}{"color": "green", "value": (*float64)(nil)},
|
||||
map[string]interface{}{"color": "red", "value": 80},
|
||||
},
|
||||
}
|
||||
@@ -524,22 +522,24 @@ func migrateDefaults(prevDefaults map[string]interface{}) map[string]interface{}
|
||||
return defaults
|
||||
}
|
||||
|
||||
if unit, ok := prevDefaults["unit"].(string); ok && unit != "" {
|
||||
if unit := GetStringValue(prevDefaults, "unit"); unit != "" {
|
||||
defaults["unit"] = unit
|
||||
}
|
||||
|
||||
if decimals, ok := prevDefaults["decimals"].(float64); ok {
|
||||
defaults["decimals"] = int(decimals)
|
||||
if decimals := GetIntValue(prevDefaults, "decimals", -1); decimals != -1 {
|
||||
defaults["decimals"] = decimals
|
||||
}
|
||||
|
||||
if alias, ok := prevDefaults["alias"].(string); ok && alias != "" {
|
||||
if alias, ok := prevDefaults["alias"].(string); ok {
|
||||
defaults["displayName"] = alias
|
||||
}
|
||||
|
||||
if align, ok := prevDefaults["align"].(string); ok && align != "" {
|
||||
alignValue := align
|
||||
var alignValue interface{}
|
||||
if align == "auto" {
|
||||
alignValue = ""
|
||||
alignValue = nil // Frontend sets to null and filters it out
|
||||
} else {
|
||||
alignValue = align
|
||||
}
|
||||
defaults["custom"].(map[string]interface{})["align"] = alignValue
|
||||
}
|
||||
@@ -576,19 +576,11 @@ func generateThresholds(thresholds []interface{}, colors []interface{}) []interf
|
||||
|
||||
steps = append(steps, map[string]interface{}{
|
||||
"color": baseColor,
|
||||
"value": nil,
|
||||
"value": (*float64)(nil),
|
||||
})
|
||||
|
||||
// Add threshold steps
|
||||
for i, threshold := range thresholds {
|
||||
var color interface{}
|
||||
// Use colors[i+1] for the i-th threshold (colors[0] was used for base step)
|
||||
if i+1 < len(colors) && colors[i+1] != nil {
|
||||
color = colors[i+1]
|
||||
} else {
|
||||
color = "red"
|
||||
}
|
||||
|
||||
var value float64
|
||||
switch v := threshold.(type) {
|
||||
case string:
|
||||
@@ -601,10 +593,17 @@ func generateThresholds(thresholds []interface{}, colors []interface{}) []interf
|
||||
value = float64(v)
|
||||
}
|
||||
|
||||
steps = append(steps, map[string]interface{}{
|
||||
"color": color,
|
||||
step := map[string]interface{}{
|
||||
"value": value,
|
||||
})
|
||||
}
|
||||
|
||||
// Only add color if there's a corresponding color in the colors array
|
||||
// This matches the frontend behavior where colors[idx] might be undefined
|
||||
if i+1 < len(colors) && colors[i+1] != nil {
|
||||
step["color"] = colors[i+1]
|
||||
}
|
||||
|
||||
steps = append(steps, step)
|
||||
}
|
||||
|
||||
return steps
|
||||
|
||||
Reference in New Issue
Block a user