,
- ) => (
-
-
- {children}
-
- ),
- ),
- );
+/**
+ * Creates a micro app from a component. The component must be loaded using dynamic import.
+ * Note that the map should be created in a different module than the component.
+ *
+ * Example:
+ * ```ts
+ * createMicroApp('Map', 'Map', () => import('./map'), config);
+ * ```
+ */
+export function createMicroApp(
+ appId: string,
+ displayName: string,
+ loadComponent: () => Promise<{ default: React.ComponentType
}>,
+ props: (settings: Settings) => React.PropsWithoutRef
& React.Attributes,
+): MicroAppManifest {
+ const LazyComponent = React.lazy(loadComponent);
+ return {
+ appId,
+ displayName,
+ Component: React.forwardRef(
+ ({ children, ...otherProps }: React.PropsWithChildren, ref) => {
+ const settings = useSettings();
+ return (
+
+
+
+
+ {/* this contains the resize handle */}
+ {children}
+
+ );
+ },
+ ) as React.ComponentType,
+ };
}
diff --git a/packages/dashboard/src/components/private-route.test.tsx b/packages/dashboard/src/components/private-route.test.tsx
deleted file mode 100644
index 23706687c..000000000
--- a/packages/dashboard/src/components/private-route.test.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { render } from '@testing-library/react';
-import { createMemoryHistory, MemoryHistory } from 'history';
-import { Router } from 'react-router-dom';
-import { beforeEach, describe, expect, it } from 'vitest';
-
-import { PrivateRoute } from './private-route';
-
-describe('PrivateRoute', () => {
- let history: MemoryHistory;
-
- beforeEach(() => {
- history = createMemoryHistory();
- history.push('/private');
- });
-
- it('renders unauthorizedComponent when unauthenticated', () => {
- const root = render(
-
-
- ,
- );
- expect(() => root.getByText('test')).not.toThrow();
- });
-
- it('renders children when authenticated', () => {
- const root = render(
-
-
- authorized
-
- ,
- );
- expect(() => root.getByText('authorized')).not.toThrow();
- });
-});
diff --git a/packages/dashboard/src/components/private-route.tsx b/packages/dashboard/src/components/private-route.tsx
deleted file mode 100644
index 81cf292aa..000000000
--- a/packages/dashboard/src/components/private-route.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-import { RouteProps } from 'react-router-dom';
-
-export interface PrivateRouteProps {
- user: string | null;
- /**
- * Component to render if `user` is undefined.
- */
- unauthorizedComponent?: React.ReactNode;
-}
-
-export const PrivateRoute = ({
- user,
- unauthorizedComponent = 'Unauthorized',
- children,
-}: React.PropsWithChildren): JSX.Element => {
- return <>{user ? children : unauthorizedComponent}>;
-};
-
-export default PrivateRoute;
diff --git a/packages/dashboard/src/components/react-three-fiber-hack.d.ts b/packages/dashboard/src/components/react-three-fiber-hack.d.ts
new file mode 100644
index 000000000..5887002ee
--- /dev/null
+++ b/packages/dashboard/src/components/react-three-fiber-hack.d.ts
@@ -0,0 +1,10 @@
+import '../../../react-components/lib/react-three-fiber-hack';
+
+// hack to export only the intrinsic elements that we use
+import { ThreeElements } from '@react-three/fiber';
+
+declare global {
+ namespace JSX {
+ interface IntrinsicElements extends Pick {}
+ }
+}
diff --git a/packages/dashboard/src/components/rmf-app.tsx b/packages/dashboard/src/components/rmf-app.tsx
deleted file mode 100644
index 61113a742..000000000
--- a/packages/dashboard/src/components/rmf-app.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-
-import { AppConfigContext, AuthenticatorContext } from '../app-config';
-import { RmfIngress } from '../services/rmf-ingress';
-import { UserProfileProvider } from './user-profile-provider';
-
-export * from '../services/rmf-ingress';
-
-export const RmfAppContext = React.createContext(undefined);
-
-export interface RmfAppProps extends React.PropsWithChildren<{}> {}
-
-export function RmfApp(props: RmfAppProps): JSX.Element {
- const appConfig = React.useContext(AppConfigContext);
- const authenticator = React.useContext(AuthenticatorContext);
- const [rmfIngress, setRmfIngress] = React.useState(undefined);
-
- React.useEffect(() => {
- if (authenticator.user) {
- return setRmfIngress(new RmfIngress(appConfig, authenticator));
- } else {
- authenticator.once('userChanged', () =>
- setRmfIngress(new RmfIngress(appConfig, authenticator)),
- );
- return undefined;
- }
- }, [authenticator, appConfig]);
-
- return (
-
- {props.children}
-
- );
-}
diff --git a/packages/dashboard/src/components/rmf-dashboard.tsx b/packages/dashboard/src/components/rmf-dashboard.tsx
new file mode 100644
index 000000000..feb40bd8a
--- /dev/null
+++ b/packages/dashboard/src/components/rmf-dashboard.tsx
@@ -0,0 +1,342 @@
+import {
+ Alert,
+ AlertProps,
+ Container,
+ CssBaseline,
+ Snackbar,
+ Tab,
+ ThemeProvider,
+ Typography,
+} from '@mui/material';
+import React, { useTransition } from 'react';
+import { getDefaultTaskDefinition, LocalizationProvider } from 'react-components';
+import { matchPath, Navigate, Outlet, Route, Routes, useLocation, useNavigate } from 'react-router';
+import { BrowserRouter } from 'react-router-dom';
+
+import { AppController, AppControllerProvider } from '../hooks/use-app-controller';
+import { AuthenticatorProvider } from '../hooks/use-authenticator';
+import { Resources, ResourcesProvider } from '../hooks/use-resources';
+import { RmfApiProvider } from '../hooks/use-rmf-api';
+import { SettingsProvider } from '../hooks/use-settings';
+import { TaskRegistry, TaskRegistryProvider } from '../hooks/use-task-registry';
+import { UserProfileProvider, useUserProfile } from '../hooks/use-user-profile';
+import { LoginPage } from '../pages';
+import { Authenticator, UserProfile } from '../services/authenticator';
+import { DefaultRmfApi } from '../services/rmf-api';
+import { loadSettings, saveSettings, Settings } from '../services/settings';
+import { AlertManager } from './alert-manager';
+import AppBar, { APP_BAR_HEIGHT } from './appbar';
+import { DeliveryAlertStore } from './delivery-alert-store';
+import { DashboardThemes } from './theme';
+
+const DefaultAlertDuration = 2000;
+
+export interface DashboardHome {}
+
+export interface DashboardTab {
+ name: string;
+ route: string;
+ element: React.ReactNode;
+}
+
+export interface AllowedTask {
+ /**
+ * The task definition to configure.
+ */
+ taskDefinitionId: 'patrol' | 'delivery' | 'compose-clean' | 'custom_compose';
+
+ /**
+ * Configure the display name for the task definition.
+ */
+ displayName?: string;
+
+ /**
+ * The color of the event when rendered on the task scheduler in the form of a CSS color string.
+ */
+ scheduleEventColor?: string;
+}
+
+export interface TaskRegistryInput extends Omit {
+ allowedTasks: AllowedTask[];
+}
+
+export interface RmfDashboardProps {
+ /**
+ * Url of the RMF api server.
+ */
+ apiServerUrl: string;
+
+ /**
+ * Url of the RMF trajectory server.
+ */
+ trajectoryServerUrl: string;
+
+ authenticator: Authenticator;
+
+ /**
+ * Url to be linked for the "help" button.
+ */
+ helpLink: string;
+
+ /**
+ * Url to be linked for the "report issue" button.
+ */
+ reportIssueLink: string;
+
+ themes?: DashboardThemes;
+
+ /**
+ * Set various resources (icons, logo etc) used. Different resource can be used based on the theme, `default` is always required.
+ */
+ resources: Resources;
+
+ /**
+ * List of allowed tasks that can be requested
+ */
+ tasks: TaskRegistryInput;
+
+ /**
+ * List of tabs on the app bar.
+ */
+ tabs: DashboardTab[];
+
+ /**
+ * Prefix where other routes will be based on, defaults to `import.meta.env.BASE_URL`.
+ * Must end with a slash
+ */
+ baseUrl?: string;
+
+ /**
+ * Url to a file to be played when an alert occurs on the dashboard.
+ */
+ alertAudioPath?: string;
+}
+
+export function RmfDashboard(props: RmfDashboardProps) {
+ const {
+ apiServerUrl,
+ trajectoryServerUrl,
+ authenticator,
+ themes,
+ resources,
+ tasks,
+ alertAudioPath,
+ } = props;
+
+ const rmfApi = React.useMemo(
+ () => new DefaultRmfApi(apiServerUrl, trajectoryServerUrl, authenticator),
+ [apiServerUrl, trajectoryServerUrl, authenticator],
+ );
+
+ // FIXME(koonepng): This should be fully definition in tasks resources when the dashboard actually
+ // supports configuring all the fields.
+ const taskRegistry = React.useMemo(
+ () => ({
+ taskDefinitions: tasks.allowedTasks.map((t) => {
+ const defaultTaskDefinition = getDefaultTaskDefinition(t.taskDefinitionId);
+ if (!defaultTaskDefinition) {
+ throw Error(`Invalid tasks configured for dashboard: [${t.taskDefinitionId}]`);
+ }
+ const taskDefinition = { ...defaultTaskDefinition };
+ if (t.displayName !== undefined) {
+ taskDefinition.taskDisplayName = t.displayName;
+ }
+ if (t.scheduleEventColor !== undefined) {
+ taskDefinition.scheduleEventColor = t.scheduleEventColor;
+ }
+ return taskDefinition;
+ }),
+ pickupZones: tasks.pickupZones,
+ cartIds: tasks.cartIds,
+ }),
+ [tasks.allowedTasks, tasks.pickupZones, tasks.cartIds],
+ );
+
+ const [userProfile, setUserProfile] = React.useState(null);
+ React.useEffect(() => {
+ (async () => {
+ await authenticator.init();
+ const user = (await rmfApi.defaultApi.getUserUserGet()).data;
+ const perm = (await rmfApi.defaultApi.getEffectivePermissionsPermissionsGet()).data;
+ setUserProfile({ user, permissions: perm });
+ })();
+ }, [authenticator, rmfApi]);
+
+ const [settings, setSettings] = React.useState(() => loadSettings());
+ const updateSettings = React.useCallback((newSettings: Settings) => {
+ saveSettings(newSettings);
+ setSettings(newSettings);
+ }, []);
+
+ const [showAlert, setShowAlert] = React.useState(false);
+ const [alertSeverity, setAlertSeverity] = React.useState('error');
+ const [alertMessage, setAlertMessage] = React.useState('');
+ const [alertDuration, setAlertDuration] = React.useState(DefaultAlertDuration);
+ const [extraAppbarItems, setExtraAppbarItems] = React.useState(null);
+ const appController = React.useMemo(
+ () => ({
+ updateSettings,
+ showAlert: (severity, message, autoHideDuration) => {
+ setAlertSeverity(severity);
+ setAlertMessage(message);
+ setShowAlert(true);
+ setAlertDuration(autoHideDuration || DefaultAlertDuration);
+ },
+ setExtraAppbarItems,
+ }),
+ [updateSettings],
+ );
+
+ const theme = React.useMemo(() => {
+ if (!themes) {
+ return null;
+ }
+ return themes[settings.themeMode] || themes.default;
+ }, [themes, settings.themeMode]);
+
+ const providers = userProfile && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* TODO: Support stacking of alerts */}
+ setShowAlert(false)}
+ autoHideDuration={alertDuration}
+ >
+ setShowAlert(false)}
+ severity={alertSeverity}
+ sx={{ width: '100%' }}
+ >
+ {alertMessage}
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ return theme ? {providers} : providers;
+}
+
+interface RequireAuthProps {
+ redirectTo: string;
+ children: React.ReactNode;
+}
+
+function RequireAuth({ redirectTo, children }: RequireAuthProps) {
+ const userProfile = useUserProfile();
+ return userProfile ? children : ;
+}
+
+function NotFound() {
+ return (
+
+
+ 404 - Not Found
+
+
+ The page you're looking for doesn't exist.
+
+
+ );
+}
+
+interface DashboardContentsProps extends RmfDashboardProps {
+ extraAppbarItems: React.ReactNode;
+}
+
+function DashboardContents({
+ authenticator,
+ helpLink,
+ reportIssueLink,
+ themes,
+ resources,
+ tabs,
+ baseUrl = import.meta.env.BASE_URL,
+ extraAppbarItems,
+}: DashboardContentsProps) {
+ const location = useLocation();
+ const currentTab = tabs.find((t) => matchPath(t.route, location.pathname));
+
+ const [pendingTransition, startTransition] = useTransition();
+ const navigate = useNavigate();
+
+ // TODO(koonpeng): enable admin tab when authz is implemented.
+ const allTabs = tabs;
+ // const allTabs = React.useMemo(
+ // () => [...tabs, { name: 'Admin', route: 'admin', element: }],
+ // [tabs],
+ // );
+
+ return (
+
+
+ authenticator.login(`${window.location.origin}${baseUrl}`)}
+ />
+ }
+ />
+
+ (
+ {t.name}}
+ value={t.name}
+ onClick={() => {
+ startTransition(() => {
+ navigate(`${baseUrl}${t.route}`);
+ });
+ }}
+ />
+ ))}
+ tabValue={currentTab!.name}
+ themes={themes}
+ helpLink={helpLink}
+ reportIssueLink={reportIssueLink}
+ extraToolbarItems={extraAppbarItems}
+ />
+ {!pendingTransition && }
+ >
+ }
+ >
+ {allTabs.map((t) => (
+ {t.element}}
+ />
+ ))}
+
+
+ } />
+
+ );
+}
diff --git a/packages/dashboard/src/components/robots/robot-decommission.tsx b/packages/dashboard/src/components/robots/robot-decommission.tsx
index bb9de0a13..854e88233 100644
--- a/packages/dashboard/src/components/robots/robot-decommission.tsx
+++ b/packages/dashboard/src/components/robots/robot-decommission.tsx
@@ -11,9 +11,9 @@ import { RobotState } from 'api-client';
import React from 'react';
import { ConfirmationDialog } from 'react-components';
-import { AppControllerContext } from '../app-contexts';
+import { useAppController } from '../../hooks/use-app-controller';
+import { useRmfApi } from '../../hooks/use-rmf-api';
import { AppEvents } from '../app-events';
-import { RmfAppContext } from '../rmf-app';
export interface RobotDecommissionButtonProp extends Omit {
fleet: string;
@@ -24,9 +24,9 @@ export function RobotDecommissionButton({
fleet,
robotState,
...otherProps
-}: RobotDecommissionButtonProp): JSX.Element {
- const rmf = React.useContext(RmfAppContext);
- const appController = React.useContext(AppControllerContext);
+}: RobotDecommissionButtonProp) {
+ const rmfApi = useRmfApi();
+ const appController = useAppController();
const [reassignTasks, setReassignTasks] = React.useState(true);
const [allowIdleBehavior, setAllowIdleBehavior] = React.useState(false);
const [openConfirmDialog, setOpenConfirmDialog] = React.useState(false);
@@ -60,10 +60,7 @@ export function RobotDecommissionButton({
return;
}
try {
- if (!rmf) {
- throw new Error('fleets api not available');
- }
- const resp = await rmf.fleetsApi?.decommissionRobotFleetsNameDecommissionPost(
+ const resp = await rmfApi.fleetsApi?.decommissionRobotFleetsNameDecommissionPost(
fleet,
robotState.name,
reassignTasks,
@@ -109,17 +106,14 @@ export function RobotDecommissionButton({
}
resetDecommissionConfiguration();
AppEvents.refreshRobotApp.next();
- }, [appController, fleet, robotState, reassignTasks, allowIdleBehavior, rmf]);
+ }, [appController, fleet, robotState, reassignTasks, allowIdleBehavior, rmfApi]);
const handleRecommission = React.useCallback(async () => {
if (!robotState || !robotState.name) {
return;
}
try {
- if (!rmf) {
- throw new Error('fleets api not available');
- }
- const resp = await rmf.fleetsApi?.recommissionRobotFleetsNameRecommissionPost(
+ const resp = await rmfApi.fleetsApi?.recommissionRobotFleetsNameRecommissionPost(
fleet,
robotState.name,
);
@@ -141,7 +135,7 @@ export function RobotDecommissionButton({
}
resetDecommissionConfiguration();
AppEvents.refreshRobotApp.next();
- }, [appController, fleet, robotState, rmf]);
+ }, [appController, fleet, robotState, rmfApi]);
return (
<>
diff --git a/packages/dashboard/src/components/robots/robot-info-app.tsx b/packages/dashboard/src/components/robots/robot-info-card.tsx
similarity index 87%
rename from packages/dashboard/src/components/robots/robot-info-app.tsx
rename to packages/dashboard/src/components/robots/robot-info-card.tsx
index 400f5815b..d534852f6 100644
--- a/packages/dashboard/src/components/robots/robot-info-app.tsx
+++ b/packages/dashboard/src/components/robots/robot-info-card.tsx
@@ -4,19 +4,15 @@ import React from 'react';
import { RobotInfo } from 'react-components';
import { combineLatest, EMPTY, mergeMap, of, switchMap, throttleTime } from 'rxjs';
+import { useRmfApi } from '../../hooks/use-rmf-api';
import { AppEvents } from '../app-events';
-import { createMicroApp } from '../micro-app';
-import { RmfAppContext } from '../rmf-app';
-export const RobotInfoApp = createMicroApp('Robot Info', () => {
- const rmf = React.useContext(RmfAppContext);
+export const RobotInfoCard = () => {
+ const rmfApi = useRmfApi();
const [robotState, setRobotState] = React.useState(null);
const [taskState, setTaskState] = React.useState(null);
React.useEffect(() => {
- if (!rmf) {
- return;
- }
const sub = AppEvents.robotSelect
.pipe(
switchMap((data) => {
@@ -24,12 +20,12 @@ export const RobotInfoApp = createMicroApp('Robot Info', () => {
return of([null, null]);
}
const [fleet, name] = data;
- return rmf.getFleetStateObs(fleet).pipe(
+ return rmfApi.getFleetStateObs(fleet).pipe(
throttleTime(3000, undefined, { leading: true, trailing: true }),
mergeMap((fleetState) => {
const robotState = fleetState?.robots?.[name];
const taskObs = robotState?.task_id
- ? rmf.getTaskStateObs(robotState.task_id)
+ ? rmfApi.getTaskStateObs(robotState.task_id)
: of(null);
return robotState ? combineLatest([of(robotState), taskObs]) : EMPTY;
}),
@@ -41,7 +37,7 @@ export const RobotInfoApp = createMicroApp('Robot Info', () => {
setTaskState(taskState);
});
return () => sub.unsubscribe();
- }, [rmf]);
+ }, [rmfApi]);
const taskProgress = React.useMemo(() => {
if (
@@ -87,4 +83,6 @@ export const RobotInfoApp = createMicroApp('Robot Info', () => {
)}
);
-});
+};
+
+export default RobotInfoCard;
diff --git a/packages/dashboard/src/components/robots/robot-mutex-group-app.tsx b/packages/dashboard/src/components/robots/robot-mutex-group-table.tsx
similarity index 87%
rename from packages/dashboard/src/components/robots/robot-mutex-group-app.tsx
rename to packages/dashboard/src/components/robots/robot-mutex-group-table.tsx
index 67301c66e..7fbfe6c2e 100644
--- a/packages/dashboard/src/components/robots/robot-mutex-group-app.tsx
+++ b/packages/dashboard/src/components/robots/robot-mutex-group-table.tsx
@@ -2,15 +2,14 @@ import { TableContainer, Typography } from '@mui/material';
import React from 'react';
import { ConfirmationDialog, MutexGroupData, MutexGroupTable } from 'react-components';
-import { AppControllerContext } from '../app-contexts';
-import { createMicroApp } from '../micro-app';
-import { RmfAppContext } from '../rmf-app';
+import { useAppController } from '../../hooks/use-app-controller';
+import { useRmfApi } from '../../hooks/use-rmf-api';
const RefreshMutexGroupTableInterval = 5000;
-export const MutexGroupsApp = createMicroApp('Mutex Groups', () => {
- const rmf = React.useContext(RmfAppContext);
- const appController = React.useContext(AppControllerContext);
+export const RobotMutexGroupsTable = () => {
+ const rmfApi = useRmfApi();
+ const appController = useAppController();
const [mutexGroups, setMutexGroups] = React.useState>({});
const [selectedMutexGroup, setSelectedMutexGroup] = React.useState(null);
@@ -40,13 +39,8 @@ export const MutexGroupsApp = createMicroApp('Mutex Groups', () => {
};
React.useEffect(() => {
- if (!rmf) {
- console.error('Unable to get latest robot information, fleets API unavailable');
- return;
- }
-
const refreshMutexGroupTable = async () => {
- const fleets = (await rmf.fleetsApi.getFleetsFleetsGet()).data;
+ const fleets = (await rmfApi.fleetsApi.getFleetsFleetsGet()).data;
const updatedMutexGroups: Record = {};
for (const fleet of fleets) {
if (!fleet.name || !fleet.robots) {
@@ -112,7 +106,7 @@ export const MutexGroupsApp = createMicroApp('Mutex Groups', () => {
return () => {
clearInterval(refreshInterval);
};
- }, [rmf]);
+ }, [rmfApi]);
const handleUnlockMutexGroup = React.useCallback(async () => {
if (!selectedMutexGroup || !selectedMutexGroup.lockedBy) {
@@ -125,11 +119,7 @@ export const MutexGroupsApp = createMicroApp('Mutex Groups', () => {
}
try {
- if (!rmf) {
- throw new Error('fleets api not available');
- }
-
- await rmf.fleetsApi?.unlockMutexGroupFleetsNameUnlockMutexGroupPost(
+ await rmfApi.fleetsApi?.unlockMutexGroupFleetsNameUnlockMutexGroupPost(
fleet,
robot,
selectedMutexGroup.name,
@@ -147,10 +137,10 @@ export const MutexGroupsApp = createMicroApp('Mutex Groups', () => {
);
}
setSelectedMutexGroup(null);
- }, [selectedMutexGroup, rmf, appController]);
+ }, [selectedMutexGroup, rmfApi, appController]);
return (
-
+
{
@@ -177,4 +167,6 @@ export const MutexGroupsApp = createMicroApp('Mutex Groups', () => {
);
-});
+};
+
+export default RobotMutexGroupsTable;
diff --git a/packages/dashboard/src/components/robots/robot-summary.tsx b/packages/dashboard/src/components/robots/robot-summary.tsx
index d7ab60381..e0071d59c 100644
--- a/packages/dashboard/src/components/robots/robot-summary.tsx
+++ b/packages/dashboard/src/components/robots/robot-summary.tsx
@@ -34,7 +34,7 @@ import React from 'react';
import { base, RobotTableData } from 'react-components';
import { combineLatest, EMPTY, mergeMap, of } from 'rxjs';
-import { RmfAppContext } from '../rmf-app';
+import { useRmfApi } from '../../hooks/use-rmf-api';
import { TaskCancelButton } from '../tasks/task-cancellation';
import { TaskInspector } from '../tasks/task-inspector';
import { RobotDecommissionButton } from './robot-decommission';
@@ -104,7 +104,7 @@ const showBatteryIcon = (robot: RobotState, robotBattery: number) => {
export const RobotSummary = React.memo(({ onClose, robot }: RobotSummaryProps) => {
const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)');
- const rmf = React.useContext(RmfAppContext);
+ const rmfApi = useRmfApi();
const [isOpen, setIsOpen] = React.useState(true);
const [robotState, setRobotState] = React.useState(null);
@@ -114,15 +114,14 @@ export const RobotSummary = React.memo(({ onClose, robot }: RobotSummaryProps) =
const [navigationDestination, setNavigationDestination] = React.useState(null);
React.useEffect(() => {
- if (!rmf) {
- return;
- }
- const sub = rmf
+ const sub = rmfApi
.getFleetStateObs(robot.fleet)
.pipe(
mergeMap((fleetState) => {
const robotState = fleetState?.robots?.[robot.name];
- const taskObs = robotState?.task_id ? rmf.getTaskStateObs(robotState.task_id) : of(null);
+ const taskObs = robotState?.task_id
+ ? rmfApi.getTaskStateObs(robotState.task_id)
+ : of(null);
return robotState ? combineLatest([of(robotState), taskObs]) : EMPTY;
}),
)
@@ -131,7 +130,7 @@ export const RobotSummary = React.memo(({ onClose, robot }: RobotSummaryProps) =
setTaskState(taskState);
});
return () => sub.unsubscribe();
- }, [rmf, robot.fleet, robot.name]);
+ }, [rmfApi, robot.fleet, robot.name]);
const taskProgress = React.useMemo(() => {
if (
diff --git a/packages/dashboard/src/components/robots/robots-app.tsx b/packages/dashboard/src/components/robots/robots-table.tsx
similarity index 86%
rename from packages/dashboard/src/components/robots/robots-app.tsx
rename to packages/dashboard/src/components/robots/robots-table.tsx
index 14ba43c5d..0bc99c56c 100644
--- a/packages/dashboard/src/components/robots/robots-app.tsx
+++ b/packages/dashboard/src/components/robots/robots-table.tsx
@@ -3,28 +3,22 @@ import { TaskStateOutput as TaskState } from 'api-client';
import React from 'react';
import { RobotDataGridTable, RobotTableData } from 'react-components';
+import { useRmfApi } from '../../hooks/use-rmf-api';
import { AppEvents } from '../app-events';
-import { createMicroApp } from '../micro-app';
-import { RmfAppContext } from '../rmf-app';
import { RobotSummary } from './robot-summary';
const RefreshRobotTableInterval = 10000;
-export const RobotsApp = createMicroApp('Robots', () => {
- const rmf = React.useContext(RmfAppContext);
+export const RobotsTable = () => {
+ const rmfApi = useRmfApi();
const [robots, setRobots] = React.useState>({});
const [openRobotSummary, setOpenRobotSummary] = React.useState(false);
const [selectedRobot, setSelectedRobot] = React.useState();
React.useEffect(() => {
- if (!rmf) {
- console.error('Unable to get latest robot information, fleets API unavailable');
- return;
- }
-
const refreshRobotTable = async () => {
- const fleets = (await rmf.fleetsApi.getFleetsFleetsGet()).data;
+ const fleets = (await rmfApi.fleetsApi.getFleetsFleetsGet()).data;
for (const fleet of fleets) {
// fetch active tasks
const taskIds = fleet.robots
@@ -38,7 +32,7 @@ export const RobotsApp = createMicroApp('Robots', () => {
const tasks =
taskIds.length > 0
- ? (await rmf.tasksApi.queryTaskStatesTasksGet(taskIds.join(','))).data.reduce(
+ ? (await rmfApi.tasksApi.queryTaskStatesTasksGet(taskIds.join(','))).data.reduce(
(acc, task) => {
acc[task.booking.id] = task;
return acc;
@@ -93,10 +87,10 @@ export const RobotsApp = createMicroApp('Robots', () => {
clearInterval(refreshInterval);
sub.unsubscribe();
};
- }, [rmf]);
+ }, [rmfApi]);
return (
-
+
r)}
onRobotClick={(_ev, robot) => {
@@ -110,4 +104,6 @@ export const RobotsApp = createMicroApp('Robots', () => {
)}
);
-});
+};
+
+export default RobotsTable;
diff --git a/packages/dashboard/src/components/tasks/task-cancellation.tsx b/packages/dashboard/src/components/tasks/task-cancellation.tsx
index a8b253f71..2c078b254 100644
--- a/packages/dashboard/src/components/tasks/task-cancellation.tsx
+++ b/packages/dashboard/src/components/tasks/task-cancellation.tsx
@@ -3,12 +3,11 @@ import { TaskStateOutput as TaskState } from 'api-client';
import React from 'react';
import { ConfirmationDialog } from 'react-components';
-import { UserProfile } from '../../services/authenticator';
+import { useAppController } from '../../hooks/use-app-controller';
+import { useRmfApi } from '../../hooks/use-rmf-api';
+import { useUserProfile } from '../../hooks/use-user-profile';
import { Enforcer } from '../../services/permissions';
-import { AppControllerContext } from '../app-contexts';
import { AppEvents } from '../app-events';
-import { RmfAppContext } from '../rmf-app';
-import { UserProfileContext } from '../user-profile-provider';
export interface TaskCancelButtonProp extends ButtonProps {
taskId: string | null;
@@ -20,22 +19,22 @@ export function TaskCancelButton({
buttonText,
...otherProps
}: TaskCancelButtonProp): JSX.Element {
- const rmf = React.useContext(RmfAppContext);
- const appController = React.useContext(AppControllerContext);
- const profile: UserProfile | null = React.useContext(UserProfileContext);
+ const rmfApi = useRmfApi();
+ const appController = useAppController();
+ const profile = useUserProfile();
const [taskState, setTaskState] = React.useState(null);
const [openConfirmDialog, setOpenConfirmDialog] = React.useState(false);
React.useEffect(() => {
- if (!rmf || !taskId) {
+ if (!taskId) {
return;
}
- const sub = rmf.getTaskStateObs(taskId).subscribe((state) => {
+ const sub = rmfApi.getTaskStateObs(taskId).subscribe((state) => {
setTaskState(state);
});
return () => sub.unsubscribe();
- }, [rmf, taskId]);
+ }, [rmfApi, taskId]);
const isTaskCancellable = (state: TaskState | null) => {
return (
@@ -54,10 +53,7 @@ export function TaskCancelButton({
return;
}
try {
- if (!rmf) {
- throw new Error('tasks api not available');
- }
- await rmf.tasksApi?.postCancelTaskTasksCancelTaskPost({
+ await rmfApi.tasksApi?.postCancelTaskTasksCancelTaskPost({
type: 'cancel_task_request',
task_id: taskState.booking.id,
labels: profile ? [profile.user.username] : undefined,
@@ -69,7 +65,7 @@ export function TaskCancelButton({
appController.showAlert('error', `Failed to cancel task: ${(e as Error).message}`);
}
setOpenConfirmDialog(false);
- }, [appController, taskState, rmf, profile]);
+ }, [appController, taskState, rmfApi, profile]);
return (
<>
diff --git a/packages/dashboard/src/components/tasks/task-details-app.tsx b/packages/dashboard/src/components/tasks/task-details-app.tsx
index e4331017d..55c429276 100644
--- a/packages/dashboard/src/components/tasks/task-details-app.tsx
+++ b/packages/dashboard/src/components/tasks/task-details-app.tsx
@@ -5,31 +5,27 @@ import { TaskInfo } from 'react-components';
// import { UserProfileContext } from 'rmf-auth';
import { of, switchMap } from 'rxjs';
-import { AppControllerContext } from '../app-contexts';
+import { useAppController } from '../../hooks/use-app-controller';
+import { useRmfApi } from '../../hooks/use-rmf-api';
import { AppEvents } from '../app-events';
-import { createMicroApp } from '../micro-app';
// import { Enforcer } from '../permissions';
-import { RmfAppContext } from '../rmf-app';
-export const TaskDetailsApp = createMicroApp('Task Details', () => {
+export const TaskDetailsCard = () => {
const theme = useTheme();
- const rmf = React.useContext(RmfAppContext);
- const appController = React.useContext(AppControllerContext);
+ const rmfApi = useRmfApi();
+ const appController = useAppController();
const [taskState, setTaskState] = React.useState(null);
React.useEffect(() => {
- if (!rmf) {
- return;
- }
const sub = AppEvents.taskSelect
.pipe(
switchMap((selectedTask) =>
- selectedTask ? rmf.getTaskStateObs(selectedTask.booking.id) : of(null),
+ selectedTask ? rmfApi.getTaskStateObs(selectedTask.booking.id) : of(null),
),
)
.subscribe(setTaskState);
return () => sub.unsubscribe();
- }, [rmf]);
+ }, [rmfApi]);
// const profile = React.useContext(UserProfileContext);
const taskCancellable =
@@ -43,10 +39,7 @@ export const TaskDetailsApp = createMicroApp('Task Details', () => {
return;
}
try {
- if (!rmf) {
- throw new Error('tasks api not available');
- }
- await rmf.tasksApi?.postCancelTaskTasksCancelTaskPost({
+ await rmfApi.tasksApi?.postCancelTaskTasksCancelTaskPost({
type: 'cancel_task_request',
task_id: taskState.booking.id,
});
@@ -55,7 +48,7 @@ export const TaskDetailsApp = createMicroApp('Task Details', () => {
} catch (e) {
appController.showAlert('error', `Failed to cancel task: ${(e as Error).message}`);
}
- }, [appController, taskState, rmf]);
+ }, [appController, taskState, rmfApi]);
return (
@@ -89,4 +82,6 @@ export const TaskDetailsApp = createMicroApp('Task Details', () => {
)}
);
-});
+};
+
+export default TaskDetailsCard;
diff --git a/packages/dashboard/src/components/tasks/task-inspector.tsx b/packages/dashboard/src/components/tasks/task-inspector.tsx
index 777565943..8ac0ea7d0 100644
--- a/packages/dashboard/src/components/tasks/task-inspector.tsx
+++ b/packages/dashboard/src/components/tasks/task-inspector.tsx
@@ -4,7 +4,7 @@ import { TaskEventLog, TaskStateOutput as TaskState } from 'api-client';
import React from 'react';
import { TaskInfo } from 'react-components';
-import { RmfAppContext } from '../rmf-app';
+import { useRmfApi } from '../../hooks/use-rmf-api';
import { TaskCancelButton } from './task-cancellation';
import { TaskLogs } from './task-logs';
@@ -15,23 +15,23 @@ export interface TableDataGridState {
export function TaskInspector({ task, onClose }: TableDataGridState): JSX.Element {
const theme = useTheme();
- const rmf = React.useContext(RmfAppContext);
+ const rmfApi = useRmfApi();
const [taskState, setTaskState] = React.useState(null);
const [taskLogs, setTaskLogs] = React.useState(null);
const [isOpen, setIsOpen] = React.useState(true);
React.useEffect(() => {
- if (!rmf || !task) {
+ if (!task) {
setTaskState(null);
setTaskLogs(null);
return;
}
- const sub = rmf.getTaskStateObs(task.booking.id).subscribe((subscribedTask) => {
+ const sub = rmfApi.getTaskStateObs(task.booking.id).subscribe((subscribedTask) => {
(async () => {
try {
const logs = (
- await rmf.tasksApi.getTaskLogTasksTaskIdLogGet(
+ await rmfApi.tasksApi.getTaskLogTasksTaskIdLogGet(
subscribedTask.booking.id,
`0,${Number.MAX_SAFE_INTEGER}`,
)
@@ -45,7 +45,7 @@ export function TaskInspector({ task, onClose }: TableDataGridState): JSX.Elemen
})();
});
return () => sub.unsubscribe();
- }, [rmf, task]);
+ }, [rmfApi, task]);
return (
<>
diff --git a/packages/dashboard/src/components/tasks/task-logs-app.tsx b/packages/dashboard/src/components/tasks/task-logs-app.tsx
index 7ff8044ba..ff2b6dd61 100644
--- a/packages/dashboard/src/components/tasks/task-logs-app.tsx
+++ b/packages/dashboard/src/components/tasks/task-logs-app.tsx
@@ -2,19 +2,15 @@ import { CardContent } from '@mui/material';
import { TaskEventLog, TaskStateOutput as TaskState } from 'api-client';
import React from 'react';
+import { useRmfApi } from '../../hooks/use-rmf-api';
import { AppEvents } from '../app-events';
-import { createMicroApp } from '../micro-app';
-import { RmfAppContext } from '../rmf-app';
import { TaskLogs } from './task-logs';
-export const TaskLogsApp = createMicroApp('Task Logs', () => {
- const rmf = React.useContext(RmfAppContext);
+export const TaskLogsCard = () => {
+ const rmfApi = useRmfApi();
const [taskState, setTaskState] = React.useState(null);
const [taskLogs, setTaskLogs] = React.useState(null);
React.useEffect(() => {
- if (!rmf) {
- return;
- }
const sub = AppEvents.taskSelect.subscribe((task) => {
if (!task) {
setTaskState(null);
@@ -26,7 +22,7 @@ export const TaskLogsApp = createMicroApp('Task Logs', () => {
// Unlike with state events, we can't just subscribe to logs updates.
try {
const logs = (
- await rmf.tasksApi.getTaskLogTasksTaskIdLogGet(
+ await rmfApi.tasksApi.getTaskLogTasksTaskIdLogGet(
task.booking.id,
`0,${Number.MAX_SAFE_INTEGER}`,
)
@@ -40,11 +36,13 @@ export const TaskLogsApp = createMicroApp('Task Logs', () => {
})();
});
return () => sub.unsubscribe();
- }, [rmf]);
+ }, [rmfApi]);
return (
);
-});
+};
+
+export default TaskLogsCard;
diff --git a/packages/dashboard/src/components/tasks/task-schedule.tsx b/packages/dashboard/src/components/tasks/task-schedule.tsx
index 89e419704..2b72d95a5 100644
--- a/packages/dashboard/src/components/tasks/task-schedule.tsx
+++ b/packages/dashboard/src/components/tasks/task-schedule.tsx
@@ -8,7 +8,7 @@ import {
import { DayProps } from '@aldabil/react-scheduler/views/Day';
import { MonthProps } from '@aldabil/react-scheduler/views/Month';
import { WeekProps } from '@aldabil/react-scheduler/views/Week';
-import { Button, Typography } from '@mui/material';
+import { Button, Theme, Typography, useTheme } from '@mui/material';
import { ScheduledTask, ScheduledTaskScheduleOutput as ApiSchedule } from 'api-client';
import React from 'react';
import {
@@ -19,12 +19,12 @@ import {
Schedule,
} from 'react-components';
-import { allowedTasks } from '../../app-config';
-import { useCreateTaskFormData } from '../../hooks/useCreateTaskForm';
-import useGetUsername from '../../hooks/useFetchUser';
-import { AppControllerContext } from '../app-contexts';
+import { useAppController } from '../../hooks/use-app-controller';
+import { useCreateTaskFormData } from '../../hooks/use-create-task-form';
+import { useRmfApi } from '../../hooks/use-rmf-api';
+import { useTaskRegistry } from '../../hooks/use-task-registry';
+import { useUserProfile } from '../../hooks/use-user-profile';
import { AppEvents } from '../app-events';
-import { RmfAppContext } from '../rmf-app';
import {
apiScheduleToSchedule,
getScheduledTaskColor,
@@ -49,6 +49,7 @@ interface CustomCalendarEditorProps {
const disablingCellsWithoutEvents = (
events: ProcessedEvent[],
{ start, ...props }: CellRenderedProps,
+ theme: Theme,
): React.ReactElement => {
const filteredEvents = events.filter((event) => start.getTime() !== event.start.getTime());
const disabled = filteredEvents.length > 0 || events.length === 0;
@@ -57,7 +58,7 @@ const disablingCellsWithoutEvents = (