Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8edfdff1fa |
@@ -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
|
||||||
|
}
|
||||||
@@ -99,5 +99,9 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func addDefaultingFuncs(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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -99,5 +99,9 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func addDefaultingFuncs(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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user