From b92bd5451f755f5cc6ac78d5869f3e841622e526 Mon Sep 17 00:00:00 2001 From: ConnorN Date: Tue, 16 Dec 2025 15:06:43 -0500 Subject: [PATCH] feat: dynamic tile creation --- src/components/panels/MosaicDashboard.tsx | 243 ++++++++++++++++++++-- 1 file changed, 225 insertions(+), 18 deletions(-) diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx index fea6482..be73a84 100644 --- a/src/components/panels/MosaicDashboard.tsx +++ b/src/components/panels/MosaicDashboard.tsx @@ -1,11 +1,12 @@ 'use client'; -import React, { useState, ReactElement } from 'react'; +import React, { useState, ReactElement, useContext, useRef, useEffect, memo } from 'react'; import { Mosaic, MosaicWindow, MosaicNode, MosaicPath, + MosaicContext, } from 'react-mosaic-component'; import 'react-mosaic-component/react-mosaic-component.css'; import MapView from './MapView'; @@ -17,10 +18,142 @@ import GasSensor from './GasSensor'; import NetworkHealthTelemetryPanel from './NetworkHealthTelemetryPanel'; import VideoControls from './VideoControls'; -type MosaicKey = 'mapView' | 'rosMonitor' | 'waypointList' | 'videoControls' | 'gasSensor' | 'orientationDisplay' | 'goalSetter' | 'networkHealthMonitor'; +type MosaicKey = + | 'mapView' + | 'rosMonitor' + | 'waypointList' + | 'videoControls' + | 'gasSensor' + | 'orientationDisplay' + | 'goalSetter' + | 'networkHealthMonitor'; + +const TILE_DISPLAY_NAMES: Record = { + mapView: 'Map View', + rosMonitor: 'System Telemetry', + waypointList: 'Waypoint List', + videoControls: 'Video Stream', + gasSensor: 'Science', + orientationDisplay: 'Rover Orientation', + goalSetter: 'Nav2', + networkHealthMonitor: 'Connection Health', +}; + +const ALL_TILES: MosaicKey[] = [ + 'mapView', + 'rosMonitor', + 'networkHealthMonitor', + 'orientationDisplay', + 'videoControls', + 'waypointList', + 'gasSensor', + 'goalSetter', +]; + +type PendingAdd = { + pathKey: string; + path: MosaicPath; + direction: 'row' | 'column'; +} | null; + + +const Controls = memo<{ + id: MosaicKey; + path: MosaicPath; + pendingAdd: PendingAdd; + setPendingAdd: (value: PendingAdd) => void; +}>(({ id, path, pendingAdd, setPendingAdd }) => { + const { mosaicActions } = useContext(MosaicContext); + const pathKey = JSON.stringify(path); + const showDropdown = pendingAdd?.pathKey === pathKey; + const dropdownRef = useRef(null); + + const splitAndAdd = (direction: 'row' | 'column', newTile: MosaicKey) => { + const splitNode: MosaicNode = { + direction, + first: id, + second: newTile, + splitPercentage: 60, + }; + + mosaicActions.replaceWith(path, splitNode); + setPendingAdd(null); + }; + + // Close dropdown when clicking outside + useEffect(() => { + if (!showDropdown) return; + + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setPendingAdd(null); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showDropdown, setPendingAdd]); + + return ( +
+ + + + + {showDropdown ? ( + + ) : null} +
+ ); +}); + +Controls.displayName = 'Controls'; const MosaicDashboard: React.FC = () => { - // TODO: paramaterize layout for custom layout configs + // TODO: parameterize layout for custom layout configs const [mosaicLayout, setMosaicLayout] = useState | null>({ direction: 'row', first: { @@ -56,61 +189,110 @@ const MosaicDashboard: React.FC = () => { splitPercentage: 60, }); + const [pendingAdd, setPendingAdd] = useState(null); + const renderTile = (id: MosaicKey, path: MosaicPath): ReactElement => { switch (id) { case 'mapView': return ( - title="Map View" path={path}> + + title="Map View" + path={path} + additionalControls={} + >
- +
); + case 'waypointList': return ( - title="Waypoint List" path={path}> + + title="Waypoint List" + path={path} + additionalControls={} + > ); + case 'videoControls': return ( - title="Video Stream" path={path}> + + title="Video Stream" + path={path} + additionalControls={} + > ); + case 'rosMonitor': return ( - title="System Telemetry" path={path}> + + title="System Telemetry" + path={path} + additionalControls={} + > ); + case 'networkHealthMonitor': return ( - title="Connection Health" path={path}> + + title="Connection Health" + path={path} + additionalControls={} + > ); + case 'orientationDisplay': return ( - title="Rover Orientation" path={path}> + + title="Rover Orientation" + path={path} + additionalControls={} + > ); + case 'gasSensor': - return ( - title="Science" path={path}> - - - ); - + return ( + + title="Science" + path={path} + additionalControls={} + > + + + ); + case 'goalSetter': return ( - title="Nav2" path={path}> + + title="Nav2" + path={path} + additionalControls={} + > ); + default: - return
Unknown tile
; + return ( + + title="Unknown tile" + path={path} + additionalControls={} + > +
Unknown tile
+ + ); } }; @@ -141,6 +323,31 @@ const MosaicDashboard: React.FC = () => { background-color: #1e1e1e; color: #f1f1f1; } + .tile-btn { + background: transparent; + border: 1px solid #444; + color: #2d2d2d; + border-radius: 6px; + padding: 2px 6px; + cursor: pointer; + line-height: 1; + } + .tile-btn:hover { + border-color: #666; + } + .tile-select { + background: #2d2d2d; + color: #f1f1f1; + border: 1px solid #444; + border-radius: 6px; + padding: 2px 6px; + } + .mosaic-window-toolbar .expand-button { + display: none !important; + } + .mosaic-window-toolbar .bp5-button.bp5-icon-more .control-text { + display: none; + } `} );