From 8449a2e4fbf9e6b1f1be7bea990157082ddce538 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 16 Oct 2025 17:32:42 +0200 Subject: [PATCH] Fix v16 grid position calculation for fractional spans (#112428) Fix v16 grid position calculation for fractional spans Match frontend span-to-width conversion logic by flooring span first, then multiplying by width factor, instead of multiplying first then flooring. This fixes dashboard layout inconsistencies where panels with fractional spans (e.g. 5.929860088365242) would have different widths between backend and frontend migration paths. - Backend old: Math.floor(5.93 * 2) = Math.floor(11.86) = 11 - Backend new: Math.floor(5.93) * 2 = 5 * 2 = 10 (matches frontend) Includes comprehensive unit test to prevent regression. --- .../pkg/migration/schemaversion/v16.go | 4 +- .../pkg/migration/schemaversion/v16_test.go | 59 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/apps/dashboard/pkg/migration/schemaversion/v16.go b/apps/dashboard/pkg/migration/schemaversion/v16.go index 343f496f6dc..779498e864a 100644 --- a/apps/dashboard/pkg/migration/schemaversion/v16.go +++ b/apps/dashboard/pkg/migration/schemaversion/v16.go @@ -338,7 +338,9 @@ func calculatePanelDimensionsFromSpan(span float64, panel map[string]interface{} } } - panelWidth := int(math.Floor(span * widthFactor)) + // Match frontend logic: Math.floor(panel.span) * widthFactor (line 914 in DashboardMigrator.ts) + // Frontend floors the span FIRST, then multiplies by widthFactor + panelWidth := int(math.Floor(span)) * int(widthFactor) panelHeight := defaultHeight if panelHeightValue, hasHeight := panel["height"]; hasHeight { diff --git a/apps/dashboard/pkg/migration/schemaversion/v16_test.go b/apps/dashboard/pkg/migration/schemaversion/v16_test.go index 007e912fc85..57d758c81cb 100644 --- a/apps/dashboard/pkg/migration/schemaversion/v16_test.go +++ b/apps/dashboard/pkg/migration/schemaversion/v16_test.go @@ -1649,6 +1649,65 @@ func TestV16(t *testing.T) { }, }, }, + { + name: "should correctly calculate panel width from fractional spans", + input: map[string]interface{}{ + "schemaVersion": 15, + "rows": []interface{}{ + map[string]interface{}{ + "collapse": false, + "height": 55.625 * 38, // Original height from oldest-historical-1913-dashboard-nobreak.json + "panels": []interface{}{ + map[string]interface{}{ + "id": 3, + "type": "text", + "title": "Nobreak APC Modulo - X", + "span": 6.070139911634757, // Fractional span from real dashboard + }, + map[string]interface{}{ + "id": 5, + "type": "text", + "title": "Nobreak APC Modulo - Y", + "span": 5.929860088365242, // Fractional span from real dashboard + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 16, + "panels": []interface{}{ + map[string]interface{}{ + "id": 3, + "type": "text", + "title": "Nobreak APC Modulo - X", + "gridPos": map[string]interface{}{ + "x": 0, + "y": 0, + // Critical: Frontend logic Math.floor(6.070139911634757) * 2 = 6 * 2 = 12 + // NOT Math.floor(6.070139911634757 * 2) = Math.floor(12.140279823269514) = 12 + // Both give same result here, but test documents the correct order + "w": 12, + "h": 56, // ceil(55.625 * 38 / 38) = 56 + }, + }, + map[string]interface{}{ + "id": 5, + "type": "text", + "title": "Nobreak APC Modulo - Y", + "gridPos": map[string]interface{}{ + "x": 12, + "y": 0, + // Critical: Frontend logic Math.floor(5.929860088365242) * 2 = 5 * 2 = 10 + // NOT Math.floor(5.929860088365242 * 2) = Math.floor(11.859720176730484) = 11 + // This is the actual bug we fixed - old backend would give w: 11, new gives w: 10 + "w": 10, + "h": 56, + }, + }, + }, + }, + }, } runMigrationTests(t, tests, schemaversion.V16)