Files
grafana/apps/dashboard/pkg/migration/schemaversion/v31.go
Ivan Ortega Alba a72e02f88a 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)
2025-09-24 12:20:25 +02:00

137 lines
4.3 KiB
Go

package schemaversion
import "context"
// V31 adds a merge transformer after any labelsToFields transformer in panel transformations.
//
// This migration addresses data processing workflow optimization by automatically inserting
// a merge transformation after the labelsToFields transformation. When the labelsToFields
// transformer converts time series labels to individual fields, it can result in multiple
// data frames that need to be consolidated for proper visualization and analysis.
//
// The migration works by:
// 1. Iterating through all panels in the dashboard, including nested panels in collapsed rows
// 2. Examining the transformations array within each panel
// 3. Identifying transformations with id 'labelsToFields'
// 4. Inserting a merge transformation with empty options immediately after each labelsToFields transformation
// 5. Preserving the original labelsToFields options (mode, keepLabels, valueLabel, etc.) without modification
// 6. Using empty options for merge transformations to enable optimal default consolidation behavior
// 7. Preserving the original order and configuration of all other transformations
//
// The migration handles complex scenarios including:
// - Multiple labelsToFields transformations within a single panel
// - Panels with mixed transformation types
// - Nested panels within collapsed row panels
// - Panels with no transformations (left unchanged)
// - Panels with transformations but no labelsToFields (left unchanged)
//
// Example transformation:
//
// Before migration:
//
// panel: {
// "transformations": [
// { "id": "organize", "options": {} },
// { "id": "labelsToFields", "options": {} },
// { "id": "calculateField", "options": {} },
// { "id": "labelsToFields", "options": { "mode": "rows", "keepLabels": ["job", "instance"] } },
// ]
// }
//
// After migration:
//
// panel: {
// "transformations": [
// { "id": "organize", "options": {} },
// { "id": "labelsToFields", "options": {} },
// { "id": "merge", "options": {} },
// { "id": "calculateField", "options": {} },
// { "id": "labelsToFields", "options": { "mode": "rows", "keepLabels": ["job", "instance"] } },
// { "id": "merge", "options": {} }
// ]
// }
func V31(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = int(31)
panels, ok := dashboard["panels"].([]interface{})
if !ok {
return nil
}
// Process all panels, including nested ones
processPanelsV31(panels)
return nil
}
// processPanelsV31 recursively processes panels, including nested panels within rows
func processPanelsV31(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" {
nestedPanels, hasNested := p["panels"].([]interface{})
if !hasNested {
continue
}
processPanelsV31(nestedPanels)
continue
}
transformations, ok := p["transformations"].([]interface{})
if !ok {
continue
}
// Check if we have any labelsToFields transformations
hasLabelsToFields := false
for _, transformation := range transformations {
t, ok := transformation.(map[string]interface{})
if !ok {
continue
}
if t["id"] == "labelsToFields" {
hasLabelsToFields = true
break
}
}
if !hasLabelsToFields {
continue
}
// Create new transformations array with merge transformations added
newTransformations := []interface{}{}
for _, transformation := range transformations {
t, ok := transformation.(map[string]interface{})
if !ok {
newTransformations = append(newTransformations, transformation)
continue
}
// Add the current transformation (preserving all original options)
newTransformations = append(newTransformations, transformation)
// If this is a labelsToFields transformation, add a merge transformation after it
// with empty options to enable optimal default consolidation behavior
if t["id"] == "labelsToFields" {
mergeTransformation := map[string]interface{}{
"id": "merge",
"options": map[string]interface{}{},
}
newTransformations = append(newTransformations, mergeTransformation)
}
}
// Update the panel with the new transformations - only if not empty
if len(newTransformations) > 0 {
p["transformations"] = newTransformations
}
}
}