Dashboard Migrations: v12 - template variables (#110253)

* migrate to v19

* migrate to v18

* Migration to be verified: v17 Convert minSpan to maxPerRow in panels

* Migration to be verified: 16 Grid layout migration

* Refactor v17 and v19 migrations to use shared helper functions

* Migration to be verified: 15 No-op migration for schema consistency

* Migration to be verified: 14 Shared crosshair to graph tooltip migration

* cleanup

* wip

* complete migration

* fix lint issues

* refactor and test with minimal graph config

* update tests

* migrate to v12

* extract defaults outside the func

* lint

* lint

* add missing showValues prop

* update

* add context and fix latest version

* add context

* generate snapshots

* v13 should be no-op

* clean up

* fix tests

* fix test

* remove v28

* remove singlestat migraiton from frontend migrator because this is an automigration

* remove unused function

* Remove v24 table plugin logic

* cleanup

* remove plugin version for automigrate as it was used only in v24 and v28 that have been removed

* cleanup

* es int

---------

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Haris Rozajac
2025-10-09 13:29:06 -06:00
committed by GitHub
co-authored by Dominik Prokop
parent 6af50482a1
commit 5c815a1733
10 changed files with 589 additions and 58 deletions
@@ -2,6 +2,8 @@ package migration
import (
"sort"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
)
// applyFrontendDefaults applies all DashboardModel constructor defaults
@@ -793,10 +795,12 @@ func cleanupVariable(variable map[string]interface{}) {
if variableType, ok := variable["type"].(string); ok {
switch variableType {
case "query":
// Query variables: keep options: [] if refresh !== never
// Since refresh is not specified in the input, it defaults to not "never"
if _, hasOptions := variable["options"]; !hasOptions {
variable["options"] = []interface{}{}
// Query variables: keep options: [] if refresh !== never (matches frontend getSaveModel logic)
refresh := schemaversion.GetIntValue(variable, "refresh", 1) // Default to 1 (onDashboardLoad) if not specified
if refresh != 0 { // 0 = VariableRefreshNever
if _, hasOptions := variable["options"]; !hasOptions {
variable["options"] = []interface{}{}
}
}
case "constant":
// Constant variables: remove options completely
@@ -7,7 +7,7 @@ import (
)
const (
MIN_VERSION = 12
MIN_VERSION = 11
LATEST_VERSION = 42
)
@@ -35,6 +35,7 @@ type PanelPluginInfo struct {
func GetMigrations(dsInfoProvider DataSourceInfoProvider) map[int]SchemaVersionMigrationFunc {
return map[int]SchemaVersionMigrationFunc{
12: V12,
13: V13,
14: V14,
15: V15,
@@ -0,0 +1,66 @@
package schemaversion
import "context"
// V12 migrates template variables to update their refresh and hide properties.
// This migration ensures that:
// 1. Variables with refresh=true get refresh=1, and variables with refresh=false get refresh=0
// 2. Variables with hideVariable=true get hide=2 (hide variable)
// 3. Variables with hideLabel=true get hide=1 (hide label)
//
// Example before migration:
//
// "templating": {
// "list": [
// { "type": "query", "name": "var1", "refresh": true, "hideVariable": true },
// { "type": "query", "name": "var2", "refresh": false, "hideLabel": true }
// ]
// }
//
// Example after migration:
//
// "templating": {
// "list": [
// { "type": "query", "name": "var1", "refresh": 1, "hide": 2 },
// { "type": "query", "name": "var2", "refresh": 0, "hide": 1 }
// ]
// }
func V12(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 12
templating, ok := dashboard["templating"].(map[string]interface{})
if !ok {
return nil
}
list, ok := templating["list"].([]interface{})
if !ok {
return nil
}
for _, v := range list {
variable, ok := v.(map[string]interface{})
if !ok {
continue
}
// Update refresh property
if _, hasRefresh := variable["refresh"]; hasRefresh {
if GetBoolValue(variable, "refresh") {
variable["refresh"] = 1
} else {
variable["refresh"] = 0
}
}
// Update hide property based on hideVariable and hideLabel
// hideVariable takes priority over hideLabel
if GetBoolValue(variable, "hideVariable") {
variable["hide"] = 2
} else if GetBoolValue(variable, "hideLabel") {
variable["hide"] = 1
}
}
return nil
}
@@ -0,0 +1,316 @@
package schemaversion_test
import (
"testing"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
)
func TestV12(t *testing.T) {
tests := []migrationTestCase{
{
name: "variable with refresh=true gets refresh=1",
input: map[string]interface{}{
"title": "V12 Refresh True Test",
"schemaVersion": 11,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "refresh_true_var",
"refresh": true,
},
},
},
},
expected: map[string]interface{}{
"title": "V12 Refresh True Test",
"schemaVersion": 12,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "refresh_true_var",
"refresh": 1,
},
},
},
},
},
{
name: "variable with refresh=false gets refresh=0",
input: map[string]interface{}{
"title": "V12 Refresh False Test",
"schemaVersion": 11,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "refresh_false_var",
"refresh": false,
},
},
},
},
expected: map[string]interface{}{
"title": "V12 Refresh False Test",
"schemaVersion": 12,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "refresh_false_var",
"refresh": 0,
},
},
},
},
},
{
name: "variable with hideVariable=true gets hide=2",
input: map[string]interface{}{
"title": "V12 Hide Variable Test",
"schemaVersion": 11,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "hide_variable_var",
"hideVariable": true,
},
},
},
},
expected: map[string]interface{}{
"title": "V12 Hide Variable Test",
"schemaVersion": 12,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "hide_variable_var",
"hideVariable": true,
"hide": 2,
},
},
},
},
},
{
name: "variable with hideLabel=true gets hide=1",
input: map[string]interface{}{
"title": "V12 Hide Label Test",
"schemaVersion": 11,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "hide_label_var",
"hideLabel": true,
},
},
},
},
expected: map[string]interface{}{
"title": "V12 Hide Label Test",
"schemaVersion": 12,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "hide_label_var",
"hideLabel": true,
"hide": 1,
},
},
},
},
},
{
name: "variable with both hideVariable and hideLabel prioritizes hideVariable",
input: map[string]interface{}{
"title": "V12 Hide Priority Test",
"schemaVersion": 11,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "priority_var",
"hideVariable": true,
"hideLabel": true,
},
},
},
},
expected: map[string]interface{}{
"title": "V12 Hide Priority Test",
"schemaVersion": 12,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "priority_var",
"hideVariable": true,
"hideLabel": true,
"hide": 2,
},
},
},
},
},
{
name: "variable with no refresh or hide properties is unchanged",
input: map[string]interface{}{
"title": "V12 No Properties Test",
"schemaVersion": 11,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "no_properties_var",
},
},
},
},
expected: map[string]interface{}{
"title": "V12 No Properties Test",
"schemaVersion": 12,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "no_properties_var",
},
},
},
},
},
{
name: "dashboard without templating is unchanged",
input: map[string]interface{}{
"title": "V12 No Templating Test",
"schemaVersion": 11,
},
expected: map[string]interface{}{
"title": "V12 No Templating Test",
"schemaVersion": 12,
},
},
{
name: "dashboard with empty templating list is unchanged",
input: map[string]interface{}{
"title": "V12 Empty Templating Test",
"schemaVersion": 11,
"templating": map[string]interface{}{
"list": []interface{}{},
},
},
expected: map[string]interface{}{
"title": "V12 Empty Templating Test",
"schemaVersion": 12,
"templating": map[string]interface{}{
"list": []interface{}{},
},
},
},
{
name: "variables with hideVariable=false and hideLabel=false do not get hide property",
input: map[string]interface{}{
"title": "V12 False Hide Properties Test",
"schemaVersion": 11,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "false_hide_var",
"hideVariable": false,
"hideLabel": false,
},
},
},
},
expected: map[string]interface{}{
"title": "V12 False Hide Properties Test",
"schemaVersion": 12,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "false_hide_var",
"hideVariable": false,
"hideLabel": false,
},
},
},
},
},
{
name: "variable with hideVariable=false but hideLabel=true gets hide=1",
input: map[string]interface{}{
"title": "V12 Mixed Hide Properties Test",
"schemaVersion": 11,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "mixed_hide_var",
"hideVariable": false,
"hideLabel": true,
},
},
},
},
expected: map[string]interface{}{
"title": "V12 Mixed Hide Properties Test",
"schemaVersion": 12,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "mixed_hide_var",
"hideVariable": false,
"hideLabel": true,
"hide": 1,
},
},
},
},
},
{
name: "variable with all properties gets all migrations applied",
input: map[string]interface{}{
"title": "V12 All Properties Test",
"schemaVersion": 11,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "all_properties_var",
"refresh": true,
"hideVariable": true,
"hideLabel": false,
},
},
},
},
expected: map[string]interface{}{
"title": "V12 All Properties Test",
"schemaVersion": 12,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"name": "all_properties_var",
"refresh": 1,
"hideVariable": true,
"hideLabel": false,
"hide": 2,
},
},
},
},
},
}
runMigrationTests(t, tests, schemaversion.V12)
}
@@ -139,7 +139,8 @@ func migrateTemplateVariables(dashboard map[string]interface{}, datasources []Da
ds, exists := varMap["datasource"]
// Handle null datasource variables by setting to default (matches frontend behavior)
if !exists || ds == nil {
// Only add datasource if it's explicitly null, not if it doesn't exist
if exists && ds == nil {
varMap["datasource"] = GetDataSourceRef(defaultDS)
}
// Note: Frontend v36 migration only converts null datasources to default objects
@@ -0,0 +1,38 @@
{
"title": "V12 Template Variables Migration Test",
"schemaVersion": 11,
"templating": {
"list": [
{
"type": "query",
"name": "refresh_true_var",
"refresh": true
},
{
"type": "query",
"name": "refresh_false_var",
"refresh": false
},
{
"type": "query",
"name": "hide_variable_var",
"hideVariable": true
},
{
"type": "query",
"name": "hide_label_var",
"hideLabel": true
},
{
"type": "query",
"name": "priority_var",
"hideVariable": true,
"hideLabel": true
},
{
"type": "query",
"name": "no_properties_var"
}
]
}
}
@@ -0,0 +1,81 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"panels": [],
"refresh": "",
"schemaVersion": 42,
"tags": [],
"templating": {
"list": [
{
"name": "refresh_true_var",
"options": [],
"refresh": 1,
"type": "query"
},
{
"name": "refresh_false_var",
"options": [],
"refresh": 1,
"type": "query"
},
{
"hide": 2,
"hideVariable": true,
"name": "hide_variable_var",
"options": [],
"refresh": 1,
"type": "query"
},
{
"hide": 1,
"hideLabel": true,
"name": "hide_label_var",
"options": [],
"refresh": 1,
"type": "query"
},
{
"hide": 2,
"hideLabel": true,
"hideVariable": true,
"name": "priority_var",
"options": [],
"refresh": 1,
"type": "query"
},
{
"name": "no_properties_var",
"options": [],
"refresh": 1,
"type": "query"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "V12 Template Variables Migration Test",
"weekStart": ""
}
@@ -0,0 +1,75 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"panels": [],
"schemaVersion": 12,
"tags": [],
"templating": {
"list": [
{
"name": "refresh_true_var",
"options": [],
"refresh": 1,
"type": "query"
},
{
"name": "refresh_false_var",
"refresh": 0,
"type": "query"
},
{
"hide": 2,
"hideVariable": true,
"name": "hide_variable_var",
"options": [],
"type": "query"
},
{
"hide": 1,
"hideLabel": true,
"name": "hide_label_var",
"options": [],
"type": "query"
},
{
"hide": 2,
"hideLabel": true,
"hideVariable": true,
"name": "priority_var",
"options": [],
"type": "query"
},
{
"name": "no_properties_var",
"options": [],
"type": "query"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "V12 Template Variables Migration Test",
"weekStart": ""
}
+1 -1
View File
@@ -2544,7 +2544,7 @@
"count": 2
},
"@typescript-eslint/no-explicit-any": {
"count": 20
"count": 19
}
},
"public/app/features/dashboard/state/DashboardModel.repeat.test.ts": {
@@ -347,57 +347,6 @@ export class DashboardMigrator {
});
}
if (oldVersion < 12 && finalTargetVersion >= 12) {
// update graph yaxes changes
panelUpgrades.push((panel: any) => {
if (panel.type !== 'graph') {
return panel;
}
if (!panel.grid) {
return panel;
}
if (!panel.yaxes) {
panel.yaxes = [
{
show: panel['y-axis'],
min: panel.grid.leftMin,
max: panel.grid.leftMax,
logBase: panel.grid.leftLogBase,
format: panel.y_formats[0],
label: panel.leftYAxisLabel,
},
{
show: panel['y-axis'],
min: panel.grid.rightMin,
max: panel.grid.rightMax,
logBase: panel.grid.rightLogBase,
format: panel.y_formats[1],
label: panel.rightYAxisLabel,
},
];
panel.xaxis = {
show: panel['x-axis'],
};
delete panel.grid.leftMin;
delete panel.grid.leftMax;
delete panel.grid.leftLogBase;
delete panel.grid.rightMin;
delete panel.grid.rightMax;
delete panel.grid.rightLogBase;
delete panel.y_formats;
delete panel.leftYAxisLabel;
delete panel.rightYAxisLabel;
delete panel['y-axis'];
delete panel['x-axis'];
}
return panel;
});
}
if (oldVersion < 13 && finalTargetVersion >= 13) {
// graph panel auto migrates to either barchart, bargauge, histogram or timeseries (all standard Grafana plugins)
// see public/app/features/dashboard/state/getPanelPluginToMigrateTo.ts