Dashboard Migrations: V23 handle variable multi and current properties (#108937)
Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
This commit is contained in:
co-authored by
Ivan Ortega
parent
a5abc6727f
commit
4b78e54304
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
MIN_VERSION = 23
|
||||
MIN_VERSION = 22
|
||||
LATEST_VERSION = 41
|
||||
)
|
||||
|
||||
@@ -38,6 +38,7 @@ type PanelPluginInfoProvider interface {
|
||||
|
||||
func GetMigrations(dsInfoProvider DataSourceInfoProvider, panelProvider PanelPluginInfoProvider) map[int]SchemaVersionMigrationFunc {
|
||||
return map[int]SchemaVersionMigrationFunc{
|
||||
23: V23,
|
||||
24: V24(panelProvider),
|
||||
25: V25,
|
||||
26: V26,
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package schemaversion
|
||||
|
||||
import "github.com/grafana/grafana/apps/dashboard/pkg/migration/utils"
|
||||
|
||||
// V23 migrates multi variables to ensure their current property is aligned with their multi property.
|
||||
// This migration ensures that variables with multi=true have current.value and current.text as arrays,
|
||||
// and variables with multi=false have current.value and current.text as single values.
|
||||
//
|
||||
// Example before migration:
|
||||
//
|
||||
// "templating": {
|
||||
// "list": [
|
||||
// { "type": "query", "multi": true, "current": { "value": "A", "text": "A" } },
|
||||
// { "type": "query", "multi": false, "current": { "value": ["B"], "text": ["B"] } }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// Example after migration:
|
||||
//
|
||||
// "templating": {
|
||||
// "list": [
|
||||
// { "type": "query", "multi": true, "current": { "value": ["A"], "text": ["A"] } },
|
||||
// { "type": "query", "multi": false, "current": { "value": "B", "text": "B" } }
|
||||
// ]
|
||||
// }
|
||||
func V23(dashboard map[string]interface{}) error {
|
||||
dashboard["schemaVersion"] = 23
|
||||
|
||||
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 !isMulti(variable) {
|
||||
continue
|
||||
}
|
||||
|
||||
current, ok := variable["current"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if isEmptyObject(current) {
|
||||
continue
|
||||
}
|
||||
|
||||
multi, ok := variable["multi"].(bool)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
variable["current"] = alignCurrentWithMulti(current, multi)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isMulti checks if a variable supports multi-selection
|
||||
func isMulti(variable map[string]interface{}) bool {
|
||||
_, hasMulti := variable["multi"]
|
||||
return hasMulti
|
||||
}
|
||||
|
||||
func isEmptyObject(value interface{}) bool {
|
||||
if value == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
obj, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return len(obj) == 0
|
||||
}
|
||||
|
||||
// alignCurrentWithMulti aligns the current property with the multi property
|
||||
func alignCurrentWithMulti(current map[string]interface{}, multi bool) map[string]interface{} {
|
||||
if current == nil {
|
||||
return current
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
for k, v := range current {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
if multi {
|
||||
// Convert single values to arrays
|
||||
if value, ok := result["value"]; ok {
|
||||
if !utils.IsArray(value) {
|
||||
result["value"] = []interface{}{value}
|
||||
}
|
||||
}
|
||||
if text, ok := result["text"]; ok {
|
||||
if !utils.IsArray(text) {
|
||||
result["text"] = []interface{}{text}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Convert arrays to single values
|
||||
if value, ok := result["value"]; ok {
|
||||
if utils.IsArray(value) {
|
||||
if arr, ok := value.([]interface{}); ok && len(arr) > 0 {
|
||||
result["value"] = arr[0]
|
||||
} else {
|
||||
result["value"] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
if text, ok := result["text"]; ok {
|
||||
if utils.IsArray(text) {
|
||||
if arr, ok := text.([]interface{}); ok && len(arr) > 0 {
|
||||
result["text"] = arr[0]
|
||||
} else {
|
||||
result["text"] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package schemaversion_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
)
|
||||
|
||||
func TestV23(t *testing.T) {
|
||||
tests := []migrationTestCase{
|
||||
{
|
||||
name: "multi variable with single value gets converted to array",
|
||||
input: map[string]interface{}{
|
||||
"title": "V23 Multi Variable Single Value Test",
|
||||
"schemaVersion": 22,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "multi_single_value",
|
||||
"multi": true,
|
||||
"current": map[string]interface{}{"value": "A", "text": "A", "selected": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V23 Multi Variable Single Value Test",
|
||||
"schemaVersion": 23,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "multi_single_value",
|
||||
"multi": true,
|
||||
"current": map[string]interface{}{"value": []interface{}{"A"}, "text": []interface{}{"A"}, "selected": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multi variable with array value stays as array",
|
||||
input: map[string]interface{}{
|
||||
"title": "V23 Multi Variable Array Value Test",
|
||||
"schemaVersion": 22,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "multi_array_value",
|
||||
"multi": true,
|
||||
"current": map[string]interface{}{"value": []interface{}{"B", "C"}, "text": []interface{}{"B", "C"}, "selected": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V23 Multi Variable Array Value Test",
|
||||
"schemaVersion": 23,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "multi_array_value",
|
||||
"multi": true,
|
||||
"current": map[string]interface{}{"value": []interface{}{"B", "C"}, "text": []interface{}{"B", "C"}, "selected": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-multi variable with array value gets converted to single value",
|
||||
input: map[string]interface{}{
|
||||
"title": "V23 Non-Multi Variable Array Value Test",
|
||||
"schemaVersion": 22,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "non_multi_array_value",
|
||||
"multi": false,
|
||||
"current": map[string]interface{}{"value": []interface{}{"D"}, "text": []interface{}{"D"}, "selected": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V23 Non-Multi Variable Array Value Test",
|
||||
"schemaVersion": 23,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "non_multi_array_value",
|
||||
"multi": false,
|
||||
"current": map[string]interface{}{"value": "D", "text": "D", "selected": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-multi variable with single value stays as single value",
|
||||
input: map[string]interface{}{
|
||||
"title": "V23 Non-Multi Variable Single Value Test",
|
||||
"schemaVersion": 22,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "non_multi_single_value",
|
||||
"multi": false,
|
||||
"current": map[string]interface{}{"value": "E", "text": "E", "selected": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V23 Non-Multi Variable Single Value Test",
|
||||
"schemaVersion": 23,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "non_multi_single_value",
|
||||
"multi": false,
|
||||
"current": map[string]interface{}{"value": "E", "text": "E", "selected": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "variable without multi property is unchanged",
|
||||
input: map[string]interface{}{
|
||||
"title": "V23 No Multi Property Test",
|
||||
"schemaVersion": 22,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "no_multi_property",
|
||||
"current": map[string]interface{}{"value": "F", "text": "F", "selected": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V23 No Multi Property Test",
|
||||
"schemaVersion": 23,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "no_multi_property",
|
||||
"current": map[string]interface{}{"value": "F", "text": "F", "selected": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "variable with empty current is unchanged",
|
||||
input: map[string]interface{}{
|
||||
"title": "V23 Empty Current Test",
|
||||
"schemaVersion": 22,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "empty_current",
|
||||
"multi": true,
|
||||
"current": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V23 Empty Current Test",
|
||||
"schemaVersion": 23,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "empty_current",
|
||||
"multi": true,
|
||||
"current": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "variable with nil current is unchanged",
|
||||
input: map[string]interface{}{
|
||||
"title": "V23 Nil Current Test",
|
||||
"schemaVersion": 22,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "nil_current",
|
||||
"multi": true,
|
||||
"current": nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V23 Nil Current Test",
|
||||
"schemaVersion": 23,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "query",
|
||||
"name": "nil_current",
|
||||
"multi": true,
|
||||
"current": nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runMigrationTests(t, tests, schemaversion.V23)
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"title": "V23 Multi Variables Migration Test Dashboard",
|
||||
"schemaVersion": 22,
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"name": "multi_single_value",
|
||||
"type": "query",
|
||||
"datasource": "prometheus",
|
||||
"multi": true,
|
||||
"current": {
|
||||
"value": "A",
|
||||
"text": "A",
|
||||
"selected": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "multi_array_value",
|
||||
"type": "query",
|
||||
"datasource": "prometheus",
|
||||
"multi": true,
|
||||
"current": {
|
||||
"value": ["B", "C"],
|
||||
"text": ["B", "C"],
|
||||
"selected": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "non_multi_array_value",
|
||||
"type": "query",
|
||||
"datasource": "prometheus",
|
||||
"multi": false,
|
||||
"current": {
|
||||
"value": ["D"],
|
||||
"text": ["D"],
|
||||
"selected": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "non_multi_single_value",
|
||||
"type": "query",
|
||||
"datasource": "prometheus",
|
||||
"multi": false,
|
||||
"current": {
|
||||
"value": "E",
|
||||
"text": "E",
|
||||
"selected": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "no_multi_property",
|
||||
"type": "query",
|
||||
"datasource": "prometheus",
|
||||
"current": {
|
||||
"value": "F",
|
||||
"text": "F",
|
||||
"selected": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "empty_current",
|
||||
"type": "query",
|
||||
"datasource": "prometheus",
|
||||
"multi": true,
|
||||
"current": {}
|
||||
},
|
||||
{
|
||||
"name": "nil_current",
|
||||
"type": "query",
|
||||
"datasource": "prometheus",
|
||||
"multi": true,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"name": "custom_variable",
|
||||
"type": "custom",
|
||||
"multi": true,
|
||||
"current": {
|
||||
"value": "G",
|
||||
"text": "G",
|
||||
"selected": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "textbox_variable",
|
||||
"type": "textbox",
|
||||
"multi": false,
|
||||
"current": {
|
||||
"value": "H",
|
||||
"text": "H",
|
||||
"selected": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "datasource_variable",
|
||||
"type": "datasource",
|
||||
"multi": true,
|
||||
"current": {
|
||||
"value": ["prometheus", "influxdb"],
|
||||
"text": ["Prometheus", "InfluxDB"],
|
||||
"selected": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "interval_variable",
|
||||
"type": "interval",
|
||||
"multi": false,
|
||||
"current": {
|
||||
"value": ["1m"],
|
||||
"text": ["1m"],
|
||||
"selected": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Test Panel",
|
||||
"type": "stat",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "up"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"justifyMode": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"expr": "up",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Test Panel",
|
||||
"type": "stat"
|
||||
}
|
||||
],
|
||||
"refresh": "",
|
||||
"schemaVersion": 41,
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"A"
|
||||
],
|
||||
"value": [
|
||||
"A"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"multi": true,
|
||||
"name": "multi_single_value",
|
||||
"refresh": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"B",
|
||||
"C"
|
||||
],
|
||||
"value": [
|
||||
"B",
|
||||
"C"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"multi": true,
|
||||
"name": "multi_array_value",
|
||||
"refresh": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": "D",
|
||||
"value": "D"
|
||||
},
|
||||
"datasource": {
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"multi": false,
|
||||
"name": "non_multi_array_value",
|
||||
"refresh": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": "E",
|
||||
"value": "E"
|
||||
},
|
||||
"datasource": {
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"multi": false,
|
||||
"name": "non_multi_single_value",
|
||||
"refresh": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": "F",
|
||||
"value": "F"
|
||||
},
|
||||
"datasource": {
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"name": "no_multi_property",
|
||||
"refresh": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"datasource": {
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"multi": true,
|
||||
"name": "empty_current",
|
||||
"refresh": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": null,
|
||||
"datasource": {
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"multi": true,
|
||||
"name": "nil_current",
|
||||
"refresh": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"G"
|
||||
],
|
||||
"value": [
|
||||
"G"
|
||||
]
|
||||
},
|
||||
"multi": true,
|
||||
"name": "custom_variable",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": "H",
|
||||
"value": "H"
|
||||
},
|
||||
"multi": false,
|
||||
"name": "textbox_variable",
|
||||
"type": "textbox"
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"Prometheus",
|
||||
"InfluxDB"
|
||||
],
|
||||
"value": [
|
||||
"prometheus",
|
||||
"influxdb"
|
||||
]
|
||||
},
|
||||
"multi": true,
|
||||
"name": "datasource_variable",
|
||||
"type": "datasource"
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": "1m",
|
||||
"value": "1m"
|
||||
},
|
||||
"multi": false,
|
||||
"name": "interval_variable",
|
||||
"type": "interval"
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "V23 Multi Variables Migration Test Dashboard"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package utils
|
||||
|
||||
func IsArray(value interface{}) bool {
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := value.([]interface{})
|
||||
return ok
|
||||
}
|
||||
@@ -144,7 +144,7 @@ describe('Backend / Frontend result comparison', () => {
|
||||
*/
|
||||
if (jsonInput.schemaVersion <= 27) {
|
||||
for (const panel of frontendModel.panels) {
|
||||
if (panel.type === 'stat') {
|
||||
if (panel.type === 'stat' && panel.autoMigrateFrom) {
|
||||
// Set the plugin version if it doesn't exist
|
||||
if (!statPanelPlugin.meta.info) {
|
||||
statPanelPlugin.meta.info = {
|
||||
|
||||
Reference in New Issue
Block a user