Skip to content

Commit

Permalink
ported custom tabs
Browse files Browse the repository at this point in the history
Signed-off-by: Teo Koon Peng <[email protected]>
  • Loading branch information
koonpeng committed Aug 22, 2024
1 parent 932b04b commit cf84011
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 84 deletions.
72 changes: 44 additions & 28 deletions packages/dashboard/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 (
Expand All @@ -68,17 +72,29 @@ export default function App() {
{
name: 'Map',
route: '',
element: <StaticWorkspace initialState={homeWorkspace} />,
element: <Workspace initialWindows={homeWorkspace} />,
},
{
name: 'Robots',
route: 'robots',
element: <StaticWorkspace initialState={robotsWorkspace} />,
element: <Workspace initialWindows={robotsWorkspace} />,
},
{
name: 'Tasks',
route: 'tasks',
element: <StaticWorkspace initialState={tasksWorkspace} />,
element: <Workspace initialWindows={tasksWorkspace} />,
},
{
name: 'Custom',
route: 'custom',
element: (
<LocallyPersistentWorkspace
defaultWindows={[]}
allowDesignMode
appRegistry={appRegistry}
storageKey="custom-workspace"
/>
),
},
]}
/>
Expand Down
255 changes: 208 additions & 47 deletions packages/dashboard/src/components/workspace.tsx
Original file line number Diff line number Diff line change
@@ -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<WindowLayout, 'i'>;
Component: React.ComponentType<MicroAppProps>;
}

export interface WorkspaceState {
windows: Record<string, WindowState>;
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<Record<string, MicroAppManifest>>({});
const [layout, setLayout] = React.useState(() =>
initialWindows.map<WindowLayout>((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<HTMLElement | null>(null);

return (
<WindowContainer
layout={layout}
onLayoutChange={(newLayout) => {
const newState = { ...state };
newLayout.forEach((l) => (newState.windows[l.i].layout = l));
onStateChange(newState);
}}
designMode={designMode}
>
{Object.entries(state.windows).map(([k, w]) => (
<w.Component
key={k}
onClose={() => {
const newState = { ...state };
delete newState.windows[k];
onStateChange(newState);
React.useEffect(() => {
if (!allowDesignMode) {
return;
}

appController.setExtraAppbarItems(
<IconButton
color="inherit"
sx={{ opacity: designMode ? undefined : theme.palette.action.disabledOpacity }}
onClick={() => setDesignMode((prev) => !prev)}
>
<DesignModeIcon />
</IconButton>,
);
return () => appController.setExtraAppbarItems(null);
}, [allowDesignMode, appController, designMode, theme]);

return (
<>
<WindowContainer
layout={layout}
onLayoutChange={(newLayout) => {
onLayoutChange &&
onLayoutChange(
newLayout.map((l) => ({ layout: l, microApp: windowApps.current[l.i] })),
);
setLayout(newLayout);
}}
/>
))}
</WindowContainer>
);
};
designMode={designMode}
>
{layout.map((l) => {
const microApp: MicroAppManifest | undefined = windowApps.current[l.i];
if (!microApp) {
return null;
}
return (
<microApp.Component
key={l.i}
onClose={() => {
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];
}}
/>
);
})}
</WindowContainer>
{layout.length === 0 && allowDesignMode && (
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Typography variant="h6">
Click <DesignModeIcon /> in the app bar to start customizing the layout
</Typography>
</Box>
)}
{designMode && (
<>
<Fab
color="primary"
sx={{ position: 'fixed', right: '2vw', bottom: '2vw' }}
onClick={(e) => setAddMenuAnchor(e.currentTarget)}
>
<AddIcon />
</Fab>
<Menu
anchorEl={addMenuAnchor}
anchorOrigin={{ vertical: 'center', horizontal: 'center' }}
transformOrigin={{ vertical: 'bottom', horizontal: 'right' }}
open={!!addMenuAnchor}
onClose={() => setAddMenuAnchor(null)}
>
{appRegistry.map((manifest) => (
<MenuItem
key={manifest.appId}
onClick={() => {
const newKey = `window-${windowId.current}`;
windowApps.current[newKey] = manifest;
++windowId.current;
const newLayout = [...layout, { i: newKey, x: 0, y: 0, w: 2, h: 2 }];
onLayoutChange &&
onLayoutChange(
newLayout.map((l) => ({ layout: l, microApp: windowApps.current[l.i] })),
);
setLayout(newLayout);
setAddMenuAnchor(null);
}}
>
{manifest.displayName}
</MenuItem>
))}
</Menu>
</>
)}
</>
);
},
);

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 <Workspace state={wsState} onStateChange={setWsState} />;
export interface LocallyPersistentWorkspaceProps
extends Omit<WorkspaceProps, 'initialWindows' | 'onLayoutChange'> {
/**
* 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<InitialWindow[]>(() => {
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 (
<Workspace
initialWindows={initialWindows}
appRegistry={appRegistry}
onLayoutChange={(newLayout) => {
localStorage.setItem(
storageKey,
JSON.stringify(
newLayout.map<SavedWorkspaceLayout>((l) => ({
layout: l.layout,
appId: l.microApp.appId,
})),
),
);
}}
{...otherProps}
/>
);
};
Loading

0 comments on commit cf84011

Please sign in to comment.