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
@@ -269,8 +269,8 @@ func processLegacyMapping(mappingMap map[string]interface{}, thresholds map[stri
|
||||
color := getColorFromThresholds(mappingMap, thresholds)
|
||||
|
||||
// Convert legacy type numbers to new format
|
||||
if mappingType, ok := mappingMap["type"].(float64); ok {
|
||||
switch int(mappingType) {
|
||||
if mappingType := GetIntValue(mappingMap, "type", -1); mappingType != -1 {
|
||||
switch mappingType {
|
||||
case 1: // ValueToText
|
||||
hasValueMappings = processValueToTextMapping(mappingMap, color, thresholds, valueMaps, newMappings, hasValueMappings)
|
||||
case 2: // RangeToText
|
||||
@@ -281,25 +281,16 @@ func processLegacyMapping(mappingMap map[string]interface{}, thresholds map[stri
|
||||
return hasValueMappings
|
||||
}
|
||||
|
||||
// getColorFromThresholds extracts color from thresholds based on mapping values
|
||||
// getColorFromThresholds extracts color from thresholds based on mapping text (matches frontend behavior)
|
||||
func getColorFromThresholds(mappingMap map[string]interface{}, thresholds map[string]interface{}) interface{} {
|
||||
if thresholds == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to get color from threshold based on the mapping value
|
||||
if value, ok := mappingMap["value"]; ok {
|
||||
if valueStr, ok := value.(string); ok {
|
||||
if numeric, err := strconv.ParseFloat(valueStr, 64); err == nil {
|
||||
return getActiveThresholdColor(numeric, thresholds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For range mappings, use the 'from' value to determine color
|
||||
if fromVal, ok := mappingMap["from"]; ok {
|
||||
if fromStr, ok := fromVal.(string); ok {
|
||||
if numeric, err := strconv.ParseFloat(fromStr, 64); err == nil {
|
||||
// Try to get color from threshold based on the mapping text (matches frontend behavior)
|
||||
if text, ok := mappingMap["text"]; ok {
|
||||
if textStr, ok := text.(string); ok {
|
||||
if numeric, err := strconv.ParseFloat(textStr, 64); err == nil {
|
||||
return getActiveThresholdColor(numeric, thresholds)
|
||||
}
|
||||
}
|
||||
@@ -325,19 +316,18 @@ func processValueToTextMapping(mappingMap map[string]interface{}, color interfac
|
||||
|
||||
// processNullValueMapping creates a special value mapping for null values
|
||||
func processNullValueMapping(mappingMap map[string]interface{}, color interface{}, thresholds map[string]interface{}, newMappings *[]interface{}) {
|
||||
// For null values, use the base threshold color (lowest step)
|
||||
if thresholds != nil && color == nil {
|
||||
color = getBaseThresholdColor(thresholds)
|
||||
result := map[string]interface{}{
|
||||
"text": mappingMap["text"],
|
||||
}
|
||||
if color != nil {
|
||||
result["color"] = color
|
||||
}
|
||||
|
||||
*newMappings = append(*newMappings, map[string]interface{}{
|
||||
"type": "special",
|
||||
"options": map[string]interface{}{
|
||||
"match": "null",
|
||||
"result": map[string]interface{}{
|
||||
"text": mappingMap["text"],
|
||||
"color": color,
|
||||
},
|
||||
"match": "null",
|
||||
"result": result,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -376,9 +366,13 @@ func processRangeToTextMapping(mappingMap map[string]interface{}, color interfac
|
||||
})
|
||||
}
|
||||
|
||||
// getActiveThresholdColor returns the color for a value based on thresholds
|
||||
// getActiveThresholdColor returns the color for a value based on thresholds (matches frontend getActiveThreshold)
|
||||
func getActiveThresholdColor(value float64, thresholds map[string]interface{}) interface{} {
|
||||
if steps, ok := thresholds["steps"].([]interface{}); ok {
|
||||
if len(steps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var activeStep map[string]interface{}
|
||||
|
||||
for _, step := range steps {
|
||||
@@ -390,9 +384,11 @@ func getActiveThresholdColor(value float64, thresholds map[string]interface{}) i
|
||||
continue
|
||||
}
|
||||
|
||||
if stepNum, ok := stepValue.(float64); ok {
|
||||
if stepNum := GetFloatValue(stepMap, "value", -1); stepNum != -1 {
|
||||
if value >= stepNum {
|
||||
activeStep = stepMap
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,24 +403,6 @@ func getActiveThresholdColor(value float64, thresholds map[string]interface{}) i
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBaseThresholdColor returns the base threshold color (first step with null value)
|
||||
func getBaseThresholdColor(thresholds map[string]interface{}) interface{} {
|
||||
if steps, ok := thresholds["steps"].([]interface{}); ok {
|
||||
for _, step := range steps {
|
||||
if stepMap, ok := step.(map[string]interface{}); ok {
|
||||
if stepValue, ok := stepMap["value"]; ok {
|
||||
if stepValue == nil {
|
||||
// This is the base color (null value step)
|
||||
return stepMap["color"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateTooltipOptions renames tooltipOptions to tooltip for specific panel types
|
||||
func migrateTooltipOptions(panel map[string]interface{}) {
|
||||
panelType, ok := panel["type"].(string)
|
||||
|
||||
Reference in New Issue
Block a user