diff --git a/packages/dashboard/src/app.tsx b/packages/dashboard/src/app.tsx
index 3793d9206..bd80c7b9b 100644
--- a/packages/dashboard/src/app.tsx
+++ b/packages/dashboard/src/app.tsx
@@ -5,7 +5,8 @@ import '@fontsource/roboto/700.css';
import './app.css';
import { appConfig } from './app-config';
-import { RmfDashboard, StaticWorkspace, WorkspaceState } from './components';
+import { InitialWindow, LocallyPersistentWorkspace, RmfDashboard, Workspace } from './components';
+import { MicroAppManifest } from './components/micro-app';
import doorsApp from './micro-apps/doors-app';
import liftsApp from './micro-apps/lifts-app';
import createMapApp from './micro-apps/map-app';
@@ -21,34 +22,37 @@ const mapApp = createMapApp({
defaultZoom: appConfig.defaultZoom,
});
-const homeWorkspace: WorkspaceState = {
- windows: {
- map: {
- layout: { x: 0, y: 0, w: 12, h: 6 },
- Component: mapApp.Component,
- },
- },
-};
+const appRegistry: MicroAppManifest[] = [
+ mapApp,
+ doorsApp,
+ liftsApp,
+ robotsApp,
+ robotMutexGroupsApp,
+ tasksApp,
+];
-const robotsWorkspace: WorkspaceState = {
- windows: {
- robots: {
- layout: { x: 0, y: 0, w: 7, h: 4 },
- 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 },
+const homeWorkspace: InitialWindow[] = [
+ {
+ layout: { x: 0, y: 0, w: 12, h: 6 },
+ microApp: mapApp,
},
-};
+];
-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 },
+const robotsWorkspace: InitialWindow[] = [
+ {
+ layout: { x: 0, y: 0, w: 7, h: 4 },
+ microApp: robotsApp,
},
-};
+ { layout: { x: 8, y: 0, w: 5, h: 8 }, microApp: mapApp },
+ { layout: { x: 0, y: 0, w: 7, h: 4 }, microApp: doorsApp },
+ { layout: { x: 0, y: 0, w: 7, h: 4 }, microApp: liftsApp },
+ { layout: { x: 8, y: 0, w: 5, h: 4 }, microApp: robotMutexGroupsApp },
+];
+
+const tasksWorkspace: InitialWindow[] = [
+ { layout: { x: 0, y: 0, w: 7, h: 8 }, microApp: tasksApp },
+ { layout: { x: 8, y: 0, w: 5, h: 8 }, microApp: mapApp },
+];
export default function App() {
return (
@@ -68,17 +72,29 @@ export default function App() {
{
name: 'Map',
route: '',
- element: ,
+ element: ,
},
{
name: 'Robots',
route: 'robots',
- element: ,
+ element: ,
},
{
name: 'Tasks',
route: 'tasks',
- element: ,
+ element: ,
+ },
+ {
+ name: 'Custom',
+ route: 'custom',
+ element: (
+
+ ),
},
]}
/>
diff --git a/packages/dashboard/src/components/workspace.tsx b/packages/dashboard/src/components/workspace.tsx
index 175e18577..72ad2b130 100644
--- a/packages/dashboard/src/components/workspace.tsx
+++ b/packages/dashboard/src/components/workspace.tsx
@@ -1,65 +1,226 @@
+import AddIcon from '@mui/icons-material/Add';
+import DesignModeIcon from '@mui/icons-material/AutoFixNormal';
+import { Box, Fab, IconButton, Menu, MenuItem, Typography, useTheme } from '@mui/material';
import React from 'react';
import { WindowContainer, WindowLayout } from 'react-components';
-import { MicroAppProps } from './micro-app';
+import { useAppController } from '../hooks/use-app-controller';
+import { MicroAppManifest } from './micro-app';
-export interface WindowState {
+export interface InitialWindow {
layout: Omit;
- Component: React.ComponentType;
-}
-
-export interface WorkspaceState {
- windows: Record;
+ microApp: MicroAppManifest;
}
export interface WorkspaceProps {
- state: WorkspaceState;
- onStateChange: (state: WorkspaceState) => void;
- designMode?: boolean;
+ /**
+ * Initial windows in the workspace, this only affects the initial list of windows, changing it
+ * will not affect the current state of the workspace.
+ */
+ initialWindows: InitialWindow[];
+
+ /**
+ * Whether to allow customizing the workspace.
+ */
+ allowDesignMode?: boolean;
+
+ /**
+ * List of micro apps available when customizing the workspace.
+ */
+ appRegistry?: MicroAppManifest[];
+
+ onLayoutChange?: (windows: { layout: WindowLayout; microApp: MicroAppManifest }[]) => void;
}
-export const Workspace = ({ state, onStateChange, designMode = false }: WorkspaceProps) => {
- const layout: WindowLayout[] = Object.entries(state.windows).map(([k, w]) => ({
- i: k,
- ...w.layout,
- }));
+export const Workspace = React.memo(
+ ({
+ initialWindows,
+ allowDesignMode = false,
+ appRegistry = [],
+ onLayoutChange,
+ }: WorkspaceProps) => {
+ const theme = useTheme();
+ const appController = useAppController();
+ const windowId = React.useRef(0);
+ const windowApps = React.useRef>({});
+ const [layout, setLayout] = React.useState(() =>
+ initialWindows.map((w) => {
+ const l = { i: `window-${windowId.current}`, ...w.layout };
+ windowApps.current[l.i] = w.microApp;
+ ++windowId.current;
+ return l;
+ }),
+ );
+ const [designMode, setDesignMode] = React.useState(false);
+ const [addMenuAnchor, setAddMenuAnchor] = React.useState(null);
- return (
- {
- const newState = { ...state };
- 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);
+ React.useEffect(() => {
+ if (!allowDesignMode) {
+ return;
+ }
+
+ appController.setExtraAppbarItems(
+ setDesignMode((prev) => !prev)}
+ >
+
+ ,
+ );
+ return () => appController.setExtraAppbarItems(null);
+ }, [allowDesignMode, appController, designMode, theme]);
+
+ return (
+ <>
+ {
+ onLayoutChange &&
+ onLayoutChange(
+ newLayout.map((l) => ({ layout: l, microApp: windowApps.current[l.i] })),
+ );
+ setLayout(newLayout);
}}
- />
- ))}
-
- );
-};
+ designMode={designMode}
+ >
+ {layout.map((l) => {
+ const microApp: MicroAppManifest | undefined = windowApps.current[l.i];
+ if (!microApp) {
+ return null;
+ }
+ return (
+ {
+ const newLayout = layout.filter((l2) => l2.i !== l2.i);
+ onLayoutChange &&
+ onLayoutChange(
+ newLayout.map((l) => ({ layout: l, microApp: windowApps.current[l.i] })),
+ );
+ setLayout(newLayout);
+ delete windowApps.current[l.i];
+ }}
+ />
+ );
+ })}
+
+ {layout.length === 0 && allowDesignMode && (
+
+
+ Click in the app bar to start customizing the layout
+
+
+ )}
+ {designMode && (
+ <>
+ setAddMenuAnchor(e.currentTarget)}
+ >
+
+
+
+ >
+ )}
+ >
+ );
+ },
+);
-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;
+interface SavedWorkspaceLayout {
+ layout: WindowLayout;
+ appId: string;
}
/**
- * A simple predefined workspace where the layout is fixed.
+ * A workspace that saves the state into `localStorage`.
*/
-export const StaticWorkspace = ({ initialState }: StaticWorkspaceProps) => {
- const [wsState, setWsState] = React.useState(initialState);
- return ;
+export interface LocallyPersistentWorkspaceProps
+ extends Omit {
+ /**
+ * Default list of windows when there is nothing saved yet.
+ */
+ defaultWindows: InitialWindow[];
+ storageKey: string;
+}
+
+export const LocallyPersistentWorkspace = ({
+ defaultWindows,
+ storageKey,
+ appRegistry = [],
+ ...otherProps
+}: LocallyPersistentWorkspaceProps) => {
+ const initialWindows = React.useMemo(() => {
+ const json = localStorage.getItem(storageKey);
+ if (!json) {
+ return defaultWindows;
+ }
+ const saved: SavedWorkspaceLayout[] = JSON.parse(json);
+ const loadedLayout: InitialWindow[] = [];
+ for (const s of saved) {
+ const microApp = appRegistry.find((app) => app.appId === s.appId);
+ if (!microApp) {
+ console.warn(`fail to load micro app [${s.appId}]`);
+ continue;
+ }
+ loadedLayout.push({ layout: s.layout, microApp });
+ }
+ return loadedLayout;
+ }, [defaultWindows, storageKey, appRegistry]);
+
+ return (
+ {
+ localStorage.setItem(
+ storageKey,
+ JSON.stringify(
+ newLayout.map((l) => ({
+ layout: l.layout,
+ appId: l.microApp.appId,
+ })),
+ ),
+ );
+ }}
+ {...otherProps}
+ />
+ );
};
diff --git a/packages/react-components/lib/window/window.tsx b/packages/react-components/lib/window/window.tsx
index 8796beab3..d9643a4a1 100644
--- a/packages/react-components/lib/window/window.tsx
+++ b/packages/react-components/lib/window/window.tsx
@@ -35,7 +35,7 @@ export const Window = styled(
ref={ref}
variant="outlined"
sx={{
- cursor: 'move',
+ cursor: windowManagerState.designMode ? 'move' : undefined,
borderRadius: theme.shape.borderRadius,
'& > :not(.custom-resize-handle)': {
pointerEvents: windowManagerState.designMode ? 'none' : undefined,
@@ -53,13 +53,7 @@ export const Window = styled(
>
{toolbar}
{windowManagerState.designMode && (
- {
- console.log('clicked');
- onClose && onClose();
- }}
- >
+ onClose && onClose()}>
)}
@@ -67,7 +61,7 @@ export const Window = styled(
{childComponents}
- {resizeComponent}
+ {windowManagerState.designMode && resizeComponent}
);
},