From a60937ab6811522b988290d3f2466589e4aaea7b Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 4 Dec 2024 14:03:46 +0800 Subject: [PATCH 01/27] Remove low resolution custom behaviors Signed-off-by: Aaron Chong --- .../src/components/alert-manager.tsx | 24 ++++---- .../src/components/appbar.tsx | 6 +- .../beacons/beacon-table-datagrid.tsx | 11 +--- .../src/components/confirmation-dialog.tsx | 10 +-- .../components/doors/door-table-datagrid.tsx | 19 +++--- .../src/components/lifts/lift-controls.tsx | 5 +- .../src/components/lifts/lift-summary.tsx | 20 ++---- .../components/lifts/lift-table-datagrid.tsx | 24 ++------ .../src/components/map/layers-controller.tsx | 61 +++---------------- .../src/components/map/map.tsx | 17 ++---- .../components/robots/mutex-group-table.tsx | 9 +-- .../src/components/robots/robot-summary.tsx | 19 +++--- .../robots/robot-table-datagrid.tsx | 12 +--- .../src/components/tasks/task-form.tsx | 58 +++++++----------- .../src/components/tasks/task-summary.tsx | 15 ++--- .../src/components/tasks/tasks-window.tsx | 38 ++---------- .../tasks/types/delivery-custom.tsx | 40 ++++++------ .../src/components/tasks/types/patrol.tsx | 16 +++-- 18 files changed, 123 insertions(+), 281 deletions(-) diff --git a/packages/rmf-dashboard-framework/src/components/alert-manager.tsx b/packages/rmf-dashboard-framework/src/components/alert-manager.tsx index e18d56ecb..54aeeaf23 100644 --- a/packages/rmf-dashboard-framework/src/components/alert-manager.tsx +++ b/packages/rmf-dashboard-framework/src/components/alert-manager.tsx @@ -6,7 +6,6 @@ import { DialogTitle, Divider, TextField, - useMediaQuery, useTheme, } from '@mui/material'; import { @@ -33,7 +32,6 @@ const AlertDialog = React.memo((props: AlertDialogProps) => { const [isOpen, setIsOpen] = React.useState(true); const { showAlert } = useAppController(); const rmfApi = useRmfApi(); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); const [additionalAlertMessage, setAdditionalAlertMessage] = React.useState(null); const respondToAlert = async (alert_id: string, response: string) => { @@ -131,7 +129,7 @@ const AlertDialog = React.memo((props: AlertDialogProps) => { boxShadow: 'none', }, }} - maxWidth={isScreenHeightLessThan800 ? 'xs' : 'sm'} + maxWidth="sm" fullWidth={true} open={isOpen} key={alertRequest.id} @@ -148,14 +146,14 @@ const AlertDialog = React.memo((props: AlertDialogProps) => { variant="filled" sx={{ '& .MuiFilledInput-root': { - fontSize: isScreenHeightLessThan800 ? '0.8rem' : '1.15', + fontSize: '1.15', }, background: theme.palette.background.default, '&:hover': { backgroundColor: theme.palette.background.default, }, }} - InputLabelProps={{ style: { fontSize: isScreenHeightLessThan800 ? 16 : 20 } }} + InputLabelProps={{ style: { fontSize: 20 } }} InputProps={{ readOnly: true }} fullWidth={true} margin="dense" @@ -169,14 +167,14 @@ const AlertDialog = React.memo((props: AlertDialogProps) => { variant="filled" sx={{ '& .MuiFilledInput-root': { - fontSize: isScreenHeightLessThan800 ? '0.8rem' : '1.15', + fontSize: '1.15', }, background: theme.palette.background.default, '&:hover': { backgroundColor: theme.palette.background.default, }, }} - InputLabelProps={{ style: { fontSize: isScreenHeightLessThan800 ? 16 : 20 } }} + InputLabelProps={{ style: { fontSize: 20 } }} InputProps={{ readOnly: true }} fullWidth={true} multiline @@ -199,8 +197,8 @@ const AlertDialog = React.memo((props: AlertDialogProps) => { autoFocus key={`${alertRequest.id}-${response}`} sx={{ - fontSize: isScreenHeightLessThan800 ? '0.8rem' : '1rem', - padding: isScreenHeightLessThan800 ? '4px 8px' : '6px 12px', + fontSize: '1rem', + padding: '6px 12px', }} onClick={async () => { await respondToAlert(alertRequest.id, response); @@ -219,8 +217,8 @@ const AlertDialog = React.memo((props: AlertDialogProps) => { color="secondary" buttonText={'Cancel task'} sx={{ - fontSize: isScreenHeightLessThan800 ? '0.8rem' : '1rem', - padding: isScreenHeightLessThan800 ? '4px 8px' : '6px 12px', + fontSize: '1rem', + padding: '6px 12px', }} /> ) : null} @@ -229,8 +227,8 @@ const AlertDialog = React.memo((props: AlertDialogProps) => { variant="contained" autoFocus sx={{ - fontSize: isScreenHeightLessThan800 ? '0.8rem' : '1rem', - padding: isScreenHeightLessThan800 ? '4px 8px' : '6px 12px', + fontSize: '1rem', + padding: '6px 12px', }} onClick={() => { onDismiss(); diff --git a/packages/rmf-dashboard-framework/src/components/appbar.tsx b/packages/rmf-dashboard-framework/src/components/appbar.tsx index 4132a7e7f..59c06f8ad 100644 --- a/packages/rmf-dashboard-framework/src/components/appbar.tsx +++ b/packages/rmf-dashboard-framework/src/components/appbar.tsx @@ -372,7 +372,7 @@ export const AppBar = React.memo( color="inherit" onClick={() => window.open(helpLink, '_blank')} > - + @@ -382,7 +382,7 @@ export const AppBar = React.memo( color="inherit" onClick={() => window.open(reportIssueLink, '_blank')} > - + @@ -392,7 +392,7 @@ export const AppBar = React.memo( color="inherit" onClick={(event) => setAnchorEl(event.currentTarget)} > - + { const opModeStateLabelStyle: SxProps = (() => { @@ -30,7 +29,7 @@ export function BeaconDataGridTable({ beacons }: BeaconDataGridTableProps): JSX. component="p" sx={{ fontWeight: 'bold', - fontSize: isScreenHeightLessThan800 ? 12 : 16, + fontSize: 16, }} > {params.row.online ? 'ONLINE' : 'OFFLINE'} @@ -123,11 +122,7 @@ export function BeaconDataGridTable({ beacons }: BeaconDataGridTableProps): JSX. rowHeight={38} columns={columns} rowsPerPageOptions={[5]} - sx={{ - fontSize: isScreenHeightLessThan800 ? '0.7rem' : 'inherit', - }} - autoPageSize={isScreenHeightLessThan800} - density={isScreenHeightLessThan800 ? 'compact' : 'standard'} + density="standard" localeText={{ noRowsLabel: 'No beacons available.', }} diff --git a/packages/rmf-dashboard-framework/src/components/confirmation-dialog.tsx b/packages/rmf-dashboard-framework/src/components/confirmation-dialog.tsx index 1d98bdf63..98c6adca2 100644 --- a/packages/rmf-dashboard-framework/src/components/confirmation-dialog.tsx +++ b/packages/rmf-dashboard-framework/src/components/confirmation-dialog.tsx @@ -8,7 +8,6 @@ import { DialogTitle, Grid, styled, - useMediaQuery, } from '@mui/material'; import clsx from 'clsx'; import React from 'react'; @@ -51,17 +50,14 @@ export function ConfirmationDialog({ children, ...otherProps }: ConfirmationDialogProps): JSX.Element { - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); return ( @@ -87,7 +83,7 @@ export function ConfirmationDialog({ onClick={(ev) => onClose && onClose(ev, 'escapeKeyDown')} disabled={submitting} className={clsx(dialogClasses.actionBtn, classes?.button)} - size={isScreenHeightLessThan800 ? 'small' : 'medium'} + size="medium" > {cancelText} @@ -97,7 +93,7 @@ export function ConfirmationDialog({ color="primary" disabled={submitting} className={clsx(dialogClasses.actionBtn, classes?.button)} - size={isScreenHeightLessThan800 ? 'small' : 'medium'} + size="medium" > {confirmText} diff --git a/packages/rmf-dashboard-framework/src/components/doors/door-table-datagrid.tsx b/packages/rmf-dashboard-framework/src/components/doors/door-table-datagrid.tsx index caa0d6380..da03e8f9a 100644 --- a/packages/rmf-dashboard-framework/src/components/doors/door-table-datagrid.tsx +++ b/packages/rmf-dashboard-framework/src/components/doors/door-table-datagrid.tsx @@ -1,4 +1,4 @@ -import { Box, Button, SxProps, Typography, useMediaQuery, useTheme } from '@mui/material'; +import { Box, Button, SxProps, Typography, useTheme } from '@mui/material'; import { DataGrid, GridCellParams, @@ -41,7 +41,6 @@ export interface DoorDataGridTableProps { export function DoorDataGridTable({ doors, onDoorClick }: DoorDataGridTableProps): JSX.Element { const theme = useTheme(); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); const OpModeState = (params: GridCellParams): React.ReactNode => { const opModeStateLabelStyle: SxProps = (() => { @@ -128,7 +127,7 @@ export function DoorDataGridTable({ doors, onDoorClick }: DoorDataGridTableProps component="p" sx={{ fontWeight: 'bold', - fontSize: isScreenHeightLessThan800 ? 10 : 16, + fontSize: 16, }} > {params.row.doorState ? doorModeToString(params.row.doorState.current_mode.value) : -1} @@ -146,8 +145,8 @@ export function DoorDataGridTable({ doors, onDoorClick }: DoorDataGridTableProps aria-label="open" sx={{ minWidth: 'auto', - fontSize: isScreenHeightLessThan800 ? 10 : 16, - marginRight: isScreenHeightLessThan800 ? 0 : 0, + fontSize: 16, + marginRight: 0, }} onClick={params.row.onClickOpen} > @@ -159,8 +158,8 @@ export function DoorDataGridTable({ doors, onDoorClick }: DoorDataGridTableProps aria-label="close" sx={{ minWidth: 'auto', - fontSize: isScreenHeightLessThan800 ? 10 : 16, - marginRight: isScreenHeightLessThan800 ? 0 : 0, + fontSize: 16, + marginRight: 0, }} onClick={params.row.onClickClose} > @@ -239,11 +238,7 @@ export function DoorDataGridTable({ doors, onDoorClick }: DoorDataGridTableProps rowHeight={38} columns={columns} rowsPerPageOptions={[5]} - sx={{ - fontSize: isScreenHeightLessThan800 ? '0.7rem' : 'inherit', - }} - autoPageSize={isScreenHeightLessThan800} - density={isScreenHeightLessThan800 ? 'compact' : 'standard'} + density={'standard'} localeText={{ noRowsLabel: 'No doors available.', }} diff --git a/packages/rmf-dashboard-framework/src/components/lifts/lift-controls.tsx b/packages/rmf-dashboard-framework/src/components/lifts/lift-controls.tsx index 6aa8b9935..0cde440d3 100644 --- a/packages/rmf-dashboard-framework/src/components/lifts/lift-controls.tsx +++ b/packages/rmf-dashboard-framework/src/components/lifts/lift-controls.tsx @@ -1,4 +1,4 @@ -import { Button, useMediaQuery } from '@mui/material'; +import { Button } from '@mui/material'; import React from 'react'; import { LiftRequestDialog, LiftRequestDialogProps } from './lift-request-dialog'; @@ -18,7 +18,6 @@ export function LiftControls({ onClose, ...otherProps }: LiftControlsProps): JSX.Element { - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); const [showDialog, setShowDialog] = React.useState(false); // Doing `{showDialog &&
}` will unomunt it before the animations are done. // Instead we give a `key` to the form to make react spawn a new instance. @@ -36,7 +35,7 @@ export function LiftControls({ }} sx={{ minWidth: 'auto', - fontSize: isScreenHeightLessThan800 ? 10 : 16, + fontSize: 16, }} > Request diff --git a/packages/rmf-dashboard-framework/src/components/lifts/lift-summary.tsx b/packages/rmf-dashboard-framework/src/components/lifts/lift-summary.tsx index 814d70deb..fdf624fa9 100644 --- a/packages/rmf-dashboard-framework/src/components/lifts/lift-summary.tsx +++ b/packages/rmf-dashboard-framework/src/components/lifts/lift-summary.tsx @@ -1,12 +1,4 @@ -import { - Dialog, - DialogContent, - DialogTitle, - Divider, - TextField, - useMediaQuery, - useTheme, -} from '@mui/material'; +import { Dialog, DialogContent, DialogTitle, Divider, TextField, useTheme } from '@mui/material'; import { Lift } from 'api-client'; import React from 'react'; @@ -21,7 +13,6 @@ interface LiftSummaryProps { } export const LiftSummary = ({ onClose, lift }: LiftSummaryProps): JSX.Element => { - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); const rmfApi = useRmfApi(); const [liftData, setLiftData] = React.useState({ index: 0, @@ -77,12 +68,9 @@ export const LiftSummary = ({ onClose, lift }: LiftSummaryProps): JSX.Element => onClose(); }} fullWidth - maxWidth={isScreenHeightLessThan800 ? 'xs' : 'sm'} + maxWidth="sm" > - + Lift summary @@ -128,7 +116,7 @@ export const LiftSummary = ({ onClose, lift }: LiftSummaryProps): JSX.Element => value={displayValue} sx={{ '& .MuiFilledInput-root': { - fontSize: isScreenHeightLessThan800 ? '0.8rem' : '1.15', + fontSize: '1.15', }, background: theme.palette.background.default, '&:hover': { diff --git a/packages/rmf-dashboard-framework/src/components/lifts/lift-table-datagrid.tsx b/packages/rmf-dashboard-framework/src/components/lifts/lift-table-datagrid.tsx index 8d035cf2b..5ad2f816c 100644 --- a/packages/rmf-dashboard-framework/src/components/lifts/lift-table-datagrid.tsx +++ b/packages/rmf-dashboard-framework/src/components/lifts/lift-table-datagrid.tsx @@ -1,6 +1,6 @@ import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; -import { Box, SxProps, Typography, useMediaQuery, useTheme } from '@mui/material'; +import { Box, SxProps, Typography, useTheme } from '@mui/material'; import { DataGrid, GridCellParams, @@ -42,7 +42,6 @@ export interface LiftDataGridTableProps { export function LiftDataGridTable({ lifts, onLiftClick }: LiftDataGridTableProps): JSX.Element { const theme = useTheme(); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); const handleEvent: GridEventListener<'rowClick'> = ( params: GridRowParams, @@ -94,7 +93,7 @@ export function LiftDataGridTable({ lifts, onLiftClick }: LiftDataGridTableProps component="p" sx={{ fontWeight: 'bold', - fontSize: isScreenHeightLessThan800 ? 10 : 16, + fontSize: 16, }} > {liftModeToString(params.row.liftState?.current_mode).toUpperCase()} @@ -108,12 +107,10 @@ export function LiftDataGridTable({ lifts, onLiftClick }: LiftDataGridTableProps const currMotion = motionStateToString(params.row?.motionState); const motionArrowActiveStyle: SxProps = { - transform: `scale(${isScreenHeightLessThan800 ? 0.8 : 1})`, color: theme.palette.primary.main, }; const motionArrowIdleStyle: SxProps = { - transform: `scale(${isScreenHeightLessThan800 ? 0.8 : 1})`, color: theme.palette.action.disabled, opacity: theme.palette.action.disabledOpacity, }; @@ -144,16 +141,9 @@ export function LiftDataGridTable({ lifts, onLiftClick }: LiftDataGridTableProps @@ -243,11 +233,7 @@ export function LiftDataGridTable({ lifts, onLiftClick }: LiftDataGridTableProps rowHeight={38} columns={columns} rowsPerPageOptions={[5]} - sx={{ - fontSize: isScreenHeightLessThan800 ? '0.7rem' : 'inherit', - }} - autoPageSize={isScreenHeightLessThan800} - density={isScreenHeightLessThan800 ? 'compact' : 'standard'} + density={'standard'} localeText={{ noRowsLabel: 'No lifts available.', }} diff --git a/packages/rmf-dashboard-framework/src/components/map/layers-controller.tsx b/packages/rmf-dashboard-framework/src/components/map/layers-controller.tsx index 7548cf745..e7c02a167 100644 --- a/packages/rmf-dashboard-framework/src/components/map/layers-controller.tsx +++ b/packages/rmf-dashboard-framework/src/components/map/layers-controller.tsx @@ -11,7 +11,6 @@ import { IconButton, MenuItem, TextField, - useMediaQuery, } from '@mui/material'; import { Level } from 'api-client'; import { ChangeEvent } from 'react'; @@ -39,7 +38,6 @@ export const LayersController = ({ handleZoomOut, }: LayersControllerProps) => { const [isHovered, setIsHovered] = React.useState(false); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); return ( - +
- - + +
- - + +
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> - - + + {isHovered && (
@@ -129,7 +101,7 @@ export const LayersController = ({ { @@ -139,20 +111,7 @@ export const LayersController = ({ }} /> } - label={ - isScreenHeightLessThan800 ? ( - - {layerName} - - ) : ( - layerName - ) - } + label={layerName} sx={{ margin: '0' }} /> diff --git a/packages/rmf-dashboard-framework/src/components/map/map.tsx b/packages/rmf-dashboard-framework/src/components/map/map.tsx index 676fb8c41..026d3c39a 100644 --- a/packages/rmf-dashboard-framework/src/components/map/map.tsx +++ b/packages/rmf-dashboard-framework/src/components/map/map.tsx @@ -1,4 +1,4 @@ -import { Box, styled, Typography, useMediaQuery } from '@mui/material'; +import { Box, styled, Typography } from '@mui/material'; import { Line } from '@react-three/drei'; import { Canvas, useLoader } from '@react-three/fiber'; import { BuildingMap, FleetState, Level, Lift } from 'api-client'; @@ -57,7 +57,6 @@ export interface MapProps { export const Map = styled((props: MapProps) => { const authenticator = useAuthenticator(); const { fleets: fleetResources } = useResources(); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); const rmfApi = useRmfApi(); const { showAlert } = useAppController(); const [currentLevel, setCurrentLevel] = React.useState(undefined); @@ -196,11 +195,7 @@ export const Map = styled((props: MapProps) => { }, [rmfApi, props.defaultMapLevel]); const [imageUrl, setImageUrl] = React.useState(null); - // Since the configurable zoom level is for supporting the lowest resolution - // settings, we will double it for anything that is operating within modern - // resolution settings. - const defaultZoom = isScreenHeightLessThan800 ? props.defaultZoom : props.defaultZoom * 2; - const [zoom, setZoom] = React.useState(defaultZoom); + const [zoom, setZoom] = React.useState(props.defaultZoom); const [sceneBoundingBox, setSceneBoundingBox] = React.useState(undefined); const [distance, setDistance] = React.useState(0); @@ -208,7 +203,7 @@ export const Map = styled((props: MapProps) => { const subs: Subscription[] = []; subs.push( AppEvents.zoom.subscribe((currentValue) => { - setZoom(currentValue || defaultZoom); + setZoom(currentValue || props.defaultZoom); }), ); subs.push( @@ -220,7 +215,7 @@ export const Map = styled((props: MapProps) => { const center = newSceneBoundingBox.getCenter(new Vector3()); const size = newSceneBoundingBox.getSize(new Vector3()); const distance = Math.max(size.x, size.y, size.z) * 0.7; - const newZoom = defaultZoom; + const newZoom = props.defaultZoom; AppEvents.resetCamera.next([center.x, center.y, center.z + distance, newZoom]); } setCurrentLevel(currentValue ?? undefined); @@ -232,7 +227,7 @@ export const Map = styled((props: MapProps) => { sub.unsubscribe(); } }; - }, [defaultZoom]); + }, [props.defaultZoom]); React.useEffect(() => { if (!currentLevel?.images[0]) { @@ -501,7 +496,7 @@ export const Map = styled((props: MapProps) => { const center = sceneBoundingBox.getCenter(new Vector3()); const size = sceneBoundingBox.getSize(new Vector3()); const distance = Math.max(size.x, size.y, size.z) * 0.7; - const newZoom = defaultZoom; + const newZoom = props.defaultZoom; AppEvents.resetCamera.next([center.x, center.y, center.z + distance, newZoom]); }} handleZoomIn={() => AppEvents.zoomIn.next()} diff --git a/packages/rmf-dashboard-framework/src/components/robots/mutex-group-table.tsx b/packages/rmf-dashboard-framework/src/components/robots/mutex-group-table.tsx index 5f48bbea6..2eb8e6f39 100644 --- a/packages/rmf-dashboard-framework/src/components/robots/mutex-group-table.tsx +++ b/packages/rmf-dashboard-framework/src/components/robots/mutex-group-table.tsx @@ -1,4 +1,3 @@ -import { useMediaQuery } from '@mui/material'; import { DataGrid, GridColDef, @@ -27,8 +26,6 @@ export function MutexGroupTable({ onMutexGroupClick, mutexGroups, }: MutexGroupTableProps): JSX.Element { - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); - const handleEvent: GridEventListener<'rowClick'> = ( params: GridRowParams, event: MuiEvent>, @@ -74,11 +71,7 @@ export function MutexGroupTable({ rowHeight={38} columns={columns} rowsPerPageOptions={[5]} - sx={{ - fontSize: isScreenHeightLessThan800 ? '0.7rem' : 'inherit', - }} - autoPageSize={isScreenHeightLessThan800} - density={isScreenHeightLessThan800 ? 'compact' : 'standard'} + density={'standard'} onRowClick={handleEvent} initialState={{ sorting: { diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-summary.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-summary.tsx index ea4b7e848..c12882fbc 100644 --- a/packages/rmf-dashboard-framework/src/components/robots/robot-summary.tsx +++ b/packages/rmf-dashboard-framework/src/components/robots/robot-summary.tsx @@ -23,7 +23,6 @@ import { TextField, Theme, Typography, - useMediaQuery, useTheme, } from '@mui/material'; import { @@ -104,7 +103,6 @@ const showBatteryIcon = (robot: RobotState, robotBattery: number) => { }; export const RobotSummary = React.memo(({ onClose, robot }: RobotSummaryProps) => { - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); const rmfApi = useRmfApi(); const [isOpen, setIsOpen] = React.useState(true); @@ -240,7 +238,7 @@ export const RobotSummary = React.memo(({ onClose, robot }: RobotSummaryProps) = value={message.value} sx={{ '& .MuiFilledInput-root': { - fontSize: isScreenHeightLessThan800 ? '0.8rem' : '1.15', + fontSize: '1.15', }, background: theme.palette.background.default, '&:hover': { @@ -268,15 +266,12 @@ export const RobotSummary = React.memo(({ onClose, robot }: RobotSummaryProps) = onClose(); }} fullWidth - maxWidth={isScreenHeightLessThan800 ? 'xs' : 'sm'} + maxWidth={'sm'} > - + Robot summary: {robotState?.name} @@ -311,8 +306,8 @@ export const RobotSummary = React.memo(({ onClose, robot }: RobotSummaryProps) = variant="contained" color="secondary" sx={{ - fontSize: isScreenHeightLessThan800 ? '0.8rem' : '1rem', - padding: isScreenHeightLessThan800 ? '4px 8px' : '6px 12px', + fontSize: '1rem', + padding: '6px 12px', }} /> diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-table-datagrid.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-table-datagrid.tsx index 01e1bc358..1d5c6db6b 100644 --- a/packages/rmf-dashboard-framework/src/components/robots/robot-table-datagrid.tsx +++ b/packages/rmf-dashboard-framework/src/components/robots/robot-table-datagrid.tsx @@ -1,4 +1,4 @@ -import { Box, SxProps, Typography, useMediaQuery, useTheme } from '@mui/material'; +import { Box, SxProps, Typography, useTheme } from '@mui/material'; import { DataGrid, GridCellParams, @@ -30,8 +30,6 @@ export interface RobotDataGridTableProps { } export function RobotDataGridTable({ onRobotClick, robots }: RobotDataGridTableProps): JSX.Element { - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); - const handleEvent: GridEventListener<'rowClick'> = ( params: GridRowParams, event: MuiEvent>, @@ -89,7 +87,7 @@ export function RobotDataGridTable({ onRobotClick, robots }: RobotDataGridTableP component="p" sx={{ fontWeight: 'bold', - fontSize: isScreenHeightLessThan800 ? 10 : 16, + fontSize: 16, }} > {robotDecommissioned ? 'DECOMMISSIONED' : robotStatusToUpperCase(params.row.status)} @@ -172,11 +170,7 @@ export function RobotDataGridTable({ onRobotClick, robots }: RobotDataGridTableP rowHeight={38} columns={columns} rowsPerPageOptions={[5]} - sx={{ - fontSize: isScreenHeightLessThan800 ? '0.7rem' : 'inherit', - }} - autoPageSize={isScreenHeightLessThan800} - density={isScreenHeightLessThan800 ? 'compact' : 'standard'} + density={'standard'} onRowClick={handleEvent} initialState={{ sorting: { diff --git a/packages/rmf-dashboard-framework/src/components/tasks/task-form.tsx b/packages/rmf-dashboard-framework/src/components/tasks/task-form.tsx index ffbdc8ed8..a86ca8123 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/task-form.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/task-form.tsx @@ -36,7 +36,6 @@ import { TextField, Tooltip, Typography, - useMediaQuery, useTheme, } from '@mui/material'; import { DatePicker, DateTimePicker } from '@mui/x-date-pickers'; @@ -144,7 +143,6 @@ function FavoriteTask({ setCallToUpdate, }: FavoriteTaskProps) { const theme = useTheme(); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); return ( <> @@ -174,7 +172,7 @@ function FavoriteTask({ listItemClick(); }} > - + - + @@ -242,7 +240,6 @@ interface DaySelectorSwitchProps { const DaySelectorSwitch: React.VFC = ({ disabled, onChange, value }) => { const theme = useTheme(); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); const renderChip = (idx: number, text: string) => ( = ({ disabled, onChan sx={{ '&:hover': {}, margin: theme.spacing(0.25), - fontSize: isScreenHeightLessThan800 ? '0.8rem' : '1rem', + fontSize: '1rem', }} variant={value[idx] && !disabled ? 'filled' : 'outlined'} disabled={disabled} @@ -360,8 +357,6 @@ export function TaskForm({ const [callToUpdateFavoriteTask, setCallToUpdateFavoriteTask] = React.useState(false); const [deletingFavoriteTask, setDeletingFavoriteTask] = React.useState(false); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); - // Note that we are not checking if the number of supported tasks is larger // than 0, this will cause the dashboard to fail when the create task form is // opened. This is intentional as it is a misconfiguration and will require @@ -902,7 +897,7 @@ export function TaskForm({ <> @@ -914,14 +909,10 @@ export function TaskForm({ - + - - Favorite tasks - + Favorite tasks {favoritesTasks.map((favoriteTask, index) => { return ( {validTasks.map((taskDefinition) => { return ( @@ -1120,7 +1111,7 @@ export function TaskForm({ aria-label="Save as a favorite task" variant={callToUpdateFavoriteTask ? 'contained' : 'outlined'} color="primary" - size={isScreenHeightLessThan800 ? 'small' : 'medium'} + size="medium" startIcon={callToUpdateFavoriteTask ? : } onClick={() => { !callToUpdateFavoriteTask && @@ -1141,7 +1132,7 @@ export function TaskForm({ disabled={submitting} className={classes.actionBtn} onClick={(ev) => onClose && onClose(ev, 'escapeKeyDown')} - size={isScreenHeightLessThan800 ? 'small' : 'medium'} + size="medium" > Cancel @@ -1152,7 +1143,7 @@ export function TaskForm({ disabled={submitting || !formFullyFilled || robotDispatchTarget !== null} className={classes.actionBtn} onClick={() => setOpenSchedulingDialog(true)} - size={isScreenHeightLessThan800 ? 'small' : 'medium'} + size="medium" startIcon={} > Add to Schedule @@ -1165,7 +1156,7 @@ export function TaskForm({ disabled={submitting || !formFullyFilled || robotDispatchTarget !== null} className={classes.actionBtn} onClick={() => setOpenSchedulingDialog(true)} - size={isScreenHeightLessThan800 ? 'small' : 'medium'} + size="medium" startIcon={} > Edit schedule @@ -1179,15 +1170,10 @@ export function TaskForm({ className={classes.actionBtn} aria-label="Submit Now" onClick={handleSubmitNow} - size={isScreenHeightLessThan800 ? 'small' : 'medium'} + size="medium" startIcon={} > - + Submit Now @@ -1308,18 +1294,16 @@ export function TaskForm({ } - label={ - Never - } - sx={{ fontSize: isScreenHeightLessThan800 ? '0.8rem' : '1rem' }} + control={} + label={Never} + sx={{ fontSize: '1rem' }} /> } - label={On} + control={} + label={On} /> diff --git a/packages/rmf-dashboard-framework/src/components/tasks/task-summary.tsx b/packages/rmf-dashboard-framework/src/components/tasks/task-summary.tsx index 9af94b7d8..545a1f242 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/task-summary.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/task-summary.tsx @@ -6,7 +6,6 @@ import { TextField, Theme, Typography, - useMediaQuery, useTheme, } from '@mui/material'; import Dialog from '@mui/material/Dialog'; @@ -66,7 +65,6 @@ export interface TaskSummaryProps { } export const TaskSummary = React.memo((props: TaskSummaryProps) => { - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); const rmfApi = useRmfApi(); const { onClose, task } = props; @@ -153,7 +151,7 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { value={message.value} sx={{ '& .MuiFilledInput-root': { - fontSize: isScreenHeightLessThan800 ? '0.8rem' : '1.15', + fontSize: '1.15', }, background: theme.palette.background.default, '&:hover': { @@ -181,12 +179,9 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { onClose(); }} fullWidth - maxWidth={isScreenHeightLessThan800 ? 'xs' : 'sm'} + maxWidth={'sm'} > - + Task Summary @@ -203,8 +198,8 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { variant="contained" color="secondary" sx={{ - fontSize: isScreenHeightLessThan800 ? '0.8rem' : '1rem', - padding: isScreenHeightLessThan800 ? '4px 8px' : '6px 12px', + fontSize: '1rem', + padding: '6px 12px', }} /> diff --git a/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.tsx b/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.tsx index 2286c41e7..24e0de738 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.tsx @@ -11,7 +11,6 @@ import { TableContainer, Tabs, Tooltip, - useMediaQuery, } from '@mui/material'; import { TaskStateInput as TaskState } from 'api-client'; import React from 'react'; @@ -77,6 +76,8 @@ function TabPanel(props: TabPanelProps) { ); } +const StyledDiv = styled('div'); + export const TasksWindow = React.memo( React.forwardRef( ( @@ -102,18 +103,13 @@ export const TasksWindow = React.memo( const [filterFields, setFilterFields] = React.useState({ model: undefined }); const [sortFields, setSortFields] = React.useState({ model: undefined }); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); const classes = { typography: 'MuiTypography-root', button: 'MuiButton-text', }; const StyledDiv = styled('div')(() => ({ - [`& .${classes.typography}`]: { - fontSize: isScreenHeightLessThan800 ? '0.7rem' : 'inherit', - }, - [`& .${classes.button}`]: { - fontSize: isScreenHeightLessThan800 ? '0.7rem' : 'inherit', - }, + [`& .${classes.typography}`]: { fontSize: 'inherit' }, + [`& .${classes.button}`]: { fontSize: 'inherit' }, })); React.useEffect(() => { @@ -303,12 +299,6 @@ export const TasksWindow = React.memo( @@ -353,12 +341,6 @@ export const TasksWindow = React.memo(
@@ -389,17 +369,11 @@ export const TasksWindow = React.memo( label="Queue" id={tabId(TaskTablePanel.QueueTable)} aria-controls={tabPanelId(TaskTablePanel.QueueTable)} - sx={{ - fontSize: isScreenHeightLessThan800 ? '0.7rem' : 'inherit', - }} /> diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/delivery-custom.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/delivery-custom.tsx index 6901dbc5e..86f87fd7b 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/types/delivery-custom.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/delivery-custom.tsx @@ -1,4 +1,4 @@ -import { Autocomplete, Grid, TextField, useMediaQuery, useTheme } from '@mui/material'; +import { Autocomplete, Grid, TextField, useTheme } from '@mui/material'; import React from 'react'; import { TaskBookingLabels } from '../booking-label'; @@ -355,7 +355,6 @@ export function DeliveryPickupTaskForm({ onValidate, }: DeliveryPickupTaskFormProps): React.JSX.Element { const theme = useTheme(); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); const onInputChange = (desc: DeliveryPickupTaskDescription) => { onValidate(isDeliveryPickupTaskDescriptionValid(desc, pickupPoints, dropoffPoints)); onChange(desc); @@ -385,8 +384,8 @@ export function DeliveryPickupTaskForm({ }} sx={{ '& .MuiOutlinedInput-root': { - height: isScreenHeightLessThan800 ? '3rem' : '3.5rem', - fontSize: isScreenHeightLessThan800 ? 14 : 20, + height: '3.5rem', + fontSize: 20, }, }} renderInput={(params) => ( @@ -394,7 +393,7 @@ export function DeliveryPickupTaskForm({ {...params} label="Pickup Location" required - InputLabelProps={{ style: { fontSize: isScreenHeightLessThan800 ? 14 : 20 } }} + InputLabelProps={{ style: { fontSize: 20 } }} error={ !Object.keys(pickupPoints).includes( taskDesc.phases[0].activity.description.activities[0].description, @@ -426,8 +425,8 @@ export function DeliveryPickupTaskForm({ }} sx={{ '& .MuiOutlinedInput-root': { - height: isScreenHeightLessThan800 ? '3rem' : '3.5rem', - fontSize: isScreenHeightLessThan800 ? 14 : 20, + height: '3.5rem', + fontSize: 20, }, }} renderInput={(params) => ( @@ -435,7 +434,7 @@ export function DeliveryPickupTaskForm({ {...params} label="Cart ID" required - InputLabelProps={{ style: { fontSize: isScreenHeightLessThan800 ? 14 : 20 } }} + InputLabelProps={{ style: { fontSize: 20 } }} error={ taskDesc.phases[0].activity.description.activities[1].description.description .cart_id.length === 0 @@ -463,8 +462,8 @@ export function DeliveryPickupTaskForm({ }} sx={{ '& .MuiOutlinedInput-root': { - height: isScreenHeightLessThan800 ? '3rem' : '3.5rem', - fontSize: isScreenHeightLessThan800 ? 14 : 20, + height: '3.5rem', + fontSize: 20, }, }} renderInput={(params) => ( @@ -472,7 +471,7 @@ export function DeliveryPickupTaskForm({ {...params} label="Dropoff Location" required - InputLabelProps={{ style: { fontSize: isScreenHeightLessThan800 ? 14 : 20 } }} + InputLabelProps={{ style: { fontSize: 20 } }} error={ !Object.keys(dropoffPoints).includes( taskDesc.phases[1].activity.description.activities[0].description, @@ -568,7 +567,6 @@ export function DeliveryCustomTaskForm({ onValidate, }: DeliveryCustomProps): React.JSX.Element { const theme = useTheme(); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); const onInputChange = (desc: DeliveryCustomTaskDescription) => { onValidate(isDeliveryCustomTaskDescriptionValid(desc, pickupZones, dropoffPoints)); onChange(desc); @@ -596,8 +594,8 @@ export function DeliveryCustomTaskForm({ }} sx={{ '& .MuiOutlinedInput-root': { - height: isScreenHeightLessThan800 ? '3rem' : '3.5rem', - fontSize: isScreenHeightLessThan800 ? 14 : 20, + height: '3.5rem', + fontSize: 20, }, }} renderInput={(params) => ( @@ -605,7 +603,7 @@ export function DeliveryCustomTaskForm({ {...params} label="Pickup Zone" required - InputLabelProps={{ style: { fontSize: isScreenHeightLessThan800 ? 14 : 20 } }} + InputLabelProps={{ style: { fontSize: 20 } }} error={ !pickupZones.includes( taskDesc.phases[0].activity.description.activities[0].description, @@ -640,8 +638,8 @@ export function DeliveryCustomTaskForm({ }} sx={{ '& .MuiOutlinedInput-root': { - height: isScreenHeightLessThan800 ? '3rem' : '3.5rem', - fontSize: isScreenHeightLessThan800 ? 14 : 20, + height: '3.5rem', + fontSize: 20, }, }} renderInput={(params) => ( @@ -649,7 +647,7 @@ export function DeliveryCustomTaskForm({ {...params} label="Cart ID" required - InputLabelProps={{ style: { fontSize: isScreenHeightLessThan800 ? 14 : 20 } }} + InputLabelProps={{ style: { fontSize: 20 } }} error={ taskDesc.phases[0].activity.description.activities[1].description.description .cart_id.length === 0 @@ -680,8 +678,8 @@ export function DeliveryCustomTaskForm({ }} sx={{ '& .MuiOutlinedInput-root': { - height: isScreenHeightLessThan800 ? '3rem' : '3.5rem', - fontSize: isScreenHeightLessThan800 ? 14 : 20, + height: '3.5rem', + fontSize: 20, }, }} renderInput={(params) => ( @@ -689,7 +687,7 @@ export function DeliveryCustomTaskForm({ {...params} label="Dropoff Location" required - InputLabelProps={{ style: { fontSize: isScreenHeightLessThan800 ? 14 : 20 } }} + InputLabelProps={{ style: { fontSize: 20 } }} error={ !dropoffPoints.includes( taskDesc.phases[1].activity.description.activities[0].description, diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.tsx index 707dff5e4..e69f70c2e 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.tsx @@ -9,7 +9,6 @@ import { ListItemIcon, ListItemText, TextField, - useMediaQuery, useTheme, } from '@mui/material'; import React from 'react'; @@ -119,7 +118,6 @@ export function PatrolTaskForm({ onValidate, }: PatrolTaskFormProps): React.JSX.Element { const theme = useTheme(); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); const onInputChange = (desc: PatrolTaskDescription) => { onValidate(isPatrolTaskDescriptionValid(desc)); onChange(desc); @@ -131,7 +129,7 @@ export function PatrolTaskForm({ return ( - + ( @@ -155,19 +153,19 @@ export function PatrolTaskForm({ {...params} label="Place Name" required={true} - InputLabelProps={{ style: { fontSize: isScreenHeightLessThan800 ? 14 : 20 } }} + InputLabelProps={{ style: { fontSize: 20 } }} /> )} /> - + Date: Wed, 4 Dec 2024 16:07:11 +0800 Subject: [PATCH 02/27] robot decommission Signed-off-by: Aaron Chong --- .../robots/robot-decommission.stories.tsx | 18 ++++++ .../robots/robot-decommission.test.tsx | 58 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 packages/rmf-dashboard-framework/src/components/robots/robot-decommission.stories.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/robots/robot-decommission.test.tsx diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.stories.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.stories.tsx new file mode 100644 index 000000000..14f53db9b --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.stories.tsx @@ -0,0 +1,18 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { RobotDecommissionButton } from './robot-decommission'; +import { makeRobot } from './test-utils.test'; + +export default { + title: 'RobotDecommissionButton', + component: RobotDecommissionButton, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + fleet: 'test_fleet', + robotState: makeRobot({ name: 'test_robot' }), + }, +}; diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.test.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.test.tsx new file mode 100644 index 000000000..963d29350 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.test.tsx @@ -0,0 +1,58 @@ +import { fireEvent } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { RobotDecommissionButton } from './robot-decommission'; +import { makeRobot } from './test-utils.test'; + +describe('Robot decommission button', () => { + const rmfApi = new MockRmfApi(); + // mock out some api calls so they never resolves + rmfApi.fleetsApi.decommissionRobotFleetsNameDecommissionPost = () => new Promise(() => {}); + rmfApi.fleetsApi.recommissionRobotFleetsNameRecommissionPost = () => new Promise(() => {}); + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + + it('Renders decommission button', () => { + const root = render( + + + , + ); + + expect(root.getByText('Decommission')).toBeTruthy(); + fireEvent.click(root.getByText('Decommission')); + expect(root.getByText('Decommission [test_fleet:test_robot]')).toBeTruthy(); + }); + + it('Renders recommission button', () => { + const root = render( + + + , + ); + + expect(root.getByText('Recommission')).toBeTruthy(); + fireEvent.click(root.getByText('Recommission')); + expect(root.getByText('Recommission [test_fleet:test_robot]')).toBeTruthy(); + }); +}); From 503f19bff9fa50686fa98d5d1450b962c3ad9e4b Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 4 Dec 2024 16:07:20 +0800 Subject: [PATCH 03/27] icons Signed-off-by: Aaron Chong --- .../src/components/icons/CloseFullscreen.tsx | 6 ------ .../rmf-dashboard-framework/src/components/icons/index.ts | 1 - 2 files changed, 7 deletions(-) delete mode 100644 packages/rmf-dashboard-framework/src/components/icons/CloseFullscreen.tsx diff --git a/packages/rmf-dashboard-framework/src/components/icons/CloseFullscreen.tsx b/packages/rmf-dashboard-framework/src/components/icons/CloseFullscreen.tsx deleted file mode 100644 index 7b8b4a8e4..000000000 --- a/packages/rmf-dashboard-framework/src/components/icons/CloseFullscreen.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createSvgIcon } from '@mui/material'; - -export default createSvgIcon( - , - 'CloseFullscreen', -); diff --git a/packages/rmf-dashboard-framework/src/components/icons/index.ts b/packages/rmf-dashboard-framework/src/components/icons/index.ts index 054cb77f0..211788eb7 100644 --- a/packages/rmf-dashboard-framework/src/components/icons/index.ts +++ b/packages/rmf-dashboard-framework/src/components/icons/index.ts @@ -1,2 +1 @@ -export * from './CloseFullscreen'; export * from './OpenInFull'; From bcdc611f0fc70c059c9fcbce86555657d9750f46 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 4 Dec 2024 17:14:06 +0800 Subject: [PATCH 04/27] Remove unused components, test and storybook robot decommission and mutext table Signed-off-by: Aaron Chong --- .../src/components/robots/index.ts | 3 +- .../robots/mutex-group-table.stories.tsx | 2 +- .../robots/robot-decommission.stories.tsx | 2 +- .../robots/robot-decommission.test.tsx | 2 +- .../src/components/robots/robot-info-card.tsx | 87 ---------- .../components/robots/robot-info.stories.tsx | 22 --- .../src/components/robots/robot-info.test.tsx | 62 ------- .../src/components/robots/robot-info.tsx | 117 -------------- .../robots/robot-mutex-group-table.test.tsx | 153 ++++++++++++++++++ .../robots/robot-table-datagrid.stories.tsx | 2 +- .../src/components/tasks/tasks-window.tsx | 2 - 11 files changed, 158 insertions(+), 296 deletions(-) delete mode 100644 packages/rmf-dashboard-framework/src/components/robots/robot-info-card.tsx delete mode 100644 packages/rmf-dashboard-framework/src/components/robots/robot-info.stories.tsx delete mode 100644 packages/rmf-dashboard-framework/src/components/robots/robot-info.test.tsx delete mode 100644 packages/rmf-dashboard-framework/src/components/robots/robot-info.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/robots/robot-mutex-group-table.test.tsx diff --git a/packages/rmf-dashboard-framework/src/components/robots/index.ts b/packages/rmf-dashboard-framework/src/components/robots/index.ts index 1eafe4d9c..89f9e7f6e 100644 --- a/packages/rmf-dashboard-framework/src/components/robots/index.ts +++ b/packages/rmf-dashboard-framework/src/components/robots/index.ts @@ -1,5 +1,4 @@ export * from './mutex-group-table'; -export * from './robot-info'; -export * from './robot-info-card'; +export * from './robot-decommission'; export * from './robot-summary'; export * from './robots-table'; diff --git a/packages/rmf-dashboard-framework/src/components/robots/mutex-group-table.stories.tsx b/packages/rmf-dashboard-framework/src/components/robots/mutex-group-table.stories.tsx index 18e8ec442..f992592e1 100644 --- a/packages/rmf-dashboard-framework/src/components/robots/mutex-group-table.stories.tsx +++ b/packages/rmf-dashboard-framework/src/components/robots/mutex-group-table.stories.tsx @@ -3,7 +3,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { MutexGroupTable } from './mutex-group-table'; const meta: Meta = { - title: 'MutexGroupTable', + title: 'Robots/MutexGroupTable', component: MutexGroupTable, decorators: [ (Story) => ( diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.stories.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.stories.tsx index 14f53db9b..e62ba9ce9 100644 --- a/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.stories.tsx +++ b/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.stories.tsx @@ -4,7 +4,7 @@ import { RobotDecommissionButton } from './robot-decommission'; import { makeRobot } from './test-utils.test'; export default { - title: 'RobotDecommissionButton', + title: 'Robots/RobotDecommissionButton', component: RobotDecommissionButton, } satisfies Meta; diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.test.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.test.tsx index 963d29350..e95e2daab 100644 --- a/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.test.tsx +++ b/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.test.tsx @@ -1,5 +1,5 @@ import { fireEvent } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { RmfApiProvider } from '../../hooks'; import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-info-card.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-info-card.tsx deleted file mode 100644 index d7c55a1e4..000000000 --- a/packages/rmf-dashboard-framework/src/components/robots/robot-info-card.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { Box, CardContent, Typography } from '@mui/material'; -import { RobotState, TaskStateOutput } from 'api-client'; -import React from 'react'; -import { combineLatest, EMPTY, mergeMap, of, switchMap, throttleTime } from 'rxjs'; - -import { useRmfApi } from '../../hooks'; -import { AppEvents } from '../app-events'; -import { RobotInfo } from './robot-info'; - -export const RobotInfoCard = () => { - const rmfApi = useRmfApi(); - - const [robotState, setRobotState] = React.useState(null); - const [taskState, setTaskState] = React.useState(null); - React.useEffect(() => { - const sub = AppEvents.robotSelect - .pipe( - switchMap((data) => { - if (!data) { - return of([null, null]); - } - const [fleet, name] = data; - return rmfApi.getFleetStateObs(fleet).pipe( - throttleTime(3000, undefined, { leading: true, trailing: true }), - mergeMap((fleetState) => { - const robotState = fleetState?.robots?.[name]; - const taskObs = robotState?.task_id - ? rmfApi.getTaskStateObs(robotState.task_id) - : of(null); - return robotState ? combineLatest([of(robotState), taskObs]) : EMPTY; - }), - ); - }), - ) - .subscribe(([robotState, taskState]) => { - setRobotState(robotState); - setTaskState(taskState); - }); - return () => sub.unsubscribe(); - }, [rmfApi]); - - const taskProgress = React.useMemo(() => { - if ( - !taskState || - !taskState.estimate_millis || - !taskState.unix_millis_start_time || - !taskState.unix_millis_finish_time - ) { - return undefined; - } - return Math.min( - 1.0 - - taskState.estimate_millis / - (taskState.unix_millis_finish_time - taskState.unix_millis_start_time), - 1, - ); - }, [taskState]); - - return ( - - {robotState ? ( - - ) : ( - - - Click on a robot to view more information - - - )} - - ); -}; - -export default RobotInfoCard; diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-info.stories.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-info.stories.tsx deleted file mode 100644 index 2097ae4b3..000000000 --- a/packages/rmf-dashboard-framework/src/components/robots/robot-info.stories.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; - -import { RobotInfo } from './robot-info'; - -export default { - title: 'Robots/Detailed Info', - component: RobotInfo, -} satisfies Meta; - -type Story = StoryObj; - -export const Default: Story = { - name: 'Detailed Info', - args: { - robotName: 'Robot Name', - assignedTask: 'mytask', - battery: 0.5, - taskProgress: 0.5, - taskStatus: 'underway', - estFinishTime: Date.now(), - }, -}; diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-info.test.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-info.test.tsx deleted file mode 100644 index ea57ca3f6..000000000 --- a/packages/rmf-dashboard-framework/src/components/robots/robot-info.test.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { render } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; - -import { RobotInfo } from './robot-info'; - -describe('RobotInfo', () => { - it('information renders correctly', () => { - const root = render( - , - ); - expect(() => root.getByText('test_robot')).not.toThrow(); - expect(() => root.getByText('test_task')).not.toThrow(); - expect(() => root.getByText('50.00%')).not.toThrow(); // battery - expect(() => root.getByText('60%')).not.toThrow(); // task progress - expect(() => root.getByText(/.*underway/)).not.toThrow(); - // TODO: use a less convoluted test when - // https://github.com/testing-library/react-testing-library/issues/1160 - // is resolved. - expect(() => - root.getByText((_, node) => { - if (!node) { - return false; - } - const hasText = (node: Element) => node.textContent === new Date(0).toLocaleString(); - const nodeHasText = hasText(node); - const childrenDontHaveText = Array.from(node.children).every((child) => !hasText(child)); - return nodeHasText && childrenDontHaveText; - }), - ).not.toThrow(); - }); - - describe('Task status', () => { - it('shows no task when there is no assigned task and task status', () => { - const root = render(); - expect(() => root.getByText(/No Task/)).not.toThrow(); - }); - - it('shows unknown when there is an assigned task but no status', () => { - const root = render( - , - ); - expect(() => root.getByText(/Unknown/)).not.toThrow(); - }); - }); - - it('defaults to 0% when no battery is available', () => { - const root = render(); - expect(() => root.getByText('0%')).not.toThrow(); - }); -}); diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-info.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-info.tsx deleted file mode 100644 index 704c2062c..000000000 --- a/packages/rmf-dashboard-framework/src/components/robots/robot-info.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Button, Divider, Grid, styled, Typography, useTheme } from '@mui/material'; -import type { TaskStateOutput as TaskState } from 'api-client'; - -import { CircularProgressBar } from './circular-progress-bar'; -import { LinearProgressBar } from './linear-progress-bar'; - -function getTaskStatusDisplay(assignedTask?: string, taskStatus?: string | null) { - if (assignedTask && !taskStatus) { - return 'Unknown'; - } - if (assignedTask && taskStatus) { - return taskStatus; - } else { - return 'No Task'; - } -} - -const classes = { - button: 'robot-info-button', -}; -const StyledDiv = styled('div')(() => ({ - [`& .${classes.button}`]: { - '&:hover': { - background: 'none', - cursor: 'default', - }, - }, -})); - -type TaskStatus = Required['status']; - -export interface RobotInfoProps { - robotName: string; - battery?: number; - assignedTask?: string; - taskStatus?: TaskStatus; - taskProgress?: number; - estFinishTime?: number; -} - -const finishedStatus: TaskStatus[] = ['failed', 'completed', 'skipped', 'killed', 'canceled']; - -export function RobotInfo({ - robotName, - battery, - assignedTask, - taskStatus, - taskProgress, - estFinishTime, -}: RobotInfoProps): JSX.Element { - const theme = useTheme(); - const hasConcreteEndTime = taskStatus && taskStatus in finishedStatus; - - return ( - - - {robotName} - - -
- - - - {`Task Progress - ${getTaskStatusDisplay(assignedTask, taskStatus)}`} - - - - {taskProgress && } - - - - Assigned Tasks - - - - - - - - Battery - - - - - {!hasConcreteEndTime && 'Est. '}Finish Time - - - - - {`${battery ? (battery * 100).toFixed(2) : 0}%`} - - - - - - -
- ); -} diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-mutex-group-table.test.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-mutex-group-table.test.tsx new file mode 100644 index 000000000..1f4047965 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/robots/robot-mutex-group-table.test.tsx @@ -0,0 +1,153 @@ +import { fireEvent } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { RobotMutexGroupsTable } from './robot-mutex-group-table'; + +describe('Robot mutex groups table', () => { + it('Renders robot mutex groups empty table', () => { + const rmfApi = new MockRmfApi(); + // mock out some api calls so they never resolves + rmfApi.fleetsApi.getFleetsFleetsGet = () => new Promise(() => {}); + rmfApi.fleetsApi.unlockMutexGroupFleetsNameUnlockMutexGroupPost = () => new Promise(() => {}); + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + const root = render( + + + , + ); + + expect(root.getByText('Group')).toBeTruthy(); + expect(root.getByText('Locked')).toBeTruthy(); + expect(root.getByText('Waiting')).toBeTruthy(); + }); + + it('Renders robot mutex groups table with a locked mutex', async () => { + const rmfApi = new MockRmfApi(); + // mock out some api calls so they never resolves + const mockFleets = [ + { + name: 'fleet_1', + robots: { + robot_1: { + name: 'robot_1', + mutex_groups: { + locked: ['mutex_group_1'], + requesting: [], + }, + }, + }, + }, + { + name: 'fleet_2', + robots: { + robot_2: { + name: 'robot_2', + mutex_groups: { + locked: [], + requesting: [], + }, + }, + }, + }, + ]; + rmfApi.fleetsApi.getFleetsFleetsGet = vi.fn().mockResolvedValue({ data: mockFleets }); + rmfApi.fleetsApi.unlockMutexGroupFleetsNameUnlockMutexGroupPost = () => new Promise(() => {}); + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + const root = render( + + + , + ); + + expect(root.getByText('Group')).toBeTruthy(); + expect(root.getByText('Locked')).toBeTruthy(); + expect(root.getByText('Waiting')).toBeTruthy(); + + await root.findByText('mutex_group_1'); + expect(root.getByText('mutex_group_1')).toBeTruthy(); + expect(root.getByText('fleet_1/robot_1')).toBeTruthy(); + expect(root.queryByText('fleet_2/robot_2')).not.toBeTruthy(); + }); + + it('Renders robot mutex groups table with a locked mutex that is waited on', async () => { + const rmfApi = new MockRmfApi(); + // mock out some api calls so they never resolves + const mockFleets = [ + { + name: 'fleet_1', + robots: { + robot_1: { + name: 'robot_1', + mutex_groups: { + locked: ['mutex_group_1'], + requesting: [], + }, + }, + }, + }, + { + name: 'fleet_2', + robots: { + robot_2: { + name: 'robot_2', + mutex_groups: { + locked: [], + requesting: ['mutex_group_1'], + }, + }, + }, + }, + ]; + rmfApi.fleetsApi.getFleetsFleetsGet = vi.fn().mockResolvedValue({ data: mockFleets }); + const mockUnlock = vi.fn().mockImplementation(async () => { + // Simulate some asynchronous operation that has no return value + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + // rmfApi.fleetsApi.unlockMutexGroupFleetsNameUnlockMutexGroupPost = () => new Promise(() => {}); + rmfApi.fleetsApi.unlockMutexGroupFleetsNameUnlockMutexGroupPost = mockUnlock; + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + const root = render( + + + , + ); + + expect(root.getByText('Group')).toBeTruthy(); + expect(root.getByText('Locked')).toBeTruthy(); + expect(root.getByText('Waiting')).toBeTruthy(); + + await root.findByText('mutex_group_1'); + expect(root.getByText('mutex_group_1')).toBeTruthy(); + expect(root.getByText('fleet_1/robot_1')).toBeTruthy(); + expect(root.getByText('fleet_2/robot_2')).toBeTruthy(); + + // Try to unlock + fireEvent.click(root.getByText('mutex_group_1')); + expect( + root.getByText('Confirm unlock mutex group [mutex_group_1] for [fleet_1/robot_1]?'), + ).toBeTruthy(); + expect(root.getByText('Confirm unlock')).toBeTruthy(); + fireEvent.click(root.getByText('Confirm unlock')); + expect(mockUnlock).toBeCalled(); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-table-datagrid.stories.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-table-datagrid.stories.tsx index 35b0c189f..740b528b4 100644 --- a/packages/rmf-dashboard-framework/src/components/robots/robot-table-datagrid.stories.tsx +++ b/packages/rmf-dashboard-framework/src/components/robots/robot-table-datagrid.stories.tsx @@ -4,7 +4,7 @@ import { ApiServerModelsRmfApiRobotStateStatus as RobotStatus } from 'api-client import { RobotDataGridTable } from './robot-table-datagrid'; const meta: Meta = { - title: 'RobotDataGridTable', + title: 'Robots/RobotDataGridTable', component: RobotDataGridTable, decorators: [ (Story) => ( diff --git a/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.tsx b/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.tsx index 24e0de738..bd697f144 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.tsx @@ -76,8 +76,6 @@ function TabPanel(props: TabPanelProps) { ); } -const StyledDiv = styled('div'); - export const TasksWindow = React.memo( React.forwardRef( ( From cfe5baba2c17c9e75580ee0b08ac195ea1932b26 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 4 Dec 2024 17:20:46 +0800 Subject: [PATCH 05/27] Test mock posts Signed-off-by: Aaron Chong --- .../robots/robot-decommission.test.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.test.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.test.tsx index e95e2daab..27f8f073e 100644 --- a/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.test.tsx +++ b/packages/rmf-dashboard-framework/src/components/robots/robot-decommission.test.tsx @@ -1,5 +1,5 @@ import { fireEvent } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { RmfApiProvider } from '../../hooks'; import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; @@ -9,8 +9,16 @@ import { makeRobot } from './test-utils.test'; describe('Robot decommission button', () => { const rmfApi = new MockRmfApi(); // mock out some api calls so they never resolves - rmfApi.fleetsApi.decommissionRobotFleetsNameDecommissionPost = () => new Promise(() => {}); - rmfApi.fleetsApi.recommissionRobotFleetsNameRecommissionPost = () => new Promise(() => {}); + const mockDecommission = vi.fn().mockImplementation(async () => { + // Simulate some asynchronous operation that has no return value + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + const mockRecommission = vi.fn().mockImplementation(async () => { + // Simulate some asynchronous operation that has no return value + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + rmfApi.fleetsApi.decommissionRobotFleetsNameDecommissionPost = mockDecommission; + rmfApi.fleetsApi.recommissionRobotFleetsNameRecommissionPost = mockRecommission; const Base = (props: React.PropsWithChildren<{}>) => { return ( @@ -32,6 +40,9 @@ describe('Robot decommission button', () => { expect(root.getByText('Decommission')).toBeTruthy(); fireEvent.click(root.getByText('Decommission')); expect(root.getByText('Decommission [test_fleet:test_robot]')).toBeTruthy(); + expect(root.getByText('Confirm')); + fireEvent.click(root.getByText('Confirm')); + expect(mockDecommission).toHaveBeenCalledOnce(); }); it('Renders recommission button', () => { @@ -54,5 +65,8 @@ describe('Robot decommission button', () => { expect(root.getByText('Recommission')).toBeTruthy(); fireEvent.click(root.getByText('Recommission')); expect(root.getByText('Recommission [test_fleet:test_robot]')).toBeTruthy(); + expect(root.getByText('Confirm')); + fireEvent.click(root.getByText('Confirm')); + expect(mockRecommission).toHaveBeenCalledOnce(); }); }); From ca8450e0bb8ecf8cfd6a23bceb4aee546593f31a Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Thu, 5 Dec 2024 10:36:55 +0800 Subject: [PATCH 06/27] robot-summary Signed-off-by: Aaron Chong --- .../robots/robot-summary.stories.tsx | 31 ++++++++ .../components/robots/robot-summary.test.tsx | 73 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 packages/rmf-dashboard-framework/src/components/robots/robot-summary.stories.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/robots/robot-summary.test.tsx diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-summary.stories.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-summary.stories.tsx new file mode 100644 index 000000000..adda3214e --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/robots/robot-summary.stories.tsx @@ -0,0 +1,31 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { ApiServerModelsRmfApiRobotStateStatus as Status } from 'api-client'; + +import { RobotSummary } from './robot-summary'; + +export default { + title: 'Robots/RobotSummary', + component: RobotSummary, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + onClose: () => {}, + robot: { + fleet: 'test_fleet', + name: 'test_robot', + status: Status.Idle, + battery: 60, + estFinishTime: 1000000, + lastUpdateTime: 900000, + level: 'L1', + commission: { + dispatch_tasks: true, + direct_tasks: true, + idle_behavior: true, + }, + }, + }, +}; diff --git a/packages/rmf-dashboard-framework/src/components/robots/robot-summary.test.tsx b/packages/rmf-dashboard-framework/src/components/robots/robot-summary.test.tsx new file mode 100644 index 000000000..b456a85a6 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/robots/robot-summary.test.tsx @@ -0,0 +1,73 @@ +import { waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { ApiServerModelsRmfApiRobotStateStatus as Status, FleetState } from 'api-client'; +import { act } from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { makeTaskState } from './../tasks/make-tasks.test'; +import { RobotSummary } from './robot-summary'; +import { RobotTableData } from './robot-table-datagrid'; +import { makeRobot } from './test-utils.test'; + +describe('Robot summary', () => { + const rmfApi = new MockRmfApi(); + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + + it('Renders robot summary', async () => { + const onCloseMock = vi.fn(); + const robotTableData: RobotTableData = { + fleet: 'test_fleet', + name: 'test_robot', + status: Status.Idle, + battery: 0.6, + estFinishTime: 1000000, + lastUpdateTime: 900000, + level: 'L1', + commission: { + dispatch_tasks: true, + direct_tasks: true, + idle_behavior: true, + }, + }; + + const root = render( + + + , + ); + + // Create the subject for the fleet + rmfApi.getFleetStateObs('test_fleet'); + const mockFleetState: FleetState = { + name: 'test_fleet', + robots: { + ['test_robot']: makeRobot({ name: 'test_robot', task_id: 'test_task_id' }), + }, + }; + act(() => { + rmfApi.fleetStateObsStore['test_fleet'].next(mockFleetState); + }); + + // Create the subject for the task + rmfApi.getTaskStateObs('test_task_id'); + const mockTaskState = makeTaskState('test_task_id'); + act(() => { + rmfApi.taskStateObsStore['test_task_id'].next(mockTaskState); + }); + + expect(root.getByText(/Robot summary/i)).toBeTruthy(); + expect(root.getByText(/test_robot/i)).toBeTruthy(); + expect(root.getByText(/Assigned tasks/i)).toBeTruthy(); + expect(root.getByText(/test_task_id/i)).toBeTruthy(); + userEvent.keyboard('{Escape}'); + await waitFor(() => expect(onCloseMock).toHaveBeenCalledTimes(1)); + }); +}); From 30a8f17bd0044f5c9225acc2af6a997bef92f503 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Thu, 5 Dec 2024 15:39:48 +0800 Subject: [PATCH 07/27] robots-table Signed-off-by: Aaron Chong --- .../robots/robots-table.stories.tsx | 14 ++++++ .../components/robots/robots-table.test.tsx | 50 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 packages/rmf-dashboard-framework/src/components/robots/robots-table.stories.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/robots/robots-table.test.tsx diff --git a/packages/rmf-dashboard-framework/src/components/robots/robots-table.stories.tsx b/packages/rmf-dashboard-framework/src/components/robots/robots-table.stories.tsx new file mode 100644 index 000000000..65ffd7919 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/robots/robots-table.stories.tsx @@ -0,0 +1,14 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { RobotsTable } from './robots-table'; + +export default { + title: 'Robots/RobotsTable', + component: RobotsTable, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; diff --git a/packages/rmf-dashboard-framework/src/components/robots/robots-table.test.tsx b/packages/rmf-dashboard-framework/src/components/robots/robots-table.test.tsx new file mode 100644 index 000000000..fb9571827 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/robots/robots-table.test.tsx @@ -0,0 +1,50 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { makeTaskState } from './../tasks/make-tasks.test'; +import { RobotsTable } from './robots-table'; +import { makeRobot } from './test-utils.test'; + +describe('Robots table', () => { + const rmfApi = new MockRmfApi(); + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + + it('Renders robots table', async () => { + const mockReturnFleets = vi.fn().mockResolvedValue({ + data: [ + { + name: 'test_fleet', + robots: { + ['test_robot1']: makeRobot({ name: 'test_robot1', task_id: 'test_task_id' }), + ['test_robot2']: makeRobot({ name: 'test_robot2', task_id: undefined }), + }, + }, + ], + }); + const mockTaskState = makeTaskState('test_task_id'); + mockTaskState.unix_millis_finish_time = 0; + const mockReturnTasks = vi.fn().mockResolvedValue({ + data: [mockTaskState], + }); + + rmfApi.fleetsApi.getFleetsFleetsGet = mockReturnFleets; + rmfApi.tasksApi.queryTaskStatesTasksGet = mockReturnTasks; + + const root = render( + + + , + ); + + await root.findByText(/test_robot1/i); + expect(root.getByText(/test_robot1/i)).toBeTruthy(); + expect(root.getByText(/test_robot2/i)).toBeTruthy(); + }); +}); From 66e33980413917413a1ebb13c6b79b7c883b332b Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Fri, 6 Dec 2024 10:43:52 +0800 Subject: [PATCH 08/27] cleaned up robots/utils, test compose-clean Signed-off-by: Aaron Chong --- .../src/components/robots/utils.ts | 24 ------- .../tasks/types/compose-clean.stories.tsx | 19 ++++++ .../tasks/types/compose-clean.test.tsx | 66 +++++++++++++++++++ .../components/tasks/types/compose-clean.tsx | 24 ++++--- 4 files changed, 99 insertions(+), 34 deletions(-) create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.stories.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.test.tsx diff --git a/packages/rmf-dashboard-framework/src/components/robots/utils.ts b/packages/rmf-dashboard-framework/src/components/robots/utils.ts index 1a9c7f752..df9d6e946 100644 --- a/packages/rmf-dashboard-framework/src/components/robots/utils.ts +++ b/packages/rmf-dashboard-framework/src/components/robots/utils.ts @@ -1,5 +1,4 @@ import { ApiServerModelsRmfApiRobotStateStatus as Status2 } from 'api-client'; -import { RobotMode as RmfRobotMode } from 'rmf-models/ros/rmf_fleet_msgs/msg'; /** * Returns a uniquely identifiable string representing a robot. @@ -28,26 +27,3 @@ export function robotStatusToUpperCase(status: Status2): string { return `UNKNOWN (${status})`; } } - -export function robotModeToString(robotMode: RmfRobotMode): string { - switch (robotMode.mode) { - case RmfRobotMode.MODE_CHARGING: - return 'Charging'; - case RmfRobotMode.MODE_DOCKING: - return 'Docking'; - case RmfRobotMode.MODE_EMERGENCY: - return 'Emergency'; - case RmfRobotMode.MODE_GOING_HOME: - return 'Going Home'; - case RmfRobotMode.MODE_IDLE: - return 'Idle'; - case RmfRobotMode.MODE_MOVING: - return 'Moving'; - case RmfRobotMode.MODE_PAUSED: - return 'Paused'; - case RmfRobotMode.MODE_WAITING: - return 'Waiting'; - default: - return `Unknown (${robotMode.mode})`; - } -} diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.stories.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.stories.tsx new file mode 100644 index 000000000..d19e963f1 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.stories.tsx @@ -0,0 +1,19 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { ComposeCleanTaskForm, makeDefaultComposeCleanTaskDescription } from './compose-clean'; + +export default { + title: 'Tasks/ComposeCleanTaskForm', + component: ComposeCleanTaskForm, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + taskDesc: makeDefaultComposeCleanTaskDescription(), + cleaningZones: ['clean_zone_1', 'clean_zone_2'], + onChange: () => {}, + onValidate: () => {}, + }, +}; diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.test.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.test.tsx new file mode 100644 index 000000000..91683bcd1 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.test.tsx @@ -0,0 +1,66 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { + ComposeCleanTaskDefinition, + ComposeCleanTaskForm, + insertCleaningZone, + isComposeCleanTaskDescriptionValid, + makeComposeCleanTaskBookingLabel, + makeComposeCleanTaskShortDescription, + makeDefaultComposeCleanTaskDescription, +} from './compose-clean'; + +describe('Compose clean task form', () => { + it('Renders compose clean task form', () => { + const onChange = vi.fn(); + const onValidate = vi.fn(); + + render( + , + ); + + expect(screen.getByText('Cleaning Zone')).toBeDefined(); + }); + + it('Insert cleaning zone into task description', () => { + const desc = makeDefaultComposeCleanTaskDescription(); + const insertedDesc = insertCleaningZone(desc, 'clean_zone_3'); + expect(insertedDesc.phases[0].activity.description.activities[0].description).toBe( + 'clean_zone_3', + ); + expect( + insertedDesc.phases[0].activity.description.activities[1].description + .expected_finish_location, + ).toBe('clean_zone_3'); + expect( + insertedDesc.phases[0].activity.description.activities[1].description.description.zone, + ).toBe('clean_zone_3'); + }); + + it('Validate task description', () => { + const desc = makeDefaultComposeCleanTaskDescription(); + const insertedDesc = insertCleaningZone(desc, 'clean_zone_3'); + expect(isComposeCleanTaskDescriptionValid(insertedDesc)).toBeTruthy(); + }); + + it('Booking label', () => { + const desc = makeDefaultComposeCleanTaskDescription(); + const insertedDesc = insertCleaningZone(desc, 'clean_zone_3'); + const bookingLabel = makeComposeCleanTaskBookingLabel(insertedDesc); + expect(bookingLabel.task_definition_id).toBe(ComposeCleanTaskDefinition.taskDefinitionId); + expect(bookingLabel.destination).toBe('clean_zone_3'); + }); + + it('Short description', () => { + const desc = makeDefaultComposeCleanTaskDescription(); + const insertedDesc = insertCleaningZone(desc, 'clean_zone_3'); + const defaultShortDesc = makeComposeCleanTaskShortDescription(insertedDesc, undefined); + expect(defaultShortDesc).toBe('[Clean] zone [clean_zone_3]'); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.tsx index 6865f944b..f9b081620 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.tsx @@ -108,6 +108,16 @@ export function makeComposeCleanTaskShortDescription( }]`; } +export function insertCleaningZone( + taskDesc: ComposeCleanTaskDescription, + zone: string, +): ComposeCleanTaskDescription { + taskDesc.phases[0].activity.description.activities[0].description = zone; + taskDesc.phases[0].activity.description.activities[1].description.expected_finish_location = zone; + taskDesc.phases[0].activity.description.activities[1].description.description.zone = zone; + return taskDesc; +} + interface ComposeCleanTaskFormProps { taskDesc: ComposeCleanTaskDescription; cleaningZones: string[]; @@ -135,19 +145,13 @@ export function ComposeCleanTaskForm({ value={taskDesc.phases[0].activity.description.activities[0].description} onChange={(_ev, newValue) => { const zone = newValue ?? ''; - taskDesc.phases[0].activity.description.activities[0].description = zone; - taskDesc.phases[0].activity.description.activities[1].description.expected_finish_location = - zone; - taskDesc.phases[0].activity.description.activities[1].description.description.zone = zone; - onInputChange(taskDesc); + const updatedTaskDesc = insertCleaningZone(taskDesc, zone); + onInputChange(updatedTaskDesc); }} onBlur={(ev) => { const zone = (ev.target as HTMLInputElement).value; - taskDesc.phases[0].activity.description.activities[0].description = zone; - taskDesc.phases[0].activity.description.activities[1].description.expected_finish_location = - zone; - taskDesc.phases[0].activity.description.activities[1].description.description.zone = zone; - onInputChange(taskDesc); + const updatedTaskDesc = insertCleaningZone(taskDesc, zone); + onInputChange(updatedTaskDesc); }} renderInput={(params) => } /> From f38fb481a9faff024eb7a9232df4d5cb11b76fb1 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Fri, 6 Dec 2024 11:10:46 +0800 Subject: [PATCH 09/27] custom-compose Signed-off-by: Aaron Chong --- .../tasks/types/compose-clean.test.tsx | 2 ++ .../tasks/types/custom-compose.test.tsx | 31 +++++++++++++++++++ .../tasks/types/cutsom-compose.stories.tsx | 18 +++++++++++ 3 files changed, 51 insertions(+) create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/types/custom-compose.test.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/types/cutsom-compose.stories.tsx diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.test.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.test.tsx index 91683bcd1..074a341e3 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.test.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/compose-clean.test.tsx @@ -45,6 +45,8 @@ describe('Compose clean task form', () => { it('Validate task description', () => { const desc = makeDefaultComposeCleanTaskDescription(); + const emptyZoneDesc = insertCleaningZone(desc, ''); + expect(isComposeCleanTaskDescriptionValid(emptyZoneDesc)).not.toBeTruthy(); const insertedDesc = insertCleaningZone(desc, 'clean_zone_3'); expect(isComposeCleanTaskDescriptionValid(insertedDesc)).toBeTruthy(); }); diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/custom-compose.test.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/custom-compose.test.tsx new file mode 100644 index 000000000..718a93d6f --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/custom-compose.test.tsx @@ -0,0 +1,31 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { CustomComposeTaskForm } from './custom-compose'; + +describe('Custom compose task form', () => { + it('Renders custom compose task form', async () => { + const onChange = vi.fn(); + const onValidate = vi.fn(); + + render(); + }); + + it('CustomComposeTaskForm validates input', () => { + const onChange = vi.fn(); + const onValidate = vi.fn(); + + render(); + + const textArea = screen.getByRole('textbox', { name: /multiline/i }); + + // Invalid input (invalid JSON) + fireEvent.change(textArea, { target: { value: 'invalid json' } }); + expect(onValidate).toHaveBeenCalledWith(false); + + // Valid input + const validTaskDesc = '{"valid": "json"}'; + fireEvent.change(textArea, { target: { value: validTaskDesc } }); + expect(onValidate).toHaveBeenCalledWith(true); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/cutsom-compose.stories.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/cutsom-compose.stories.tsx new file mode 100644 index 000000000..f31f76dff --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/cutsom-compose.stories.tsx @@ -0,0 +1,18 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { CustomComposeTaskForm } from './custom-compose'; + +export default { + title: 'Tasks/CustomComposeTaskForm', + component: CustomComposeTaskForm, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + taskDesc: '', + onChange: () => {}, + onValidate: () => {}, + }, +}; From bca4188398a01ed94d840ff4797e662a8d52a7d2 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Fri, 6 Dec 2024 14:03:47 +0800 Subject: [PATCH 10/27] patrol Signed-off-by: Aaron Chong --- .../tasks/types/custom-compose.test.tsx | 22 ++++- .../components/tasks/types/custom-compose.tsx | 2 +- .../components/tasks/types/patrol.stories.tsx | 19 ++++ .../components/tasks/types/patrol.test.tsx | 89 +++++++++++++++++++ .../src/components/tasks/types/patrol.tsx | 20 +++-- 5 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/types/patrol.stories.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/types/patrol.test.tsx diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/custom-compose.test.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/custom-compose.test.tsx index 718a93d6f..e14c7e18f 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/types/custom-compose.test.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/custom-compose.test.tsx @@ -1,7 +1,13 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import { CustomComposeTaskForm } from './custom-compose'; +import { + CustomComposeTaskDefinition, + CustomComposeTaskForm, + isCustomTaskDescriptionValid, + makeCustomComposeTaskBookingLabel, + makeCustomComposeTaskShortDescription, +} from './custom-compose'; describe('Custom compose task form', () => { it('Renders custom compose task form', async () => { @@ -28,4 +34,18 @@ describe('Custom compose task form', () => { fireEvent.change(textArea, { target: { value: validTaskDesc } }); expect(onValidate).toHaveBeenCalledWith(true); }); + + it('Validate description', () => { + expect(isCustomTaskDescriptionValid('invalid json')).not.toBeTruthy(); + expect(isCustomTaskDescriptionValid('{"valid": "json"}')).toBeTruthy(); + }); + + it('Booking label', () => { + const bookingLabel = makeCustomComposeTaskBookingLabel(); + expect(bookingLabel.task_definition_id).toBe(CustomComposeTaskDefinition.taskDefinitionId); + }); + + it('Short description', () => { + expect(makeCustomComposeTaskShortDescription('{"valid": "json"}')).toBe('{"valid": "json"}'); + }); }); diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/custom-compose.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/custom-compose.tsx index bd256ddf7..00a1b58a2 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/types/custom-compose.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/custom-compose.tsx @@ -23,7 +23,7 @@ export function makeCustomComposeTaskShortDescription(desc: CustomComposeTaskDes return desc; } -const isCustomTaskDescriptionValid = (taskDescription: string): boolean => { +export const isCustomTaskDescriptionValid = (taskDescription: string): boolean => { if (taskDescription.length === 0) { return false; } diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.stories.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.stories.tsx new file mode 100644 index 000000000..01bd0b397 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.stories.tsx @@ -0,0 +1,19 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { makeDefaultPatrolTaskDescription, PatrolTaskForm } from './patrol'; + +export default { + title: 'Tasks/PatrolTaskForm', + component: PatrolTaskForm, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + taskDesc: makeDefaultPatrolTaskDescription(), + patrolWaypoints: ['waypoint_1', 'waypoint_2', 'waypoint_3'], + onChange: () => {}, + onValidate: () => {}, + }, +}; diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.test.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.test.tsx new file mode 100644 index 000000000..1f670bb3d --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.test.tsx @@ -0,0 +1,89 @@ +import { fireEvent, render, screen, within } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { + addPlaceToPatrolTaskDescription, + isPatrolTaskDescriptionValid, + makeDefaultPatrolTaskDescription, + makePatrolTaskBookingLabel, + makePatrolTaskShortDescription, + PatrolTaskDefinition, + PatrolTaskForm, +} from './patrol'; + +const mockWaypoints = ['waypoint_1', 'waypoint_2', 'waypoint_3']; + +describe('Patrol task form', () => { + it('PatrolTaskForm renders, changes and validates', async () => { + const onChange = vi.fn(); + const onValidate = vi.fn(); + + render( + , + ); + + const autocomplete = screen.getByTestId('place-name'); + const input = within(autocomplete).getByLabelText(/place name/i); + autocomplete.focus(); + fireEvent.change(input, { target: { value: 'a' } }); + fireEvent.keyDown(autocomplete, { key: 'ArrowDown' }); + fireEvent.keyDown(autocomplete, { key: 'Enter' }); + + expect(onChange).toHaveBeenCalled(); + expect(onValidate).toHaveBeenCalled(); + }); + + it('PatrolTaskForm renders and has places', async () => { + const onChange = vi.fn(); + const onValidate = vi.fn(); + + const desc = makeDefaultPatrolTaskDescription(); + const updatedDesc = addPlaceToPatrolTaskDescription(desc, 'waypoint_1'); + + const root = render( + , + ); + + expect(root.getByText(/waypoint_1/i)); + }); + + it('booking label', () => { + let desc = makeDefaultPatrolTaskDescription(); + desc = addPlaceToPatrolTaskDescription(desc, 'waypoint_1'); + let label = makePatrolTaskBookingLabel(desc); + expect(label.task_definition_id).toBe(PatrolTaskDefinition.taskDefinitionId); + expect(label.destination).toBe('waypoint_1'); + + desc = addPlaceToPatrolTaskDescription(desc, 'waypoint_2'); + label = makePatrolTaskBookingLabel(desc); + expect(label.task_definition_id).toBe(PatrolTaskDefinition.taskDefinitionId); + expect(label.destination).toBe('waypoint_2'); + }); + + it('validity', () => { + let desc = makeDefaultPatrolTaskDescription(); + expect(isPatrolTaskDescriptionValid(desc)).not.toBeTruthy(); + + desc = addPlaceToPatrolTaskDescription(desc, 'waypoint_1'); + expect(isPatrolTaskDescriptionValid(desc)).toBeTruthy(); + }); + + it('short description', () => { + let desc = makeDefaultPatrolTaskDescription(); + desc = addPlaceToPatrolTaskDescription(desc, 'waypoint_1'); + desc = addPlaceToPatrolTaskDescription(desc, 'waypoint_2'); + expect(makePatrolTaskShortDescription(desc, undefined)).toBe( + '[Patrol] [1] round/s, along [waypoint_1], [waypoint_2]', + ); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.tsx index e69f70c2e..eb3cd20d8 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.tsx @@ -76,6 +76,7 @@ interface PlaceListProps { function PlaceList({ places, onClick }: PlaceListProps) { const theme = useTheme(); + console.log(places); return ( ( onClick(index)}> @@ -104,6 +106,17 @@ function PlaceList({ places, onClick }: PlaceListProps) { ); } +export function addPlaceToPatrolTaskDescription( + taskDesc: PatrolTaskDescription, + place: string, +): PatrolTaskDescription { + const updatedTaskDesc = { + ...taskDesc, + places: taskDesc.places.concat(place).filter((el: string) => el), + }; + return updatedTaskDesc; +} + interface PatrolTaskFormProps { taskDesc: PatrolTaskDescription; patrolWaypoints: string[]; @@ -132,15 +145,12 @@ export function PatrolTaskForm({ - newValue !== null && - onInputChange({ - ...taskDesc, - places: taskDesc.places.concat(newValue).filter((el: string) => el), - }) + newValue !== null && onInputChange(addPlaceToPatrolTaskDescription(taskDesc, newValue)) } sx={{ '& .MuiOutlinedInput-root': { From 2ac7e96d67e2bd5fad59b5d205e5031842353e81 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Fri, 6 Dec 2024 16:25:30 +0800 Subject: [PATCH 11/27] delivery Signed-off-by: Aaron Chong --- .../tasks/types/delivery.stories.tsx | 20 +++ .../components/tasks/types/delivery.test.tsx | 163 ++++++++++++++++++ .../src/components/tasks/types/delivery.tsx | 10 +- .../src/components/tasks/types/patrol.tsx | 1 - 4 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/types/delivery.stories.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/types/delivery.test.tsx diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/delivery.stories.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/delivery.stories.tsx new file mode 100644 index 000000000..88870ee22 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/delivery.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { DeliveryTaskForm, makeDefaultDeliveryTaskDescription } from './delivery'; + +export default { + title: 'Tasks/DeliveryTaskForm', + component: DeliveryTaskForm, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + taskDesc: makeDefaultDeliveryTaskDescription(), + pickupPoints: { pickup_1: 'handler_1', pickup_2: 'handler_2' }, + dropoffPoints: { dropoff_1: 'handler_3', dropoff_2: 'handler_4' }, + onChange: () => {}, + onValidate: () => {}, + }, +}; diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/delivery.test.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/delivery.test.tsx new file mode 100644 index 000000000..4407c46d7 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/delivery.test.tsx @@ -0,0 +1,163 @@ +import { fireEvent, render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it, vi } from 'vitest'; + +import { + DeliveryTaskDefinition, + DeliveryTaskForm, + isDeliveryTaskDescriptionValid, + makeDefaultDeliveryTaskDescription, + makeDeliveryTaskBookingLabel, + makeDeliveryTaskShortDescription, +} from './delivery'; + +const mockPickupPoints = { + pickup_1: 'handler_1', + pickup_2: 'handler_2', +}; +const mockDropoffPoints = { + dropoff_1: 'handler_3', + dropoff_2: 'handler_4', +}; + +describe('Delivery task form', () => { + it('Delivery task form renders, changes and validates', async () => { + const onChange = vi.fn(); + const onValidate = vi.fn(); + + render( + , + ); + + let triggerCount = 1; + + const pickupPlace = screen.getByTestId('pickup-location'); + let input = within(pickupPlace).getByLabelText(/pickup location/i); + pickupPlace.focus(); + fireEvent.change(input, { target: { value: 'a' } }); + fireEvent.keyDown(pickupPlace, { key: 'ArrowDown' }); + fireEvent.keyDown(pickupPlace, { key: 'Enter' }); + expect(onChange).toHaveBeenCalledTimes(triggerCount); + expect(onValidate).toHaveBeenCalledTimes(triggerCount); + triggerCount += 1; + + const pickupPayload = screen.getByTestId('pickup-sku'); + pickupPayload.focus(); + input = within(pickupPayload).getByLabelText(/pickup sku/i); + fireEvent.change(input, { target: { value: 'coke' } }); + expect(onChange).toHaveBeenCalledTimes(triggerCount); + expect(onValidate).toHaveBeenCalledTimes(triggerCount); + triggerCount += 1; + + const pickupQuantity = screen.getByTestId('pickup-quantity'); + pickupQuantity.focus(); + input = within(pickupQuantity).getByLabelText(/quantity/i); + await userEvent.type(input, '1'); + expect(onChange).toHaveBeenCalledTimes(triggerCount); + expect(onValidate).toHaveBeenCalledTimes(triggerCount); + triggerCount += 1; + + const dropoffPlace = screen.getByTestId('dropoff-location'); + dropoffPlace.focus(); + input = within(dropoffPlace).getByLabelText(/dropoff location/i); + fireEvent.change(input, { target: { value: 'a' } }); + fireEvent.keyDown(dropoffPlace, { key: 'ArrowDown' }); + fireEvent.keyDown(dropoffPlace, { key: 'Enter' }); + expect(onChange).toHaveBeenCalledTimes(triggerCount); + expect(onValidate).toHaveBeenCalledTimes(triggerCount); + triggerCount += 1; + + const dropoffPayload = screen.getByTestId('dropoff-sku'); + dropoffPayload.focus(); + input = within(dropoffPayload).getByLabelText(/dropoff sku/i); + fireEvent.change(input, { target: { value: 'coke' } }); + expect(onChange).toHaveBeenCalledTimes(triggerCount); + expect(onValidate).toHaveBeenCalledTimes(triggerCount); + triggerCount += 1; + + const dropoffQuantity = screen.getByTestId('dropoff-quantity'); + pickupQuantity.focus(); + input = within(dropoffQuantity).getByLabelText(/quantity/i); + await userEvent.type(input, '1'); + expect(onChange).toHaveBeenCalledTimes(triggerCount); + expect(onValidate).toHaveBeenCalledTimes(triggerCount); + triggerCount += 1; + }); + + it('booking label', () => { + const desc = makeDefaultDeliveryTaskDescription(); + desc.pickup = { + handler: 'handler_1', + place: 'pickup_1', + payload: { + sku: 'coke', + quantity: 1, + }, + }; + desc.dropoff = { + handler: 'handler_3', + place: 'dropoff_1', + payload: { + sku: 'coke', + quantity: 1, + }, + }; + const label = makeDeliveryTaskBookingLabel(desc); + expect(label.task_definition_id).toBe(DeliveryTaskDefinition.taskDefinitionId); + expect(label.pickup).toBe('pickup_1'); + expect(label.destination).toBe('dropoff_1'); + expect(label.payload).toBe('coke'); + }); + + it('validity', () => { + const desc = makeDefaultDeliveryTaskDescription(); + expect(isDeliveryTaskDescriptionValid(desc)).not.toBeTruthy(); + + desc.pickup = { + handler: 'handler_1', + place: 'pickup_1', + payload: { + sku: 'coke', + quantity: 1, + }, + }; + desc.dropoff = { + handler: 'handler_3', + place: 'dropoff_1', + payload: { + sku: 'coke', + quantity: 1, + }, + }; + expect(isDeliveryTaskDescriptionValid(desc)).toBeTruthy(); + }); + + it('short description', () => { + const desc = makeDefaultDeliveryTaskDescription(); + desc.pickup = { + handler: 'handler_1', + place: 'pickup_1', + payload: { + sku: 'coke', + quantity: 1, + }, + }; + desc.dropoff = { + handler: 'handler_3', + place: 'dropoff_1', + payload: { + sku: 'coke', + quantity: 1, + }, + }; + expect(makeDeliveryTaskShortDescription(desc, undefined)).toBe( + '[Delivery] Pickup [coke] from [pickup_1], dropoff [coke] at [dropoff_1]', + ); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/delivery.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/delivery.tsx index 46a3134a7..8badd3cef 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/types/delivery.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/delivery.tsx @@ -34,7 +34,7 @@ export function makeDeliveryTaskBookingLabel( task_definition_id: DeliveryTaskDefinition.taskDefinitionId, pickup: task_description.pickup.place, destination: task_description.dropoff.place, - cart_id: task_description.pickup.payload.sku, + payload: task_description.pickup.payload.sku, }; } @@ -47,7 +47,7 @@ function isTaskPlaceValid(place: TaskPlace): boolean { ); } -function isDeliveryTaskDescriptionValid(taskDescription: DeliveryTaskDescription): boolean { +export function isDeliveryTaskDescriptionValid(taskDescription: DeliveryTaskDescription): boolean { return isTaskPlaceValid(taskDescription.pickup) && isTaskPlaceValid(taskDescription.dropoff); } @@ -107,6 +107,7 @@ export function DeliveryTaskForm({ { @@ -183,6 +186,7 @@ export function DeliveryTaskForm({ { diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.tsx index eb3cd20d8..550601170 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/patrol.tsx @@ -76,7 +76,6 @@ interface PlaceListProps { function PlaceList({ places, onClick }: PlaceListProps) { const theme = useTheme(); - console.log(places); return ( Date: Thu, 12 Dec 2024 13:49:30 +0800 Subject: [PATCH 12/27] delivery custom Signed-off-by: Aaron Chong --- .../tasks/types/delivery-custom.test.tsx | 214 +++++++++++++++++- .../tasks/types/delivery-custom.tsx | 10 +- pnpm-lock.yaml | 2 +- 3 files changed, 222 insertions(+), 4 deletions(-) diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/delivery-custom.test.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/delivery-custom.test.tsx index beb544ab2..2a6ae1724 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/types/delivery-custom.test.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/delivery-custom.test.tsx @@ -1,20 +1,46 @@ -import { describe, expect, it } from 'vitest'; +import { fireEvent, render, screen, within } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; import { + DeliveryAreaPickupTaskDefinition, deliveryCustomInsertCartId, deliveryCustomInsertDropoff, deliveryCustomInsertOnCancel, deliveryCustomInsertPickup, DeliveryCustomTaskDescription, + DeliveryCustomTaskForm, deliveryInsertCartId, deliveryInsertDropoff, deliveryInsertOnCancel, deliveryInsertPickup, + DeliveryPickupTaskDefinition, DeliveryPickupTaskDescription, + DeliveryPickupTaskForm, + DeliverySequentialLotPickupTaskDefinition, + isDeliveryCustomTaskDescriptionValid, + isDeliveryPickupTaskDescriptionValid, makeDefaultDeliveryCustomTaskDescription, makeDefaultDeliveryPickupTaskDescription, + makeDeliveryCustomTaskBookingLabel, + makeDeliveryCustomTaskShortDescription, + makeDeliveryPickupTaskBookingLabel, + makeDeliveryPickupTaskShortDescription, } from '.'; +const mockPickupPoints = { + pickup_1: 'handler_1', + pickup_2: 'handler_2', +}; +const mockCartIds = ['cart_1', 'cart_2', 'cart_3']; +const mockDropoffPoints = { + dropoff_1: 'handler_3', + dropoff_2: 'handler_4', +}; +const mockPickupZones = { + pickup_1: 'zone_1', + pickup_2: 'zone_2', +}; + describe('Custom deliveries', () => { it('delivery pickup', () => { let deliveryPickupTaskDescription: DeliveryPickupTaskDescription | null = null; @@ -133,6 +159,89 @@ describe('Custom deliveries', () => { expect(deliveryPickupTaskDescription).toEqual(description); }); + it('delivery pickup task form renders, changes and validates', async () => { + const onChange = vi.fn(); + const onValidate = vi.fn(); + + render( + , + ); + + let triggerCount = 1; + + const pickupPlace = screen.getByTestId('pickup-location'); + let input = within(pickupPlace).getByLabelText(/pickup location/i); + pickupPlace.focus(); + fireEvent.change(input, { target: { value: 'a' } }); + fireEvent.keyDown(pickupPlace, { key: 'ArrowDown' }); + fireEvent.keyDown(pickupPlace, { key: 'Enter' }); + expect(onChange).toHaveBeenCalledTimes(triggerCount); + expect(onValidate).toHaveBeenCalledTimes(triggerCount); + triggerCount += 1; + + const cartId = screen.getByTestId('cart-id'); + cartId.focus(); + input = within(cartId).getByLabelText(/cart id/i); + fireEvent.change(input, { target: { value: 'a' } }); + fireEvent.keyDown(pickupPlace, { key: 'ArrowDown' }); + fireEvent.keyDown(pickupPlace, { key: 'Enter' }); + expect(onChange).toHaveBeenCalledTimes(triggerCount); + expect(onValidate).toHaveBeenCalledTimes(triggerCount); + triggerCount += 1; + + const dropoffPlace = screen.getByTestId('dropoff-location'); + dropoffPlace.focus(); + input = within(dropoffPlace).getByLabelText(/dropoff location/i); + fireEvent.change(input, { target: { value: 'a' } }); + fireEvent.keyDown(dropoffPlace, { key: 'ArrowDown' }); + fireEvent.keyDown(dropoffPlace, { key: 'Enter' }); + expect(onChange).toHaveBeenCalledTimes(triggerCount); + expect(onValidate).toHaveBeenCalledTimes(triggerCount); + triggerCount += 1; + }); + + it('delivery pickup booking label', () => { + let desc = makeDefaultDeliveryPickupTaskDescription(); + desc = deliveryInsertPickup(desc, 'test_place', 'test_lot'); + desc = deliveryInsertCartId(desc, 'test_cart_id'); + desc = deliveryInsertDropoff(desc, 'test_dropoff'); + const label = makeDeliveryPickupTaskBookingLabel(desc); + expect(label.task_definition_id).toBe(DeliveryPickupTaskDefinition.taskDefinitionId); + expect(label.pickup).toBe('test_lot'); + expect(label.destination).toBe('test_dropoff'); + expect(label.cart_id).toBe('test_cart_id'); + }); + + it('delivery pickup validity', () => { + let desc = makeDefaultDeliveryPickupTaskDescription(); + expect( + isDeliveryPickupTaskDescriptionValid(desc, mockPickupPoints, mockDropoffPoints), + ).not.toBeTruthy(); + desc = deliveryInsertPickup(desc, 'pickup_1', 'handler_1'); + desc = deliveryInsertCartId(desc, 'cart_1'); + desc = deliveryInsertDropoff(desc, 'dropoff_1'); + expect( + isDeliveryPickupTaskDescriptionValid(desc, mockPickupPoints, mockDropoffPoints), + ).toBeTruthy(); + }); + + it('delivery pickup short description', () => { + let desc = makeDefaultDeliveryPickupTaskDescription(); + desc = deliveryInsertPickup(desc, 'pickup_1', 'handler_1'); + desc = deliveryInsertCartId(desc, 'cart_1'); + desc = deliveryInsertDropoff(desc, 'dropoff_1'); + expect(makeDeliveryPickupTaskShortDescription(desc, undefined)).toBe( + '[Delivery - 1:1] payload [cart_1] from [pickup_1] to [dropoff_1]', + ); + }); + it('delivery_sequential_lot_pickup', () => { let deliveryCustomTaskDescription: DeliveryCustomTaskDescription | null = null; try { @@ -369,4 +478,107 @@ describe('Custom deliveries', () => { ]); expect(deliveryCustomTaskDescription).toEqual(description); }); + + it('delivery custom task form renders, changes and validates', async () => { + const onChange = vi.fn(); + const onValidate = vi.fn(); + + render( + , + ); + + let triggerCount = 1; + + const pickupPlace = screen.getByTestId('pickup-zone'); + let input = within(pickupPlace).getByLabelText(/pickup zone/i); + pickupPlace.focus(); + fireEvent.change(input, { target: { value: 'a' } }); + fireEvent.keyDown(pickupPlace, { key: 'ArrowDown' }); + fireEvent.keyDown(pickupPlace, { key: 'Enter' }); + expect(onChange).toHaveBeenCalledTimes(triggerCount); + expect(onValidate).toHaveBeenCalledTimes(triggerCount); + triggerCount += 1; + + const cartId = screen.getByTestId('cart-id'); + cartId.focus(); + input = within(cartId).getByLabelText(/cart id/i); + fireEvent.change(input, { target: { value: 'a' } }); + fireEvent.keyDown(pickupPlace, { key: 'ArrowDown' }); + fireEvent.keyDown(pickupPlace, { key: 'Enter' }); + expect(onChange).toHaveBeenCalledTimes(triggerCount); + expect(onValidate).toHaveBeenCalledTimes(triggerCount); + triggerCount += 1; + + const dropoffPlace = screen.getByTestId('dropoff-location'); + dropoffPlace.focus(); + input = within(dropoffPlace).getByLabelText(/dropoff location/i); + fireEvent.change(input, { target: { value: 'a' } }); + fireEvent.keyDown(dropoffPlace, { key: 'ArrowDown' }); + fireEvent.keyDown(dropoffPlace, { key: 'Enter' }); + expect(onChange).toHaveBeenCalledTimes(triggerCount); + expect(onValidate).toHaveBeenCalledTimes(triggerCount); + triggerCount += 1; + }); + + it('delivery custom booking label', () => { + let desc = makeDefaultDeliveryCustomTaskDescription('delivery_sequential_lot_pickup'); + desc = deliveryCustomInsertPickup(desc, 'test_place', 'test_lot'); + desc = deliveryCustomInsertCartId(desc, 'test_cart_id'); + desc = deliveryCustomInsertDropoff(desc, 'test_dropoff'); + let label = makeDeliveryCustomTaskBookingLabel(desc); + expect(label.task_definition_id).toBe( + DeliverySequentialLotPickupTaskDefinition.taskDefinitionId, + ); + expect(label.pickup).toBe('test_lot'); + expect(label.destination).toBe('test_dropoff'); + expect(label.cart_id).toBe('test_cart_id'); + + desc = makeDefaultDeliveryCustomTaskDescription('delivery_area_pickup'); + desc = deliveryCustomInsertPickup(desc, 'test_place', 'test_lot'); + desc = deliveryCustomInsertCartId(desc, 'test_cart_id'); + desc = deliveryCustomInsertDropoff(desc, 'test_dropoff'); + label = makeDeliveryCustomTaskBookingLabel(desc); + expect(label.task_definition_id).toBe(DeliveryAreaPickupTaskDefinition.taskDefinitionId); + expect(label.pickup).toBe('test_lot'); + expect(label.destination).toBe('test_dropoff'); + expect(label.cart_id).toBe('test_cart_id'); + }); + + it('delivery custom validity', () => { + let desc = makeDefaultDeliveryCustomTaskDescription('delivery_sequential_lot_pickup'); + expect( + isDeliveryCustomTaskDescriptionValid( + desc, + Object.values(mockPickupZones), + Object.keys(mockDropoffPoints), + ), + ).not.toBeTruthy(); + desc = deliveryCustomInsertPickup(desc, 'pickup_1', 'zone_1'); + desc = deliveryCustomInsertCartId(desc, 'cart_1'); + desc = deliveryCustomInsertDropoff(desc, 'dropoff_1'); + expect( + isDeliveryCustomTaskDescriptionValid( + desc, + Object.values(mockPickupZones), + Object.keys(mockDropoffPoints), + ), + ).toBeTruthy(); + }); + + it('delivery custom short description', () => { + let desc = makeDefaultDeliveryCustomTaskDescription('delivery_sequential_lot_pickup'); + desc = deliveryCustomInsertPickup(desc, 'pickup_1', 'zone_1'); + desc = deliveryCustomInsertCartId(desc, 'cart_1'); + desc = deliveryCustomInsertDropoff(desc, 'dropoff_1'); + expect(makeDeliveryCustomTaskShortDescription(desc, undefined)).toBe( + '[Delivery - Sequential lot pick up] payload [cart_1] from [pickup_1] to [dropoff_1]', + ); + }); }); diff --git a/packages/rmf-dashboard-framework/src/components/tasks/types/delivery-custom.tsx b/packages/rmf-dashboard-framework/src/components/tasks/types/delivery-custom.tsx index 86f87fd7b..4d607bb5d 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/types/delivery-custom.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/types/delivery-custom.tsx @@ -237,7 +237,7 @@ export function makeDeliveryCustomTaskShortDescription( return '[Unknown] delivery pickup task'; } -const isDeliveryPickupTaskDescriptionValid = ( +export const isDeliveryPickupTaskDescriptionValid = ( taskDescription: DeliveryPickupTaskDescription, pickupPoints: Record, dropoffPoints: Record, @@ -255,7 +255,7 @@ const isDeliveryPickupTaskDescriptionValid = ( ); }; -const isDeliveryCustomTaskDescriptionValid = ( +export const isDeliveryCustomTaskDescriptionValid = ( taskDescription: DeliveryCustomTaskDescription, pickupZones: string[], dropoffPoints: string[], @@ -365,6 +365,7 @@ export function DeliveryPickupTaskForm({ Date: Fri, 13 Dec 2024 15:21:08 +0800 Subject: [PATCH 13/27] task form, removed task details card, task logs app Signed-off-by: Aaron Chong --- .../src/components/tasks/index.ts | 1 - .../components/tasks/task-details-card.tsx | 86 ------------------- .../src/components/tasks/task-form.test.tsx | 70 +++++++++++++++ .../src/components/tasks/task-logs-app.tsx | 48 ----------- 4 files changed, 70 insertions(+), 135 deletions(-) delete mode 100644 packages/rmf-dashboard-framework/src/components/tasks/task-details-card.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/task-form.test.tsx delete mode 100644 packages/rmf-dashboard-framework/src/components/tasks/task-logs-app.tsx diff --git a/packages/rmf-dashboard-framework/src/components/tasks/index.ts b/packages/rmf-dashboard-framework/src/components/tasks/index.ts index 93c8e0abd..887592973 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/index.ts +++ b/packages/rmf-dashboard-framework/src/components/tasks/index.ts @@ -1,6 +1,5 @@ export * from './task-booking-label-utils'; export * from './task-cancellation'; -export * from './task-details-card'; export * from './task-form'; export * from './task-info'; export * from './task-inspector'; diff --git a/packages/rmf-dashboard-framework/src/components/tasks/task-details-card.tsx b/packages/rmf-dashboard-framework/src/components/tasks/task-details-card.tsx deleted file mode 100644 index 039f8b5c0..000000000 --- a/packages/rmf-dashboard-framework/src/components/tasks/task-details-card.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Button, CardContent, Grid, Typography, useTheme } from '@mui/material'; -import { TaskStateOutput as TaskState } from 'api-client'; -import React from 'react'; -// import { UserProfileContext } from 'rmf-auth'; -import { of, switchMap } from 'rxjs'; - -import { useAppController, useRmfApi } from '../../hooks'; -import { AppEvents } from '../app-events'; -import { TaskInfo } from './task-info'; -// import { Enforcer } from '../permissions'; - -export const TaskDetailsCard = () => { - const theme = useTheme(); - const rmfApi = useRmfApi(); - const appController = useAppController(); - - const [taskState, setTaskState] = React.useState(null); - React.useEffect(() => { - const sub = AppEvents.taskSelect - .pipe( - switchMap((selectedTask) => - selectedTask ? rmfApi.getTaskStateObs(selectedTask.booking.id) : of(null), - ), - ) - .subscribe(setTaskState); - return () => sub.unsubscribe(); - }, [rmfApi]); - - // const profile = React.useContext(UserProfileContext); - const taskCancellable = - taskState && - // profile && - // Enforcer.canCancelTask(profile) && - taskState.status && - !['canceled', 'killed', 'completed', 'failed'].includes(taskState.status); - const handleCancelTaskClick = React.useCallback(async () => { - if (!taskState) { - return; - } - try { - await rmfApi.tasksApi?.postCancelTaskTasksCancelTaskPost({ - type: 'cancel_task_request', - task_id: taskState.booking.id, - }); - appController.showAlert('success', 'Successfully cancelled task'); - AppEvents.taskSelect.next(null); - } catch (e) { - appController.showAlert('error', `Failed to cancel task: ${(e as Error).message}`); - } - }, [appController, taskState, rmfApi]); - - return ( - - {taskState ? ( - <> - - - - - - - - ) : ( - - - - Click on a task to view more information - - - - )} - - ); -}; - -export default TaskDetailsCard; diff --git a/packages/rmf-dashboard-framework/src/components/tasks/task-form.test.tsx b/packages/rmf-dashboard-framework/src/components/tasks/task-form.test.tsx new file mode 100644 index 000000000..d594b8cbf --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/task-form.test.tsx @@ -0,0 +1,70 @@ +import { render } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; + +import { LocalizationProvider } from './../locale'; +import { TaskForm } from './task-form'; + +const mockUser = 'mock_user'; +const mockFleets = { + fleet_1: ['robot_1'], + fleet_2: ['robot_2', 'robot_3'], +}; +const mockCleanZones = ['clean_zone_1', 'clean_zone_2']; +const mockWaypoints = ['waypoint_1', 'waypoint_2', 'waypoint_3']; +const mockPickupZones = ['pickup_zone_1', 'pickup_zone_2']; +const mockCartIds = ['cart_1', 'cart_2', 'cart_3']; +const mockPickupPoints = { + pickup_1: 'handler_1', + pickup_2: 'handler_2', +}; +const mockDropoffPoints = { + dropoff_1: 'handler_3', + dropoff_2: 'handler_4', +}; + +const onDispatchTask = vi.fn(); +const onScheduleTask = vi.fn(); +const onEditScheduleTask = vi.fn(); +const onSuccess = vi.fn(); +const onFail = vi.fn(); +const onSuccessFavoriteTask = vi.fn(); +const onFailFavoriteTask = vi.fn(); +const submitFavoriteTask = vi.fn(); +const deleteFavoriteTask = vi.fn(); +const onSuccessScheduling = vi.fn(); +const onFailScheduling = vi.fn(); + +describe('Task form', () => { + it('Task form renders', async () => { + render( + + + , + ); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/tasks/task-logs-app.tsx b/packages/rmf-dashboard-framework/src/components/tasks/task-logs-app.tsx deleted file mode 100644 index 436333d82..000000000 --- a/packages/rmf-dashboard-framework/src/components/tasks/task-logs-app.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { CardContent } from '@mui/material'; -import { TaskEventLog, TaskStateOutput as TaskState } from 'api-client'; -import React from 'react'; - -import { useRmfApi } from '../../hooks'; -import { AppEvents } from '../app-events'; -import { TaskLogs } from './task-logs'; - -export const TaskLogsCard = () => { - const rmfApi = useRmfApi(); - const [taskState, setTaskState] = React.useState(null); - const [taskLogs, setTaskLogs] = React.useState(null); - React.useEffect(() => { - const sub = AppEvents.taskSelect.subscribe((task) => { - if (!task) { - setTaskState(null); - setTaskLogs(null); - return; - } - (async () => { - // TODO: Get full logs, then subscribe to log updates for new logs. - // Unlike with state events, we can't just subscribe to logs updates. - try { - const logs = ( - await rmfApi.tasksApi.getTaskLogTasksTaskIdLogGet( - task.booking.id, - `0,${Number.MAX_SAFE_INTEGER}`, - ) - ).data; - setTaskLogs(logs); - } catch { - console.log(`Failed to fetch task logs for ${task.booking.id}`); - setTaskLogs(null); - } - setTaskState(task); - })(); - }); - return () => sub.unsubscribe(); - }, [rmfApi]); - - return ( - - - - ); -}; - -export default TaskLogsCard; From b31341fa32fdd4f6c5ac3afd7aefd6d89a24614a Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 18 Dec 2024 00:59:46 +0800 Subject: [PATCH 14/27] task-inspector Signed-off-by: Aaron Chong --- .../components/tasks/task-inspector.test.tsx | 44 +++++++++++++++++++ .../src/components/tasks/task-inspector.tsx | 3 +- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/task-inspector.test.tsx diff --git a/packages/rmf-dashboard-framework/src/components/tasks/task-inspector.test.tsx b/packages/rmf-dashboard-framework/src/components/tasks/task-inspector.test.tsx new file mode 100644 index 000000000..615ff224b --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/task-inspector.test.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { TaskInspector } from './task-inspector'; +import { makeTaskLog, makeTaskState } from './test-data.test'; + +describe('Task inspector', () => { + const rmfApi = new MockRmfApi(); + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + + it('Task inspector without task', async () => { + const onClose = vi.fn(); + const root = render( + + + , + ); + expect(root.getByText(/Click on a task to view more information/i)).toBeTruthy(); + }); + + it('Task inspector renders', async () => { + const mockTaskState = makeTaskState('mock_task_id'); + const mockTaskLogs = makeTaskLog('mock_task_id'); + rmfApi.tasksApi.getTaskLogTasksTaskIdLogGet = vi.fn().mockResolvedValue({ data: mockTaskLogs }); + + const onClose = vi.fn(); + const root = render( + + + , + ); + + expect(root.getByText(/mock_task_id/i)).toBeTruthy(); + expect(root.getByTestId('task-cancel-button')).toBeTruthy(); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/tasks/task-inspector.tsx b/packages/rmf-dashboard-framework/src/components/tasks/task-inspector.tsx index 48f5e31a5..eba00d467 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/task-inspector.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/task-inspector.tsx @@ -17,7 +17,7 @@ export function TaskInspector({ task, onClose }: TableDataGridState): JSX.Elemen const theme = useTheme(); const rmfApi = useRmfApi(); - const [taskState, setTaskState] = React.useState(null); + const [taskState, setTaskState] = React.useState(task); const [taskLogs, setTaskLogs] = React.useState(null); const [isOpen, setIsOpen] = React.useState(true); @@ -79,6 +79,7 @@ export function TaskInspector({ task, onClose }: TableDataGridState): JSX.Elemen Date: Wed, 18 Dec 2024 10:51:26 +0800 Subject: [PATCH 15/27] task-summary Signed-off-by: Aaron Chong --- .../components/tasks/task-summary.test.tsx | 31 +++++++++++++++++++ .../src/components/tasks/task-summary.tsx | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/task-summary.test.tsx diff --git a/packages/rmf-dashboard-framework/src/components/tasks/task-summary.test.tsx b/packages/rmf-dashboard-framework/src/components/tasks/task-summary.test.tsx new file mode 100644 index 000000000..641b09d7f --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/task-summary.test.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { TaskSummary } from './task-summary'; +import { makeTaskState } from './test-data.test'; + +describe('Task Summary', () => { + const rmfApi = new MockRmfApi(); + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + + it('Task Summary renders', async () => { + const mockTaskState = makeTaskState('mock_task_id'); + + const onClose = vi.fn(); + const root = render( + + + , + ); + + expect(root.getByText(/mock_task_id/i)).toBeTruthy(); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/tasks/task-summary.tsx b/packages/rmf-dashboard-framework/src/components/tasks/task-summary.tsx index 545a1f242..f3dd32be9 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/task-summary.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/task-summary.tsx @@ -70,7 +70,7 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { const { onClose, task } = props; const [openTaskDetailsLogs, setOpenTaskDetailsLogs] = React.useState(false); - const [taskState, setTaskState] = React.useState(null); + const [taskState, setTaskState] = React.useState(task ?? null); const [labels, setLabels] = React.useState(null); const [isOpen, setIsOpen] = React.useState(true); From ac51ab7d75eda15ec55872d84410c2f4497c4d80 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 18 Dec 2024 11:49:56 +0800 Subject: [PATCH 16/27] task-schedule Signed-off-by: Aaron Chong --- .../components/tasks/task-schedule.test.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/task-schedule.test.tsx diff --git a/packages/rmf-dashboard-framework/src/components/tasks/task-schedule.test.tsx b/packages/rmf-dashboard-framework/src/components/tasks/task-schedule.test.tsx new file mode 100644 index 000000000..375569898 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/task-schedule.test.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { describe, it } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { TaskSchedule } from './task-schedule'; + +describe('Task schedule', () => { + const rmfApi = new MockRmfApi(); + rmfApi.tasksApi.getScheduledTasksScheduledTasksGet = () => new Promise(() => {}); + rmfApi.tasksApi.addExceptDateScheduledTasksTaskIdExceptDatePost = () => new Promise(() => {}); + rmfApi.tasksApi.delScheduledTasksScheduledTasksTaskIdDelete = () => new Promise(() => {}); + + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + + it('Task schedule renders', async () => { + render( + + + , + ); + }); +}); From c6d51d24ae37fb57de03074282dfbc1ccadc0653 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 18 Dec 2024 12:23:46 +0800 Subject: [PATCH 17/27] task-schedule-utils Signed-off-by: Aaron Chong --- .../tasks/task-schedule-utils.test.ts | 499 ++++++++++++++++++ 1 file changed, 499 insertions(+) create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/task-schedule-utils.test.ts diff --git a/packages/rmf-dashboard-framework/src/components/tasks/task-schedule-utils.test.ts b/packages/rmf-dashboard-framework/src/components/tasks/task-schedule-utils.test.ts new file mode 100644 index 000000000..f4da64a31 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/task-schedule-utils.test.ts @@ -0,0 +1,499 @@ +import { Period, ScheduledTask, ScheduledTaskScheduleOutput } from 'api-client'; +import { addMinutes, endOfDay, endOfMinute, startOfDay } from 'date-fns'; +import { describe, expect, it, vi } from 'vitest'; + +import { RecurringDays } from './task-form'; +import { + apiScheduleToSchedule, + scheduleToEvents, + scheduleWithSelectedDay, + toISOStringWithTimezone, +} from './task-schedule-utils'; +import { makeTaskRequest } from './test-data.test'; + +describe('scheduleToEvents', () => { + const getEventId = () => 1; + const getEventTitle = () => 'Test Event'; + const getEventColor = () => '#ff0000'; + + const defaultTask: ScheduledTask = { + id: 1, + task_request: makeTaskRequest(), + created_by: 'user', + schedules: [], + last_ran: null, + start_from: null, + until: null, + except_dates: [], + }; + + it('should return an empty array if schedule.at is missing', () => { + const start = startOfDay(new Date(2023, 0, 1)); + const end = endOfDay(new Date(2023, 0, 7)); + const schedule: ScheduledTaskScheduleOutput = { + every: null, + period: 'day', + at: '', + }; + const events = scheduleToEvents( + start, + end, + schedule, + defaultTask, + getEventId, + getEventTitle, + getEventColor, + ); + expect(events).toEqual([]); + }); + + it('should return an empty array if schedule.period is invalid', () => { + const start = startOfDay(new Date(2023, 0, 1)); + const end = endOfDay(new Date(2023, 0, 7)); + const schedule: ScheduledTaskScheduleOutput = { + every: null, + period: 'invalid' as Period, + at: '09:00', + }; + const events = scheduleToEvents( + start, + end, + schedule, + defaultTask, + getEventId, + getEventTitle, + getEventColor, + ); + expect(events).toEqual([]); + }); + + it('should generate events for a daily schedule', () => { + const start = startOfDay(new Date(2023, 0, 1)); + const end = endOfDay(new Date(2023, 0, 7)); + const schedule: ScheduledTaskScheduleOutput = { + every: null, + period: 'day', + at: '09:00', + }; + const events = scheduleToEvents( + start, + end, + schedule, + defaultTask, + getEventId, + getEventTitle, + getEventColor, + ); + expect(events.length).toBe(7); + expect(events[0].start).toEqual(new Date(2023, 0, 1, 9, 0)); + expect(events[0].end).toEqual(addMinutes(new Date(2023, 0, 1, 9, 0), 45)); + expect(events[6].start).toEqual(new Date(2023, 0, 7, 9, 0)); + expect(events[6].end).toEqual(addMinutes(new Date(2023, 0, 7, 9, 0), 45)); + }); + + it('should generate events for a weekly schedule (Monday)', () => { + const start = startOfDay(new Date(2023, 0, 1)); // Sunday + const end = endOfDay(new Date(2023, 0, 15)); + const schedule: ScheduledTaskScheduleOutput = { + every: null, + period: 'monday', + at: '10:00', + }; + const events = scheduleToEvents( + start, + end, + schedule, + defaultTask, + getEventId, + getEventTitle, + getEventColor, + ); + expect(events.length).toBe(2); + expect(events[0].start).toEqual(new Date(2023, 0, 2, 10, 0)); + expect(events[1].start).toEqual(new Date(2023, 0, 9, 10, 0)); + }); + + it('should generate events for a weekly schedule (Wednesday)', () => { + const start = startOfDay(new Date(2023, 0, 1)); // Sunday + const end = endOfDay(new Date(2023, 0, 15)); + const schedule: ScheduledTaskScheduleOutput = { + every: null, + period: 'wednesday', + at: '10:00', + }; + const events = scheduleToEvents( + start, + end, + schedule, + defaultTask, + getEventId, + getEventTitle, + getEventColor, + ); + expect(events.length).toBe(2); + expect(events[0].start).toEqual(new Date(2023, 0, 4, 10, 0)); + expect(events[1].start).toEqual(new Date(2023, 0, 11, 10, 0)); + }); + + it('should respect start_from', () => { + const start = startOfDay(new Date(2023, 0, 1)); + const end = endOfDay(new Date(2023, 0, 7)); + const schedule: ScheduledTaskScheduleOutput = { + every: null, + period: 'day', + at: '09:00', + }; + const task: ScheduledTask = { + ...defaultTask, + start_from: '2023-01-03T00:00:00', + }; + const events = scheduleToEvents( + start, + end, + schedule, + task, + getEventId, + getEventTitle, + getEventColor, + ); + expect(events.length).toBe(5); + expect(events[0].start).toEqual(new Date(2023, 0, 3, 9, 0)); + }); + + it('should respect until', () => { + const start = startOfDay(new Date(2023, 0, 1)); + const end = endOfDay(new Date(2023, 0, 7)); + const schedule: ScheduledTaskScheduleOutput = { + every: null, + period: 'day', + at: '09:00', + }; + const task: ScheduledTask = { + ...defaultTask, + until: '2023-01-05T23:59:59', + }; + const events = scheduleToEvents( + start, + end, + schedule, + task, + getEventId, + getEventTitle, + getEventColor, + ); + expect(events.length).toBe(5); + expect(events[4].start).toEqual(new Date(2023, 0, 5, 9, 0)); + }); + + it('should respect except_dates', () => { + const start = startOfDay(new Date(2023, 0, 1)); + const end = endOfDay(new Date(2023, 0, 7)); + const schedule: ScheduledTaskScheduleOutput = { + every: null, + period: 'day', + at: '09:00', + }; + const task: ScheduledTask = { + ...defaultTask, + except_dates: ['2023-01-03', '2023-01-05'], + }; + const events = scheduleToEvents( + start, + end, + schedule, + task, + getEventId, + getEventTitle, + getEventColor, + ); + expect(events.length).toBe(5); + expect(events[1].start).toEqual(new Date(2023, 0, 2, 9, 0)); + expect(events[2].start).toEqual(new Date(2023, 0, 4, 9, 0)); + expect(events[3].start).toEqual(new Date(2023, 0, 6, 9, 0)); + }); +}); + +describe('scheduleWithSelectedDay', () => { + it('should create a schedule for Sunday', () => { + const date = new Date(2023, 0, 1); // Sunday + const scheduleTask: ScheduledTaskScheduleOutput[] = [ + { + every: null, + period: 'day', + at: '09:00', + }, + ]; + const expectedDays: RecurringDays = [false, false, false, false, false, false, true]; + const result = scheduleWithSelectedDay(scheduleTask, date); + expect(result.days).toEqual(expectedDays); + expect(result.startOn).toEqual(date); + expect(result.until).toEqual(endOfDay(date)); + expect(result.at).toEqual(new Date('09:00')); + }); + + it('should create a schedule for Monday', () => { + const date = new Date(2023, 0, 2); // Monday + const scheduleTask: ScheduledTaskScheduleOutput[] = [ + { + every: null, + period: 'day', + at: '10:00', + }, + ]; + const expectedDays: RecurringDays = [true, false, false, false, false, false, false]; + const result = scheduleWithSelectedDay(scheduleTask, date); + expect(result.days).toEqual(expectedDays); + expect(result.startOn).toEqual(date); + expect(result.until).toEqual(endOfDay(date)); + expect(result.at).toEqual(new Date('10:00')); + }); + + it('should create a schedule for Tuesday', () => { + const date = new Date(2023, 0, 3); // Tuesday + const scheduleTask: ScheduledTaskScheduleOutput[] = [ + { + every: null, + period: 'day', + at: '11:00', + }, + ]; + const expectedDays: RecurringDays = [false, true, false, false, false, false, false]; + const result = scheduleWithSelectedDay(scheduleTask, date); + expect(result.days).toEqual(expectedDays); + expect(result.startOn).toEqual(date); + expect(result.until).toEqual(endOfDay(date)); + expect(result.at).toEqual(new Date('11:00')); + }); + + it('should create a schedule for Wednesday', () => { + const date = new Date(2023, 0, 4); // Wednesday + const scheduleTask: ScheduledTaskScheduleOutput[] = [ + { + every: null, + period: 'day', + at: '12:00', + }, + ]; + const expectedDays: RecurringDays = [false, false, true, false, false, false, false]; + const result = scheduleWithSelectedDay(scheduleTask, date); + expect(result.days).toEqual(expectedDays); + expect(result.startOn).toEqual(date); + expect(result.until).toEqual(endOfDay(date)); + expect(result.at).toEqual(new Date('12:00')); + }); + + it('should create a schedule for Thursday', () => { + const date = new Date(2023, 0, 5); // Thursday + const scheduleTask: ScheduledTaskScheduleOutput[] = [ + { + every: null, + period: 'day', + at: '13:00', + }, + ]; + const expectedDays: RecurringDays = [false, false, false, true, false, false, false]; + const result = scheduleWithSelectedDay(scheduleTask, date); + expect(result.days).toEqual(expectedDays); + expect(result.startOn).toEqual(date); + expect(result.until).toEqual(endOfDay(date)); + expect(result.at).toEqual(new Date('13:00')); + }); + + it('should create a schedule for Friday', () => { + const date = new Date(2023, 0, 6); // Friday + const scheduleTask: ScheduledTaskScheduleOutput[] = [ + { + every: null, + period: 'day', + at: '14:00', + }, + ]; + const expectedDays: RecurringDays = [false, false, false, false, true, false, false]; + const result = scheduleWithSelectedDay(scheduleTask, date); + expect(result.days).toEqual(expectedDays); + expect(result.startOn).toEqual(date); + expect(result.until).toEqual(endOfDay(date)); + expect(result.at).toEqual(new Date('14:00')); + }); + + it('should create a schedule for Saturday', () => { + const date = new Date(2023, 0, 7); // Saturday + const scheduleTask: ScheduledTaskScheduleOutput[] = [ + { + every: null, + period: 'day', + at: '15:00', + }, + ]; + const expectedDays: RecurringDays = [false, false, false, false, false, true, false]; + const result = scheduleWithSelectedDay(scheduleTask, date); + expect(result.days).toEqual(expectedDays); + expect(result.startOn).toEqual(date); + expect(result.until).toEqual(endOfDay(date)); + expect(result.at).toEqual(new Date('15:00')); + }); + + it('should use the current date if scheduleTask[0].at is undefined', () => { + const date = new Date(2023, 0, 1); // Sunday + const scheduleTask: ScheduledTaskScheduleOutput[] = [ + { + every: null, + period: 'day', + at: '', + }, + ]; + const result = scheduleWithSelectedDay(scheduleTask, date); + expect(result.at).toEqual(new Date()); + }); +}); + +describe('apiScheduleToSchedule', () => { + it('should convert a single day schedule', () => { + const apiSchedule: ScheduledTaskScheduleOutput = { + every: null, + period: 'monday', + at: '09:00', + }; + const task: ScheduledTask = { + id: 1, + task_request: makeTaskRequest(), + created_by: 'user', + schedules: [apiSchedule], + last_ran: null, + start_from: '2023-01-02T00:00:00', + until: '2023-01-08T23:59:59', + except_dates: [], + }; + const expectedDays: RecurringDays = [true, false, false, false, false, false, false]; + const result = apiScheduleToSchedule(task); + expect(result.days).toEqual(expectedDays); + expect(result.startOn).toEqual(new Date('2023-01-02T00:00:00')); + expect(result.until).toEqual(endOfMinute(new Date('2023-01-08T23:59:59'))); + expect(result.at).toEqual(new Date('09:00')); + }); + + it('should convert a multiple day schedule', () => { + const apiSchedules: ScheduledTaskScheduleOutput[] = [ + { + every: null, + period: 'tuesday', + at: '10:00', + }, + { + every: null, + period: 'friday', + at: '12:00', + }, + ]; + const task: ScheduledTask = { + id: 1, + task_request: makeTaskRequest(), + created_by: 'user', + schedules: apiSchedules, + last_ran: null, + start_from: '2023-01-03T00:00:00', + until: '2023-01-09T23:59:59', + except_dates: [], + }; + const expectedDays: RecurringDays = [false, true, false, false, true, false, false]; + const result = apiScheduleToSchedule(task); + expect(result.days).toEqual(expectedDays); + expect(result.startOn).toEqual(new Date('2023-01-03T00:00:00')); + expect(result.until).toEqual(endOfMinute(new Date('2023-01-09T23:59:59'))); + expect(result.at).toEqual(new Date('10:00')); // Should take the time from the first schedule + }); + + it('should throw an error for an invalid day', () => { + const apiSchedule: ScheduledTaskScheduleOutput = { + every: null, + period: 'invalid' as Period, + at: '09:00', + }; + const task: ScheduledTask = { + id: 1, + task_request: makeTaskRequest(), + created_by: 'user', + schedules: [apiSchedule], + last_ran: null, + start_from: null, + until: null, + except_dates: [], + }; + expect(() => apiScheduleToSchedule(task)).toThrowError(`Invalid day: ${apiSchedule}`); + }); + + it('should use current date if start_from is not provided', () => { + const apiSchedule: ScheduledTaskScheduleOutput = { + every: null, + period: 'monday', + at: '09:00', + }; + const task: ScheduledTask = { + id: 1, + task_request: makeTaskRequest(), + created_by: 'user', + schedules: [apiSchedule], + last_ran: null, + start_from: null, + until: null, + except_dates: [], + }; + const result = apiScheduleToSchedule(task); + expect(result.startOn).toEqual(new Date()); + }); + + it('should use undefined for until if not provided', () => { + const apiSchedule: ScheduledTaskScheduleOutput = { + every: null, + period: 'monday', + at: '09:00', + }; + const task: ScheduledTask = { + id: 1, + task_request: makeTaskRequest(), + created_by: 'user', + schedules: [apiSchedule], + last_ran: null, + start_from: null, + until: null, + except_dates: [], + }; + const result = apiScheduleToSchedule(task); + expect(result.until).toBeUndefined(); + }); + + it('should use the current date if schedules[0].at is not provided', () => { + const apiSchedule: ScheduledTaskScheduleOutput = { + every: null, + period: 'monday', + at: '', + }; + const task: ScheduledTask = { + id: 1, + task_request: makeTaskRequest(), + created_by: 'user', + schedules: [apiSchedule], + last_ran: null, + start_from: null, + until: null, + except_dates: [], + }; + const result = apiScheduleToSchedule(task); + expect(result.at).toEqual(new Date()); + }); +}); + +describe('toISOStringWithTimezone', () => { + it('should format date with positive timezone offset', () => { + const date = new Date('2023-10-27T01:00:00.000Z'); // UTC + const expectedOffset = '+08:00'; // Example: Singapore Time (UTC+8) + + // Mock the timezone offset of the date object. + const spy = vi.spyOn(date, 'getTimezoneOffset').mockImplementation(() => -480); // -480 minutes = +08:00 hours + + const result = toISOStringWithTimezone(date); + expect(result).toContain(expectedOffset); + expect(result).toEqual('2023-10-27T09:00:00+08:00'); + spy.mockRestore(); + }); +}); From a3649e61e90bdea019aa17f68282584f9de7ee06 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 18 Dec 2024 16:29:11 +0800 Subject: [PATCH 18/27] tasks-window Signed-off-by: Aaron Chong --- .../tasks/task-schedule-utils.test.ts | 18 +----- .../components/tasks/tasks-window.test.tsx | 48 ++++++++++++++ .../src/components/tasks/tasks-window.tsx | 63 +++++++++++-------- 3 files changed, 85 insertions(+), 44 deletions(-) create mode 100644 packages/rmf-dashboard-framework/src/components/tasks/tasks-window.test.tsx diff --git a/packages/rmf-dashboard-framework/src/components/tasks/task-schedule-utils.test.ts b/packages/rmf-dashboard-framework/src/components/tasks/task-schedule-utils.test.ts index f4da64a31..1ec6e0735 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/task-schedule-utils.test.ts +++ b/packages/rmf-dashboard-framework/src/components/tasks/task-schedule-utils.test.ts @@ -1,13 +1,12 @@ import { Period, ScheduledTask, ScheduledTaskScheduleOutput } from 'api-client'; import { addMinutes, endOfDay, endOfMinute, startOfDay } from 'date-fns'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { RecurringDays } from './task-form'; import { apiScheduleToSchedule, scheduleToEvents, scheduleWithSelectedDay, - toISOStringWithTimezone, } from './task-schedule-utils'; import { makeTaskRequest } from './test-data.test'; @@ -482,18 +481,3 @@ describe('apiScheduleToSchedule', () => { expect(result.at).toEqual(new Date()); }); }); - -describe('toISOStringWithTimezone', () => { - it('should format date with positive timezone offset', () => { - const date = new Date('2023-10-27T01:00:00.000Z'); // UTC - const expectedOffset = '+08:00'; // Example: Singapore Time (UTC+8) - - // Mock the timezone offset of the date object. - const spy = vi.spyOn(date, 'getTimezoneOffset').mockImplementation(() => -480); // -480 minutes = +08:00 hours - - const result = toISOStringWithTimezone(date); - expect(result).toContain(expectedOffset); - expect(result).toEqual('2023-10-27T09:00:00+08:00'); - spy.mockRestore(); - }); -}); diff --git a/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.test.tsx b/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.test.tsx new file mode 100644 index 000000000..c479a28ff --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.test.tsx @@ -0,0 +1,48 @@ +import { fireEvent, screen } from '@testing-library/react'; +import React, { act } from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { AppEvents } from '../app-events'; +import { TasksWindow } from './tasks-window'; + +vi.mock('../app-events', () => ({ + AppEvents: { + refreshTaskApp: { + subscribe: vi.fn(() => ({ unsubscribe: vi.fn() })), + next: vi.fn(), + }, + }, +})); + +describe('Tasks window', () => { + const rmfApi = new MockRmfApi(); + rmfApi.tasksApi.queryTaskStatesTasksGet = vi.fn().mockResolvedValue({ data: [] }); + + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + + it('renders without crashing', () => { + const root = render( + + + , + ); + expect(root.getByText('Tasks')).toBeTruthy(); + }); + + it('triggers task refresh when Refresh button is clicked', () => { + render( {}} />); + const refreshButton = screen.getByTestId('refresh-button'); + act(() => { + fireEvent.click(refreshButton); + }); + expect(AppEvents.refreshTaskApp.next).toHaveBeenCalled(); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.tsx b/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.tsx index bd697f144..5f18e609a 100644 --- a/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.tsx +++ b/packages/rmf-dashboard-framework/src/components/tasks/tasks-window.tsx @@ -33,7 +33,7 @@ import { exportCsvFull, exportCsvMinimal } from './utils'; const RefreshTaskQueueTableInterval = 15000; const QueryLimit = 100; -enum TaskTablePanel { +export enum TaskTablePanel { QueueTable = 0, Schedule = 1, } @@ -182,35 +182,43 @@ export const TasksWindow = React.memo( labelFilter = `${filterColumn.substring(6)}=${filterValue}`; } - const resp = await rmfApi.tasksApi.queryTaskStatesTasksGet( - filterColumn && filterColumn === 'id_' ? filterValue : undefined, - filterColumn && filterColumn === 'category' ? filterValue : undefined, - filterColumn && filterColumn === 'requester' ? filterValue : undefined, - filterColumn && filterColumn === 'assigned_to' ? filterValue : undefined, - filterColumn && filterColumn === 'status' ? filterValue : undefined, - labelFilter, - filterColumn && filterColumn === 'unix_millis_request_time' ? filterValue : undefined, - filterColumn && filterColumn === 'unix_millis_start_time' ? filterValue : undefined, - filterColumn && filterColumn === 'unix_millis_finish_time' ? filterValue : undefined, - GET_LIMIT, - (tasksState.page - 1) * GET_LIMIT, // Datagrid component need to start in page 1. Otherwise works wrong - orderBy, - undefined, - ); - const results = resp.data as TaskState[]; - const newTasks = results.slice(0, GET_LIMIT); + try { + const resp = await rmfApi.tasksApi.queryTaskStatesTasksGet( + filterColumn && filterColumn === 'id_' ? filterValue : undefined, + filterColumn && filterColumn === 'category' ? filterValue : undefined, + filterColumn && filterColumn === 'requester' ? filterValue : undefined, + filterColumn && filterColumn === 'assigned_to' ? filterValue : undefined, + filterColumn && filterColumn === 'status' ? filterValue : undefined, + labelFilter, + filterColumn && filterColumn === 'unix_millis_request_time' ? filterValue : undefined, + filterColumn && filterColumn === 'unix_millis_start_time' ? filterValue : undefined, + filterColumn && filterColumn === 'unix_millis_finish_time' ? filterValue : undefined, + GET_LIMIT, + (tasksState.page - 1) * GET_LIMIT, // Datagrid component need to start in page 1. Otherwise works wrong + orderBy, + undefined, + ); + const results = resp.data as TaskState[]; + const newTasks = results.slice(0, GET_LIMIT); - setTasksState((old) => ({ - ...old, - isLoading: false, - data: newTasks, - total: - results.length === GET_LIMIT - ? tasksState.page * GET_LIMIT + 1 - : tasksState.page * GET_LIMIT - 9, - })); + setTasksState((old) => ({ + ...old, + isLoading: false, + data: newTasks, + total: + results.length === GET_LIMIT + ? tasksState.page * GET_LIMIT + 1 + : tasksState.page * GET_LIMIT - 9, + })); + } catch (e) { + appController.showAlert( + 'error', + `Failed to query task states: ${(e as Error).message}`, + ); + } })(); }, [ + appController, rmfApi, refreshTaskAppCount, tasksState.page, @@ -340,6 +348,7 @@ export const TasksWindow = React.memo(