Skip to content

Commit

Permalink
Task prioritization and cleanup of task form (#1000)
Browse files Browse the repository at this point in the history
* Basic implementation of priority based on legacy implementation

Signed-off-by: Aaron Chong <[email protected]>

* Use icons for favorite

Signed-off-by: Aaron Chong <[email protected]>

* Fix warning time, use switch and made buttons nicer

Signed-off-by: Aaron Chong <[email protected]>

* To display priority

Signed-off-by: Aaron Chong <[email protected]>

* Lint

Signed-off-by: Aaron Chong <[email protected]>

* Helper function to handle null and undefined priority, clean up creation

Signed-off-by: Aaron Chong <[email protected]>

* Use switch for priority, flip low priority icon

Signed-off-by: Aaron Chong <[email protected]>

* Remove getDefaultTaskPriorty

Signed-off-by: Aaron Chong <[email protected]>

---------

Signed-off-by: Aaron Chong <[email protected]>
  • Loading branch information
aaronchongth authored Sep 3, 2024
1 parent fc440d1 commit c8e0e14
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 70 deletions.
114 changes: 70 additions & 44 deletions packages/react-components/lib/tasks/create-task.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@

import UpdateIcon from '@mui/icons-material/Create';
import DeleteIcon from '@mui/icons-material/Delete';
import FavoriteBorder from '@mui/icons-material/FavoriteBorder';
import SaveIcon from '@mui/icons-material/Save';
import ScheduleSendIcon from '@mui/icons-material/ScheduleSend';
import SendIcon from '@mui/icons-material/Send';
import {
Box,
Button,
Checkbox,
Chip,
Dialog,
DialogActions,
Expand All @@ -29,7 +32,9 @@ import {
Radio,
RadioGroup,
styled,
Switch,
TextField,
Tooltip,
Typography,
useMediaQuery,
useTheme,
Expand Down Expand Up @@ -83,6 +88,7 @@ import {
PatrolTaskForm,
} from './types/patrol';
import { getDefaultTaskDescription, getTaskRequestCategory } from './types/utils';
import { createTaskPriority, parseTaskPriority } from './utils';

export interface TaskDefinition {
taskDefinitionId: string;
Expand Down Expand Up @@ -214,7 +220,7 @@ function getDefaultTaskRequest(taskDefinitionId: string): TaskRequest | null {
description,
unix_millis_earliest_start_time: 0,
unix_millis_request_time: Date.now(),
priority: { type: 'binary', value: 0 },
priority: createTaskPriority(false),
requester: '',
};
}
Expand Down Expand Up @@ -462,7 +468,7 @@ export function CreateTaskForm({
}
}
const [warnTime, setWarnTime] = React.useState<Date | null>(existingWarnTime);
const handleWarnTimeCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const handleWarnTimeSwitchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
setWarnTime(new Date());
} else {
Expand Down Expand Up @@ -630,10 +636,8 @@ export function CreateTaskForm({
return;
}

const requester = scheduling ? `${user}__scheduled` : user;

const request = { ...taskRequest };
request.requester = requester;
request.requester = user;
request.unix_millis_request_time = Date.now();

if (taskDefinitionId === CustomComposeTaskDefinition.taskDefinitionId) {
Expand Down Expand Up @@ -685,6 +689,10 @@ export function CreateTaskForm({
requestBookingLabel['unix_millis_warn_time'] = `${warnTime.valueOf()}`;
}

if (scheduling) {
requestBookingLabel['scheduled'] = 'true';
}

request.labels = serializeTaskBookingLabel(requestBookingLabel);
console.log(`labels: ${request.labels}`);
} catch (e) {
Expand Down Expand Up @@ -786,6 +794,15 @@ export function CreateTaskForm({
}
};

const handlePrioritySwitchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setTaskRequest((prev) => {
return {
...prev,
priority: createTaskPriority(event.target.checked),
};
});
};

return (
<>
<StyledDialog
Expand Down Expand Up @@ -842,7 +859,7 @@ export function CreateTaskForm({
/>

<Grid>
<Grid container spacing={theme.spacing(2)}>
<Grid container spacing={theme.spacing(2)} alignItems="center">
<Grid item xs={12}>
<TextField
select
Expand Down Expand Up @@ -873,64 +890,70 @@ export function CreateTaskForm({
})}
</TextField>
</Grid>
<Grid item xs={isScreenHeightLessThan800 ? 6 : 7}>
<DateTimePicker
value={new Date()}
onChange={() => 0}
label="Start Time"
disabled
/>
</Grid>
<Grid item xs={1}>
<Checkbox
checked={warnTime !== null}
onChange={handleWarnTimeCheckboxChange}
inputProps={{ 'aria-label': 'controlled' }}
sx={{
'& .MuiSvgIcon-root': { fontSize: isScreenHeightLessThan800 ? 22 : 32 },
}}
/>
<Tooltip title="Ongoing tasks where estimated finish times are past their specified warning times will be flagged.">
<Switch checked={warnTime !== null} onChange={handleWarnTimeSwitchChange} />
</Tooltip>
</Grid>
<Grid item xs={isScreenHeightLessThan800 ? 5 : 4}>
<Grid item xs={8}>
<DateTimePicker
disabled={warnTime === null}
value={warnTime}
onChange={(date) => {
setWarnTime(date);
}}
label="Warn Time"
sx={{ marginLeft: theme.spacing(1) }}
/>
</Grid>
<Grid item xs={3} justifyContent="flex-end">
<Tooltip title="Prioritized tasks will added to the front of the task execution queue">
<FormControlLabel
control={
<Switch
checked={parseTaskPriority(taskRequest.priority)}
onChange={handlePrioritySwitchChange}
/>
}
label="Prioritize"
sx={{
color: parseTaskPriority(taskRequest.priority)
? undefined
: theme.palette.action.disabled,
}}
/>
</Tooltip>
</Grid>
</Grid>
<Divider
orientation="horizontal"
flexItem
style={{ marginTop: theme.spacing(2), marginBottom: theme.spacing(2) }}
/>
{renderTaskDescriptionForm(taskDefinitionId)}
<Grid container justifyContent="center">
<Button
aria-label="Save as a favorite task"
variant="contained"
color="primary"
size={isScreenHeightLessThan800 ? 'small' : 'medium'}
onClick={() => {
!callToUpdateFavoriteTask &&
setFavoriteTaskBuffer({ ...favoriteTaskBuffer, name: '', id: '' });
setOpenFavoriteDialog(true);
}}
style={{ marginTop: theme.spacing(2), marginBottom: theme.spacing(2) }}
// FIXME: Favorite tasks are disabled for custom compose
// tasks for now, as it will require a re-write of
// FavoriteTask's pydantic model with better typing.
disabled={taskDefinitionId === CustomComposeTaskDefinition.taskDefinitionId}
>
{callToUpdateFavoriteTask ? `Confirm edits` : 'Save as a favorite task'}
</Button>
</Grid>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button
aria-label="Save as a favorite task"
variant={callToUpdateFavoriteTask ? 'contained' : 'outlined'}
color="primary"
size={isScreenHeightLessThan800 ? 'small' : 'medium'}
startIcon={callToUpdateFavoriteTask ? <SaveIcon /> : <FavoriteBorder />}
onClick={() => {
!callToUpdateFavoriteTask &&
setFavoriteTaskBuffer({ ...favoriteTaskBuffer, name: '', id: '' });
setOpenFavoriteDialog(true);
}}
// FIXME: Favorite tasks are disabled for custom compose
// tasks for now, as it will require a re-write of
// FavoriteTask's pydantic model with better typing.
disabled={taskDefinitionId === CustomComposeTaskDefinition.taskDefinitionId}
>
{callToUpdateFavoriteTask ? `Confirm edits` : 'Save as a favorite task'}
</Button>
</DialogActions>
<DialogActions>
<Button
variant="outlined"
Expand All @@ -948,6 +971,7 @@ export function CreateTaskForm({
className={classes.actionBtn}
onClick={() => setOpenSchedulingDialog(true)}
size={isScreenHeightLessThan800 ? 'small' : 'medium'}
startIcon={<ScheduleSendIcon />}
>
{scheduleToEdit ? 'Edit schedule' : 'Add to Schedule'}
</Button>
Expand All @@ -960,6 +984,7 @@ export function CreateTaskForm({
aria-label="Submit Now"
onClick={handleSubmitNow}
size={isScreenHeightLessThan800 ? 'small' : 'medium'}
startIcon={<SendIcon />}
>
<Loading
hideChildren
Expand Down Expand Up @@ -995,6 +1020,7 @@ export function CreateTaskForm({
}
helperText="Required"
error={favoriteTaskTitleError}
fullWidth
/>
)}
{callToDeleteFavoriteTask && (
Expand Down
64 changes: 39 additions & 25 deletions packages/react-components/lib/tasks/task-table-datagrid.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { InsertInvitation as ScheduleIcon, Person as UserIcon } from '@mui/icons-material/';
import {
InsertInvitation as ScheduleIcon,
LowPriority,
Person as UserIcon,
} from '@mui/icons-material/';
import { Box, Stack, styled, Tooltip, Typography } from '@mui/material';
import {
DataGrid,
Expand All @@ -21,6 +25,7 @@ import * as React from 'react';

import { TaskBookingLabels } from './booking-label';
import { getTaskBookingLabelFromTaskState } from './task-booking-label-utils';
import { parseTaskPriority } from './utils';

const classes = {
taskActiveCell: 'MuiDataGrid-cell-active-cell',
Expand Down Expand Up @@ -110,33 +115,32 @@ export interface TableDataGridState {
setSortFields: React.Dispatch<React.SetStateAction<SortFields>>;
}

const TaskRequester = (requester: string | undefined | null): JSX.Element => {
const TaskRequester = (
requester: string | undefined | null,
scheduled: boolean,
prioritized: boolean,
): JSX.Element => {
if (!requester) {
return <Typography variant="body1">n/a</Typography>;
}

/** When a task is created as scheduled,
we save the requester as USERNAME__scheduled.
Therefore, we remove the __schedule because the different icon is enough indicator to know
if the task was adhoc or scheduled.
*/
return (
<Stack direction="row" alignItems="center" gap={1}>
{requester.includes('scheduled') ? (
<>
<Tooltip title="User scheduled">
<ScheduleIcon />
</Tooltip>
<Typography variant="body1">{requester.split('__')[0]}</Typography>
</>
{prioritized ? (
<Tooltip title="User prioritized">
<LowPriority sx={{ transform: 'rotate(0.5turn)' }} />
</Tooltip>
) : null}
{scheduled ? (
<Tooltip title="User scheduled">
<ScheduleIcon />
</Tooltip>
) : (
<>
<Tooltip title="User submitted">
<UserIcon />
</Tooltip>
<Typography variant="body1">{requester}</Typography>
</>
<Tooltip title="User submitted">
<UserIcon />
</Tooltip>
)}
<Typography variant="body1">{requester}</Typography>
</Stack>
);
};
Expand Down Expand Up @@ -192,7 +196,19 @@ export function TaskDataGridTable({
headerName: 'Requester',
width: 150,
editable: false,
renderCell: (cellValues) => TaskRequester(cellValues.row.state.booking.requester),
renderCell: (cellValues) => {
let prioritized = false;
if (cellValues.row.state.booking.priority) {
prioritized = parseTaskPriority(cellValues.row.state.booking.priority);
}

let scheduled = false;
if (cellValues.row.requestLabel && 'scheduled' in cellValues.row.requestLabel) {
scheduled = cellValues.row.requestLabel.scheduled === 'true';
}

return TaskRequester(cellValues.row.state.booking.requester, scheduled, prioritized);
},
flex: 1,
filterOperators: getMinimalStringFilterOperators,
filterable: true,
Expand Down Expand Up @@ -272,7 +288,7 @@ export function TaskDataGridTable({
renderCell: (cellValues) => {
let warnDateTime: Date | undefined = undefined;
if (cellValues.row.requestLabel && 'unix_millis_warn_time' in cellValues.row.requestLabel) {
const warnMillisNum = parseInt(cellValues.row.requestLabel['unix_millis_warn_time']);
const warnMillisNum = parseInt(cellValues.row.requestLabel.unix_millis_warn_time);
if (!Number.isNaN(warnMillisNum)) {
warnDateTime = new Date(warnMillisNum);
}
Expand Down Expand Up @@ -407,9 +423,7 @@ export function TaskDataGridTable({

let warnDateTime: Date | undefined = undefined;
if (params.row.requestLabel && 'unix_millis_warn_time' in params.row.requestLabel) {
const warnMillisNum = parseInt(
params.row.requestLabel.description['unix_millis_warn_time'],
);
const warnMillisNum = parseInt(params.row.requestLabel.unix_millis_warn_time);
if (!Number.isNaN(warnMillisNum)) {
warnDateTime = new Date(warnMillisNum);
}
Expand Down
25 changes: 24 additions & 1 deletion packages/react-components/lib/tasks/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { TaskRequest, TaskStateOutput as TaskState } from 'api-client';
import type { Priority, TaskRequest, TaskStateOutput as TaskState } from 'api-client';
import { TaskType as RmfTaskType } from 'rmf-models/ros/rmf_task_msgs/msg';

export function taskTypeToStr(taskType: number): string {
Expand Down Expand Up @@ -211,3 +211,26 @@ export function parseDestination(state?: TaskState, request?: TaskRequest): stri

return 'n/a';
}

export function createTaskPriority(prioritize: boolean): Priority {
return { type: 'binary', value: prioritize ? 1 : 0 };
}

// FIXME(ac): This method of parsing is crude, and will be fixed using schemas
// when we migrate to jsonforms.
export function parseTaskPriority(priority: Priority | null | undefined): boolean {
if (!priority) {
return false;
}

if (
typeof priority == 'object' &&
'type' in priority &&
priority['type'] === 'binary' &&
'value' in priority &&
typeof priority['value'] == 'number'
) {
return (priority['value'] as number) > 0;
}
return false;
}

0 comments on commit c8e0e14

Please sign in to comment.