diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index d55b5b611..43d109cb6 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -42,7 +42,6 @@ "date-fns": "^2.30.0", "debug": "^4.2.0", "eventemitter3": "^4.0.7", - "jsdom": "^24.1.1", "keycloak-js": "^25.0.2", "react": "^18.2.0", "react-components": "workspace:*", @@ -67,13 +66,13 @@ "@testing-library/dom": "^9.3.4", "@testing-library/react": "^14.2.2", "@testing-library/user-event": "^14.5.2", - "@types/history": "^5.0.0", "@vitejs/plugin-react-swc": "^3.7.0", "@vitest/coverage-v8": "^2.0.4", "api-server": "file:../api-server", "concurrently": "^8.2.2", "eslint": "^8.57.0", "history": "^5.3.0", + "jsdom": "^24.1.1", "storybook": "^8.0.5", "typescript": "~5.5.4", "typescript-json-schema": "^0.64.0", diff --git a/packages/dashboard/src/app.tsx b/packages/dashboard/src/app.tsx index 6f59ada27..3793d9206 100644 --- a/packages/dashboard/src/app.tsx +++ b/packages/dashboard/src/app.tsx @@ -25,7 +25,7 @@ const homeWorkspace: WorkspaceState = { windows: { map: { layout: { x: 0, y: 0, w: 12, h: 6 }, - component: mapApp.Component, + Component: mapApp.Component, }, }, }; @@ -34,19 +34,19 @@ const robotsWorkspace: WorkspaceState = { windows: { robots: { layout: { x: 0, y: 0, w: 7, h: 4 }, - component: robotsApp.Component, + Component: robotsApp.Component, }, - map: { layout: { x: 8, y: 0, w: 5, h: 8 }, component: mapApp.Component }, - doors: { layout: { x: 0, y: 0, w: 7, h: 4 }, component: doorsApp.Component }, - lifts: { layout: { x: 0, y: 0, w: 7, h: 4 }, component: liftsApp.Component }, - mutexGroups: { layout: { x: 8, y: 0, w: 5, h: 4 }, component: robotMutexGroupsApp.Component }, + map: { layout: { x: 8, y: 0, w: 5, h: 8 }, Component: mapApp.Component }, + doors: { layout: { x: 0, y: 0, w: 7, h: 4 }, Component: doorsApp.Component }, + lifts: { layout: { x: 0, y: 0, w: 7, h: 4 }, Component: liftsApp.Component }, + mutexGroups: { layout: { x: 8, y: 0, w: 5, h: 4 }, Component: robotMutexGroupsApp.Component }, }, }; const tasksWorkspace: WorkspaceState = { windows: { - tasks: { layout: { x: 0, y: 0, w: 7, h: 8 }, component: tasksApp.Component }, - map: { layout: { x: 8, y: 0, w: 5, h: 8 }, component: mapApp.Component }, + tasks: { layout: { x: 0, y: 0, w: 7, h: 8 }, Component: tasksApp.Component }, + map: { layout: { x: 8, y: 0, w: 5, h: 8 }, Component: mapApp.Component }, }, }; diff --git a/packages/dashboard/src/components/micro-app.tsx b/packages/dashboard/src/components/micro-app.tsx index 623f92c0f..087cc9097 100644 --- a/packages/dashboard/src/components/micro-app.tsx +++ b/packages/dashboard/src/components/micro-app.tsx @@ -31,16 +31,20 @@ export function createMicroApp

( return { appId, displayName, - Component: React.forwardRef((microAppProps: MicroAppProps, ref) => { - const settings = useSettings(); - return ( - - {/* TODO(koonpeng): Implement fallback */} - - - - - ); - }) as React.ComponentType, + Component: React.forwardRef( + ({ children, ...otherProps }: React.PropsWithChildren, ref) => { + const settings = useSettings(); + return ( + + {/* TODO(koonpeng): Implement fallback */} + + + + {/* this contains the resize handle */} + {children} + + ); + }, + ) as React.ComponentType, }; } diff --git a/packages/dashboard/src/components/workspace.tsx b/packages/dashboard/src/components/workspace.tsx index 0432c7896..175e18577 100644 --- a/packages/dashboard/src/components/workspace.tsx +++ b/packages/dashboard/src/components/workspace.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { WindowContainer, WindowLayout } from 'react-components'; +import { MicroAppProps } from './micro-app'; + export interface WindowState { layout: Omit; - component: React.ComponentType; + Component: React.ComponentType; } export interface WorkspaceState { @@ -13,9 +15,10 @@ export interface WorkspaceState { export interface WorkspaceProps { state: WorkspaceState; onStateChange: (state: WorkspaceState) => void; + designMode?: boolean; } -export const Workspace = ({ state, onStateChange }: WorkspaceProps) => { +export const Workspace = ({ state, onStateChange, designMode = false }: WorkspaceProps) => { const layout: WindowLayout[] = Object.entries(state.windows).map(([k, w]) => ({ i: k, ...w.layout, @@ -29,18 +32,33 @@ export const Workspace = ({ state, onStateChange }: WorkspaceProps) => { newLayout.forEach((l) => (newState.windows[l.i].layout = l)); onStateChange(newState); }} + designMode={designMode} > {Object.entries(state.windows).map(([k, w]) => ( - + { + const newState = { ...state }; + delete newState.windows[k]; + onStateChange(newState); + }} + /> ))} ); }; export interface StaticWorkspaceProps { + /** + * Initial state of the workspace. This is only used on the first render, changes on this + * after the initial render will not do anything. + */ initialState: WorkspaceState; } +/** + * A simple predefined workspace where the layout is fixed. + */ export const StaticWorkspace = ({ initialState }: StaticWorkspaceProps) => { const [wsState, setWsState] = React.useState(initialState); return ; diff --git a/packages/react-components/lib/doors/door-table-datagrid.tsx b/packages/react-components/lib/doors/door-table-datagrid.tsx index 124e71217..d8481cbe7 100644 --- a/packages/react-components/lib/doors/door-table-datagrid.tsx +++ b/packages/react-components/lib/doors/door-table-datagrid.tsx @@ -241,7 +241,6 @@ export function DoorDataGridTable({ doors, onDoorClick }: DoorDataGridTableProps rowsPerPageOptions={[5]} sx={{ fontSize: isScreenHeightLessThan800 ? '0.7rem' : 'inherit', - overflowX: 'scroll', }} autoPageSize={isScreenHeightLessThan800} density={isScreenHeightLessThan800 ? 'compact' : 'standard'} diff --git a/packages/react-components/lib/window/window-container.tsx b/packages/react-components/lib/window/window-container.tsx index 3946305ce..b5a6ea0c1 100644 --- a/packages/react-components/lib/window/window-container.tsx +++ b/packages/react-components/lib/window/window-container.tsx @@ -1,6 +1,7 @@ import 'react-grid-layout/css/styles.css'; import './no-rgl-animations.css'; +import { styled } from '@mui/material'; import React from 'react'; import { default as GridLayout_, Layout as WindowLayout, WidthProvider } from 'react-grid-layout'; @@ -58,11 +59,34 @@ export const WindowContainer: React.FC = ({ preventCollision isResizable={designMode} isDraggable={designMode} - draggableHandle=".rgl-draggable" onLayoutChange={onLayoutChange} + draggableCancel=".custom-resize-handle,.window-toolbar-items" + resizeHandle={} > {children} ); }; + +/** + * A custom resize handle that uses theme properties + */ +const ResizeHandle = styled('span')(({ theme }) => ({ + position: 'absolute', + right: theme.spacing(1), + bottom: theme.spacing(1), + width: 20, + height: 20, + cursor: 'se-resize', + '&::after': { + content: '""', + position: 'absolute', + right: 0, + bottom: 0, + width: '0.5rem', + height: '0.5rem', + borderRight: `2px solid ${theme.palette.grey[500]}`, + borderBottom: `2px solid ${theme.palette.grey[500]}`, + }, +})); diff --git a/packages/react-components/lib/window/window-toolbar.tsx b/packages/react-components/lib/window/window-toolbar.tsx index 128427e6e..3934f4b68 100644 --- a/packages/react-components/lib/window/window-toolbar.tsx +++ b/packages/react-components/lib/window/window-toolbar.tsx @@ -3,15 +3,21 @@ import React from 'react'; export interface WindowToolbarProps extends AppBarProps { title: string; + toolbarItemContainerProps?: React.ComponentProps; } -export const WindowToolbar: React.FC = ({ title, children, ...otherProps }) => { +export const WindowToolbar: React.FC = ({ + title, + toolbarItemContainerProps, + children, + ...otherProps +}) => { return ( {title} - {children} + {children} diff --git a/packages/react-components/lib/window/window.tsx b/packages/react-components/lib/window/window.tsx index 2eb261239..8796beab3 100644 --- a/packages/react-components/lib/window/window.tsx +++ b/packages/react-components/lib/window/window.tsx @@ -1,6 +1,6 @@ import type {} from '@emotion/styled'; import CloseIcon from '@mui/icons-material/Close'; -import { Box, Grid, IconButton, Paper, PaperProps, styled, useTheme } from '@mui/material'; +import { Box, IconButton, Paper, PaperProps, styled, useTheme } from '@mui/material'; import React from 'react'; import { Layout } from 'react-grid-layout'; @@ -22,31 +22,52 @@ export const Window = styled( ) => { const theme = useTheme(); + // The resize marker injected by `react-grid-layout` must be a direct children, but we + // want to wrap the window components in a div so it shows a scrollbar. So we assume that + // the injected resize marker is always the last component and render it separately. + const childrenArr = React.Children.toArray(children); + const childComponents = childrenArr.slice(0, childrenArr.length - 1); + const resizeComponent = childrenArr[childrenArr.length - 1]; + const windowManagerState = React.useContext(WindowManagerStateContext); return ( :not(.custom-resize-handle)': { + pointerEvents: windowManagerState.designMode ? 'none' : undefined, + }, + '& .window-toolbar-items': { + pointerEvents: 'auto', + }, ...sx, }} {...otherProps} > - - - {toolbar} - {windowManagerState.designMode && ( - onClose && onClose()}> - - - )} - - - - {children} + + {toolbar} + {windowManagerState.designMode && ( + { + console.log('clicked'); + onClose && onClose(); + }} + > + + + )} + + + {childComponents} + {resizeComponent} ); }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c63a47276..9568a5a43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,9 +155,6 @@ importers: eventemitter3: specifier: ^4.0.7 version: 4.0.7 - jsdom: - specifier: ^24.1.1 - version: 24.1.1(canvas@2.11.2) keycloak-js: specifier: ^25.0.2 version: 25.0.2 @@ -225,9 +222,6 @@ importers: '@testing-library/user-event': specifier: ^14.5.2 version: 14.5.2(@testing-library/dom@9.3.4) - '@types/history': - specifier: ^5.0.0 - version: 5.0.0 '@vitejs/plugin-react-swc': specifier: ^3.7.0 version: 3.7.0(vite@5.3.5(@types/node@20.14.12)(terser@5.31.6)) @@ -246,6 +240,9 @@ importers: history: specifier: ^5.3.0 version: 5.3.0 + jsdom: + specifier: ^24.1.1 + version: 24.1.1(canvas@2.11.2) storybook: specifier: ^8.0.5 version: 8.2.2(@babel/preset-env@7.24.8(@babel/core@7.24.8)) @@ -2668,10 +2665,6 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/history@5.0.0': - resolution: {integrity: sha512-hy8b7Y1J8OGe6LbAjj3xniQrj3v6lsivCcrmf4TzSgPzLkhIeKgc5IZnT7ReIqmEuodjfO8EYAuoFvIrHi/+jQ==} - deprecated: This is a stub types definition. history provides its own type definitions, so you do not need this installed. - '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} @@ -10857,10 +10850,6 @@ snapshots: dependencies: '@types/unist': 3.0.2 - '@types/history@5.0.0': - dependencies: - history: 5.3.0 - '@types/http-cache-semantics@4.0.4': {} '@types/http-errors@2.0.4': {}