11import { useMemo } from 'react'
22import { useTranslation } from 'react-i18next'
3- import { Responsive , WidthProvider , type Layout } from 'react-grid-layout/legacy'
4- import 'react-grid-layout/css/styles.css'
5- import 'react-resizable/css/styles.css'
3+ import { cn } from '@/shared/lib/utils'
64import { WidgetCard } from '@/features/dashboard/components/WidgetCard'
75import { WidgetRenderer } from '@/features/dashboard/components/WidgetRenderer'
8- import { GRID_COLS , GRID_MARGIN , GRID_ROW_HEIGHT } from '@/features/dashboard/constants'
96import type {
107 DashboardVisualization ,
118 GridLayoutItem ,
129 Visualization ,
1310} from '@/features/dashboard/types'
1411import type { TimeRange } from '@/shared/components/ui/time-range-picker'
1512
16- const ResponsiveGridLayout = WidthProvider ( Responsive )
17-
18- const BREAKPOINTS = { lg : 1200 , md : 996 , sm : 768 , xs : 480 , xxs : 0 }
19- const COLS = { lg : GRID_COLS , md : GRID_COLS , sm : 6 , xs : 4 , xxs : 2 }
13+ // Column span per width preset. Grid is responsive (1 col mobile, 2 tablet, 3
14+ // desktop), so spans are clamped at each breakpoint. Literal classes so Tailwind
15+ // keeps them.
16+ const WIDTH_CLASS : Record < number , string > = {
17+ 1 : '' ,
18+ 2 : 'md:col-span-2 xl:col-span-2' ,
19+ 3 : 'md:col-span-2 xl:col-span-3' ,
20+ }
21+ const HEIGHT_CLASS : Record < number , string > = {
22+ 1 : 'h-[340px]' ,
23+ 2 : 'h-[560px]' ,
24+ }
2025
2126export function DashboardGrid ( {
2227 items,
2328 layouts,
2429 visualizationsById,
2530 time,
2631 editing,
27- onLayoutChange,
32+ onMove,
33+ onResize,
2834 onRemoveItem,
2935} : {
3036 items : GridLayoutItem [ ]
3137 layouts : DashboardVisualization [ ]
3238 visualizationsById : Map < number , Visualization >
3339 time : TimeRange
3440 editing : boolean
35- onLayoutChange ?: ( next : GridLayoutItem [ ] ) => void
41+ onMove ?: ( id : string , dir : - 1 | 1 ) => void
42+ onResize ?: ( id : string , w : number , h : number ) => void
3643 onRemoveItem ?: ( id : number ) => void
3744} ) {
3845 const { t } = useTranslation ( )
@@ -52,52 +59,42 @@ export function DashboardGrid({
5259 }
5360
5461 return (
55- < ResponsiveGridLayout
56- className = "layout"
57- layouts = { { lg : items , md : items , sm : items , xs : items , xxs : items } }
58- breakpoints = { BREAKPOINTS }
59- cols = { COLS }
60- rowHeight = { GRID_ROW_HEIGHT }
61- margin = { GRID_MARGIN }
62- compactType = "vertical"
63- isDraggable = { editing }
64- isResizable = { editing }
65- draggableHandle = ".widget-drag-handle"
66- onLayoutChange = { ( next : Layout ) => {
67- if ( ! editing || ! onLayoutChange ) return
68- const mapped : GridLayoutItem [ ] = next . map ( ( n ) => ( {
69- i : String ( n . i ) ,
70- x : n . x ,
71- y : n . y ,
72- w : n . w ,
73- h : n . h ,
74- } ) )
75- onLayoutChange ( mapped )
76- } }
77- >
78- { items . map ( ( item ) => {
62+ // Responsive columns — adapts widgets-per-row to the screen. Each widget
63+ // spans 1–3 columns (its width preset) and is short or tall (height preset).
64+ < div className = "grid grid-cols-1 items-start gap-4 md:grid-cols-2 xl:grid-cols-3" >
65+ { items . map ( ( item , idx ) => {
7966 const dv = layoutMap . get ( item . i )
8067 const viz = dv ? visualizationsById . get ( dv . idVisualization ) : undefined
8168 const title = viz ?. name ?? t ( 'dashboards.grid.unknownVisualization' )
82- const layoutId = dv ?. id
69+ const w = item . w || 1
70+ const h = item . h || 1
8371 return (
84- < div key = { item . i } data-grid = { item } >
85- < WidgetCard
86- title = { title }
87- editing = { editing }
88- onRemove = { layoutId ? ( ) => onRemoveItem ?.( layoutId ) : undefined }
89- >
90- { viz ? (
91- < WidgetRenderer visualization = { viz } time = { time } />
92- ) : (
93- < div className = "flex h-full w-full items-center justify-center text-xs text-muted-foreground" >
94- { t ( 'dashboards.grid.missingVisualization' ) }
95- </ div >
96- ) }
97- </ WidgetCard >
72+ < div key = { item . i } className = { cn ( 'min-w-0' , WIDTH_CLASS [ w ] ?? '' ) } >
73+ < div className = { cn ( 'w-full' , HEIGHT_CLASS [ h ] ?? HEIGHT_CLASS [ 1 ] ) } >
74+ < WidgetCard
75+ title = { title }
76+ editing = { editing }
77+ width = { w }
78+ height = { h }
79+ canMoveBack = { idx > 0 }
80+ canMoveForward = { idx < items . length - 1 }
81+ onMoveBack = { ( ) => onMove ?.( item . i , - 1 ) }
82+ onMoveForward = { ( ) => onMove ?.( item . i , 1 ) }
83+ onResize = { ( nw , nh ) => onResize ?.( item . i , nw , nh ) }
84+ onRemove = { dv ? ( ) => onRemoveItem ?.( dv . id ) : undefined }
85+ >
86+ { viz ? (
87+ < WidgetRenderer visualization = { viz } time = { time } />
88+ ) : (
89+ < div className = "flex h-full w-full items-center justify-center text-xs text-muted-foreground" >
90+ { t ( 'dashboards.grid.missingVisualization' ) }
91+ </ div >
92+ ) }
93+ </ WidgetCard >
94+ </ div >
9895 </ div >
9996 )
10097 } ) }
101- </ ResponsiveGridLayout >
98+ </ div >
10299 )
103100}
0 commit comments