mirror of https://github.com/grafana/grafana.git
341 lines
8.5 KiB
Go
341 lines
8.5 KiB
Go
package schemaversion
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
)
|
|
|
|
const (
|
|
gridColumnCount = 24.0
|
|
defaultPanelSpan = 4.0
|
|
defaultRowHeight = 250.0
|
|
gridCellHeight = 30.0
|
|
gridCellVMargin = 8.0
|
|
minPanelHeight = gridCellHeight * 3.0
|
|
panelHeightStep = gridCellHeight + gridCellVMargin
|
|
)
|
|
|
|
// V16 migrates dashboard layout from the old row-based system to the modern grid-based layout.
|
|
// This migration follows the exact logic from DashboardMigrator.ts to ensure consistency between frontend and backend.
|
|
func V16(_ context.Context, dashboard map[string]interface{}) error {
|
|
dashboard["schemaVersion"] = 16
|
|
|
|
upgradeToGridLayout(dashboard)
|
|
|
|
return nil
|
|
}
|
|
|
|
func upgradeToGridLayout(dashboard map[string]interface{}) {
|
|
rowsInterface, ok := dashboard["rows"]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
rows, ok := rowsInterface.([]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Handle empty rows
|
|
if len(rows) == 0 {
|
|
delete(dashboard, "rows")
|
|
return
|
|
}
|
|
|
|
yPos := 0
|
|
widthFactor := gridColumnCount / 12.0
|
|
|
|
// Find max panel ID (lines 1014-1021 in TS)
|
|
maxPanelID := getMaxPanelID(rows)
|
|
nextRowID := maxPanelID + 1
|
|
|
|
// Match frontend: dashboard.panels already exists with top-level panels
|
|
// The frontend's this.dashboard.panels is initialized in the constructor with existing panels
|
|
// Then upgradeToGridLayout adds more panels to it
|
|
|
|
// Initialize panels array - make a copy to avoid modifying the original
|
|
panels := []interface{}{}
|
|
if existingPanels, ok := dashboard["panels"].([]interface{}); ok && len(existingPanels) > 0 {
|
|
// Copy existing panels to preserve order
|
|
panels = append(panels, existingPanels...)
|
|
}
|
|
|
|
// Add special "row" panels if even one row is collapsed, repeated or has visible title (line 1028 in TS)
|
|
showRows := shouldShowRows(rows)
|
|
|
|
// Process each row (line 1030 in TS)
|
|
for _, rowInterface := range rows {
|
|
row, ok := rowInterface.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Skip repeated rows (line 1031-1033 in TS)
|
|
if repeatIteration, hasRepeatIteration := row["repeatIteration"]; hasRepeatIteration && repeatIteration != nil {
|
|
continue
|
|
}
|
|
|
|
height := getRowHeight(row)
|
|
rowGridHeight := getGridHeight(height)
|
|
// Check if collapse property exists and get its value
|
|
collapseValue, hasCollapseProperty := row["collapse"]
|
|
isCollapsed := false
|
|
if hasCollapseProperty {
|
|
if b, ok := collapseValue.(bool); ok {
|
|
isCollapsed = b
|
|
}
|
|
}
|
|
|
|
var rowPanel map[string]interface{}
|
|
|
|
// First pass: assign IDs to panels that don't have them
|
|
panelsInRow, ok := row["panels"].([]interface{})
|
|
if !ok {
|
|
panelsInRow = []interface{}{}
|
|
}
|
|
|
|
for _, panelInterface := range panelsInRow {
|
|
panel, ok := panelInterface.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
// Assign ID if missing
|
|
if _, hasID := panel["id"]; !hasID {
|
|
panel["id"] = nextRowID
|
|
nextRowID++
|
|
}
|
|
}
|
|
|
|
if showRows {
|
|
// add special row panel (lines 1041-1058 in TS)
|
|
rowPanel = map[string]interface{}{
|
|
"id": nextRowID,
|
|
"type": "row",
|
|
"title": GetStringValue(row, "title"),
|
|
"repeat": GetStringValue(row, "repeat"),
|
|
"panels": []interface{}{},
|
|
"gridPos": map[string]interface{}{
|
|
"x": 0,
|
|
"y": yPos,
|
|
"w": int(gridColumnCount),
|
|
"h": rowGridHeight,
|
|
},
|
|
}
|
|
|
|
// Match frontend behavior: rowPanel.collapsed = row.collapse (line 1065 in TS)
|
|
// Only set collapsed property if the original row had a collapse property
|
|
if hasCollapseProperty {
|
|
rowPanel["collapsed"] = isCollapsed
|
|
}
|
|
nextRowID++
|
|
yPos++
|
|
}
|
|
|
|
rowArea := newRowArea(rowGridHeight, gridColumnCount, yPos)
|
|
|
|
// Process all panels in this row (lines 1062-1087 in TS)
|
|
for _, panelInterface := range panelsInRow {
|
|
panel, ok := panelInterface.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Match frontend logic: panel.span = panel.span || DEFAULT_PANEL_SPAN (line 1082 in TS)
|
|
span := GetFloatValue(panel, "span", 0)
|
|
if span == 0 {
|
|
span = defaultPanelSpan
|
|
}
|
|
|
|
panelWidth, panelHeight := calculatePanelDimensionsFromSpan(span, panel, widthFactor, rowGridHeight)
|
|
|
|
panelPos := rowArea.getPanelPosition(panelHeight, panelWidth)
|
|
yPos = rowArea.yPos
|
|
|
|
// Set gridPos (lines 1072-1077 in TS)
|
|
panel["gridPos"] = map[string]interface{}{
|
|
"x": GetIntValue(panelPos, "x", 0),
|
|
"y": yPos + GetIntValue(panelPos, "y", 0),
|
|
"w": panelWidth,
|
|
"h": panelHeight,
|
|
}
|
|
rowArea.addPanel(panel["gridPos"].(map[string]interface{}))
|
|
|
|
// Remove span (line 1080 in TS)
|
|
delete(panel, "span")
|
|
|
|
// Match frontend logic: lines 1101-1105 in TS
|
|
if rowPanel != nil && isCollapsed {
|
|
// Add to collapsed row's nested panels (line 1102)
|
|
if rowPanelPanels, ok := rowPanel["panels"].([]interface{}); ok {
|
|
rowPanel["panels"] = append(rowPanelPanels, panel)
|
|
}
|
|
} else {
|
|
// Add directly to panels array like frontend (line 1104)
|
|
panels = append(panels, panel)
|
|
}
|
|
}
|
|
|
|
// Add row panel after regular panels from this row (lines 1108-1110 in TS)
|
|
if rowPanel != nil {
|
|
panels = append(panels, rowPanel)
|
|
}
|
|
|
|
// Update yPos (lines 1093-1095 in TS)
|
|
if rowPanel == nil || !isCollapsed {
|
|
yPos += rowGridHeight
|
|
}
|
|
}
|
|
|
|
// Update the dashboard
|
|
dashboard["panels"] = panels
|
|
delete(dashboard, "rows")
|
|
}
|
|
|
|
// rowArea represents dashboard row filled by panels
|
|
type rowArea struct {
|
|
area []int
|
|
yPos int
|
|
height int
|
|
}
|
|
|
|
func newRowArea(height int, width int, rowYPos int) *rowArea {
|
|
area := make([]int, width)
|
|
return &rowArea{
|
|
area: area,
|
|
yPos: rowYPos,
|
|
height: height,
|
|
}
|
|
}
|
|
|
|
func (r *rowArea) reset() {
|
|
for i := range r.area {
|
|
r.area[i] = 0
|
|
}
|
|
}
|
|
|
|
func (r *rowArea) addPanel(gridPos map[string]interface{}) {
|
|
x := GetIntValue(gridPos, "x", 0)
|
|
y := GetIntValue(gridPos, "y", 0)
|
|
w := GetIntValue(gridPos, "w", 0)
|
|
h := GetIntValue(gridPos, "h", 0)
|
|
|
|
for i := x; i < x+w && i < len(r.area); i++ {
|
|
newHeight := y + h - r.yPos
|
|
if newHeight > r.area[i] {
|
|
r.area[i] = newHeight
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *rowArea) getPanelPosition(panelHeight int, panelWidth int) map[string]interface{} {
|
|
var startPlace, endPlace int
|
|
found := false
|
|
|
|
// Find available space from right to left
|
|
for i := len(r.area) - 1; i >= 0; i-- {
|
|
if r.height-r.area[i] > 0 {
|
|
if !found {
|
|
endPlace = i
|
|
found = true
|
|
} else {
|
|
if i < len(r.area)-1 && r.area[i] <= r.area[i+1] {
|
|
startPlace = i
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
if found && endPlace-startPlace >= panelWidth-1 {
|
|
// Find max height in the range
|
|
yPos := 0
|
|
for i := startPlace; i <= endPlace && i < len(r.area); i++ {
|
|
if r.area[i] > yPos {
|
|
yPos = r.area[i]
|
|
}
|
|
}
|
|
return map[string]interface{}{
|
|
"x": startPlace,
|
|
"y": yPos,
|
|
}
|
|
}
|
|
|
|
// Wrap to next row
|
|
r.yPos += r.height
|
|
r.reset()
|
|
return r.getPanelPosition(panelHeight, panelWidth)
|
|
}
|
|
|
|
func getMaxPanelID(rows []interface{}) int {
|
|
maxID := 0
|
|
for _, rowInterface := range rows {
|
|
if row, ok := rowInterface.(map[string]interface{}); ok {
|
|
if panels, ok := row["panels"].([]interface{}); ok {
|
|
for _, panelInterface := range panels {
|
|
if panel, ok := panelInterface.(map[string]interface{}); ok {
|
|
if id := GetIntValue(panel, "id", 0); id > maxID {
|
|
maxID = id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return maxID
|
|
}
|
|
|
|
func shouldShowRows(rows []interface{}) bool {
|
|
for _, rowInterface := range rows {
|
|
if row, ok := rowInterface.(map[string]interface{}); ok {
|
|
collapse := GetBoolValue(row, "collapse")
|
|
showTitle := GetBoolValue(row, "showTitle")
|
|
repeat := GetStringValue(row, "repeat")
|
|
|
|
if collapse || showTitle || repeat != "" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getRowHeight(row map[string]interface{}) float64 {
|
|
if height, ok := row["height"]; ok {
|
|
if h, ok := ConvertToFloat(height); ok {
|
|
return h
|
|
}
|
|
}
|
|
return defaultRowHeight
|
|
}
|
|
|
|
func getGridHeight(height float64) int {
|
|
if height < minPanelHeight {
|
|
height = minPanelHeight
|
|
}
|
|
return int(math.Ceil(height / panelHeightStep))
|
|
}
|
|
|
|
func calculatePanelDimensionsFromSpan(span float64, panel map[string]interface{}, widthFactor float64, defaultHeight int) (int, int) {
|
|
// span should already be normalized by caller (line 1082 in DashboardMigrator.ts)
|
|
|
|
if minSpan, hasMinSpan := panel["minSpan"]; hasMinSpan {
|
|
if minSpanFloat, ok := ConvertToFloat(minSpan); ok && minSpanFloat > 0 {
|
|
panel["minSpan"] = int(math.Min(float64(gridColumnCount), (float64(gridColumnCount)/12.0)*minSpanFloat))
|
|
}
|
|
}
|
|
|
|
panelWidth := int(math.Floor(span * widthFactor))
|
|
panelHeight := defaultHeight
|
|
|
|
if panelHeightValue, hasHeight := panel["height"]; hasHeight {
|
|
if h, ok := ConvertToFloat(panelHeightValue); ok {
|
|
panelHeight = getGridHeight(h)
|
|
}
|
|
}
|
|
|
|
return panelWidth, panelHeight
|
|
}
|