Dashboard Migrations: V29 query variable refresh property and options (#108088)

* Dashboard Migrations: V31 LabelsToFields-Merge Migration

* Dashboard Migrations: V32 No-op migration

* simplify

* Refactor to reduce nesting

* Dashboard Migrations: V30 value mappings and tooltip options

* Do not automigrate since graph is migrated in v27

* Refactor to reduce nesting

* Add test case for invalid mapping

* migrate to v29

---------

Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
This commit is contained in:
Haris Rozajac
2025-07-15 17:27:04 +00:00
committed by GitHub
co-authored by Ivan Ortega
parent 1200b684c6
commit 2f8ec01c6c
6 changed files with 502 additions and 2 deletions
@@ -5,7 +5,7 @@ import (
)
const (
MIN_VERSION = 29
MIN_VERSION = 28
LATEST_VERSION = 41
)
@@ -26,6 +26,7 @@ type DataSourceInfoProvider interface {
func GetMigrations(dsInfoProvider DataSourceInfoProvider) map[int]SchemaVersionMigrationFunc {
return map[int]SchemaVersionMigrationFunc{
29: V29,
30: V30,
31: V31,
32: V32,
@@ -0,0 +1,59 @@
package schemaversion
// V29 migrates query variables to ensure their refresh property is set to 1 (on dashboard load)
// if it is not 1 or 2, and clears their options array if present.
//
// Example before migration:
//
// "templating": {
// "list": [
// { "type": "query", "refresh": 0, "options": [{ "text": "A", "value": "A" }] },
// { "type": "query", "refresh": 2, "options": [{ "text": "B", "value": "B" }] },
// { "type": "query", "options": [{ "text": "C", "value": "C" }] }
// ]
// }
//
// Example after migration:
//
// "templating": {
// "list": [
// { "type": "query", "refresh": 1, "options": [] },
// { "type": "query", "refresh": 2, "options": [] },
// { "type": "query", "refresh": 1, "options": [] }
// ]
// }
func V29(dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 29
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
}
if variable["type"] != "query" {
continue
}
// Set refresh to 1 if not 1 or 2
refresh, hasRefresh := variable["refresh"]
refreshInt := 0
if r, ok := refresh.(int); ok {
refreshInt = r
}
if !hasRefresh || (refreshInt != 1 && refreshInt != 2) {
variable["refresh"] = 1
}
// Clear options if present
if _, hasOptions := variable["options"]; hasOptions {
variable["options"] = []interface{}{}
}
}
return nil
}
@@ -0,0 +1,127 @@
package schemaversion_test
import (
"testing"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
)
func TestV29(t *testing.T) {
tests := []migrationTestCase{
{
name: "query variables get migrated with refresh and options",
input: map[string]interface{}{
"title": "V29 Query Variables Migration Test Dashboard",
"schemaVersion": 28,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{"type": "query", "name": "never_refresh_with_options", "options": []interface{}{map[string]interface{}{"text": "A", "value": "A"}}, "refresh": 0},
map[string]interface{}{"type": "query", "name": "never_refresh_without_options", "options": []interface{}{}, "refresh": 0},
map[string]interface{}{"type": "query", "name": "dashboard_refresh_with_options", "options": []interface{}{map[string]interface{}{"text": "A", "value": "A"}}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "dashboard_refresh_without_options", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "timerange_refresh_with_options", "options": []interface{}{map[string]interface{}{"text": "A", "value": "A"}}, "refresh": 2},
map[string]interface{}{"type": "query", "name": "timerange_refresh_without_options", "options": []interface{}{}, "refresh": 2},
map[string]interface{}{"type": "query", "name": "no_refresh_with_options", "options": []interface{}{map[string]interface{}{"text": "A", "value": "A"}}},
map[string]interface{}{"type": "query", "name": "no_refresh_without_options", "options": []interface{}{}},
map[string]interface{}{"type": "query", "name": "unknown_refresh_with_options", "options": []interface{}{map[string]interface{}{"text": "A", "value": "A"}}, "refresh": 2001},
map[string]interface{}{"type": "query", "name": "unknown_refresh_without_options", "options": []interface{}{}, "refresh": 2001},
map[string]interface{}{"type": "custom", "name": "custom", "options": []interface{}{map[string]interface{}{"text": "custom", "value": "custom"}}},
map[string]interface{}{"type": "textbox", "name": "textbox", "options": []interface{}{map[string]interface{}{"text": "Hello", "value": "World"}}},
map[string]interface{}{"type": "datasource", "name": "datasource", "options": []interface{}{map[string]interface{}{"text": "ds", "value": "ds"}}},
map[string]interface{}{"type": "interval", "name": "interval", "options": []interface{}{map[string]interface{}{"text": "1m", "value": "1m"}}},
},
},
},
expected: map[string]interface{}{
"title": "V29 Query Variables Migration Test Dashboard",
"schemaVersion": 29,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{"type": "query", "name": "never_refresh_with_options", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "never_refresh_without_options", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "dashboard_refresh_with_options", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "dashboard_refresh_without_options", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "timerange_refresh_with_options", "options": []interface{}{}, "refresh": 2},
map[string]interface{}{"type": "query", "name": "timerange_refresh_without_options", "options": []interface{}{}, "refresh": 2},
map[string]interface{}{"type": "query", "name": "no_refresh_with_options", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "no_refresh_without_options", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "unknown_refresh_with_options", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "unknown_refresh_without_options", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "custom", "name": "custom", "options": []interface{}{map[string]interface{}{"text": "custom", "value": "custom"}}},
map[string]interface{}{"type": "textbox", "name": "textbox", "options": []interface{}{map[string]interface{}{"text": "Hello", "value": "World"}}},
map[string]interface{}{"type": "datasource", "name": "datasource", "options": []interface{}{map[string]interface{}{"text": "ds", "value": "ds"}}},
map[string]interface{}{"type": "interval", "name": "interval", "options": []interface{}{map[string]interface{}{"text": "1m", "value": "1m"}}},
},
},
},
},
{
name: "non-query variables remain unchanged",
input: map[string]interface{}{
"title": "V29 Non-Query Variables Test Dashboard",
"schemaVersion": 28,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{"type": "custom", "name": "custom", "options": []interface{}{map[string]interface{}{"text": "custom", "value": "custom"}}},
map[string]interface{}{"type": "textbox", "name": "textbox", "options": []interface{}{map[string]interface{}{"text": "Hello", "value": "World"}}},
map[string]interface{}{"type": "datasource", "name": "datasource", "options": []interface{}{map[string]interface{}{"text": "ds", "value": "ds"}}},
map[string]interface{}{"type": "interval", "name": "interval", "options": []interface{}{map[string]interface{}{"text": "1m", "value": "1m"}}},
},
},
},
expected: map[string]interface{}{
"title": "V29 Non-Query Variables Test Dashboard",
"schemaVersion": 29,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{"type": "custom", "name": "custom", "options": []interface{}{map[string]interface{}{"text": "custom", "value": "custom"}}},
map[string]interface{}{"type": "textbox", "name": "textbox", "options": []interface{}{map[string]interface{}{"text": "Hello", "value": "World"}}},
map[string]interface{}{"type": "datasource", "name": "datasource", "options": []interface{}{map[string]interface{}{"text": "ds", "value": "ds"}}},
map[string]interface{}{"type": "interval", "name": "interval", "options": []interface{}{map[string]interface{}{"text": "1m", "value": "1m"}}},
},
},
},
},
{
name: "all query variables should have options removed",
input: map[string]interface{}{
"title": "V29 Query Variables Options Removal Test Dashboard",
"schemaVersion": 28,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{"type": "query", "name": "query1", "options": []interface{}{map[string]interface{}{"text": "A", "value": "A"}}},
map[string]interface{}{"type": "query", "name": "query2", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "query3", "options": []interface{}{map[string]interface{}{"text": "B", "value": "B"}, map[string]interface{}{"text": "C", "value": "C"}}, "refresh": 2},
map[string]interface{}{"type": "query", "name": "query4", "options": []interface{}{map[string]interface{}{"text": "D", "value": "D"}}, "refresh": 0},
map[string]interface{}{"type": "query", "name": "query5", "options": []interface{}{map[string]interface{}{"text": "E", "value": "E"}}, "refresh": 2001},
map[string]interface{}{"type": "query", "name": "query6", "options": []interface{}{}},
map[string]interface{}{"type": "query", "name": "query7", "options": []interface{}{map[string]interface{}{"text": "F", "value": "F"}}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "query8", "options": []interface{}{map[string]interface{}{"text": "G", "value": "G"}}, "refresh": 2},
map[string]interface{}{"type": "query", "name": "query9", "options": []interface{}{map[string]interface{}{"text": "H", "value": "H"}}},
map[string]interface{}{"type": "query", "name": "query10", "options": []interface{}{map[string]interface{}{"text": "I", "value": "I"}}, "refresh": 999},
},
},
},
expected: map[string]interface{}{
"title": "V29 Query Variables Options Removal Test Dashboard",
"schemaVersion": 29,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{"type": "query", "name": "query1", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "query2", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "query3", "options": []interface{}{}, "refresh": 2},
map[string]interface{}{"type": "query", "name": "query4", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "query5", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "query6", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "query7", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "query8", "options": []interface{}{}, "refresh": 2},
map[string]interface{}{"type": "query", "name": "query9", "options": []interface{}{}, "refresh": 1},
map[string]interface{}{"type": "query", "name": "query10", "options": []interface{}{}, "refresh": 1},
},
},
},
},
}
runMigrationTests(t, tests, schemaversion.V29)
}