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)
}
@@ -0,0 +1,73 @@
{
"title": "V19 Panel Links Migration Test Dashboard",
"schemaVersion": 18,
"panels": [
{
"id": 1,
"type": "graph",
"title": "Panel with legacy dashboard link",
"links": [
{
"dashboard": "my dashboard",
"title": "Dashboard Link",
"keepTime": true
}
]
},
{
"id": 2,
"type": "stat",
"title": "Panel with dashUri link",
"links": [
{
"dashUri": "my-dashboard-uid",
"title": "DashUri Link",
"includeVars": true
}
]
},
{
"id": 3,
"type": "table",
"title": "Panel with custom params",
"links": [
{
"url": "http://example.com",
"params": "customParam=value",
"title": "Custom Params Link"
}
]
},
{
"id": 4,
"type": "timeseries",
"title": "Panel with complex link",
"links": [
{
"dashboard": "complex dashboard",
"keepTime": true,
"includeVars": true,
"params": "param1=value1&param2=value2",
"title": "Complex Link",
"targetBlank": true
}
]
},
{
"id": 5,
"type": "gauge",
"title": "Panel with existing URL",
"links": [
{
"url": "http://existing-url.com",
"title": "Existing URL Link"
}
]
},
{
"id": 6,
"type": "singlestat",
"title": "Panel with no links"
}
]
}
@@ -0,0 +1,206 @@
{
"panels": [
{
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"id": 1,
"links": [
{
"targetBlank": false,
"title": "Dashboard Link",
"url": "dashboard/db/my-dashboard?$__url_time_range"
}
],
"targets": [
{
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"refId": "A"
}
],
"title": "Panel with legacy dashboard link",
"type": "graph"
},
{
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"id": 2,
"links": [
{
"targetBlank": false,
"title": "DashUri Link",
"url": "dashboard/my-dashboard-uid?$__all_variables"
}
],
"options": {
"justifyMode": "auto",
"percentChangeColorMode": "standard",
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"refId": "A"
}
],
"title": "Panel with dashUri link",
"type": "stat"
},
{
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"id": 3,
"links": [
{
"targetBlank": false,
"title": "Custom Params Link",
"url": "http://example.com?customParam=value"
}
],
"targets": [
{
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"refId": "A"
}
],
"title": "Panel with custom params",
"type": "table"
},
{
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"id": 4,
"links": [
{
"targetBlank": true,
"title": "Complex Link",
"url": "dashboard/db/complex-dashboard?$__url_time_range\u0026$__all_variables\u0026param1=value1\u0026param2=value2"
}
],
"targets": [
{
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"refId": "A"
}
],
"title": "Panel with complex link",
"type": "timeseries"
},
{
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"id": 5,
"links": [
{
"targetBlank": false,
"title": "Existing URL Link",
"url": "http://existing-url.com"
}
],
"targets": [
{
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"refId": "A"
}
],
"title": "Panel with existing URL",
"type": "gauge"
},
{
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"id": 6,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "1.0.0",
"targets": [
{
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"refId": "A"
}
],
"title": "Panel with no links",
"type": "stat"
}
],
"refresh": "",
"schemaVersion": 41,
"title": "V19 Panel Links Migration Test Dashboard"
}