Skip to content

Commit a15d7f0

Browse files
sd2kclaude
andauthored
fix(tools): preserve dashboard identity fields in patch mode (#722)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0d95038 commit a15d7f0

2 files changed

Lines changed: 72 additions & 0 deletions

File tree

tools/dashboard.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ func updateDashboardWithPatches(ctx context.Context, args UpdateDashboardParams)
9494
return nil, fmt.Errorf("dashboard is not a JSON object")
9595
}
9696

97+
// Preserve the numeric ID before patching so it survives any
98+
// accidental mutation by patch operations.
99+
origID := dashboardMap["id"]
100+
97101
// Apply each patch operation
98102
for i, op := range args.Operations {
99103
switch op.Op {
@@ -110,6 +114,16 @@ func updateDashboardWithPatches(ctx context.Context, args UpdateDashboardParams)
110114
}
111115
}
112116

117+
// Restore identity fields so the Grafana API updates the existing
118+
// dashboard in place instead of creating a clone with a new UID.
119+
// The UID is always taken from the request args (the value used to
120+
// fetch the dashboard) to guarantee consistency even when the
121+
// dashboard body returned by the API did not include it.
122+
dashboardMap["uid"] = args.UID
123+
if origID != nil {
124+
dashboardMap["id"] = origID
125+
}
126+
113127
// Use the folder UID from the existing dashboard if not provided
114128
folderUID := args.FolderUID
115129
if folderUID == "" && dashboard.Meta != nil {

tools/dashboard_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,64 @@ func TestDashboardTools(t *testing.T) {
335335
assert.Equal(t, newTitle, dashboardMap["title"])
336336
})
337337

338+
t.Run("update dashboard - patch preserves UID", func(t *testing.T) {
339+
ctx := newTestContext()
340+
341+
// Get our test dashboard
342+
dashboard := getExistingTestDashboard(t, ctx, newTestDashboardName)
343+
originalUID := dashboard.UID
344+
345+
// Fetch the full dashboard to get the numeric ID
346+
fullDashboard, err := getDashboardByUID(ctx, GetDashboardByUIDParams{UID: originalUID})
347+
require.NoError(t, err)
348+
origMap, ok := fullDashboard.Dashboard.(map[string]interface{})
349+
require.True(t, ok)
350+
originalID := origMap["id"]
351+
352+
// Patch via uid + operations
353+
patchedTitle := "Patch UID Preservation Test"
354+
result, err := updateDashboard(ctx, UpdateDashboardParams{
355+
UID: originalUID,
356+
Operations: []PatchOperation{
357+
{
358+
Op: "replace",
359+
Path: "$.title",
360+
Value: patchedTitle,
361+
},
362+
},
363+
Message: "Testing UID preservation",
364+
})
365+
require.NoError(t, err)
366+
require.NotNil(t, result)
367+
368+
// The response UID must match the original — not a newly generated UUID.
369+
require.NotNil(t, result.UID, "response UID should not be nil")
370+
assert.Equal(t, originalUID, *result.UID,
371+
"patch response UID should match the original dashboard UID, not a new one")
372+
373+
// Fetch the dashboard by the original UID and verify:
374+
// 1. The title was actually changed (patch applied to the right dashboard)
375+
// 2. The numeric ID is unchanged (same dashboard, not a clone)
376+
updatedDashboard, err := getDashboardByUID(ctx, GetDashboardByUIDParams{UID: originalUID})
377+
require.NoError(t, err)
378+
379+
updatedMap, ok := updatedDashboard.Dashboard.(map[string]interface{})
380+
require.True(t, ok)
381+
assert.Equal(t, patchedTitle, updatedMap["title"],
382+
"title should be updated on the original dashboard")
383+
assert.Equal(t, originalID, updatedMap["id"],
384+
"numeric ID should be unchanged — dashboard should be updated in place, not cloned")
385+
386+
// Restore the original title so subsequent tests can find the dashboard
387+
_, err = updateDashboard(ctx, UpdateDashboardParams{
388+
UID: originalUID,
389+
Operations: []PatchOperation{
390+
{Op: "replace", Path: "$.title", Value: newTestDashboardName},
391+
},
392+
})
393+
require.NoError(t, err)
394+
})
395+
338396
t.Run("update dashboard - patch add description", func(t *testing.T) {
339397
ctx := newTestContext()
340398

0 commit comments

Comments
 (0)