Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions openaev-front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"react-dom": "19.2.4",
"react-final-form": "6.5.9",
"react-final-form-arrays": "3.1.4",
"react-grid-layout": "1.5.3",
"react-grid-layout": "2.2.2",
"react-hook-form": "7.68.0",
"react-intl": "8.0.10",
"react-markdown": "10.1.0",
Expand Down Expand Up @@ -90,7 +90,6 @@
"@types/react": "19.2.7",
"@types/react-csv": "1.1.10",
"@types/react-dom": "19.2.3",
"@types/react-grid-layout": "^1",
"@types/react-syntax-highlighter": "15",
"@typescript-eslint/utils": "8.48.0",
"@vitejs/plugin-react": "5.1.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Paper } from '@mui/material';
import { type CSSProperties, type FunctionComponent, useContext, useEffect, useMemo, useState } from 'react';
import RGL, { type Layout, WidthProvider } from 'react-grid-layout';
import ReactGridLayout, { type Layout, type LayoutItem } from 'react-grid-layout';

import { updateCustomDashboardWidgetLayout } from '../../../../actions/custom_dashboards/customdashboardwidget-action';
import { type Widget, type WidgetLayout } from '../../../../utils/api-types';
Expand All @@ -13,10 +13,10 @@ const CustomDashboardReactLayout: FunctionComponent<{
}> = ({ readOnly, style = {} }) => {
const { customDashboard, setCustomDashboard, setGridReady } = useContext(CustomDashboardContext);

// Create ReactGridLayout inside component (same pattern as OpenCTI)
const ReactGridLayout = useMemo(() => WidthProvider(RGL), []);
// Track container width for responsive grid
const [containerWidth, setContainerWidth] = useState(1200);

// Hide grid until WidthProvider has measured container (prevents initial layout animation)
// Hide grid until container has been measured (prevents initial layout animation)
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => {
Expand All @@ -26,6 +26,22 @@ const CustomDashboardReactLayout: FunctionComponent<{
return () => clearTimeout(timeout);
}, [setGridReady]);

// Measure container width on mount and resize
useEffect(() => {
const container = document.querySelector('.dashboard-container');
if (!container) return;

const updateWidth = () => {
setContainerWidth(container.clientWidth);
};

const resizeObserver = new ResizeObserver(updateWidth);
resizeObserver.observe(container);
updateWidth(); // Initial measurement

resizeObserver.disconnect();
}, []);

const [deleting, setDeleting] = useState(false);
const [idToResize, setIdToResize] = useState<string | null>(null);
const handleResize = (updatedWidget: string | null) => setIdToResize(updatedWidget);
Expand All @@ -35,14 +51,14 @@ const CustomDashboardReactLayout: FunctionComponent<{
// Map of widget layouts, refreshed when dashboard is updated (like OpenCTI).
// We use a local map of layouts to avoid a lot of computation when only changing position
// or dimension of widgets.
const [widgetsLayouts, setWidgetsLayouts] = useState<Record<string, Layout>>({});
const [widgetsLayouts, setWidgetsLayouts] = useState<Record<string, LayoutItem>>({});

// Array of all widgets, refreshed when dashboard is updated (same pattern as OpenCTI).
// Sync our local layouts immediately.
const widgetsArray = useMemo(() => {
const widgets = customDashboard?.custom_dashboard_widgets ?? [];
setWidgetsLayouts(
widgets.reduce<Record<string, Layout>>((res, widget) => {
widgets.reduce<Record<string, LayoutItem>>((res, widget) => {
if (widget.widget_layout) {
res[widget.widget_id] = {
i: widget.widget_id,
Expand Down Expand Up @@ -97,14 +113,14 @@ const CustomDashboardReactLayout: FunctionComponent<{
}));
};

const onLayoutChange = (layouts: Layout[]) => {
const onLayoutChange = (layouts: Layout) => {
if (deleting || !customDashboard) {
setDeleting(false);
return;
}

// Build maps for quick lookup
const newLayouts: Record<string, Layout> = {};
const newLayouts: Record<string, LayoutItem> = {};
const layoutMap = new Map<string, WidgetLayout>();
layouts.forEach((layout) => {
newLayouts[layout.i] = layout;
Expand Down Expand Up @@ -149,7 +165,7 @@ const CustomDashboardReactLayout: FunctionComponent<{
};

// Compute layouts directly for data-grid prop to avoid timing issues
const getWidgetLayout = (widget: Widget): Layout => {
const getWidgetLayout = (widget: Widget): LayoutItem => {
// First check local state (for user-modified positions)
if (widgetsLayouts[widget.widget_id]) {
return widgetsLayouts[widget.widget_id];
Expand All @@ -176,6 +192,7 @@ const CustomDashboardReactLayout: FunctionComponent<{

return (
<div
className="dashboard-container"
style={{
...style,
width: '100%',
Expand All @@ -184,15 +201,20 @@ const CustomDashboardReactLayout: FunctionComponent<{
>
<ReactGridLayout
className="layout"
margin={[20, 20]}
containerPadding={[0, 0]}
rowHeight={50}
cols={12}
draggableCancel=".noDrag,.MuiAutocomplete-paper,.MuiModal-backdrop,.MuiPopover-paper,.MuiDialog-paper"
isDraggable={!readOnly}
isResizable={!readOnly}
onLayoutChange={!readOnly ? onLayoutChange : () => true}
onResizeStart={!readOnly ? (_, { i }) => handleResize(i) : undefined}
width={containerWidth}
gridConfig={{
margin: [20, 20],
containerPadding: [0, 0],
rowHeight: 50,
cols: 12,
}}
dragConfig={{
cancel: '.noDrag,.MuiAutocomplete-paper,.MuiModal-backdrop,.MuiPopover-paper,.MuiDialog-paper',
enabled: !readOnly,
}}
resizeConfig={{ enabled: !readOnly }}
onLayoutChange={!readOnly ? onLayoutChange : undefined}
onResizeStart={!readOnly ? (_layout, _oldItem, newItem) => handleResize(newItem?.i ?? null) : undefined}
onResizeStop={!readOnly ? () => handleResize(null) : undefined}
>
{widgetsArray.map((widget) => {
Expand Down
20 changes: 5 additions & 15 deletions openaev-front/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3662,15 +3662,6 @@ __metadata:
languageName: node
linkType: hard

"@types/react-grid-layout@npm:^1":
version: 1.3.5
resolution: "@types/react-grid-layout@npm:1.3.5"
dependencies:
"@types/react": "npm:*"
checksum: 10c0/abd2a1dda9625c753ff2571a10b69740b2fb9ed1d3141755d54d5814cc12a9701c7c5cd78e8797e945486b441303b82543be71043a32d6a988b57a14237f93c6
languageName: node
linkType: hard

"@types/react-syntax-highlighter@npm:15":
version: 15.5.13
resolution: "@types/react-syntax-highlighter@npm:15.5.13"
Expand Down Expand Up @@ -10214,7 +10205,6 @@ __metadata:
"@types/react": "npm:19.2.7"
"@types/react-csv": "npm:1.1.10"
"@types/react-dom": "npm:19.2.3"
"@types/react-grid-layout": "npm:^1"
"@types/react-syntax-highlighter": "npm:15"
"@typescript-eslint/utils": "npm:8.48.0"
"@uiw/react-md-editor": "npm:4.0.11"
Expand Down Expand Up @@ -10271,7 +10261,7 @@ __metadata:
react-dom: "npm:19.2.4"
react-final-form: "npm:6.5.9"
react-final-form-arrays: "npm:3.1.4"
react-grid-layout: "npm:1.5.3"
react-grid-layout: "npm:2.2.2"
react-hook-form: "npm:7.68.0"
react-intl: "npm:8.0.10"
react-markdown: "npm:10.1.0"
Expand Down Expand Up @@ -10891,9 +10881,9 @@ __metadata:
languageName: node
linkType: hard

"react-grid-layout@npm:1.5.3":
version: 1.5.3
resolution: "react-grid-layout@npm:1.5.3"
"react-grid-layout@npm:2.2.2":
version: 2.2.2
resolution: "react-grid-layout@npm:2.2.2"
dependencies:
clsx: "npm:^2.1.1"
fast-equals: "npm:^4.0.3"
Expand All @@ -10904,7 +10894,7 @@ __metadata:
peerDependencies:
react: ">= 16.3.0"
react-dom: ">= 16.3.0"
checksum: 10c0/e7ddacff138e7e83121b7356411a38056fa628dcd77ee8dfa5d5290fda76c2bafdc757578ed5cf4617731696e959ba8570f391b509e597f6563158ed2dd8dbb9
checksum: 10c0/4b38496948136b3d63689c141bcdfaece5e6ce9d228bffad89f751d3b510f6ab1589609982c6fa340a805bda7e0533b212bdff0185b8a52293e031a2e57ebdb9
languageName: node
linkType: hard

Expand Down
Loading