From b92bd5451f755f5cc6ac78d5869f3e841622e526 Mon Sep 17 00:00:00 2001 From: ConnorN Date: Tue, 16 Dec 2025 15:06:43 -0500 Subject: [PATCH 1/2] 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; + } `} ); From 1188746b243a9a788d423e1d4540f8e757e5da48 Mon Sep 17 00:00:00 2001 From: ConnorN Date: Wed, 17 Dec 2025 16:20:46 -0500 Subject: [PATCH 2/2] fix: use unique tile ids --- src/components/panels/MosaicDashboard.tsx | 156 +++++++++++++--------- 1 file changed, 90 insertions(+), 66 deletions(-) diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx index be73a84..a1d4be6 100644 --- a/src/components/panels/MosaicDashboard.tsx +++ b/src/components/panels/MosaicDashboard.tsx @@ -18,7 +18,7 @@ import GasSensor from './GasSensor'; import NetworkHealthTelemetryPanel from './NetworkHealthTelemetryPanel'; import VideoControls from './VideoControls'; -type MosaicKey = +type TileType = | 'mapView' | 'rosMonitor' | 'waypointList' @@ -28,7 +28,9 @@ type MosaicKey = | 'goalSetter' | 'networkHealthMonitor'; -const TILE_DISPLAY_NAMES: Record = { +type TileId = `${TileType}:${string}`; + +const TILE_DISPLAY_NAMES: Record = { mapView: 'Map View', rosMonitor: 'System Telemetry', waypointList: 'Waypoint List', @@ -39,7 +41,7 @@ const TILE_DISPLAY_NAMES: Record = { networkHealthMonitor: 'Connection Health', }; -const ALL_TILES: MosaicKey[] = [ +const ALL_TILE_TYPES: TileType[] = [ 'mapView', 'rosMonitor', 'networkHealthMonitor', @@ -50,16 +52,26 @@ const ALL_TILES: MosaicKey[] = [ 'goalSetter', ]; -type PendingAdd = { - pathKey: string; - path: MosaicPath; - direction: 'row' | 'column'; -} | null; +function makeTileId(type: TileType): TileId { + const uid = crypto.randomUUID(); + return `${type}:${uid}`; +} + +function tileTypeOf(id: TileId): TileType { + return id.split(':', 1)[0] as TileType; +} +type PendingAdd = + | { + pathKey: string; + path: MosaicPath; + direction: 'row' | 'column'; + } + | null; -const Controls = memo<{ - id: MosaicKey; - path: MosaicPath; +const Controls = memo<{ + id: TileId; + path: MosaicPath; pendingAdd: PendingAdd; setPendingAdd: (value: PendingAdd) => void; }>(({ id, path, pendingAdd, setPendingAdd }) => { @@ -68,11 +80,14 @@ const Controls = memo<{ const showDropdown = pendingAdd?.pathKey === pathKey; const dropdownRef = useRef(null); - const splitAndAdd = (direction: 'row' | 'column', newTile: MosaicKey) => { - const splitNode: MosaicNode = { + const splitAndAdd = (direction: 'row' | 'column', newType: TileType) => { + + const newId = makeTileId(newType); + + const splitNode: MosaicNode = { direction, first: id, - second: newTile, + second: newId, splitPercentage: 60, }; @@ -80,7 +95,6 @@ const Controls = memo<{ setPendingAdd(null); }; - // Close dropdown when clicking outside useEffect(() => { if (!showDropdown) return; @@ -96,6 +110,8 @@ const Controls = memo<{ }; }, [showDropdown, setPendingAdd]); + const currentType = tileTypeOf(id); + return (