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.
This commit is contained in:
Dominik Prokop
2025-10-16 17:32:42 +02:00
committed by GitHub
parent 3bc5cf4b5d
commit 8449a2e4fb
2 changed files with 62 additions and 1 deletions
@@ -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 {
@@ -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)