Dashboard Migrations: V2 - legacy services.filter and graphite panel type (#112282)

* migrate to v2

* Fix tests

* graphite panels should be auto-migrated

* lint

* Provisioning: Fix dashboard export to preserve original API version

* Add error handling for the edge case where conversion fails but no
storedVersion is available.
This commit is contained in:
Haris Rozajac
2025-10-15 10:34:12 -06:00
committed by GitHub
parent 5b7e6ef2e1
commit d9067b27eb
13 changed files with 734 additions and 78 deletions
@@ -7,7 +7,7 @@ import (
)
const (
MIN_VERSION = 2
MIN_VERSION = 0
LATEST_VERSION = 42
)
@@ -35,6 +35,7 @@ type PanelPluginInfo struct {
func GetMigrations(dsInfoProvider DataSourceInfoProvider) map[int]SchemaVersionMigrationFunc {
return map[int]SchemaVersionMigrationFunc{
2: V2,
3: V3,
4: V4,
5: V5,
@@ -0,0 +1,71 @@
package schemaversion
import "context"
// V2 migrates dashboard from schema version 0 or 1 to 2.
// This migration handles the legacy services.filter structure.
// It matches the frontend DashboardMigrator.ts logic for oldVersion < 2 && finalTargetVersion >= 2.
//
// Key migrations:
// 1. Services filter migration: old.services.filter.time -> dashboard.time
// 2. Services filter migration: old.services.filter.list -> dashboard.templating.list
//
// Example before migration:
//
// {
// "schemaVersion": 1,
// "services": {
// "filter": {
// "time": {"from": "now-1h", "to": "now"},
// "list": [{"name": "var1", "type": "query"}]
// }
// }
// }
//
// Example after migration:
//
// {
// "schemaVersion": 2,
// "time": {"from": "now-1h", "to": "now"},
// "templating": {
// "list": [{"name": "var1", "type": "query"}]
// }
// }
func V2(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 2
// Migrate services.filter structure
migrateServicesFilter(dashboard)
return nil
}
// migrateServicesFilter migrates the legacy services.filter structure
func migrateServicesFilter(dashboard map[string]interface{}) {
services, ok := dashboard["services"].(map[string]interface{})
if !ok {
return
}
filter, ok := services["filter"].(map[string]interface{})
if !ok {
return
}
// Migrate time property
if time, ok := filter["time"]; ok {
dashboard["time"] = time
}
// Migrate templating list
if list, ok := filter["list"]; ok {
if _, exists := dashboard["templating"]; !exists {
dashboard["templating"] = map[string]interface{}{}
}
templating := dashboard["templating"].(map[string]interface{})
templating["list"] = list
}
// Remove the services property after migration
delete(dashboard, "services")
}
@@ -0,0 +1,216 @@
package schemaversion_test
import (
"testing"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
)
func TestV2(t *testing.T) {
tests := []migrationTestCase{
{
name: "services filter migration moves time and templating list",
input: map[string]interface{}{
"title": "V2 Services Filter Migration Test",
"schemaVersion": 1,
"services": map[string]interface{}{
"filter": map[string]interface{}{
"time": map[string]interface{}{
"from": "now-1h",
"to": "now",
},
"list": []interface{}{
map[string]interface{}{
"name": "var1",
"type": "query",
},
},
},
},
},
expected: map[string]interface{}{
"title": "V2 Services Filter Migration Test",
"schemaVersion": 2,
"time": map[string]interface{}{
"from": "now-1h",
"to": "now",
},
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"name": "var1",
"type": "query",
},
},
},
},
},
{
name: "comprehensive services filter migration",
input: map[string]interface{}{
"title": "V2 Comprehensive Services Migration Test",
"schemaVersion": 1,
"services": map[string]interface{}{
"filter": map[string]interface{}{
"time": map[string]interface{}{
"from": "now-6h",
"to": "now",
},
"list": []interface{}{
map[string]interface{}{
"name": "server",
"type": "query",
},
},
},
},
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graphite",
"title": "CPU Usage",
},
},
},
expected: map[string]interface{}{
"title": "V2 Comprehensive Services Migration Test",
"schemaVersion": 2,
"time": map[string]interface{}{
"from": "now-6h",
"to": "now",
},
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"name": "server",
"type": "query",
},
},
},
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graphite", // Panel types are not migrated by V2 anymore
"title": "CPU Usage",
},
},
},
},
{
name: "dashboard with no services or panels",
input: map[string]interface{}{
"title": "V2 Minimal Migration Test",
"schemaVersion": 1,
},
expected: map[string]interface{}{
"title": "V2 Minimal Migration Test",
"schemaVersion": 2,
},
},
{
name: "services filter with only time (no list)",
input: map[string]interface{}{
"title": "V2 Services Time Only Test",
"schemaVersion": 1,
"services": map[string]interface{}{
"filter": map[string]interface{}{
"time": map[string]interface{}{
"from": "now-2h",
"to": "now",
},
},
},
},
expected: map[string]interface{}{
"title": "V2 Services Time Only Test",
"schemaVersion": 2,
"time": map[string]interface{}{
"from": "now-2h",
"to": "now",
},
},
},
{
name: "services filter with only list (no time)",
input: map[string]interface{}{
"title": "V2 Services List Only Test",
"schemaVersion": 1,
"services": map[string]interface{}{
"filter": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"name": "env",
"type": "custom",
},
},
},
},
},
expected: map[string]interface{}{
"title": "V2 Services List Only Test",
"schemaVersion": 2,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"name": "env",
"type": "custom",
},
},
},
},
},
{
name: "panels remain unchanged when no services filter",
input: map[string]interface{}{
"title": "V2 Panels Unchanged Test",
"schemaVersion": 1,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graphite",
"legend": true,
"y_format": "short",
"y2_format": "bytes",
"grid": map[string]interface{}{
"min": 0,
"max": 100,
},
},
map[string]interface{}{
"id": 2,
"type": "graph",
"legend": map[string]interface{}{
"show": false,
},
},
},
},
expected: map[string]interface{}{
"title": "V2 Panels Unchanged Test",
"schemaVersion": 2,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graphite", // Panel migrations handled by auto-migration
"legend": true,
"y_format": "short",
"y2_format": "bytes",
"grid": map[string]interface{}{
"min": 0,
"max": 100,
},
},
map[string]interface{}{
"id": 2,
"type": "graph",
"legend": map[string]interface{}{
"show": false,
},
},
},
},
},
}
runMigrationTests(t, tests, schemaversion.V2)
}