diff --git a/apps/dashboard/pkg/migration/schemaversion/migrations.go b/apps/dashboard/pkg/migration/schemaversion/migrations.go index e8ae5ba6140..3ca5eea83bd 100644 --- a/apps/dashboard/pkg/migration/schemaversion/migrations.go +++ b/apps/dashboard/pkg/migration/schemaversion/migrations.go @@ -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, diff --git a/apps/dashboard/pkg/migration/schemaversion/v29.go b/apps/dashboard/pkg/migration/schemaversion/v29.go new file mode 100644 index 00000000000..27ffddcb8fc --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v29.go @@ -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 +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v29_test.go b/apps/dashboard/pkg/migration/schemaversion/v29_test.go new file mode 100644 index 00000000000..62d3b6a9051 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v29_test.go @@ -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) +} diff --git a/apps/dashboard/pkg/migration/testdata/input/v29.query_variables_refresh_and_options.json b/apps/dashboard/pkg/migration/testdata/input/v29.query_variables_refresh_and_options.json new file mode 100644 index 00000000000..00bf06ad0c0 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v29.query_variables_refresh_and_options.json @@ -0,0 +1,137 @@ +{ + "title": "V29 Query Variables Refresh and Options Migration Test Dashboard", + "schemaVersion": 28, + "templating": { + "list": [ + { + "name": "never_refresh_with_options", + "type": "query", + "datasource": "prometheus", + "options": [ + {"text": "A", "value": "A"}, + {"text": "B", "value": "B"} + ], + "refresh": 0 + }, + { + "name": "never_refresh_without_options", + "type": "query", + "datasource": "prometheus", + "options": [], + "refresh": 0 + }, + { + "name": "dashboard_refresh_with_options", + "type": "query", + "datasource": "prometheus", + "options": [ + {"text": "C", "value": "C"} + ], + "refresh": 1 + }, + { + "name": "dashboard_refresh_without_options", + "type": "query", + "datasource": "prometheus", + "options": [], + "refresh": 1 + }, + { + "name": "timerange_refresh_with_options", + "type": "query", + "datasource": "prometheus", + "options": [ + {"text": "D", "value": "D"}, + {"text": "E", "value": "E"} + ], + "refresh": 2 + }, + { + "name": "timerange_refresh_without_options", + "type": "query", + "datasource": "prometheus", + "options": [], + "refresh": 2 + }, + { + "name": "no_refresh_with_options", + "type": "query", + "datasource": "prometheus", + "options": [ + {"text": "F", "value": "F"} + ] + }, + { + "name": "no_refresh_without_options", + "type": "query", + "datasource": "prometheus", + "options": [] + }, + { + "name": "unknown_refresh_with_options", + "type": "query", + "datasource": "prometheus", + "options": [ + {"text": "G", "value": "G"} + ], + "refresh": 2001 + }, + { + "name": "unknown_refresh_without_options", + "type": "query", + "datasource": "prometheus", + "options": [], + "refresh": 2001 + }, + { + "name": "custom_variable", + "type": "custom", + "options": [ + {"text": "custom", "value": "custom"} + ] + }, + { + "name": "textbox_variable", + "type": "textbox", + "options": [ + {"text": "Hello", "value": "World"} + ] + }, + { + "name": "datasource_variable", + "type": "datasource", + "options": [ + {"text": "ds", "value": "ds"} + ] + }, + { + "name": "interval_variable", + "type": "interval", + "options": [ + {"text": "1m", "value": "1m"} + ] + } + ] + }, + "panels": [ + { + "id": 1, + "title": "Test Panel", + "type": "timeseries", + "datasource": "prometheus", + "targets": [ + { + "refId": "A", + "expr": "up" + } + ] + } + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/v29.query_variables_refresh_and_options.json b/apps/dashboard/pkg/migration/testdata/output/v29.query_variables_refresh_and_options.json new file mode 100644 index 00000000000..102a1ad1ebb --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/v29.query_variables_refresh_and_options.json @@ -0,0 +1,176 @@ +{ + "panels": [ + { + "datasource": { + "uid": "prometheus" + }, + "id": 1, + "targets": [ + { + "datasource": { + "uid": "prometheus" + }, + "expr": "up", + "refId": "A" + } + ], + "title": "Test Panel", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 41, + "templating": { + "list": [ + { + "datasource": { + "uid": "prometheus" + }, + "name": "never_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "never_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "dashboard_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "dashboard_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "timerange_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "timerange_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "no_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "no_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "unknown_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "unknown_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "name": "custom_variable", + "options": [ + { + "text": "custom", + "value": "custom" + } + ], + "type": "custom" + }, + { + "name": "textbox_variable", + "options": [ + { + "text": "Hello", + "value": "World" + } + ], + "type": "textbox" + }, + { + "name": "datasource_variable", + "options": [ + { + "text": "ds", + "value": "ds" + } + ], + "type": "datasource" + }, + { + "name": "interval_variable", + "options": [ + { + "text": "1m", + "value": "1m" + } + ], + "type": "interval" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "title": "V29 Query Variables Refresh and Options Migration Test Dashboard" +} \ No newline at end of file diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index b7ed66b05c3..00ba2415455 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -1043,7 +1043,7 @@ describe('DashboardModel', () => { }); }); - it('should have 11 variables after migration', () => { + it('should have 14 variables after migration', () => { expect(model.templating.list.length).toBe(14); });