Dashboards: Cover the Switch variable in schema transformations - part 2. (#114549)

* feat: add v2alpha_1 conversion for the switch variable

* chore: gofmt fixes

* chore: update comments in tests

* chore: fix gofmt

* Update specs

* tests: update the v2alpha1 openapi snapshot

---------

Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
This commit is contained in:
Levente Balogh
2025-11-28 14:21:33 +01:00
committed by GitHub
parent c75137b2d3
commit 930f7ce489
16 changed files with 809 additions and 148 deletions
@@ -258,6 +258,32 @@
"multi": true,
"skipUrlSync": false
},
{
"name": "switch_var",
"type": "switch",
"label": "Enable Feature",
"description": "Toggle feature on/off",
"query": "",
"current": {
"selected": false,
"text": "false",
"value": "false"
},
"options": [
{
"selected": false,
"text": "true",
"value": "true"
},
{
"selected": true,
"text": "false",
"value": "false"
}
],
"hide": 0,
"skipUrlSync": false
},
{
"name": "legacy_string_var",
"type": "query",
@@ -277,6 +277,32 @@
"skipUrlSync": false,
"type": "groupby"
},
{
"current": {
"selected": false,
"text": "false",
"value": "false"
},
"description": "Toggle feature on/off",
"hide": 0,
"label": "Enable Feature",
"name": "switch_var",
"options": [
{
"selected": false,
"text": "true",
"value": "true"
},
{
"selected": true,
"text": "false",
"value": "false"
}
],
"query": "",
"skipUrlSync": false,
"type": "switch"
},
{
"current": {
"selected": false,
@@ -317,6 +317,19 @@
"description": "Group results by field"
}
},
{
"kind": "SwitchVariable",
"spec": {
"name": "switch_var",
"current": "false",
"enabledValue": "true",
"disabledValue": "false",
"label": "Enable Feature",
"hide": "dontHide",
"skipUrlSync": false,
"description": "Toggle feature on/off"
}
},
{
"kind": "QueryVariable",
"spec": {
@@ -318,6 +318,19 @@
"description": "Group results by field"
}
},
{
"kind": "SwitchVariable",
"spec": {
"name": "switch_var",
"current": "false",
"enabledValue": "true",
"disabledValue": "false",
"label": "Enable Feature",
"hide": "dontHide",
"skipUrlSync": false,
"description": "Toggle feature on/off"
}
},
{
"kind": "QueryVariable",
"spec": {
@@ -956,6 +956,10 @@ func transformVariables(ctx context.Context, dashboard map[string]interface{}, d
if textVar, err := buildTextVariable(varMap, commonProps); err == nil {
variables = append(variables, textVar)
}
case "switch":
if switchVar, err := buildSwitchVariable(varMap, commonProps); err == nil {
variables = append(variables, switchVar)
}
case "groupby":
if groupByVar, err := buildGroupByVariable(ctx, varMap, commonProps, dsIndexProvider); err == nil {
variables = append(variables, groupByVar)
@@ -1415,6 +1419,75 @@ func buildTextVariable(varMap map[string]interface{}, commonProps CommonVariable
}, nil
}
// Helper function to extract string value from an option map (value or text field)
func getOptionValue(optMap map[string]interface{}) string {
if val, ok := optMap["value"].(string); ok && val != "" {
return val
}
if val, ok := optMap["text"].(string); ok && val != "" {
return val
}
return ""
}
// Switch Variable
func buildSwitchVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) {
current := ""
if currentVal, exists := varMap["current"]; exists {
if currentMap, ok := currentVal.(map[string]interface{}); ok {
current = getOptionValue(currentMap)
}
}
// In V1 the enabled value is the first value of the options array,
// while the disabled value is second one.
// (Falling back to "true" and "false" if options are not available)
enabledValue := "true"
disabledValue := "false"
if options, ok := varMap["options"].([]interface{}); ok {
// Get enabledValue from first option
if len(options) > 0 {
if opt1, ok := options[0].(map[string]interface{}); ok {
if val := getOptionValue(opt1); val != "" {
enabledValue = val
}
}
}
// Get disabledValue from second option
if len(options) > 1 {
if opt2, ok := options[1].(map[string]interface{}); ok {
if val := getOptionValue(opt2); val != "" {
disabledValue = val
}
}
}
}
// Set current to disabledValue if not set
if current == "" {
current = disabledValue
}
switchVar := &dashv2alpha1.DashboardSwitchVariableKind{
Kind: "SwitchVariable",
Spec: dashv2alpha1.DashboardSwitchVariableSpec{
Name: commonProps.Name,
Current: current,
EnabledValue: enabledValue,
DisabledValue: disabledValue,
Label: commonProps.Label,
Description: commonProps.Description,
Hide: commonProps.Hide,
SkipUrlSync: commonProps.SkipUrlSync,
},
}
return dashv2alpha1.DashboardVariableKind{
SwitchVariableKind: switchVar,
}, nil
}
// Adhoc Variable
func buildAdhocVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardVariableKind, error) {
datasource := varMap["datasource"]
@@ -733,6 +733,22 @@ func convertVariable_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVariableKind,
}
}
if in.SwitchVariableKind != nil {
out.SwitchVariableKind = &dashv2beta1.DashboardSwitchVariableKind{
Kind: in.SwitchVariableKind.Kind,
Spec: dashv2beta1.DashboardSwitchVariableSpec{
Name: in.SwitchVariableKind.Spec.Name,
Current: in.SwitchVariableKind.Spec.Current,
EnabledValue: in.SwitchVariableKind.Spec.EnabledValue,
DisabledValue: in.SwitchVariableKind.Spec.DisabledValue,
Label: in.SwitchVariableKind.Spec.Label,
Hide: dashv2beta1.DashboardVariableHide(in.SwitchVariableKind.Spec.Hide),
SkipUrlSync: in.SwitchVariableKind.Spec.SkipUrlSync,
Description: in.SwitchVariableKind.Spec.Description,
},
}
}
return nil
}
@@ -0,0 +1,144 @@
package conversion
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime"
dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
"github.com/grafana/grafana/apps/dashboard/pkg/migration"
migrationtestutil "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil"
)
// TestV2alpha1ToV2beta1 tests the conversion logic for v2alpha1 to v2beta1.
func TestV2alpha1ToV2beta1(t *testing.T) {
// Initialize the migrator with test providers
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
leProvider := migrationtestutil.NewLibraryElementProvider()
migration.Initialize(dsProvider, leProvider)
// Set up conversion scheme
scheme := runtime.NewScheme()
err := RegisterConversions(scheme, dsProvider, leProvider)
require.NoError(t, err)
testCases := []struct {
name string
createV2alpha1 func() *dashv2alpha1.Dashboard
validateV2beta1 func(t *testing.T, v2beta1 *dashv2beta1.Dashboard)
}{
{
name: "dashboard with switch variable",
createV2alpha1: func() *dashv2alpha1.Dashboard {
label := "Enable Feature"
description := "Toggle feature"
return &dashv2alpha1.Dashboard{
Spec: dashv2alpha1.DashboardSpec{
Title: "Test Dashboard",
Variables: []dashv2alpha1.DashboardVariableKind{
{
SwitchVariableKind: &dashv2alpha1.DashboardSwitchVariableKind{
Kind: "SwitchVariable",
Spec: dashv2alpha1.DashboardSwitchVariableSpec{
Name: "switch_var",
Current: "false",
EnabledValue: "true",
DisabledValue: "false",
Label: &label,
Description: &description,
Hide: dashv2alpha1.DashboardVariableHideDontHide,
SkipUrlSync: false,
},
},
},
},
},
}
},
validateV2beta1: func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) {
require.Len(t, v2beta1.Spec.Variables, 1)
variable := v2beta1.Spec.Variables[0]
require.NotNil(t, variable.SwitchVariableKind, "SwitchVariableKind should not be nil")
assert.Equal(t, "SwitchVariable", variable.SwitchVariableKind.Kind)
assert.Equal(t, "switch_var", variable.SwitchVariableKind.Spec.Name)
assert.Equal(t, "false", variable.SwitchVariableKind.Spec.Current)
assert.Equal(t, "true", variable.SwitchVariableKind.Spec.EnabledValue)
assert.Equal(t, "false", variable.SwitchVariableKind.Spec.DisabledValue)
assert.NotNil(t, variable.SwitchVariableKind.Spec.Label)
assert.Equal(t, "Enable Feature", *variable.SwitchVariableKind.Spec.Label)
assert.NotNil(t, variable.SwitchVariableKind.Spec.Description)
assert.Equal(t, "Toggle feature", *variable.SwitchVariableKind.Spec.Description)
assert.Equal(t, dashv2beta1.DashboardVariableHideDontHide, variable.SwitchVariableKind.Spec.Hide)
assert.False(t, variable.SwitchVariableKind.Spec.SkipUrlSync)
},
},
{
name: "dashboard with switch variable - custom values for enabled and disable states",
createV2alpha1: func() *dashv2alpha1.Dashboard {
label := "Enable Feature"
description := "Toggle feature"
return &dashv2alpha1.Dashboard{
Spec: dashv2alpha1.DashboardSpec{
Title: "Test Dashboard",
Variables: []dashv2alpha1.DashboardVariableKind{
{
SwitchVariableKind: &dashv2alpha1.DashboardSwitchVariableKind{
Kind: "SwitchVariable",
Spec: dashv2alpha1.DashboardSwitchVariableSpec{
Name: "switch_var",
Current: "true",
EnabledValue: "enabled",
DisabledValue: "disabled",
Label: &label,
Description: &description,
Hide: dashv2alpha1.DashboardVariableHideHideLabel,
SkipUrlSync: true,
},
},
},
},
},
}
},
validateV2beta1: func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) {
require.Len(t, v2beta1.Spec.Variables, 1)
variable := v2beta1.Spec.Variables[0]
require.NotNil(t, variable.SwitchVariableKind)
assert.Equal(t, "switch_var", variable.SwitchVariableKind.Spec.Name)
assert.Equal(t, "true", variable.SwitchVariableKind.Spec.Current)
assert.Equal(t, "enabled", variable.SwitchVariableKind.Spec.EnabledValue)
assert.Equal(t, "disabled", variable.SwitchVariableKind.Spec.DisabledValue)
assert.Equal(t, dashv2beta1.DashboardVariableHideHideLabel, variable.SwitchVariableKind.Spec.Hide)
assert.True(t, variable.SwitchVariableKind.Spec.SkipUrlSync)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create v2alpha1 dashboard
v2alpha1 := tc.createV2alpha1()
// Collect original statistics
originalStats := collectStatsV2alpha1(v2alpha1.Spec)
// Convert to v2beta1
var v2beta1 dashv2beta1.Dashboard
err := scheme.Convert(v2alpha1, &v2beta1, nil)
require.NoError(t, err, "Failed to convert v2alpha1 to v2beta1")
// Collect v2beta1 statistics
v2beta1Stats := collectStatsV2beta1(v2beta1.Spec)
// Verify no data loss
err = detectConversionDataLoss(originalStats, v2beta1Stats, "V2alpha1", "V2beta1")
assert.NoError(t, err, "Data loss detected in conversion")
// Run custom validation
tc.validateV2beta1(t, &v2beta1)
})
}
}
@@ -771,6 +771,23 @@ func convertVariable_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardVariableKind,
}
}
if in.SwitchVariableKind != nil {
out.SwitchVariableKind = &dashv2alpha1.DashboardSwitchVariableKind{
Kind: in.SwitchVariableKind.Kind,
Spec: dashv2alpha1.DashboardSwitchVariableSpec{
Name: in.SwitchVariableKind.Spec.Name,
Current: in.SwitchVariableKind.Spec.Current,
EnabledValue: in.SwitchVariableKind.Spec.EnabledValue,
DisabledValue: in.SwitchVariableKind.Spec.DisabledValue,
Label: in.SwitchVariableKind.Spec.Label,
Hide: dashv2alpha1.DashboardVariableHide(in.SwitchVariableKind.Spec.Hide),
SkipUrlSync: in.SwitchVariableKind.Spec.SkipUrlSync,
Description: in.SwitchVariableKind.Spec.Description,
},
}
return nil
}
return nil
}
@@ -479,6 +479,51 @@ func TestV2beta1ToV2alpha1(t *testing.T) {
assert.Equal(t, "", variable.QueryVariableKind.Spec.Query.Kind, "Empty group should result in empty kind")
},
},
{
name: "dashboard with switch variable",
createV2beta1: func() *dashv2beta1.Dashboard {
label := "Enable Feature"
description := "Toggle feature"
return &dashv2beta1.Dashboard{
Spec: dashv2beta1.DashboardSpec{
Title: "Test Dashboard",
Variables: []dashv2beta1.DashboardVariableKind{
{
SwitchVariableKind: &dashv2beta1.DashboardSwitchVariableKind{
Kind: "SwitchVariable",
Spec: dashv2beta1.DashboardSwitchVariableSpec{
Name: "switch_var",
Current: "false",
EnabledValue: "true",
DisabledValue: "false",
Label: &label,
Description: &description,
Hide: dashv2beta1.DashboardVariableHideDontHide,
SkipUrlSync: false,
},
},
},
},
},
}
},
validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) {
require.Len(t, v2alpha1.Spec.Variables, 1)
variable := v2alpha1.Spec.Variables[0]
require.NotNil(t, variable.SwitchVariableKind, "SwitchVariableKind should not be nil")
assert.Equal(t, "SwitchVariable", variable.SwitchVariableKind.Kind)
assert.Equal(t, "switch_var", variable.SwitchVariableKind.Spec.Name)
assert.Equal(t, "false", variable.SwitchVariableKind.Spec.Current)
assert.Equal(t, "true", variable.SwitchVariableKind.Spec.EnabledValue)
assert.Equal(t, "false", variable.SwitchVariableKind.Spec.DisabledValue)
assert.NotNil(t, variable.SwitchVariableKind.Spec.Label)
assert.Equal(t, "Enable Feature", *variable.SwitchVariableKind.Spec.Label)
assert.NotNil(t, variable.SwitchVariableKind.Spec.Description)
assert.Equal(t, "Toggle feature", *variable.SwitchVariableKind.Spec.Description)
assert.Equal(t, dashv2alpha1.DashboardVariableHideDontHide, variable.SwitchVariableKind.Spec.Hide)
assert.False(t, variable.SwitchVariableKind.Spec.SkipUrlSync)
},
},
{
name: "dashboard with rows layout",
createV2beta1: func() *dashv2beta1.Dashboard {