From 8edfdff1fa9683b51523f78891994e26a67fc61c Mon Sep 17 00:00:00 2001 From: Ivan Ortega Date: Tue, 16 Dec 2025 11:15:27 +0100 Subject: [PATCH] Poc: Defaults --- .../pkg/apis/dashboard/v2alpha1/defaults.go | 74 ++++++ .../pkg/apis/dashboard/v2alpha1/register.go | 6 +- .../pkg/apis/dashboard/v2beta1/defaults.go | 74 ++++++ .../apis/dashboard/v2beta1/defaults_test.go | 219 ++++++++++++++++++ .../pkg/apis/dashboard/v2beta1/register.go | 6 +- 5 files changed, 377 insertions(+), 2 deletions(-) create mode 100644 apps/dashboard/pkg/apis/dashboard/v2alpha1/defaults.go create mode 100644 apps/dashboard/pkg/apis/dashboard/v2beta1/defaults.go create mode 100644 apps/dashboard/pkg/apis/dashboard/v2beta1/defaults_test.go diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/defaults.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/defaults.go new file mode 100644 index 00000000000..66983b24ef8 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/defaults.go @@ -0,0 +1,74 @@ +package v2alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// SetDefaults_Dashboard ensures all panel queries have unique refIds +// This is called by the Kubernetes defaulting mechanism when dashboards are returned +func SetDefaults_Dashboard(obj *Dashboard) { + EnsureUniqueRefIds(&obj.Spec) +} + +// EnsureUniqueRefIds ensures all queries within each panel have unique refIds +// This matches the frontend behavior in PanelModel.ensureQueryIds() +func EnsureUniqueRefIds(spec *DashboardSpec) { + for _, element := range spec.Elements { + if element.PanelKind != nil { + ensureUniqueRefIdsForPanel(element.PanelKind) + } + } +} + +func ensureUniqueRefIdsForPanel(panel *DashboardPanelKind) { + queries := panel.Spec.Data.Spec.Queries + if len(queries) == 0 { + return + } + + // First pass: collect existing refIds + existingRefIds := make(map[string]bool) + for i := range queries { + if queries[i].Spec.RefId != "" { + existingRefIds[queries[i].Spec.RefId] = true + } + } + + // Second pass: assign unique refIds to queries without one + for i := range queries { + if queries[i].Spec.RefId == "" { + queries[i].Spec.RefId = getNextRefId(existingRefIds) + existingRefIds[queries[i].Spec.RefId] = true + } + } +} + +// getNextRefId generates the next available refId (A, B, C, ..., Z, AA, AB, etc.) +// This matches the frontend behavior in packages/grafana-data/src/query/refId.ts +func getNextRefId(existingRefIds map[string]bool) string { + for num := 0; ; num++ { + refId := getRefIdFromNumber(num) + if !existingRefIds[refId] { + return refId + } + } +} + +// getRefIdFromNumber converts a number to a refId (0=A, 1=B, ..., 25=Z, 26=AA, 27=AB, etc.) +func getRefIdFromNumber(num int) string { + const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + if num < len(letters) { + return string(letters[num]) + } + return getRefIdFromNumber(num/len(letters)-1) + string(letters[num%len(letters)]) +} + +// RegisterCustomDefaults registers custom defaulting functions for Dashboard types. +// This should be called from RegisterDefaults in zz_generated.defaults.go +// However, since that file is auto-generated, we provide this as a separate registration +func RegisterCustomDefaults(scheme *runtime.Scheme) error { + scheme.AddTypeDefaultingFunc(&Dashboard{}, func(obj interface{}) { + SetDefaults_Dashboard(obj.(*Dashboard)) + }) + return nil +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/register.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/register.go index 31aa15a72dd..f47a316037e 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/register.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/register.go @@ -99,5 +99,9 @@ func addKnownTypes(scheme *runtime.Scheme) error { } func addDefaultingFuncs(scheme *runtime.Scheme) error { - return RegisterDefaults(scheme) + if err := RegisterDefaults(scheme); err != nil { + return err + } + // Register custom defaults to ensure unique refIds in panel queries + return RegisterCustomDefaults(scheme) } diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/defaults.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/defaults.go new file mode 100644 index 00000000000..cab0ec481fa --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/defaults.go @@ -0,0 +1,74 @@ +package v2beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// SetDefaults_Dashboard ensures all panel queries have unique refIds +// This is called by the Kubernetes defaulting mechanism when dashboards are returned +func SetDefaults_Dashboard(obj *Dashboard) { + EnsureUniqueRefIds(&obj.Spec) +} + +// EnsureUniqueRefIds ensures all queries within each panel have unique refIds +// This matches the frontend behavior in PanelModel.ensureQueryIds() +func EnsureUniqueRefIds(spec *DashboardSpec) { + for _, element := range spec.Elements { + if element.PanelKind != nil { + ensureUniqueRefIdsForPanel(element.PanelKind) + } + } +} + +func ensureUniqueRefIdsForPanel(panel *DashboardPanelKind) { + queries := panel.Spec.Data.Spec.Queries + if len(queries) == 0 { + return + } + + // First pass: collect existing refIds + existingRefIds := make(map[string]bool) + for i := range queries { + if queries[i].Spec.RefId != "" { + existingRefIds[queries[i].Spec.RefId] = true + } + } + + // Second pass: assign unique refIds to queries without one + for i := range queries { + if queries[i].Spec.RefId == "" { + queries[i].Spec.RefId = getNextRefId(existingRefIds) + existingRefIds[queries[i].Spec.RefId] = true + } + } +} + +// getNextRefId generates the next available refId (A, B, C, ..., Z, AA, AB, etc.) +// This matches the frontend behavior in packages/grafana-data/src/query/refId.ts +func getNextRefId(existingRefIds map[string]bool) string { + for num := 0; ; num++ { + refId := getRefIdFromNumber(num) + if !existingRefIds[refId] { + return refId + } + } +} + +// getRefIdFromNumber converts a number to a refId (0=A, 1=B, ..., 25=Z, 26=AA, 27=AB, etc.) +func getRefIdFromNumber(num int) string { + const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + if num < len(letters) { + return string(letters[num]) + } + return getRefIdFromNumber(num/len(letters)-1) + string(letters[num%len(letters)]) +} + +// RegisterCustomDefaults registers custom defaulting functions for Dashboard types. +// This should be called from RegisterDefaults in zz_generated.defaults.go +// However, since that file is auto-generated, we provide this as a separate registration +func RegisterCustomDefaults(scheme *runtime.Scheme) error { + scheme.AddTypeDefaultingFunc(&Dashboard{}, func(obj interface{}) { + SetDefaults_Dashboard(obj.(*Dashboard)) + }) + return nil +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/defaults_test.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/defaults_test.go new file mode 100644 index 00000000000..013e9b328e3 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/defaults_test.go @@ -0,0 +1,219 @@ +package v2beta1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetRefIdFromNumber(t *testing.T) { + testCases := []struct { + num int + expected string + }{ + {0, "A"}, + {1, "B"}, + {25, "Z"}, + {26, "AA"}, + {27, "AB"}, + {51, "AZ"}, + {52, "BA"}, + {701, "ZZ"}, + {702, "AAA"}, + } + + for _, tc := range testCases { + t.Run(tc.expected, func(t *testing.T) { + result := getRefIdFromNumber(tc.num) + assert.Equal(t, tc.expected, result, "getRefIdFromNumber(%d) should return %s", tc.num, tc.expected) + }) + } +} + +func TestGetNextRefId(t *testing.T) { + testCases := []struct { + name string + existing map[string]bool + expected string + }{ + { + name: "empty map returns A", + existing: map[string]bool{}, + expected: "A", + }, + { + name: "A exists returns B", + existing: map[string]bool{"A": true}, + expected: "B", + }, + { + name: "A and B exist returns C", + existing: map[string]bool{"A": true, "B": true}, + expected: "C", + }, + { + name: "gap in sequence returns first available", + existing: map[string]bool{"A": true, "C": true, "D": true}, + expected: "B", + }, + { + name: "A-Z exist returns AA", + existing: func() map[string]bool { + m := make(map[string]bool) + for i := 0; i < 26; i++ { + m[string(rune('A'+i))] = true + } + return m + }(), + expected: "AA", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := getNextRefId(tc.existing) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestEnsureUniqueRefIds(t *testing.T) { + t.Run("assigns unique refIds to queries without refIds", func(t *testing.T) { + spec := &DashboardSpec{ + Elements: map[string]DashboardElement{ + "panel-1": { + PanelKind: &DashboardPanelKind{ + Kind: "Panel", + Spec: DashboardPanelSpec{ + Data: DashboardQueryGroupKind{ + Spec: DashboardQueryGroupSpec{ + Queries: []DashboardPanelQueryKind{ + {Spec: DashboardPanelQuerySpec{RefId: ""}}, + {Spec: DashboardPanelQuerySpec{RefId: ""}}, + {Spec: DashboardPanelQuerySpec{RefId: ""}}, + }, + }, + }, + }, + }, + }, + }, + } + + EnsureUniqueRefIds(spec) + + panel := spec.Elements["panel-1"].PanelKind + require.NotNil(t, panel) + require.Len(t, panel.Spec.Data.Spec.Queries, 3) + assert.Equal(t, "A", panel.Spec.Data.Spec.Queries[0].Spec.RefId) + assert.Equal(t, "B", panel.Spec.Data.Spec.Queries[1].Spec.RefId) + assert.Equal(t, "C", panel.Spec.Data.Spec.Queries[2].Spec.RefId) + }) + + t.Run("preserves existing refIds and fills gaps", func(t *testing.T) { + spec := &DashboardSpec{ + Elements: map[string]DashboardElement{ + "panel-1": { + PanelKind: &DashboardPanelKind{ + Kind: "Panel", + Spec: DashboardPanelSpec{ + Data: DashboardQueryGroupKind{ + Spec: DashboardQueryGroupSpec{ + Queries: []DashboardPanelQueryKind{ + {Spec: DashboardPanelQuerySpec{RefId: "A"}}, + {Spec: DashboardPanelQuerySpec{RefId: ""}}, + {Spec: DashboardPanelQuerySpec{RefId: "D"}}, + {Spec: DashboardPanelQuerySpec{RefId: ""}}, + }, + }, + }, + }, + }, + }, + }, + } + + EnsureUniqueRefIds(spec) + + panel := spec.Elements["panel-1"].PanelKind + require.NotNil(t, panel) + require.Len(t, panel.Spec.Data.Spec.Queries, 4) + assert.Equal(t, "A", panel.Spec.Data.Spec.Queries[0].Spec.RefId) + assert.Equal(t, "B", panel.Spec.Data.Spec.Queries[1].Spec.RefId) + assert.Equal(t, "D", panel.Spec.Data.Spec.Queries[2].Spec.RefId) + assert.Equal(t, "C", panel.Spec.Data.Spec.Queries[3].Spec.RefId) + }) + + t.Run("handles library panels (no modification)", func(t *testing.T) { + spec := &DashboardSpec{ + Elements: map[string]DashboardElement{ + "panel-1": { + LibraryPanelKind: &DashboardLibraryPanelKind{ + Kind: "LibraryPanel", + Spec: DashboardLibraryPanelKindSpec{ + LibraryPanel: DashboardLibraryPanelRef{ + Uid: "lib-uid", + Name: "lib-name", + }, + }, + }, + }, + }, + } + + // Should not panic + EnsureUniqueRefIds(spec) + }) + + t.Run("handles multiple panels", func(t *testing.T) { + spec := &DashboardSpec{ + Elements: map[string]DashboardElement{ + "panel-1": { + PanelKind: &DashboardPanelKind{ + Kind: "Panel", + Spec: DashboardPanelSpec{ + Data: DashboardQueryGroupKind{ + Spec: DashboardQueryGroupSpec{ + Queries: []DashboardPanelQueryKind{ + {Spec: DashboardPanelQuerySpec{RefId: ""}}, + {Spec: DashboardPanelQuerySpec{RefId: ""}}, + }, + }, + }, + }, + }, + }, + "panel-2": { + PanelKind: &DashboardPanelKind{ + Kind: "Panel", + Spec: DashboardPanelSpec{ + Data: DashboardQueryGroupKind{ + Spec: DashboardQueryGroupSpec{ + Queries: []DashboardPanelQueryKind{ + {Spec: DashboardPanelQuerySpec{RefId: ""}}, + {Spec: DashboardPanelQuerySpec{RefId: ""}}, + }, + }, + }, + }, + }, + }, + }, + } + + EnsureUniqueRefIds(spec) + + // Each panel should have unique refIds independently + panel1 := spec.Elements["panel-1"].PanelKind + panel2 := spec.Elements["panel-2"].PanelKind + require.NotNil(t, panel1) + require.NotNil(t, panel2) + + assert.Equal(t, "A", panel1.Spec.Data.Spec.Queries[0].Spec.RefId) + assert.Equal(t, "B", panel1.Spec.Data.Spec.Queries[1].Spec.RefId) + + assert.Equal(t, "A", panel2.Spec.Data.Spec.Queries[0].Spec.RefId) + assert.Equal(t, "B", panel2.Spec.Data.Spec.Queries[1].Spec.RefId) + }) +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/register.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/register.go index 28f74501e6e..fd2d9e53ca4 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/register.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/register.go @@ -99,5 +99,9 @@ func addKnownTypes(scheme *runtime.Scheme) error { } func addDefaultingFuncs(scheme *runtime.Scheme) error { - return RegisterDefaults(scheme) + if err := RegisterDefaults(scheme); err != nil { + return err + } + // Register custom defaults to ensure unique refIds in panel queries + return RegisterCustomDefaults(scheme) }