Dashboard Migrations: v19 - panel links (#109495)

* migrate to v19

* clean up

* apply feedback
This commit is contained in:
Haris Rozajac
2025-08-13 13:51:19 -06:00
committed by GitHub
parent f1dd6db456
commit d84408e52d
5 changed files with 739 additions and 1 deletions
@@ -5,7 +5,7 @@ import (
)
const (
MIN_VERSION = 19
MIN_VERSION = 18
LATEST_VERSION = 41
)
@@ -38,6 +38,7 @@ type PanelPluginInfoProvider interface {
func GetMigrations(dsInfoProvider DataSourceInfoProvider, panelProvider PanelPluginInfoProvider) map[int]SchemaVersionMigrationFunc {
return map[int]SchemaVersionMigrationFunc{
19: V19,
20: V20,
21: V21,
22: V22,
@@ -0,0 +1,166 @@
package schemaversion
import (
"regexp"
"strings"
)
// V19 migrates panel links to ensure they have proper URL structure and handle legacy properties.
// This migration converts legacy panel link properties to the new URL-based format.
//
// Example before migration:
//
// "panels": [
// {
// "links": [
// {
// "dashboard": "my dashboard",
// "keepTime": true,
// "includeVars": true,
// "params": "customParam"
// }
// ]
// }
// ]
//
// Example after migration:
//
// "panels": [
// {
// "links": [
// {
// "url": "dashboard/db/my-dashboard?$__keepTime&$__includeVars&customParam",
// "title": "",
// "targetBlank": false
// }
// ]
// }
// ]
func V19(dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 19
panels, ok := dashboard["panels"].([]interface{})
if !ok {
return nil
}
for _, p := range panels {
panel, ok := p.(map[string]interface{})
if !ok {
continue
}
links, ok := panel["links"].([]interface{})
if !ok {
continue
}
panel["links"] = upgradePanelLinks(links)
}
return nil
}
func upgradePanelLinks(links []interface{}) []interface{} {
if len(links) == 0 {
return links
}
result := []interface{}{}
for _, link := range links {
linkMap, ok := link.(map[string]interface{})
if !ok {
continue
}
result = append(result, upgradePanelLink(linkMap))
}
return result
}
func upgradePanelLink(link map[string]interface{}) map[string]interface{} {
url := buildPanelLinkURL(link)
result := map[string]interface{}{
"url": url,
"title": getStringValue(link, "title"),
"targetBlank": getBoolValue(link, "targetBlank"),
}
return result
}
// buildPanelLinkURL builds the URL for a panel link based on legacy properties
func buildPanelLinkURL(link map[string]interface{}) string {
var url string
// Check for existing URL first
if existingURL, ok := link["url"].(string); ok && existingURL != "" {
url = existingURL
} else if dashboard, ok := link["dashboard"].(string); ok && dashboard != "" {
// Convert dashboard name to slugified URL
url = "dashboard/db/" + slugifyForURL(dashboard)
} else if dashUri, ok := link["dashUri"].(string); ok && dashUri != "" {
url = "dashboard/" + dashUri
} else {
// Default fallback
url = "/"
}
// Add query parameters
params := []string{}
if getBoolValue(link, "keepTime") {
params = append(params, "$__url_time_range")
}
if getBoolValue(link, "includeVars") {
params = append(params, "$__all_variables")
}
if customParams, ok := link["params"].(string); ok && customParams != "" {
params = append(params, customParams)
}
// Append parameters to URL
paramUsed := false
for _, param := range params {
if param != "" {
if paramUsed {
url += "&"
} else {
url += "?"
paramUsed = true
}
url += param
}
}
return url
}
var reNonWordOrSpace = regexp.MustCompile(`[^a-z0-9_ ]+`)
var reSpaces = regexp.MustCompile(` +`)
// slugifyForURL converts a dashboard name to a URL-friendly slug
func slugifyForURL(name string) string {
name = strings.ToLower(name)
name = reNonWordOrSpace.ReplaceAllString(name, "")
name = reSpaces.ReplaceAllString(name, "-")
return name
}
func getStringValue(m map[string]interface{}, key string) string {
if v, ok := m[key].(string); ok {
return v
}
return ""
}
func getBoolValue(m map[string]interface{}, key string) bool {
if v, ok := m[key].(bool); ok {
return v
}
return false
}
@@ -0,0 +1,292 @@
package schemaversion_test
import (
"testing"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
)
func TestV19(t *testing.T) {
tests := []migrationTestCase{
{
name: "panel with legacy dashboard link gets upgraded to URL format",
input: map[string]interface{}{
"title": "V19 Panel Links Migration Test Dashboard",
"schemaVersion": 18,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"dashboard": "my dashboard",
"title": "Dashboard Link",
},
},
},
},
},
expected: map[string]interface{}{
"title": "V19 Panel Links Migration Test Dashboard",
"schemaVersion": 19,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"url": "dashboard/db/my-dashboard",
"title": "Dashboard Link",
"targetBlank": false,
},
},
},
},
},
},
{
name: "panel with legacy dashUri link gets upgraded to URL format",
input: map[string]interface{}{
"title": "V19 DashUri Links Migration Test Dashboard",
"schemaVersion": 18,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"dashUri": "my-dashboard-uid",
"title": "DashUri Link",
},
},
},
},
},
expected: map[string]interface{}{
"title": "V19 DashUri Links Migration Test Dashboard",
"schemaVersion": 19,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"url": "dashboard/my-dashboard-uid",
"title": "DashUri Link",
"targetBlank": false,
},
},
},
},
},
},
{
name: "panel with keepTime flag gets upgraded with keepTime parameter",
input: map[string]interface{}{
"title": "V19 KeepTime Links Migration Test Dashboard",
"schemaVersion": 18,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"url": "http://example.com",
"keepTime": true,
"title": "KeepTime Link",
},
},
},
},
},
expected: map[string]interface{}{
"title": "V19 KeepTime Links Migration Test Dashboard",
"schemaVersion": 19,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"url": "http://example.com?$__url_time_range",
"title": "KeepTime Link",
"targetBlank": false,
},
},
},
},
},
},
{
name: "panel with includeVars flag gets upgraded with includeVars parameter",
input: map[string]interface{}{
"title": "V19 IncludeVars Links Migration Test Dashboard",
"schemaVersion": 18,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"url": "http://example.com",
"includeVars": true,
"title": "IncludeVars Link",
},
},
},
},
},
expected: map[string]interface{}{
"title": "V19 IncludeVars Links Migration Test Dashboard",
"schemaVersion": 19,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"url": "http://example.com?$__all_variables",
"title": "IncludeVars Link",
"targetBlank": false,
},
},
},
},
},
},
{
name: "panel with custom params gets upgraded with params in URL",
input: map[string]interface{}{
"title": "V19 Custom Params Links Migration Test Dashboard",
"schemaVersion": 18,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"url": "http://example.com",
"params": "customParam=value",
"title": "Custom Params Link",
},
},
},
},
},
expected: map[string]interface{}{
"title": "V19 Custom Params Links Migration Test Dashboard",
"schemaVersion": 19,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"url": "http://example.com?customParam=value",
"title": "Custom Params Link",
"targetBlank": false,
},
},
},
},
},
},
{
name: "panel with multiple flags and params gets upgraded correctly",
input: map[string]interface{}{
"title": "V19 Complex Links Migration Test Dashboard",
"schemaVersion": 18,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"dashboard": "my dashboard",
"keepTime": true,
"includeVars": true,
"params": "customParam=value",
"title": "Complex Link",
"targetBlank": true,
},
},
},
},
},
expected: map[string]interface{}{
"title": "V19 Complex Links Migration Test Dashboard",
"schemaVersion": 19,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"url": "dashboard/db/my-dashboard?$__url_time_range&$__all_variables&customParam=value",
"title": "Complex Link",
"targetBlank": true,
},
},
},
},
},
},
{
name: "panel with existing URL and no legacy properties remains unchanged",
input: map[string]interface{}{
"title": "V19 Existing URL Links Migration Test Dashboard",
"schemaVersion": 18,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"url": "http://example.com",
"title": "Existing URL Link",
"targetBlank": false,
},
},
},
},
},
expected: map[string]interface{}{
"title": "V19 Existing URL Links Migration Test Dashboard",
"schemaVersion": 19,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"links": []interface{}{
map[string]interface{}{
"url": "http://example.com",
"title": "Existing URL Link",
"targetBlank": false,
},
},
},
},
},
},
{
name: "panel with no links remains unchanged",
input: map[string]interface{}{
"title": "V19 No Links Migration Test Dashboard",
"schemaVersion": 18,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
},
},
},
expected: map[string]interface{}{
"title": "V19 No Links Migration Test Dashboard",
"schemaVersion": 19,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
},
},
},
},
{
name: "dashboard with no panels remains unchanged",
input: map[string]interface{}{
"title": "V19 No Panels Migration Test Dashboard",
"schemaVersion": 18,
},
expected: map[string]interface{}{
"title": "V19 No Panels Migration Test Dashboard",
"schemaVersion": 19,
},
},
}
runMigrationTests(t, tests, schemaversion.V19)
}