Files
grafana/apps/dashboard/pkg/migration/schemaversion/v16_test.go
Dominik Prokop 8449a2e4fb 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.
2025-10-16 17:32:42 +02:00

1715 lines
37 KiB
Go

package schemaversion_test
import (
"testing"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
)
func TestV16(t *testing.T) {
tests := []migrationTestCase{
{
name: "should handle repeatIteration null",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
map[string]interface{}{
"collapse": false,
"showTitle": true,
"title": "Overview",
"type": "row",
"repeat": nil,
"repeatIteration": nil,
"repeatRowId": nil,
"panels": []interface{}{
map[string]interface{}{
"id": 2,
"type": "stat",
"span": 12,
"title": "Customer Stats",
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
// The stat panel should be processed and added
map[string]interface{}{
"id": 2,
"type": "stat",
"title": "Customer Stats",
"gridPos": map[string]interface{}{
"x": 0,
"y": 1,
"w": 24,
"h": 7, // default height
},
},
// The row panel should be created because showTitle is true
map[string]interface{}{
"id": 3, // Next ID after max panel ID (2)
"type": "row",
"title": "Overview",
"collapsed": false,
"repeat": "",
"panels": []interface{}{},
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 24,
"h": 7, // default height
},
},
},
},
},
{
name: "should create proper grid",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
// createRow({ collapse: false, height: 8 }, [[6], [6]])
map[string]interface{}{
"collapse": false,
"height": 304, // 8 * 38 (PANEL_HEIGHT_STEP)
"panels": []interface{}{
map[string]interface{}{
"span": 6,
"id": 1,
},
map[string]interface{}{
"span": 6,
"id": 2,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 12,
"h": 8,
},
},
map[string]interface{}{
"id": 2,
"gridPos": map[string]interface{}{
"x": 12,
"y": 0,
"w": 12,
"h": 8,
},
},
},
},
},
{
name: "should add special row panel if row is collapsed",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
// createRow({ collapse: true, height: 8 }, [[6], [6]])
map[string]interface{}{
"collapse": true,
"height": 304, // 8 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 6,
"id": 1,
},
map[string]interface{}{
"span": 6,
"id": 2,
},
},
},
// createRow({ height: 8 }, [[12]])
map[string]interface{}{
"height": 304, // 8 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 12,
"id": 3,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 4, // Next ID after max panel ID (3)
"type": "row",
"title": "",
"collapsed": true,
"repeat": "",
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 1,
"w": 12,
"h": 8,
},
},
map[string]interface{}{
"id": 2,
"gridPos": map[string]interface{}{
"x": 12,
"y": 1,
"w": 12,
"h": 8,
},
},
},
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 24,
"h": 8,
},
},
map[string]interface{}{
"id": 3,
"gridPos": map[string]interface{}{
"x": 0,
"y": 2,
"w": 24,
"h": 8,
},
},
map[string]interface{}{
"id": 5, // Next ID after row panel (4)
"type": "row",
"title": "",
"repeat": "",
"panels": []interface{}{},
"gridPos": map[string]interface{}{
"x": 0,
"y": 1,
"w": 24,
"h": 8,
},
},
},
},
},
{
name: "should add special row panel if row has visible title",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
// createRow({ showTitle: true, title: 'Row', height: 8 }, [[6], [6]])
map[string]interface{}{
"showTitle": true,
"title": "Row",
"height": 304, // 8 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 6,
"id": 1,
},
map[string]interface{}{
"span": 6,
"id": 2,
},
},
},
// createRow({ height: 8 }, [[12]])
map[string]interface{}{
"height": 304, // 8 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 12,
"id": 3,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 1,
"w": 12,
"h": 8,
},
},
map[string]interface{}{
"id": 2,
"gridPos": map[string]interface{}{
"x": 12,
"y": 1,
"w": 12,
"h": 8,
},
},
map[string]interface{}{
"id": 4, // Next ID after max panel ID (3)
"type": "row",
"title": "Row",
"repeat": "",
"panels": []interface{}{},
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 24,
"h": 8,
},
},
map[string]interface{}{
"id": 3,
"gridPos": map[string]interface{}{
"x": 0,
"y": 10,
"w": 24,
"h": 8,
},
},
map[string]interface{}{
"id": 5, // Next ID after row panel (4)
"type": "row",
"title": "",
"repeat": "",
"panels": []interface{}{},
"gridPos": map[string]interface{}{
"x": 0,
"y": 9,
"w": 24,
"h": 8,
},
},
},
},
},
{
name: "should not add row panel if row has not visible title or not collapsed",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
map[string]interface{}{
"collapse": true,
"height": 304, // 8 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 12,
"id": 1,
},
},
},
map[string]interface{}{
"height": 304, // 8 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 12,
"id": 2,
},
},
},
map[string]interface{}{
"height": 304, // 8 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 12,
"id": 3,
},
map[string]interface{}{
"span": 6,
"id": 4,
},
map[string]interface{}{
"span": 6,
"id": 5,
},
},
},
map[string]interface{}{
"collapse": true,
"height": 304, // 8 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 12,
"id": 6,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
// First row: collapsed row (panels go in collapsed row's panels array, row panel added after)
map[string]interface{}{
"id": 7,
"type": "row",
"title": "",
"collapsed": true,
"repeat": "",
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 1,
"w": 24,
"h": 8,
},
},
},
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 24,
"h": 8,
},
},
// Second row: normal row (regular panel first, then row panel)
map[string]interface{}{
"id": 2,
"gridPos": map[string]interface{}{
"x": 0,
"y": 2,
"w": 24,
"h": 8,
},
},
map[string]interface{}{
"id": 8,
"type": "row",
"title": "",
"repeat": "",
"panels": []interface{}{},
"gridPos": map[string]interface{}{
"x": 0,
"y": 1,
"w": 24,
"h": 8,
},
},
// Third row: normal row (regular panels first, then row panel)
map[string]interface{}{
"id": 3,
"gridPos": map[string]interface{}{
"x": 0,
"y": 11,
"w": 24,
"h": 8,
},
},
map[string]interface{}{
"id": 4,
"gridPos": map[string]interface{}{
"x": 0,
"y": 19,
"w": 12,
"h": 8,
},
},
map[string]interface{}{
"id": 5,
"gridPos": map[string]interface{}{
"x": 12,
"y": 19,
"w": 12,
"h": 8,
},
},
map[string]interface{}{
"id": 9,
"type": "row",
"title": "",
"repeat": "",
"panels": []interface{}{},
"gridPos": map[string]interface{}{
"x": 0,
"y": 10,
"w": 24,
"h": 8,
},
},
// Fourth row: collapsed row (panels go in collapsed row's panels array, row panel added after)
map[string]interface{}{
"id": 10,
"type": "row",
"title": "",
"collapsed": true,
"repeat": "",
"panels": []interface{}{
map[string]interface{}{
"id": 6,
"gridPos": map[string]interface{}{
"x": 0,
"y": 28,
"w": 24,
"h": 8,
},
},
},
"gridPos": map[string]interface{}{
"x": 0,
"y": 27,
"w": 24,
"h": 8,
},
},
},
},
},
{
name: "should add all rows if even one collapsed or titled row is present",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
// createRow({ collapse: true, height: 8 }, [[6], [6]])
map[string]interface{}{
"collapse": true,
"height": 304, // 8 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 6,
"id": 1,
},
map[string]interface{}{
"span": 6,
"id": 2,
},
},
},
// createRow({ height: 8 }, [[12]])
map[string]interface{}{
"height": 304, // 8 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 12,
"id": 3,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 4, // Next ID after max panel ID (3)
"type": "row",
"title": "",
"collapsed": true,
"repeat": "",
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 1,
"w": 12,
"h": 8,
},
},
map[string]interface{}{
"id": 2,
"gridPos": map[string]interface{}{
"x": 12,
"y": 1,
"w": 12,
"h": 8,
},
},
},
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 24,
"h": 8,
},
},
map[string]interface{}{
"id": 3,
"gridPos": map[string]interface{}{
"x": 0,
"y": 2,
"w": 24,
"h": 8,
},
},
map[string]interface{}{
"id": 5, // Next ID after row panel (4)
"type": "row",
"title": "",
"repeat": "",
"panels": []interface{}{},
"gridPos": map[string]interface{}{
"x": 0,
"y": 1,
"w": 24,
"h": 8,
},
},
},
},
},
{
name: "should properly place panels with fixed height",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
// createRow({ height: 6 }, [[6], [6, 3], [6, 3]])
map[string]interface{}{
"height": 228, // 6 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 6,
"id": 1,
},
map[string]interface{}{
"span": 6,
"height": 114, // 3 * 38
"id": 2,
},
map[string]interface{}{
"span": 6,
"height": 114, // 3 * 38
"id": 3,
},
},
},
// createRow({ height: 6 }, [[4], [4], [4, 3], [4, 3]])
map[string]interface{}{
"height": 228, // 6 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 4,
"id": 4,
},
map[string]interface{}{
"span": 4,
"id": 5,
},
map[string]interface{}{
"span": 4,
"height": 114, // 3 * 38
"id": 6,
},
map[string]interface{}{
"span": 4,
"height": 114, // 3 * 38
"id": 7,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 12,
"h": 6,
},
},
map[string]interface{}{
"id": 2,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 12,
"y": 0,
"w": 12,
"h": 3,
},
},
map[string]interface{}{
"id": 3,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 12,
"y": 3,
"w": 12,
"h": 3,
},
},
map[string]interface{}{
"id": 4,
"gridPos": map[string]interface{}{
"x": 0,
"y": 6,
"w": 8,
"h": 6,
},
},
map[string]interface{}{
"id": 5,
"gridPos": map[string]interface{}{
"x": 8,
"y": 6,
"w": 8,
"h": 6,
},
},
map[string]interface{}{
"id": 6,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 16,
"y": 6,
"w": 8,
"h": 3,
},
},
map[string]interface{}{
"id": 7,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 16,
"y": 9,
"w": 8,
"h": 3,
},
},
},
},
},
{
name: "should place panel to the right side of panel having bigger height",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
// createRow({ height: 6 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3]])
map[string]interface{}{
"height": 228, // 6 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 4,
"id": 1,
},
map[string]interface{}{
"span": 2,
"height": 114, // 3 * 38
"id": 2,
},
map[string]interface{}{
"span": 4,
"height": 228, // 6 * 38
"id": 3,
},
map[string]interface{}{
"span": 2,
"height": 114, // 3 * 38
"id": 4,
},
map[string]interface{}{
"span": 2,
"height": 114, // 3 * 38
"id": 5,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 8,
"h": 6,
},
},
map[string]interface{}{
"id": 2,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 8,
"y": 0,
"w": 4,
"h": 3,
},
},
map[string]interface{}{
"id": 3,
"height": 228, // 6 * 38
"gridPos": map[string]interface{}{
"x": 12,
"y": 0,
"w": 8,
"h": 6,
},
},
map[string]interface{}{
"id": 4,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 20,
"y": 0,
"w": 4,
"h": 3,
},
},
map[string]interface{}{
"id": 5,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 20,
"y": 3,
"w": 4,
"h": 3,
},
},
},
},
},
{
name: "should fill current row if it possible",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
// createRow({ height: 9 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])
map[string]interface{}{
"height": 342, // 9 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 4,
"id": 1,
},
map[string]interface{}{
"span": 2,
"height": 114, // 3 * 38
"id": 2,
},
map[string]interface{}{
"span": 4,
"height": 228, // 6 * 38
"id": 3,
},
map[string]interface{}{
"span": 2,
"height": 114, // 3 * 38
"id": 4,
},
map[string]interface{}{
"span": 2,
"height": 114, // 3 * 38
"id": 5,
},
map[string]interface{}{
"span": 8,
"height": 114, // 3 * 38
"id": 6,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 8,
"h": 9,
},
},
map[string]interface{}{
"id": 2,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 8,
"y": 0,
"w": 4,
"h": 3,
},
},
map[string]interface{}{
"id": 3,
"height": 228, // 6 * 38
"gridPos": map[string]interface{}{
"x": 12,
"y": 0,
"w": 8,
"h": 6,
},
},
map[string]interface{}{
"id": 4,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 20,
"y": 0,
"w": 4,
"h": 3,
},
},
map[string]interface{}{
"id": 5,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 20,
"y": 3,
"w": 4,
"h": 3,
},
},
map[string]interface{}{
"id": 6,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 8,
"y": 6,
"w": 16,
"h": 3,
},
},
},
},
},
{
name: "should fill current row if it possible (2)",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
// createRow({ height: 8 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])
map[string]interface{}{
"height": 304, // 8 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 4,
"id": 1,
},
map[string]interface{}{
"span": 2,
"height": 114, // 3 * 38
"id": 2,
},
map[string]interface{}{
"span": 4,
"height": 228, // 6 * 38
"id": 3,
},
map[string]interface{}{
"span": 2,
"height": 114, // 3 * 38
"id": 4,
},
map[string]interface{}{
"span": 2,
"height": 114, // 3 * 38
"id": 5,
},
map[string]interface{}{
"span": 8,
"height": 114, // 3 * 38
"id": 6,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 8,
"h": 8,
},
},
map[string]interface{}{
"id": 2,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 8,
"y": 0,
"w": 4,
"h": 3,
},
},
map[string]interface{}{
"id": 3,
"height": 228, // 6 * 38
"gridPos": map[string]interface{}{
"x": 12,
"y": 0,
"w": 8,
"h": 6,
},
},
map[string]interface{}{
"id": 4,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 20,
"y": 0,
"w": 4,
"h": 3,
},
},
map[string]interface{}{
"id": 5,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 20,
"y": 3,
"w": 4,
"h": 3,
},
},
map[string]interface{}{
"id": 6,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 8,
"y": 6,
"w": 16,
"h": 3,
},
},
},
},
},
{
name: "should fill current row if panel height more than row height",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
// createRow({ height: 6 }, [[4], [2, 3], [4, 8], [2, 3], [2, 3]])
map[string]interface{}{
"height": 228, // 6 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 4,
"id": 1,
},
map[string]interface{}{
"span": 2,
"height": 114, // 3 * 38
"id": 2,
},
map[string]interface{}{
"span": 4,
"height": 304, // 8 * 38
"id": 3,
},
map[string]interface{}{
"span": 2,
"height": 114, // 3 * 38
"id": 4,
},
map[string]interface{}{
"span": 2,
"height": 114, // 3 * 38
"id": 5,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 8,
"h": 6,
},
},
map[string]interface{}{
"id": 2,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 8,
"y": 0,
"w": 4,
"h": 3,
},
},
map[string]interface{}{
"id": 3,
"height": 304, // 8 * 38
"gridPos": map[string]interface{}{
"x": 12,
"y": 0,
"w": 8,
"h": 8,
},
},
map[string]interface{}{
"id": 4,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 20,
"y": 0,
"w": 4,
"h": 3,
},
},
map[string]interface{}{
"id": 5,
"height": 114, // 3 * 38
"gridPos": map[string]interface{}{
"x": 20,
"y": 3,
"w": 4,
"h": 3,
},
},
},
},
},
{
name: "should wrap panels to multiple rows",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
// createRow({ height: 6 }, [[6], [6], [12], [6], [3], [3]])
map[string]interface{}{
"height": 228, // 6 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 6,
"id": 1,
},
map[string]interface{}{
"span": 6,
"id": 2,
},
map[string]interface{}{
"span": 12,
"id": 3,
},
map[string]interface{}{
"span": 6,
"id": 4,
},
map[string]interface{}{
"span": 3,
"id": 5,
},
map[string]interface{}{
"span": 3,
"id": 6,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 12,
"h": 6,
},
},
map[string]interface{}{
"id": 2,
"gridPos": map[string]interface{}{
"x": 12,
"y": 0,
"w": 12,
"h": 6,
},
},
map[string]interface{}{
"id": 3,
"gridPos": map[string]interface{}{
"x": 0,
"y": 6,
"w": 24,
"h": 6,
},
},
map[string]interface{}{
"id": 4,
"gridPos": map[string]interface{}{
"x": 0,
"y": 12,
"w": 12,
"h": 6,
},
},
map[string]interface{}{
"id": 5,
"gridPos": map[string]interface{}{
"x": 12,
"y": 12,
"w": 6,
"h": 6,
},
},
map[string]interface{}{
"id": 6,
"gridPos": map[string]interface{}{
"x": 18,
"y": 12,
"w": 6,
"h": 6,
},
},
},
},
},
{
name: "should add repeated row if repeat set",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
map[string]interface{}{
"showTitle": true,
"title": "Row",
"height": 304, // 8 * 38
"repeat": "server",
"panels": []interface{}{
map[string]interface{}{
"span": 6,
"id": 1,
},
},
},
map[string]interface{}{
"height": 304, // 8 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 12,
"id": 2,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
// Panel from first row
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 1,
"w": 12,
"h": 8,
},
},
// Repeated row panel (comes after its panels)
map[string]interface{}{
"id": 3,
"type": "row",
"title": "Row",
"repeat": "server",
"panels": []interface{}{},
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 24,
"h": 8,
},
},
// Panel from second row
map[string]interface{}{
"id": 2,
"gridPos": map[string]interface{}{
"x": 0,
"y": 10,
"w": 24,
"h": 8,
},
},
// Second row panel (comes after its panels)
map[string]interface{}{
"id": 4,
"type": "row",
"title": "",
"repeat": "",
"panels": []interface{}{},
"gridPos": map[string]interface{}{
"x": 0,
"y": 9,
"w": 24,
"h": 8,
},
},
},
},
},
{
name: "should ignore repeated row",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
map[string]interface{}{
"showTitle": true,
"title": "Row1",
"height": 304, // 8 * 38
"repeat": "server",
"panels": []interface{}{
map[string]interface{}{
"span": 6,
"id": 1,
},
},
},
map[string]interface{}{
"showTitle": true,
"title": "Row2",
"height": 304, // 8 * 38
"repeatIteration": 12313,
"repeatRowId": 1,
"panels": []interface{}{
map[string]interface{}{
"span": 6,
"id": 2,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 1,
"w": 12,
"h": 8,
},
},
map[string]interface{}{
"id": 3, // Next ID after max panel ID (2)
"type": "row",
"title": "Row1",
"repeat": "server",
"panels": []interface{}{},
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 24,
"h": 8,
},
},
},
},
},
{
name: "should assign id",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
map[string]interface{}{
"collapse": true,
"height": 304, // 8 * 38
"panels": []interface{}{
map[string]interface{}{
"span": 6,
"id": 1,
},
map[string]interface{}{
"span": 6,
// no id - should be assigned
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 3, // Next ID after max panel ID (1) and assigned panel ID (2)
"type": "row",
"title": "",
"collapsed": true,
"repeat": "",
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"gridPos": map[string]interface{}{
"x": 0,
"y": 1,
"w": 12,
"h": 8,
},
},
map[string]interface{}{
"id": 2, // Should be assigned the next available ID
"gridPos": map[string]interface{}{
"x": 12,
"y": 1,
"w": 12,
"h": 8,
},
},
},
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 24,
"h": 8,
},
},
},
},
},
{
name: "should preserve existing panels when rows array is empty",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{},
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graph",
"title": "Existing Panel",
"datasource": map[string]interface{}{
"uid": "test-ds",
},
"gridPos": map[string]interface{}{
"h": 8,
"w": 12,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": 2,
"type": "stat",
"title": "Another Panel",
"gridPos": map[string]interface{}{
"h": 8,
"w": 12,
"x": 12,
"y": 0,
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
// panels should be preserved exactly as they were
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graph",
"title": "Existing Panel",
"datasource": map[string]interface{}{
"uid": "test-ds",
},
"gridPos": map[string]interface{}{
"h": 8,
"w": 12,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": 2,
"type": "stat",
"title": "Another Panel",
"gridPos": map[string]interface{}{
"h": 8,
"w": 12,
"x": 12,
"y": 0,
},
},
},
// rows field should be removed
},
},
{
name: "should parse string heights with px suffix during rows to panels migration",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
map[string]interface{}{
"collapse": false,
"height": "700px", // String height with px suffix
"showTitle": true,
"title": "Rollout progress",
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "barchart",
"span": 4,
"title": "Versions running",
"targets": []interface{}{
map[string]interface{}{
"expr": "up",
},
},
},
map[string]interface{}{
"id": 2,
"type": "barchart",
"span": 4,
"title": "Deployment progress",
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
// First panel
map[string]interface{}{
"id": 1,
"type": "barchart",
"title": "Versions running",
"targets": []interface{}{
map[string]interface{}{
"expr": "up",
},
},
"gridPos": map[string]interface{}{
"x": 0,
"y": 1, // After row panel
"w": 8, // 4 span * 2 = 8 width
"h": 19, // 700px parsed correctly: ceil(700/38) = 19
},
},
// Second panel
map[string]interface{}{
"id": 2,
"type": "barchart",
"title": "Deployment progress",
"gridPos": map[string]interface{}{
"x": 8, // Next to first panel
"y": 1,
"w": 8,
"h": 19,
},
},
// Row panel (created because showTitle is true)
map[string]interface{}{
"id": 3, // Next available ID
"type": "row",
"title": "Rollout progress",
"collapsed": false, // Backend always sets this
"repeat": "", // Backend always sets this
"panels": []interface{}{},
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 24,
"h": 19, // Same height as calculated from "700px"
},
},
},
// rows field should be removed
},
},
{
name: "should handle span zero by defaulting to DEFAULT_PANEL_SPAN",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
map[string]interface{}{
"collapse": false,
"showTitle": true, // Need this to create row panel
"title": "Test Row",
"height": 250,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graph",
"span": 0, // This should be defaulted to 4 (DEFAULT_PANEL_SPAN)
},
map[string]interface{}{
"id": 2,
"type": "stat",
"span": 6, // Normal span value
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graph",
"gridPos": map[string]interface{}{
"x": 0,
"y": 1,
"w": 8, // span 0 -> DEFAULT_PANEL_SPAN (4) -> 4 * 2 = 8 width
"h": 7, // default height
},
},
map[string]interface{}{
"id": 2,
"type": "stat",
"gridPos": map[string]interface{}{
"x": 8, // After first panel
"y": 1,
"w": 12, // span 6 -> 6 * 2 = 12 width
"h": 7, // default height
},
},
// Row panel should be created because showTitle is true
map[string]interface{}{
"id": 3,
"type": "row",
"title": "Test Row",
"collapsed": false, // Set because input has "collapse": false
"repeat": "",
"panels": []interface{}{},
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 24,
"h": 7,
},
},
},
},
},
{
name: "should not set collapsed property when input row has no collapse property",
input: map[string]interface{}{
"schemaVersion": 15,
"rows": []interface{}{
map[string]interface{}{
// No "collapse" property in input
"showTitle": true,
"title": "Test Row",
"height": 250,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graph",
"span": 12,
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 16,
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graph",
"gridPos": map[string]interface{}{
"x": 0,
"y": 1,
"w": 24, // span 12 -> 12 * 2 = 24 width
"h": 7, // default height
},
},
// Row panel should be created because showTitle is true
map[string]interface{}{
"id": 2,
"type": "row",
"title": "Test Row",
// No "collapsed" property because input had no "collapse" property
"repeat": "",
"panels": []interface{}{},
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"w": 24,
"h": 7,
},
},
},
},
},
{
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)
}