From 9daeb5d3e5e12dacf846c756b3f6255046b52d7e Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Sun, 18 Feb 2024 22:35:48 +0800 Subject: [PATCH 01/37] Custom task type support, refactor tasks to be singular, cleaned up Signed-off-by: Aaron Chong --- packages/dashboard/src/components/appbar.tsx | 1 + .../src/managers/resource-manager.ts | 3 + .../lib/tasks/create-task.tsx | 371 ++++++++++-------- 3 files changed, 208 insertions(+), 167 deletions(-) diff --git a/packages/dashboard/src/components/appbar.tsx b/packages/dashboard/src/components/appbar.tsx index df0a51218..e7607d0ee 100644 --- a/packages/dashboard/src/components/appbar.tsx +++ b/packages/dashboard/src/components/appbar.tsx @@ -587,6 +587,7 @@ export const AppBar = React.memo(({ extraToolbarItems }: AppBarProps): React.Rea console.error(`Failed to create schedule: ${e.message}`); showAlert('error', `Failed to submit schedule: ${e.message}`); }} + allowCustomTask={resourceManager?.allowCustomTask} /> )} diff --git a/packages/dashboard/src/managers/resource-manager.ts b/packages/dashboard/src/managers/resource-manager.ts index 2f872d855..c158d745d 100644 --- a/packages/dashboard/src/managers/resource-manager.ts +++ b/packages/dashboard/src/managers/resource-manager.ts @@ -19,6 +19,7 @@ export interface ResourceConfigurationsType { cartIds?: string[]; loggedInDisplayLevel?: string; emergencyLots?: string[]; + allowCustomTask?: boolean; } export default class ResourceManager { @@ -34,6 +35,7 @@ export default class ResourceManager { cartIds?: string[]; loggedInDisplayLevel?: string; emergencyLots?: string[]; + allowCustomTask?: boolean; /** * Gets the default resource manager using the embedded resource file (aka "assets/resources/main.json"). @@ -74,6 +76,7 @@ export default class ResourceManager { this.cartIds = resources.cartIds || []; this.loggedInDisplayLevel = resources.loggedInDisplayLevel; this.emergencyLots = resources.emergencyLots || []; + this.allowCustomTask = resources.allowCustomTask ?? false; } } diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index a281dd4c4..6143ea171 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -168,6 +168,8 @@ export interface DeliveryTaskDescription { ]; } +type CustomTaskDescription = string; + type TaskDescription = | DeliveryTaskDescription | DeliveryCustomTaskDescription @@ -223,6 +225,20 @@ const isPatrolTaskDescriptionValid = (taskDescription: PatrolTaskDescription): b return taskDescription.rounds > 0; }; +const isCustomTaskDescriptionValid = (taskDescription: string): boolean => { + if (taskDescription.length === 0) { + return false; + } + + try { + const obj = JSON.parse(taskDescription); + } catch (e) { + return false; + } + + return true; +}; + const classes = { title: 'dialogue-info-value', selectFileBtn: 'create-task-selected-file-btn', @@ -259,26 +275,30 @@ export function getShortDescription(taskRequest: TaskRequest): string { )}`; } - const goToPickup: GoToPlaceActivity = - taskRequest.description.phases[0].activity.description.activities[0]; - const pickup: LotPickupActivity = - taskRequest.description.phases[0].activity.description.activities[1]; - const cartId = pickup.description.description.cart_id; - const goToDropoff: GoToPlaceActivity = - taskRequest.description.phases[1].activity.description.activities[0]; - - switch (taskRequest.description.category) { - case 'delivery_pickup': { - return `[Delivery - 1:1] payload [${cartId}] from [${goToPickup.description}] to [${goToDropoff.description}]`; - } - case 'delivery_sequential_lot_pickup': { - return `[Delivery - Sequential lot pick up] payload [${cartId}] from [${goToPickup.description}] to [${goToDropoff.description}]`; - } - case 'delivery_area_pickup': { - return `[Delivery - Area pick up] payload [${cartId}] from [${goToPickup.description}] to [${goToDropoff.description}]`; + try { + const goToPickup: GoToPlaceActivity = + taskRequest.description.phases[0].activity.description.activities[0]; + const pickup: LotPickupActivity = + taskRequest.description.phases[0].activity.description.activities[1]; + const cartId = pickup.description.description.cart_id; + const goToDropoff: GoToPlaceActivity = + taskRequest.description.phases[1].activity.description.activities[0]; + + switch (taskRequest.description.category) { + case 'delivery_pickup': { + return `[Delivery - 1:1] payload [${cartId}] from [${goToPickup.description}] to [${goToDropoff.description}]`; + } + case 'delivery_sequential_lot_pickup': { + return `[Delivery - Sequential lot pick up] payload [${cartId}] from [${goToPickup.description}] to [${goToDropoff.description}]`; + } + case 'delivery_area_pickup': { + return `[Delivery - Area pick up] payload [${cartId}] from [${goToPickup.description}] to [${goToDropoff.description}]`; + } + default: + return `[Unknown] type "${taskRequest.description.category}"`; } - default: - return `[Unknown] type "${taskRequest.description.category}"`; + } catch (e) { + return '[Unknown] type'; } } @@ -827,6 +847,37 @@ function PatrolTaskForm({ taskDesc, patrolWaypoints, onChange, allowSubmit }: Pa ); } +interface CustomTaskFormProps { + taskDesc: CustomTaskDescription; + onChange(customTaskDescription: CustomTaskDescription): void; + allowSubmit(allow: boolean): void; +} + +function CustomTaskForm({ taskDesc, onChange, allowSubmit }: CustomTaskFormProps) { + const theme = useTheme(); + const onInputChange = (desc: CustomTaskDescription) => { + allowSubmit(isCustomTaskDescriptionValid(desc)); + onChange(desc); + }; + + return ( + + + { + onInputChange(ev.target.value); + }} + /> + + + ); +} + interface FavoriteTaskProps { listItemText: string; listItemClick: () => void; @@ -1147,7 +1198,9 @@ export interface CreateTaskFormProps dropoffPoints?: Record; favoritesTasks?: TaskFavorite[]; scheduleToEdit?: Schedule; + // requestTask is provided only when editing a schedule requestTask?: TaskRequest; + allowCustomTask?: boolean; submitTasks?(tasks: TaskRequest[], schedule: Schedule | null): Promise; tasksFromFile?(): Promise | TaskRequest[]; onSuccess?(tasks: TaskRequest[]): void; @@ -1174,6 +1227,7 @@ export function CreateTaskForm({ favoritesTasks = [], scheduleToEdit, requestTask, + allowCustomTask, submitTasks, tasksFromFile, onClose, @@ -1202,21 +1256,18 @@ export function CreateTaskForm({ const [favoriteTaskTitleError, setFavoriteTaskTitleError] = React.useState(false); const [savingFavoriteTask, setSavingFavoriteTask] = React.useState(false); - const [taskRequests, setTaskRequests] = React.useState(() => [ - requestTask ?? defaultTask(), - ]); - const [selectedTaskIdx, setSelectedTaskIdx] = React.useState(0); - const taskTitles = React.useMemo( - () => taskRequests && taskRequests.map((t, i) => `${i + 1}: ${getShortDescription(t)}`), - [taskRequests], + const [taskType, setTaskType] = React.useState(undefined); + const [taskRequest, setTaskRequest] = React.useState( + () => requestTask ?? defaultTask(), ); + const [submitting, setSubmitting] = React.useState(false); const [formFullyFilled, setFormFullyFilled] = React.useState(requestTask !== undefined || false); - const taskRequest = taskRequests[selectedTaskIdx]; const [openSchedulingDialog, setOpenSchedulingDialog] = React.useState(false); const defaultScheduleDate = new Date(); defaultScheduleDate.setSeconds(0); defaultScheduleDate.setMilliseconds(0); + const [schedule, setSchedule] = React.useState( scheduleToEdit ?? { startOn: defaultScheduleDate, @@ -1244,33 +1295,42 @@ export function CreateTaskForm({ } setScheduleUntilValue(event.target.value); }; - // schedule is not supported with batch upload - const scheduleEnabled = taskRequests.length === 1; const [warnTimeChecked, setWarnTimeChecked] = React.useState(false); const handleWarnTimeCheckboxChange = (event: React.ChangeEvent) => { setWarnTimeChecked(event.target.checked); }; - const updateTasks = () => { - setTaskRequests((prev) => { - prev.splice(selectedTaskIdx, 1, taskRequest); - return [...prev]; - }); - }; - const handleTaskDescriptionChange = (newDesc: TaskDescription) => { - taskRequest.category = 'compose'; - taskRequest.description = newDesc; + setTaskRequest((prev) => { + return { + ...prev, + category: 'compose', + description: newDesc, + }; + }); setFavoriteTaskBuffer({ ...favoriteTaskBuffer, description: newDesc, category: 'compose' }); - updateTasks(); }; const legacyHandleTaskDescriptionChange = (newCategory: string, newDesc: TaskDescription) => { - taskRequest.category = newCategory; - taskRequest.description = newDesc; + setTaskRequest((prev) => { + return { + ...prev, + category: newCategory, + description: newDesc, + }; + }); setFavoriteTaskBuffer({ ...favoriteTaskBuffer, description: newDesc, category: newCategory }); - updateTasks(); + }; + + const handleCustomTaskDescriptionChange = (newDesc: CustomTaskDescription) => { + setTaskRequest((prev) => { + return { + ...prev, + category: 'custom', + description: newDesc, + }; + }); }; const renderTaskDescriptionForm = () => { @@ -1283,6 +1343,16 @@ export function CreateTaskForm({ allowSubmit={allowSubmit} /> ); + } else if (taskRequest.category === 'custom') { + return ( + { + handleCustomTaskDescriptionChange(desc); + }} + allowSubmit={allowSubmit} + /> + ); } switch (taskRequest.description.category) { @@ -1324,18 +1394,23 @@ export function CreateTaskForm({ } }; const handleTaskTypeChange = (ev: React.ChangeEvent) => { - const newCategory = ev.target.value; - const newDesc = defaultTaskDescription(newCategory); - if (newDesc === undefined) { - return; - } - taskRequest.description = newDesc; - const category = newCategory === 'patrol' ? 'patrol' : 'compose'; - taskRequest.category = category; + const newType = ev.target.value; + setTaskType(newType); - setFavoriteTaskBuffer({ ...favoriteTaskBuffer, category, description: newDesc }); + if (newType === 'custom') { + taskRequest.category = 'custom'; + taskRequest.description = ''; + } else { + const newDesc = defaultTaskDescription(newType); + if (newDesc === undefined) { + return; + } + taskRequest.description = newDesc; + const category = newType === 'patrol' ? 'patrol' : 'compose'; + taskRequest.category = category; - updateTasks(); + setFavoriteTaskBuffer({ ...favoriteTaskBuffer, category, description: newDesc }); + } }; const allowSubmit = (allow: boolean) => { @@ -1345,69 +1420,78 @@ export function CreateTaskForm({ // no memo because deps would likely change const handleSubmit = async (scheduling: boolean) => { if (!submitTasks) { - onSuccess && onSuccess(taskRequests); + onSuccess && onSuccess([taskRequest]); return; } const requester = scheduling ? `${user}__scheduled` : user; - for (const t of taskRequests) { - t.requester = requester; - t.unix_millis_request_time = Date.now(); - - // Workaround where all the task category need to be compose. - if (t.category !== 'patrol') { - t.category = 'compose'; - - const goToOneOfThePlaces: GoToOneOfThePlacesActivity = { - category: 'go_to_place', - description: { - one_of: emergencyLots.map((placeName) => { - return { - waypoint: placeName, - }; - }), - constraints: [ - { - category: 'prefer_same_map', - description: '', - }, - ], - }, - }; - const deliveryDropoff: DropoffActivity = { - category: 'perform_action', - description: { - unix_millis_action_duration_estimate: 60000, - category: 'delivery_dropoff', - description: {}, - }, - }; - const onCancelDropoff: OnCancelDropoff = { - category: 'sequence', - description: [goToOneOfThePlaces, deliveryDropoff], - }; - taskRequest.description.phases[1].on_cancel = [onCancelDropoff]; + const request = { ...taskRequest }; + request.requester = requester; + request.unix_millis_request_time = Date.now(); + + if ( + taskType === 'delivery_pickup' || + taskType === 'delivery_sequential_lot_pickup' || + taskType === 'delivery_area_pickup' + ) { + const goToOneOfThePlaces: GoToOneOfThePlacesActivity = { + category: 'go_to_place', + description: { + one_of: emergencyLots.map((placeName) => { + return { + waypoint: placeName, + }; + }), + constraints: [ + { + category: 'prefer_same_map', + description: '', + }, + ], + }, + }; + const deliveryDropoff: DropoffActivity = { + category: 'perform_action', + description: { + unix_millis_action_duration_estimate: 60000, + category: 'delivery_dropoff', + description: {}, + }, + }; + const onCancelDropoff: OnCancelDropoff = { + category: 'sequence', + description: [goToOneOfThePlaces, deliveryDropoff], + }; + request.description.phases[1].on_cancel = [onCancelDropoff]; + } else if (taskType === 'custom') { + try { + const obj = JSON.parse(request.description); + request.category = 'compose'; + request.description = obj; + } catch (e) { + console.error('Invalid custom task description'); + onFail && onFail(e as Error, [request]); + return; } } - const submittingSchedule = scheduling && scheduleEnabled; try { setSubmitting(true); - await submitTasks(taskRequests, submittingSchedule ? schedule : null); + await submitTasks([request], scheduling ? schedule : null); setSubmitting(false); - if (submittingSchedule) { + if (scheduling) { onSuccessScheduling && onSuccessScheduling(); } else { - onSuccess && onSuccess(taskRequests); + onSuccess && onSuccess([request]); } } catch (e) { setSubmitting(false); - if (submittingSchedule) { + if (scheduling) { onFailScheduling && onFailScheduling(e as Error); } else { - onFail && onFail(e as Error, taskRequests); + onFail && onFail(e as Error, [request]); } } }; @@ -1465,7 +1549,7 @@ export function CreateTaskForm({ onSuccessFavoriteTask && onSuccessFavoriteTask('Deleted favorite task successfully', favoriteTaskBuffer); - setTaskRequests([defaultTask()]); + setTaskRequest(defaultTask()); setOpenFavoriteDialog(false); setCallToDeleteFavoriteTask(false); setCallToUpdateFavoriteTask(false); @@ -1475,29 +1559,11 @@ export function CreateTaskForm({ } }; - /* eslint-disable @typescript-eslint/no-unused-vars */ - const handleSelectFileClick: React.MouseEventHandler = () => { - if (!tasksFromFile) { - return; - } - (async () => { - const newTasks = await tasksFromFile(); - if (newTasks.length === 0) { - return; - } - setTaskRequests(newTasks); - setSelectedTaskIdx(0); - })(); - }; - - const submitText = taskRequests.length > 1 ? 'Submit All Now' : 'Submit Now'; - return ( <> 1} disableEnforceFocus {...otherProps} > @@ -1529,14 +1595,12 @@ export function CreateTaskForm({ setOpenDialog={setOpenFavoriteDialog} listItemClick={() => { setFavoriteTaskBuffer(favoriteTask); - setTaskRequests([ - { - category: favoriteTask.category, - description: favoriteTask.description, - unix_millis_earliest_start_time: Date.now(), - priority: favoriteTask.priority, - }, - ]); + setTaskRequest({ + category: favoriteTask.category, + description: favoriteTask.description, + unix_millis_earliest_start_time: 0, + priority: favoriteTask.priority, + }); }} /> ); @@ -1605,27 +1669,19 @@ export function CreateTaskForm({ > Patrol + + Custom Task + { - if (!date) { - return; - } - taskRequest.unix_millis_earliest_start_time = date.valueOf(); - setFavoriteTaskBuffer({ - ...favoriteTaskBuffer, - unix_millis_earliest_start_time: date.valueOf(), - }); - updateTasks(); - }} + value={new Date()} + onChange={() => {}} label="Start Time" renderInput={(props) => ( { + return { + ...prev, + unix_millis_warn_time: date.valueOf(), + }; + }); }} label="Warn Time" renderInput={(props) => ( @@ -1704,33 +1765,12 @@ export function CreateTaskForm({ setOpenFavoriteDialog(true); }} style={{ marginTop: theme.spacing(2), marginBottom: theme.spacing(2) }} + disabled={taskType === 'custom'} > {callToUpdateFavoriteTask ? `Confirm edits` : 'Save as a favorite task'} - {taskTitles.length > 1 && ( - <> - - - {taskTitles.map((title, idx) => ( - setSelectedTaskIdx(idx)} - className={selectedTaskIdx === idx ? classes.selectedTask : undefined} - role="listitem button" - > - - - ))} - - - )} @@ -1759,7 +1799,7 @@ export function CreateTaskForm({ color="primary" disabled={submitting || !formFullyFilled || scheduleToEdit !== undefined} className={classes.actionBtn} - aria-label={submitText} + aria-label="Submit Now" onClick={handleSubmitNow} size={isScreenHeightLessThan800 ? 'small' : 'medium'} > @@ -1769,7 +1809,7 @@ export function CreateTaskForm({ size={isScreenHeightLessThan800 ? '0.8em' : '1.5em'} color="inherit" > - {submitText} + Submit Now @@ -1837,7 +1877,6 @@ export function CreateTaskForm({ }); }} label="Start On" - disabled={!scheduleEnabled} renderInput={(props) => ( ( setSchedule((prev) => ({ ...prev, days }))} /> From 81f0bd34636b65762b2d298b5810112a5a9f535c Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Sun, 18 Feb 2024 22:55:38 +0800 Subject: [PATCH 02/37] lint Signed-off-by: Aaron Chong --- packages/react-components/lib/tasks/create-task.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 6143ea171..9ce265ec6 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -231,7 +231,7 @@ const isCustomTaskDescriptionValid = (taskDescription: string): boolean => { } try { - const obj = JSON.parse(taskDescription); + JSON.parse(taskDescription); } catch (e) { return false; } @@ -1681,7 +1681,7 @@ export function CreateTaskForm({ {}} + onChange={() => 0} label="Start Time" renderInput={(props) => ( Date: Fri, 1 Mar 2024 18:31:53 +0800 Subject: [PATCH 03/37] Make custom task always available Signed-off-by: Aaron Chong --- packages/dashboard/src/components/appbar.tsx | 1 - packages/dashboard/src/managers/resource-manager.ts | 3 --- packages/react-components/lib/tasks/create-task.tsx | 10 ++-------- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/dashboard/src/components/appbar.tsx b/packages/dashboard/src/components/appbar.tsx index e7607d0ee..df0a51218 100644 --- a/packages/dashboard/src/components/appbar.tsx +++ b/packages/dashboard/src/components/appbar.tsx @@ -587,7 +587,6 @@ export const AppBar = React.memo(({ extraToolbarItems }: AppBarProps): React.Rea console.error(`Failed to create schedule: ${e.message}`); showAlert('error', `Failed to submit schedule: ${e.message}`); }} - allowCustomTask={resourceManager?.allowCustomTask} /> )} diff --git a/packages/dashboard/src/managers/resource-manager.ts b/packages/dashboard/src/managers/resource-manager.ts index c158d745d..2f872d855 100644 --- a/packages/dashboard/src/managers/resource-manager.ts +++ b/packages/dashboard/src/managers/resource-manager.ts @@ -19,7 +19,6 @@ export interface ResourceConfigurationsType { cartIds?: string[]; loggedInDisplayLevel?: string; emergencyLots?: string[]; - allowCustomTask?: boolean; } export default class ResourceManager { @@ -35,7 +34,6 @@ export default class ResourceManager { cartIds?: string[]; loggedInDisplayLevel?: string; emergencyLots?: string[]; - allowCustomTask?: boolean; /** * Gets the default resource manager using the embedded resource file (aka "assets/resources/main.json"). @@ -76,7 +74,6 @@ export default class ResourceManager { this.cartIds = resources.cartIds || []; this.loggedInDisplayLevel = resources.loggedInDisplayLevel; this.emergencyLots = resources.emergencyLots || []; - this.allowCustomTask = resources.allowCustomTask ?? false; } } diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 9ce265ec6..0c482d2a1 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -781,6 +781,7 @@ function PatrolTaskForm({ taskDesc, patrolWaypoints, onChange, allowSubmit }: Pa allowSubmit(isPatrolTaskDescriptionValid(desc)); onChange(desc); }; + allowSubmit(isPatrolTaskDescriptionValid(taskDesc)); return ( @@ -1200,7 +1201,6 @@ export interface CreateTaskFormProps scheduleToEdit?: Schedule; // requestTask is provided only when editing a schedule requestTask?: TaskRequest; - allowCustomTask?: boolean; submitTasks?(tasks: TaskRequest[], schedule: Schedule | null): Promise; tasksFromFile?(): Promise | TaskRequest[]; onSuccess?(tasks: TaskRequest[]): void; @@ -1227,7 +1227,6 @@ export function CreateTaskForm({ favoritesTasks = [], scheduleToEdit, requestTask, - allowCustomTask, submitTasks, tasksFromFile, onClose, @@ -1669,12 +1668,7 @@ export function CreateTaskForm({ > Patrol - - Custom Task - + Custom Task From b3bdf92b8f39c65b4c03242295a6b76f555f67a0 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 4 Mar 2024 15:21:24 +0800 Subject: [PATCH 04/37] Check exception for TypeError Signed-off-by: Aaron Chong --- packages/react-components/lib/tasks/create-task.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 0c482d2a1..ccc6bb827 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -275,6 +275,7 @@ export function getShortDescription(taskRequest: TaskRequest): string { )}`; } + // This section is only valid for custom delivery types try { const goToPickup: GoToPlaceActivity = taskRequest.description.phases[0].activity.description.activities[0]; @@ -298,7 +299,12 @@ export function getShortDescription(taskRequest: TaskRequest): string { return `[Unknown] type "${taskRequest.description.category}"`; } } catch (e) { - return '[Unknown] type'; + if (e instanceof TypeError) { + console.error(`Failed to parse custom delivery: ${e.message}`); + } else { + console.error(`Failed to generate short description from task: ${(e as Error).message}`); + } + return `[Unknown] type`; } } From 279b2b1c0fae66794faf3e1c714a44122abc02f2 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 4 Mar 2024 21:17:24 +0800 Subject: [PATCH 05/37] Unifying handleTaskDescriptionChange, added FIXME for allowing favoriting custom tasks Signed-off-by: Aaron Chong --- .../lib/tasks/create-task.tsx | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index ccc6bb827..46226c2d2 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -1306,18 +1306,7 @@ export function CreateTaskForm({ setWarnTimeChecked(event.target.checked); }; - const handleTaskDescriptionChange = (newDesc: TaskDescription) => { - setTaskRequest((prev) => { - return { - ...prev, - category: 'compose', - description: newDesc, - }; - }); - setFavoriteTaskBuffer({ ...favoriteTaskBuffer, description: newDesc, category: 'compose' }); - }; - - const legacyHandleTaskDescriptionChange = (newCategory: string, newDesc: TaskDescription) => { + const handleTaskDescriptionChange = (newCategory: string, newDesc: TaskDescription) => { setTaskRequest((prev) => { return { ...prev, @@ -1328,6 +1317,9 @@ export function CreateTaskForm({ setFavoriteTaskBuffer({ ...favoriteTaskBuffer, description: newDesc, category: newCategory }); }; + // FIXME: Custom task descriptions are currently not allowed to be saved as + // favorite tasks. This will probably require a re-write of FavoriteTask's + // pydantic model with better typing. const handleCustomTaskDescriptionChange = (newDesc: CustomTaskDescription) => { setTaskRequest((prev) => { return { @@ -1344,7 +1336,7 @@ export function CreateTaskForm({ legacyHandleTaskDescriptionChange('patrol', desc)} + onChange={(desc) => handleTaskDescriptionChange('patrol', desc)} allowSubmit={allowSubmit} /> ); @@ -1372,7 +1364,7 @@ export function CreateTaskForm({ desc.category = taskRequest.description.category; desc.phases[0].activity.description.activities[1].description.category = taskRequest.description.category; - handleTaskDescriptionChange(desc); + handleTaskDescriptionChange('compose', desc); }} allowSubmit={allowSubmit} /> @@ -1389,7 +1381,7 @@ export function CreateTaskForm({ desc.category = taskRequest.description.category; desc.phases[0].activity.description.activities[1].description.category = taskRequest.description.category; - handleTaskDescriptionChange(desc); + handleTaskDescriptionChange('compose', desc); }} allowSubmit={allowSubmit} /> From fab9d5d24aad24122c8d2445b8079977ca925ef9 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 4 Mar 2024 21:31:52 +0800 Subject: [PATCH 06/37] Renaming custom to custom_compose, since the task category will always be compose, display description for short description Signed-off-by: Aaron Chong --- .../lib/tasks/create-task.tsx | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 46226c2d2..301f70a48 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -168,7 +168,7 @@ export interface DeliveryTaskDescription { ]; } -type CustomTaskDescription = string; +type CustomComposeTaskDescription = string; type TaskDescription = | DeliveryTaskDescription @@ -302,9 +302,14 @@ export function getShortDescription(taskRequest: TaskRequest): string { if (e instanceof TypeError) { console.error(`Failed to parse custom delivery: ${e.message}`); } else { - console.error(`Failed to generate short description from task: ${(e as Error).message}`); + console.error( + `Failed to generate short description from task of category: ${taskRequest.category}: ${ + (e as Error).message + }`, + ); } - return `[Unknown] type`; + console.error(JSON.stringify(taskRequest.description)); + return JSON.stringify(taskRequest.description); } } @@ -854,15 +859,15 @@ function PatrolTaskForm({ taskDesc, patrolWaypoints, onChange, allowSubmit }: Pa ); } -interface CustomTaskFormProps { - taskDesc: CustomTaskDescription; - onChange(customTaskDescription: CustomTaskDescription): void; +interface CustomComposeTaskFormProps { + taskDesc: CustomComposeTaskDescription; + onChange(customComposeTaskDescription: CustomComposeTaskDescription): void; allowSubmit(allow: boolean): void; } -function CustomTaskForm({ taskDesc, onChange, allowSubmit }: CustomTaskFormProps) { +function CustomComposeTaskForm({ taskDesc, onChange, allowSubmit }: CustomComposeTaskFormProps) { const theme = useTheme(); - const onInputChange = (desc: CustomTaskDescription) => { + const onInputChange = (desc: CustomComposeTaskDescription) => { allowSubmit(isCustomTaskDescriptionValid(desc)); onChange(desc); }; @@ -1317,14 +1322,14 @@ export function CreateTaskForm({ setFavoriteTaskBuffer({ ...favoriteTaskBuffer, description: newDesc, category: newCategory }); }; - // FIXME: Custom task descriptions are currently not allowed to be saved as - // favorite tasks. This will probably require a re-write of FavoriteTask's - // pydantic model with better typing. - const handleCustomTaskDescriptionChange = (newDesc: CustomTaskDescription) => { + // FIXME: Custom compose task descriptions are currently not allowed to be + // saved as favorite tasks. This will probably require a re-write of + // FavoriteTask's pydantic model with better typing. + const handleCustomComposeTaskDescriptionChange = (newDesc: CustomComposeTaskDescription) => { setTaskRequest((prev) => { return { ...prev, - category: 'custom', + category: 'custom_compose', description: newDesc, }; }); @@ -1340,12 +1345,12 @@ export function CreateTaskForm({ allowSubmit={allowSubmit} /> ); - } else if (taskRequest.category === 'custom') { + } else if (taskRequest.category === 'custom_compose') { return ( - { - handleCustomTaskDescriptionChange(desc); + handleCustomComposeTaskDescriptionChange(desc); }} allowSubmit={allowSubmit} /> @@ -1394,8 +1399,8 @@ export function CreateTaskForm({ const newType = ev.target.value; setTaskType(newType); - if (newType === 'custom') { - taskRequest.category = 'custom'; + if (newType === 'custom_compose') { + taskRequest.category = 'custom_compose'; taskRequest.description = ''; } else { const newDesc = defaultTaskDescription(newType); @@ -1461,13 +1466,13 @@ export function CreateTaskForm({ description: [goToOneOfThePlaces, deliveryDropoff], }; request.description.phases[1].on_cancel = [onCancelDropoff]; - } else if (taskType === 'custom') { + } else if (taskType === 'custom_compose') { try { const obj = JSON.parse(request.description); request.category = 'compose'; request.description = obj; } catch (e) { - console.error('Invalid custom task description'); + console.error('Invalid custom compose task description'); onFail && onFail(e as Error, [request]); return; } @@ -1666,7 +1671,7 @@ export function CreateTaskForm({ > Patrol - Custom Task + Custom Compose Task @@ -1757,7 +1762,7 @@ export function CreateTaskForm({ setOpenFavoriteDialog(true); }} style={{ marginTop: theme.spacing(2), marginBottom: theme.spacing(2) }} - disabled={taskType === 'custom'} + disabled={taskType === 'custom_compose'} > {callToUpdateFavoriteTask ? `Confirm edits` : 'Save as a favorite task'} From 03aa973ca9a849be52651fc6a82c4b252a41cdfd Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 6 Mar 2024 18:32:48 +0800 Subject: [PATCH 07/37] Catch possible exception with JSON.stringify, add fixmes for task description assumptions Signed-off-by: Aaron Chong --- .../components/tasks/task-schedule-utils.ts | 6 ++--- .../lib/tasks/create-task.tsx | 26 ++++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/dashboard/src/components/tasks/task-schedule-utils.ts b/packages/dashboard/src/components/tasks/task-schedule-utils.ts index fbe701b84..306c683e2 100644 --- a/packages/dashboard/src/components/tasks/task-schedule-utils.ts +++ b/packages/dashboard/src/components/tasks/task-schedule-utils.ts @@ -167,11 +167,11 @@ export const apiScheduleToSchedule = (scheduleTask: ApiSchedule[]): Schedule => }; export const getScheduledTaskTitle = (task: ScheduledTask): string => { - if (!task.task_request || !task.task_request.category) { + const shortDescription = getShortDescription(task.task_request); + if (!task.task_request || !task.task_request.category || !shortDescription) { return `[${task.id}] Unknown`; } - - return `${getShortDescription(task.task_request)}`; + return shortDescription; }; // Pad a number to 2 digits diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 301f70a48..1aedab997 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -267,7 +267,7 @@ const StyledDialog = styled((props: DialogProps) => )(({ th }, })); -export function getShortDescription(taskRequest: TaskRequest): string { +export function getShortDescription(taskRequest: TaskRequest): string | undefined { if (taskRequest.category === 'patrol') { const formattedPlaces = taskRequest.description.places.map((place: string) => `[${place}]`); return `[Patrol] [${taskRequest.description.rounds}] round/s, along ${formattedPlaces.join( @@ -276,6 +276,12 @@ export function getShortDescription(taskRequest: TaskRequest): string { } // This section is only valid for custom delivery types + // FIXME: This block looks like it makes assumptions about the structure of + // the task description in order to parse it, but it is following the + // statically defined description (object) at the top of this file. The + // descriptions should be replaced by a schema in general, however the better + // approach now should be to make each task description testable and in charge + // of their own short descriptions. try { const goToPickup: GoToPlaceActivity = taskRequest.description.phases[0].activity.description.activities[0]; @@ -308,8 +314,19 @@ export function getShortDescription(taskRequest: TaskRequest): string { }`, ); } - console.error(JSON.stringify(taskRequest.description)); - return JSON.stringify(taskRequest.description); + + try { + const descriptionString = JSON.stringify(taskRequest.description); + console.error(descriptionString); + return descriptionString; + } catch (e) { + console.error( + `Failed to parse description of task of category: ${taskRequest.category}: ${ + (e as Error).message + }`, + ); + return undefined; + } } } @@ -1453,6 +1470,9 @@ export function CreateTaskForm({ ], }, }; + + // FIXME: there should not be any statically defined duration estimates as + // it makes assumptions of the deployments. const deliveryDropoff: DropoffActivity = { category: 'perform_action', description: { From 3573531c43fb97d9960fa4c1edf5688eb0de421e Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Tue, 12 Mar 2024 14:53:01 +0800 Subject: [PATCH 08/37] Pickup and destination in task state labels Signed-off-by: Aaron Chong --- .../api-server/api_server/models/__init__.py | 1 + .../api_server/models/task_request_label.py | 22 +++++++++++ .../api_server/repositories/tasks.py | 37 ++++++++++++------ .../src/components/tasks/task-summary.tsx | 28 ++++++++------ .../src/components/tasks/tasks-app.tsx | 34 +---------------- .../dashboard/src/components/tasks/utils.ts | 15 +++----- .../lib/tasks/create-task.tsx | 38 ++++++++++++++++++- .../lib/tasks/task-table-datagrid.tsx | 17 ++++++--- packages/react-components/lib/tasks/utils.ts | 26 +++++++++++++ 9 files changed, 147 insertions(+), 71 deletions(-) create mode 100644 packages/api-server/api_server/models/task_request_label.py diff --git a/packages/api-server/api_server/models/__init__.py b/packages/api-server/api_server/models/__init__.py index 611d88b6a..31aa3099d 100644 --- a/packages/api-server/api_server/models/__init__.py +++ b/packages/api-server/api_server/models/__init__.py @@ -45,4 +45,5 @@ from .rmf_api.task_state_update import TaskStateUpdate from .rmf_api.undo_skip_phase_request import UndoPhaseSkipRequest from .rmf_api.undo_skip_phase_response import UndoPhaseSkipResponse +from .task_request_label import * from .user import * diff --git a/packages/api-server/api_server/models/task_request_label.py b/packages/api-server/api_server/models/task_request_label.py new file mode 100644 index 000000000..249e1f3b0 --- /dev/null +++ b/packages/api-server/api_server/models/task_request_label.py @@ -0,0 +1,22 @@ +from typing import Optional + +import pydantic +from pydantic import BaseModel + +# NOTE: This label model needs to exactly match the fields that are defined and +# populated by the dashboard. Any changes to either side will require syncing. + + +class TaskRequestLabel(BaseModel): + category: Optional[str] + unix_millis_warn_time: Optional[str] + pickup: Optional[str] + destination: Optional[str] + cart_id: Optional[str] + + @staticmethod + def from_json_string(json_str: str) -> Optional["TaskRequestLabel"]: + try: + return TaskRequestLabel.parse_raw(json_str) + except pydantic.error_wrappers.ValidationError: + return None diff --git a/packages/api-server/api_server/repositories/tasks.py b/packages/api-server/api_server/repositories/tasks.py index c33a176fa..44dfca1c0 100644 --- a/packages/api-server/api_server/repositories/tasks.py +++ b/packages/api-server/api_server/repositories/tasks.py @@ -1,3 +1,4 @@ +import json import sys from datetime import datetime from typing import Dict, List, Optional, Sequence, Tuple, cast @@ -16,6 +17,7 @@ Phases, TaskEventLog, TaskRequest, + TaskRequestLabel, TaskState, User, ) @@ -136,17 +138,17 @@ async def save_task_request( ) # Add pickup and destination to task state model for filter and sort - pickup = parse_pickup(task_request) - destination = parse_destination(task_state, task_request) - db_task_state = await DbTaskState.get_or_none(id_=task_state.booking.id) - if db_task_state is not None: - db_task_state.update_from_dict( - { - "pickup": pickup, - "destination": destination, - } - ) - await db_task_state.save() + # pickup = parse_pickup(task_request) + # destination = parse_destination(task_state, task_request) + # db_task_state = await DbTaskState.get_or_none(id_=task_state.booking.id) + # if db_task_state is not None: + # db_task_state.update_from_dict( + # { + # "pickup": pickup, + # "destination": destination, + # } + # ) + # await db_task_state.save() async def get_task_request(self, task_id: str) -> Optional[TaskRequest]: result = await DbTaskRequest.get_or_none(id_=task_id) @@ -162,6 +164,15 @@ async def query_task_requests(self, task_ids: List[str]) -> List[DbTaskRequest]: raise HTTPException(422, str(e)) from e async def save_task_state(self, task_state: TaskState) -> None: + labels = task_state.booking.labels + request_label = None + if labels is not None: + for l in labels: + validated_request_label = TaskRequestLabel.from_json_string(l) + if validated_request_label is not None: + request_label = validated_request_label + break + task_state_dict = { "data": task_state.json(), "category": task_state.category.__root__ if task_state.category else None, @@ -180,6 +191,10 @@ async def save_task_state(self, task_state: TaskState) -> None: "requester": task_state.booking.requester if task_state.booking.requester else None, + "pickup": request_label.pickup if request_label is not None else None, + "destination": request_label.destination + if request_label is not None + else None, } if task_state.unix_millis_warn_time is not None: diff --git a/packages/dashboard/src/components/tasks/task-summary.tsx b/packages/dashboard/src/components/tasks/task-summary.tsx index 12da073e6..5f13253b4 100644 --- a/packages/dashboard/src/components/tasks/task-summary.tsx +++ b/packages/dashboard/src/components/tasks/task-summary.tsx @@ -14,8 +14,8 @@ import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import { makeStyles, createStyles } from '@mui/styles'; -import { Status, TaskRequest, TaskState } from 'api-client'; -import { base, parseCartId, parseCategory, parseDestination, parsePickup } from 'react-components'; +import { Status, TaskState } from 'api-client'; +import { base, parseTaskRequestLabel, TaskRequestLabel } from 'react-components'; import { TaskInspector } from './task-inspector'; import { RmfAppContext } from '../rmf-app'; import { TaskCancelButton } from './task-cancellation'; @@ -69,7 +69,6 @@ const setTaskDialogColor = (taskStatus: Status | undefined) => { export interface TaskSummaryProps { onClose: () => void; task?: TaskState; - request?: TaskRequest; } export const TaskSummary = React.memo((props: TaskSummaryProps) => { @@ -77,10 +76,11 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { const classes = useStyles(); const rmf = React.useContext(RmfAppContext); - const { onClose, task, request } = props; + const { onClose, task } = props; const [openTaskDetailsLogs, setOpenTaskDetailsLogs] = React.useState(false); const [taskState, setTaskState] = React.useState(null); + const [label, setLabel] = React.useState({}); const [isOpen, setIsOpen] = React.useState(true); const taskProgress = React.useMemo(() => { @@ -106,9 +106,15 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { if (!rmf || !task) { return; } - const sub = rmf - .getTaskStateObs(task.booking.id) - .subscribe((subscribedTask) => setTaskState(subscribedTask)); + const sub = rmf.getTaskStateObs(task.booking.id).subscribe((subscribedTask) => { + const requestLabel = parseTaskRequestLabel(subscribedTask); + if (requestLabel) { + setLabel(requestLabel); + } else { + setLabel({}); + } + setTaskState(subscribedTask); + }); return () => sub.unsubscribe(); }, [rmf, task]); @@ -120,19 +126,19 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { }, { title: 'Category', - value: parseCategory(task, request), + value: label.category ?? 'n/a', }, { title: 'Pickup', - value: parsePickup(request), + value: label.pickup ?? 'n/a', }, { title: 'Cart ID', - value: parseCartId(request), + value: label.cart_id ?? 'n/a', }, { title: 'Dropoff', - value: parseDestination(task, request), + value: label.destination ?? 'n/a', }, { title: 'Est. end time', diff --git a/packages/dashboard/src/components/tasks/tasks-app.tsx b/packages/dashboard/src/components/tasks/tasks-app.tsx index 6645a448c..cb8c0be69 100644 --- a/packages/dashboard/src/components/tasks/tasks-app.tsx +++ b/packages/dashboard/src/components/tasks/tasks-app.tsx @@ -93,7 +93,6 @@ export const TasksApp = React.memo( const [tasksState, setTasksState] = React.useState({ isLoading: true, data: [], - requests: {}, total: 0, page: 1, pageSize: 10, @@ -280,32 +279,6 @@ export const TasksApp = React.memo( return allTasks; }; - const getPastMonthTaskRequests = async (tasks: TaskState[]) => { - if (!rmf) { - return {}; - } - - const taskRequestMap: Record = {}; - const allTaskIds: string[] = tasks.map((task) => task.booking.id); - const queriesRequired = Math.ceil(allTaskIds.length / QueryLimit); - for (let i = 0; i < queriesRequired; i++) { - const endingIndex = Math.min(allTaskIds.length, (i + 1) * QueryLimit); - const taskIds = allTaskIds.slice(i * QueryLimit, endingIndex); - const taskIdsQuery = taskIds.join(','); - const taskRequests = (await rmf.tasksApi.queryTaskRequestsTasksRequestsGet(taskIdsQuery)) - .data; - - let requestIndex = 0; - for (const id of taskIds) { - if (requestIndex < taskRequests.length && taskRequests[requestIndex]) { - taskRequestMap[id] = taskRequests[requestIndex]; - } - ++requestIndex; - } - } - return taskRequestMap; - }; - const exportTasksToCsv = async (minimal: boolean) => { AppEvents.loadingBackdrop.next(true); const now = new Date(); @@ -315,11 +288,7 @@ export const TasksApp = React.memo( return; } if (minimal) { - // FIXME: Task requests are currently required for parsing pickup and - // destination information. Once we start using TaskState.Booking.Labels - // to encode these fields, we can skip querying for task requests. - const pastMonthTaskRequests = await getPastMonthTaskRequests(pastMonthTasks); - exportCsvMinimal(now, pastMonthTasks, pastMonthTaskRequests); + exportCsvMinimal(now, pastMonthTasks); } else { exportCsvFull(now, pastMonthTasks); } @@ -477,7 +446,6 @@ export const TasksApp = React.memo( setOpenTaskSummary(false)} task={selectedTask ?? undefined} - request={selectedTask ? tasksState.requests[selectedTask.booking.id] : undefined} /> )} {children} diff --git a/packages/dashboard/src/components/tasks/utils.ts b/packages/dashboard/src/components/tasks/utils.ts index 93d74a9f5..da5d83109 100644 --- a/packages/dashboard/src/components/tasks/utils.ts +++ b/packages/dashboard/src/components/tasks/utils.ts @@ -1,5 +1,5 @@ import { PostScheduledTaskRequest, TaskRequest, TaskState } from 'api-client'; -import { Schedule, parsePickup, parseDestination } from 'react-components'; +import { parseTaskRequestLabel, Schedule } from 'react-components'; import schema from 'api-client/dist/schema'; import { ajv } from '../utils'; @@ -47,11 +47,7 @@ export function exportCsvFull(timestamp: Date, allTasks: TaskState[]) { }); } -export function exportCsvMinimal( - timestamp: Date, - allTasks: TaskState[], - taskRequestMap: Record, -) { +export function exportCsvMinimal(timestamp: Date, allTasks: TaskState[]) { const columnSeparator = ';'; const rowSeparator = '\n'; let csvContent = `sep=${columnSeparator}` + rowSeparator; @@ -67,7 +63,8 @@ export function exportCsvMinimal( ]; csvContent += keys.join(columnSeparator) + rowSeparator; allTasks.forEach((task) => { - const request: TaskRequest | undefined = taskRequestMap[task.booking.id]; + let requestLabel = parseTaskRequestLabel(task); + const values = [ // Date task.booking.unix_millis_request_time @@ -76,9 +73,9 @@ export function exportCsvMinimal( // Requester task.booking.requester ? task.booking.requester : 'n/a', // Pickup - parsePickup(request), + requestLabel ? requestLabel.pickup : 'n/a', // Destination - parseDestination(task, request), + requestLabel ? requestLabel.destination : 'n/a', // Robot task.assigned_to ? task.assigned_to.name : 'n/a', // Start Time diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 1aedab997..11b420130 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -43,6 +43,7 @@ import React from 'react'; import { Loading } from '..'; import { ConfirmationDialog, ConfirmationDialogProps } from '../confirmation-dialog'; import { PositiveIntField } from '../form-inputs'; +import { TaskRequestLabel } from './utils'; // A bunch of manually defined descriptions to avoid using `any`. export interface PatrolTaskDescription { @@ -1287,6 +1288,7 @@ export function CreateTaskForm({ const [taskRequest, setTaskRequest] = React.useState( () => requestTask ?? defaultTask(), ); + const [requestLabel, setRequestLabel] = React.useState({}); const [submitting, setSubmitting] = React.useState(false); const [formFullyFilled, setFormFullyFilled] = React.useState(requestTask !== undefined || false); @@ -1358,7 +1360,13 @@ export function CreateTaskForm({ handleTaskDescriptionChange('patrol', desc)} + onChange={(desc) => { + handleTaskDescriptionChange('patrol', desc); + setRequestLabel({ + category: taskRequest.category, + destination: desc.places.at(-1), + }); + }} allowSubmit={allowSubmit} /> ); @@ -1368,6 +1376,9 @@ export function CreateTaskForm({ taskDesc={taskRequest.description as CustomComposeTaskDescription} onChange={(desc) => { handleCustomComposeTaskDescriptionChange(desc); + setRequestLabel({ + category: taskRequest.category, + }); }} allowSubmit={allowSubmit} /> @@ -1387,6 +1398,14 @@ export function CreateTaskForm({ desc.phases[0].activity.description.activities[1].description.category = taskRequest.description.category; handleTaskDescriptionChange('compose', desc); + const pickupPerformAction = + desc.phases[0].activity.description.activities[1].description.description; + setRequestLabel({ + category: taskRequest.description.category, + pickup: pickupPerformAction.pickup_lot, + cart_id: pickupPerformAction.cart_id, + destination: desc.phases[1].activity.description.activities[0].description, + }); }} allowSubmit={allowSubmit} /> @@ -1404,6 +1423,14 @@ export function CreateTaskForm({ desc.phases[0].activity.description.activities[1].description.category = taskRequest.description.category; handleTaskDescriptionChange('compose', desc); + const pickupPerformAction = + desc.phases[0].activity.description.activities[1].description.description; + setRequestLabel({ + category: taskRequest.description.category, + pickup: pickupPerformAction.pickup_zone, + cart_id: pickupPerformAction.cart_id, + destination: desc.phases[1].activity.description.activities[0].description, + }); }} allowSubmit={allowSubmit} /> @@ -1498,6 +1525,15 @@ export function CreateTaskForm({ } } + try { + const labelString = JSON.stringify(requestLabel); + if (labelString) { + request.labels = ['testing', labelString]; + } + } catch (e) { + console.error('Failed to generate string for task request label'); + } + try { setSubmitting(true); await submitTasks([request], scheduling ? schedule : null); diff --git a/packages/react-components/lib/tasks/task-table-datagrid.tsx b/packages/react-components/lib/tasks/task-table-datagrid.tsx index e5abc03e9..a20ea0135 100644 --- a/packages/react-components/lib/tasks/task-table-datagrid.tsx +++ b/packages/react-components/lib/tasks/task-table-datagrid.tsx @@ -15,7 +15,7 @@ import { styled, Stack, Typography, Tooltip, useMediaQuery, SxProps, Theme } fro import * as React from 'react'; import { TaskState, TaskRequest, Status } from 'api-client'; import { InsertInvitation as ScheduleIcon, Person as UserIcon } from '@mui/icons-material/'; -import { parsePickup, parseDestination } from './utils'; +import { parseTaskRequestLabel } from './utils'; const classes = { taskActiveCell: 'MuiDataGrid-cell-active-cell', @@ -57,7 +57,6 @@ const StyledDataGrid = styled(DataGrid)(({ theme }) => ({ export interface Tasks { isLoading: boolean; data: TaskState[]; - requests: Record; total: number; page: number; pageSize: number; @@ -185,8 +184,11 @@ export function TaskDataGridTable({ width: 150, editable: false, valueGetter: (params: GridValueGetterParams) => { - const request: TaskRequest | undefined = tasks.requests[params.row.booking.id]; - return parsePickup(request); + const requestLabel = parseTaskRequestLabel(params.row); + if (requestLabel && requestLabel.pickup) { + return requestLabel.pickup; + } + return 'n/a'; }, flex: 1, filterOperators: getMinimalStringFilterOperators, @@ -198,8 +200,11 @@ export function TaskDataGridTable({ width: 150, editable: false, valueGetter: (params: GridValueGetterParams) => { - const request: TaskRequest | undefined = tasks.requests[params.row.booking.id]; - return parseDestination(params.row, request); + const requestLabel = parseTaskRequestLabel(params.row); + if (requestLabel && requestLabel.destination) { + return requestLabel.destination; + } + return 'n/a'; }, flex: 1, filterOperators: getMinimalStringFilterOperators, diff --git a/packages/react-components/lib/tasks/utils.ts b/packages/react-components/lib/tasks/utils.ts index 1ec734fc5..b40050be2 100644 --- a/packages/react-components/lib/tasks/utils.ts +++ b/packages/react-components/lib/tasks/utils.ts @@ -20,6 +20,32 @@ export function taskTypeToStr(taskType: number): string { } } +export interface TaskRequestLabel { + category?: string; + unix_millis_warn_time?: number; + pickup?: string; + destination?: string; + cart_id?: string; +} + +export function parseTaskRequestLabel(taskState: TaskState): TaskRequestLabel | null { + let requestLabel: TaskRequestLabel | null = null; + if (taskState.booking.labels) { + for (const label of taskState.booking.labels) { + try { + const parsedLabel: TaskRequestLabel = JSON.parse(label); + if (parsedLabel) { + requestLabel = parsedLabel; + break; + } + } catch (e) { + continue; + } + } + } + return requestLabel; +} + function parsePhaseDetail(phases: TaskState['phases'], category?: string) { if (phases) { if (category === 'Loop') { From be580066ce934e5490f76eeb1abb126e0a0fea4b Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Tue, 12 Mar 2024 16:40:38 +0800 Subject: [PATCH 09/37] Clean up past workaround where pickup and destination is saved when a request is saved Signed-off-by: Aaron Chong --- .../api_server/repositories/tasks.py | 110 ------------------ 1 file changed, 110 deletions(-) diff --git a/packages/api-server/api_server/repositories/tasks.py b/packages/api-server/api_server/repositories/tasks.py index 44dfca1c0..1b1d36c61 100644 --- a/packages/api-server/api_server/repositories/tasks.py +++ b/packages/api-server/api_server/repositories/tasks.py @@ -1,4 +1,3 @@ -import json import sys from datetime import datetime from typing import Dict, List, Optional, Sequence, Tuple, cast @@ -30,102 +29,6 @@ from api_server.rmf_io import task_events -def parse_pickup(task_request: TaskRequest) -> Optional[str]: - # patrol - if task_request.category.lower() == "patrol": - return None - - # custom deliveries - supportedDeliveries = [ - "delivery_pickup", - "delivery_sequential_lot_pickup", - "delivery_area_pickup", - ] - if ( - "category" not in task_request.description - or task_request.description["category"] not in supportedDeliveries - ): - return None - - category = task_request.description["category"] - try: - perform_action_description = task_request.description["phases"][0]["activity"][ - "description" - ]["activities"][1]["description"]["description"] - if category == "delivery_pickup": - return perform_action_description["pickup_lot"] - return perform_action_description["pickup_zone"] - except Exception as e: # pylint: disable=W0703 - logger.error(format_exception(e)) - logger.error(f"Failed to parse pickup for task of category {category}") - return None - - -def parse_destination( - task_state: TaskState, task_request: TaskRequest -) -> Optional[str]: - # patrol - try: - if ( - task_request.category.lower() == "patrol" - and task_request.description["places"] is not None - and len(task_request.description["places"]) > 0 - ): - return task_request.description["places"][-1] - except Exception as e: # pylint: disable=W0703 - logger.error(format_exception(e)) - logger.error("Failed to parse destination for patrol") - return None - - # custom deliveries - supportedDeliveries = [ - "delivery_pickup", - "delivery_sequential_lot_pickup", - "delivery_area_pickup", - ] - if ( - "category" not in task_request.description - or task_request.description["category"] not in supportedDeliveries - ): - return None - - category = task_request.description["category"] - try: - destination = task_request.description["phases"][1]["activity"]["description"][ - "activities" - ][0]["description"] - return destination - except Exception as e: # pylint: disable=W0703 - logger.error(format_exception(e)) - logger.error( - f"Failed to parse destination from task request of category {category}" - ) - - # automated tasks that can only be parsed with state - if task_state.category is not None and task_state.category == "Charge Battery": - try: - if ( - task_state.phases is None - or "1" not in task_state.phases - or task_state.phases["1"].events is None - or "1" not in task_state.phases["1"].events - or task_state.phases["1"].events["1"].name is None - ): - raise ValueError - - charge_event_name = task_state.phases["1"].events["1"].name - charge_place_split = charge_event_name.split("[place:")[1] - charge_place = charge_place_split.split("]")[0] - return charge_place - except Exception as e: # pylint: disable=W0703 - logger.error(format_exception(e)) - logger.error( - f"Failed to parse charging point from task state of id {task_state.booking.id}" - ) - return None - return None - - class TaskRepository: def __init__(self, user: User): self.user = user @@ -137,19 +40,6 @@ async def save_task_request( {"request": task_request.json()}, id_=task_state.booking.id ) - # Add pickup and destination to task state model for filter and sort - # pickup = parse_pickup(task_request) - # destination = parse_destination(task_state, task_request) - # db_task_state = await DbTaskState.get_or_none(id_=task_state.booking.id) - # if db_task_state is not None: - # db_task_state.update_from_dict( - # { - # "pickup": pickup, - # "destination": destination, - # } - # ) - # await db_task_state.save() - async def get_task_request(self, task_id: str) -> Optional[TaskRequest]: result = await DbTaskRequest.get_or_none(id_=task_id) if result is None: From 194ed21886c95f16f4efbb3abaa36b9de163bd96 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 13 Mar 2024 21:43:18 +0800 Subject: [PATCH 10/37] Migration script Signed-off-by: Aaron Chong --- .../models/migrate/migrate_db_912.py | 197 ++++++++++++++++++ .../lib/tasks/task-table-datagrid.tsx | 2 +- 2 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 packages/api-server/api_server/models/migrate/migrate_db_912.py diff --git a/packages/api-server/api_server/models/migrate/migrate_db_912.py b/packages/api-server/api_server/models/migrate/migrate_db_912.py new file mode 100644 index 000000000..2010975de --- /dev/null +++ b/packages/api-server/api_server/models/migrate/migrate_db_912.py @@ -0,0 +1,197 @@ +import asyncio +import os +from typing import Optional + +from tortoise import Tortoise + +import api_server.models.tortoise_models as ttm +from api_server.app_config import app_config, load_config +from api_server.models import TaskRequest, TaskRequestLabel, TaskState + +# NOTE: This script is for migrating TaskState in an existing database to work +# with https://github.com/open-rmf/rmf-web/pull/912. +# Before migration: +# - Pickup, destination, cart ID, category information will be unavailable on +# the Task Queue Table on the dashboard, as we no longer gather those +# fields from the TaskRequest +# After migration: +# - Dashboard will behave the same as before #912, however it is no longer +# dependent on TaskRequest to fill out those fields. It gathers those fields +# from the json string in TaskState.booking.labels. +# This script performs the following: +# - Construct TaskRequestLabel from its TaskRequest if it is available. +# - Update the respective TaskState.data json TaskState.booking.labels field +# with the newly constructed TaskRequestLabel json string. + + +app_config = load_config( + os.environ.get( + "RMF_API_SERVER_CONFIG", + f"{os.path.dirname(__file__)}/../../default_config.py", + ) +) + + +def parse_category(task_request: TaskRequest) -> Optional[str]: + category = None + if task_request.category.lower() == "patrol": + category = "Patrol" + elif task_request.description and task_request.description["category"]: + category = task_request.description["category"] + return category + + +def parse_pickup(task_request: TaskRequest) -> Optional[str]: + # patrol + if task_request.category.lower() == "patrol": + return None + + # custom deliveries + supportedDeliveries = [ + "delivery_pickup", + "delivery_sequential_lot_pickup", + "delivery_area_pickup", + ] + if ( + "category" not in task_request.description + or task_request.description["category"] not in supportedDeliveries + ): + return None + + category = task_request.description["category"] + try: + perform_action_description = task_request.description["phases"][0]["activity"][ + "description" + ]["activities"][1]["description"]["description"] + if category == "delivery_pickup": + return perform_action_description["pickup_lot"] + return perform_action_description["pickup_zone"] + except Exception as e: # pylint: disable=W0703 + print(f"Failed to parse pickup for task of category {category}") + return None + + +def parse_destination(task_request: TaskRequest) -> Optional[str]: + # patrol + try: + if ( + task_request.category.lower() == "patrol" + and task_request.description["places"] is not None + and len(task_request.description["places"]) > 0 + ): + return task_request.description["places"][-1] + except Exception as e: # pylint: disable=W0703 + print("Failed to parse destination for patrol") + return None + + # custom deliveries + supportedDeliveries = [ + "delivery_pickup", + "delivery_sequential_lot_pickup", + "delivery_area_pickup", + ] + if ( + "category" not in task_request.description + or task_request.description["category"] not in supportedDeliveries + ): + return None + + category = task_request.description["category"] + try: + destination = task_request.description["phases"][1]["activity"]["description"][ + "activities" + ][0]["description"] + return destination + except Exception as e: # pylint: disable=W0703 + print(f"Failed to parse destination from task request of category {category}") + return None + + +def parse_cart_id(task_request: TaskRequest) -> Optional[str]: + # patrol + if task_request.category.lower() == "patrol": + return None + + # custom deliveries + supportedDeliveries = [ + "delivery_pickup", + "delivery_sequential_lot_pickup", + "delivery_area_pickup", + ] + if ( + "category" not in task_request.description + or task_request.description["category"] not in supportedDeliveries + ): + return None + + category = task_request.description["category"] + try: + perform_action_description = task_request.description["phases"][0]["activity"][ + "description" + ]["activities"][1]["description"]["description"] + return perform_action_description["cart_id"] + except Exception as e: # pylint: disable=W0703 + print(f"Failed to parse cart ID for task of category {category}") + return None + + +async def migrate(): + await Tortoise.init( + db_url=app_config.db_url, + modules={"models": ["api_server.models.tortoise_models"]}, + ) + await Tortoise.generate_schemas() + + # Acquire all existing TaskStates + states = await ttm.TaskState.all() + print(f"Migrating {len(states)} TaskState models") + + # Migrate each TaskState + for state in states: + state_model = TaskState(**state.data) + task_id = state_model.booking.id + + # If the request is not available we skip migrating this TaskState + request = await ttm.TaskRequest.get_or_none(id_=task_id) + if request is None: + continue + request_model = TaskRequest(**request.request) + + # Construct TaskRequestLabel based on TaskRequest + pickup = parse_pickup(request_model) + destination = parse_destination(request_model) + label = TaskRequestLabel( + category=parse_category(request_model), + pickup=pickup, + destination=destination, + cart_id=parse_cart_id(request_model), + ) + print(label) + + # Update data json + if state_model.booking.labels is None: + state_model.booking.labels = [label.json()] + else: + state_model.booking.labels.append(label.json()) + print(state_model) + + state.update_from_dict( + { + "data": state_model.json(), + "pickup": pickup, + "destination": destination, + } + ) + await state.save() + + await Tortoise.close_connections() + + +def main(): + print("Migration started") + asyncio.run(migrate()) + print("Migration done") + + +if __name__ == "__main__": + main() diff --git a/packages/react-components/lib/tasks/task-table-datagrid.tsx b/packages/react-components/lib/tasks/task-table-datagrid.tsx index a20ea0135..b11569c64 100644 --- a/packages/react-components/lib/tasks/task-table-datagrid.tsx +++ b/packages/react-components/lib/tasks/task-table-datagrid.tsx @@ -13,7 +13,7 @@ import { } from '@mui/x-data-grid'; import { styled, Stack, Typography, Tooltip, useMediaQuery, SxProps, Theme } from '@mui/material'; import * as React from 'react'; -import { TaskState, TaskRequest, Status } from 'api-client'; +import { TaskState, Status } from 'api-client'; import { InsertInvitation as ScheduleIcon, Person as UserIcon } from '@mui/icons-material/'; import { parseTaskRequestLabel } from './utils'; From c9d4833ce6ad682f6c627822406a35cd41b43e1f Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 13 Mar 2024 21:56:13 +0800 Subject: [PATCH 11/37] Remove stale testing label, push label instead of setting Signed-off-by: Aaron Chong --- packages/react-components/lib/tasks/create-task.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 11b420130..18a598fdf 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -1528,7 +1528,11 @@ export function CreateTaskForm({ try { const labelString = JSON.stringify(requestLabel); if (labelString) { - request.labels = ['testing', labelString]; + if (request.labels) { + request.labels.push(labelString); + } else { + request.labels = [labelString]; + } } } catch (e) { console.error('Failed to generate string for task request label'); From 0008c10b4996550b4bd635aad8bc9c12297a3ba8 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 13 Mar 2024 22:22:41 +0800 Subject: [PATCH 12/37] Lint Signed-off-by: Aaron Chong --- packages/api-server/api_server/models/migrate/migrate_db_912.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/api-server/api_server/models/migrate/migrate_db_912.py b/packages/api-server/api_server/models/migrate/migrate_db_912.py index 2010975de..c201d22dc 100644 --- a/packages/api-server/api_server/models/migrate/migrate_db_912.py +++ b/packages/api-server/api_server/models/migrate/migrate_db_912.py @@ -162,6 +162,7 @@ async def migrate(): destination = parse_destination(request_model) label = TaskRequestLabel( category=parse_category(request_model), + unix_millis_warn_time=None, pickup=pickup, destination=destination, cart_id=parse_cart_id(request_model), From c4e7fc68657e4184ab33938b735785552fc86398 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 20 Mar 2024 09:27:50 +0800 Subject: [PATCH 13/37] Hammer/use labels schedules (#921) * Getting ScheduledTask as well Signed-off-by: Aaron Chong * Handle schedules as well Signed-off-by: Aaron Chong * lint Signed-off-by: Aaron Chong --------- Signed-off-by: Aaron Chong --- .../models/migrate/migrate_db_912.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/api-server/api_server/models/migrate/migrate_db_912.py b/packages/api-server/api_server/models/migrate/migrate_db_912.py index c201d22dc..405c0af2c 100644 --- a/packages/api-server/api_server/models/migrate/migrate_db_912.py +++ b/packages/api-server/api_server/models/migrate/migrate_db_912.py @@ -22,6 +22,7 @@ # - Construct TaskRequestLabel from its TaskRequest if it is available. # - Update the respective TaskState.data json TaskState.booking.labels field # with the newly constructed TaskRequestLabel json string. +# - Update ScheduledTask to use labels too app_config = load_config( @@ -185,6 +186,43 @@ async def migrate(): ) await state.save() + # Acquire all ScheduledTask + scheduled_tasks = await ttm.ScheduledTask.all() + print(f"Migrating {len(scheduled_tasks)} ScheduledTask models") + + # Migrate each ScheduledTask + for scheduled_task in scheduled_tasks: + scheduled_task_model = await ttm.ScheduledTaskPydantic.from_tortoise_orm( + scheduled_task + ) + task_request = TaskRequest( + **scheduled_task_model.task_request # pyright: ignore[reportGeneralTypeIssues] + ) + print(task_request) + + # Construct TaskRequestLabel based on TaskRequest + pickup = parse_pickup(task_request) + destination = parse_destination(task_request) + label = TaskRequestLabel( + category=parse_category(task_request), + unix_millis_warn_time=None, + pickup=pickup, + destination=destination, + cart_id=parse_cart_id(task_request), + ) + print(label) + + # Update TaskRequest + if task_request.labels is None: + task_request.labels = [label.json()] + else: + task_request.labels.append(label.json()) + print(task_request) + + # Update ScheduledTask + scheduled_task.update_from_dict({"task_request": task_request.json()}) + await scheduled_task.save() + await Tortoise.close_connections() From 0dbe2fdd7565768e55a35af2f08e5f12699349db Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 17 Apr 2024 11:56:39 +0800 Subject: [PATCH 14/37] Using a new generated file to handle parsing between json and object Signed-off-by: Aaron Chong --- .../api_server/models/task_request_label.py | 2 +- .../migrate => migrations}/migrate_db_912.py | 34 ++- .../lib/tasks/create-task.tsx | 12 +- .../lib/tasks/task-request-label.tsx | 212 ++++++++++++++++++ packages/react-components/lib/tasks/utils.ts | 16 +- 5 files changed, 247 insertions(+), 29 deletions(-) rename packages/api-server/{api_server/models/migrate => migrations}/migrate_db_912.py (87%) create mode 100644 packages/react-components/lib/tasks/task-request-label.tsx diff --git a/packages/api-server/api_server/models/task_request_label.py b/packages/api-server/api_server/models/task_request_label.py index 249e1f3b0..1ec838c15 100644 --- a/packages/api-server/api_server/models/task_request_label.py +++ b/packages/api-server/api_server/models/task_request_label.py @@ -8,7 +8,7 @@ class TaskRequestLabel(BaseModel): - category: Optional[str] + task_identifier: str unix_millis_warn_time: Optional[str] pickup: Optional[str] destination: Optional[str] diff --git a/packages/api-server/api_server/models/migrate/migrate_db_912.py b/packages/api-server/migrations/migrate_db_912.py similarity index 87% rename from packages/api-server/api_server/models/migrate/migrate_db_912.py rename to packages/api-server/migrations/migrate_db_912.py index 405c0af2c..b4faf17de 100644 --- a/packages/api-server/api_server/models/migrate/migrate_db_912.py +++ b/packages/api-server/migrations/migrate_db_912.py @@ -11,8 +11,8 @@ # NOTE: This script is for migrating TaskState in an existing database to work # with https://github.com/open-rmf/rmf-web/pull/912. # Before migration: -# - Pickup, destination, cart ID, category information will be unavailable on -# the Task Queue Table on the dashboard, as we no longer gather those +# - Pickup, destination, cart ID, task identifier information will be unavailable +# on the Task Queue Table on the dashboard, as we no longer gather those # fields from the TaskRequest # After migration: # - Dashboard will behave the same as before #912, however it is no longer @@ -33,13 +33,13 @@ ) -def parse_category(task_request: TaskRequest) -> Optional[str]: - category = None +def parse_task_identifier(task_request: TaskRequest) -> Optional[str]: + identifier = None if task_request.category.lower() == "patrol": - category = "Patrol" + identifier = "Patrol" elif task_request.description and task_request.description["category"]: - category = task_request.description["category"] - return category + identifier = task_request.description["category"] + return identifier def parse_pickup(task_request: TaskRequest) -> Optional[str]: @@ -148,6 +148,8 @@ async def migrate(): print(f"Migrating {len(states)} TaskState models") # Migrate each TaskState + skipped_task_states_count = 0 + failed_task_states_count = 0 for state in states: state_model = TaskState(**state.data) task_id = state_model.booking.id @@ -155,14 +157,22 @@ async def migrate(): # If the request is not available we skip migrating this TaskState request = await ttm.TaskRequest.get_or_none(id_=task_id) if request is None: + skipped_task_states_count += 1 continue request_model = TaskRequest(**request.request) # Construct TaskRequestLabel based on TaskRequest pickup = parse_pickup(request_model) destination = parse_destination(request_model) + + # If the task identifier could not be parsed, we skip migrating this TaskState + task_identifier = parse_task_identifier(request_model) + if task_identifier is None: + failed_task_states_count += 1 + continue + label = TaskRequestLabel( - category=parse_category(request_model), + task_identifier=task_identifier, unix_millis_warn_time=None, pickup=pickup, destination=destination, @@ -191,6 +201,7 @@ async def migrate(): print(f"Migrating {len(scheduled_tasks)} ScheduledTask models") # Migrate each ScheduledTask + failed_scheduled_task_count = 0 for scheduled_task in scheduled_tasks: scheduled_task_model = await ttm.ScheduledTaskPydantic.from_tortoise_orm( scheduled_task @@ -200,11 +211,16 @@ async def migrate(): ) print(task_request) + task_identifier = parse_task_identifier(task_request) + if task_identifier is None: + failed_scheduled_task_count += 1 + continue + # Construct TaskRequestLabel based on TaskRequest pickup = parse_pickup(task_request) destination = parse_destination(task_request) label = TaskRequestLabel( - category=parse_category(task_request), + task_identifier=task_identifier, unix_millis_warn_time=None, pickup=pickup, destination=destination, diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 18a598fdf..ecb4dbb17 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -43,7 +43,7 @@ import React from 'react'; import { Loading } from '..'; import { ConfirmationDialog, ConfirmationDialogProps } from '../confirmation-dialog'; import { PositiveIntField } from '../form-inputs'; -import { TaskRequestLabel } from './utils'; +import { TaskRequestLabel } from './task-request-label'; // A bunch of manually defined descriptions to avoid using `any`. export interface PatrolTaskDescription { @@ -1288,7 +1288,7 @@ export function CreateTaskForm({ const [taskRequest, setTaskRequest] = React.useState( () => requestTask ?? defaultTask(), ); - const [requestLabel, setRequestLabel] = React.useState({}); + const [requestLabel, setRequestLabel] = React.useState(null); const [submitting, setSubmitting] = React.useState(false); const [formFullyFilled, setFormFullyFilled] = React.useState(requestTask !== undefined || false); @@ -1363,7 +1363,7 @@ export function CreateTaskForm({ onChange={(desc) => { handleTaskDescriptionChange('patrol', desc); setRequestLabel({ - category: taskRequest.category, + task_identifier: taskRequest.category, destination: desc.places.at(-1), }); }} @@ -1377,7 +1377,7 @@ export function CreateTaskForm({ onChange={(desc) => { handleCustomComposeTaskDescriptionChange(desc); setRequestLabel({ - category: taskRequest.category, + task_identifier: taskRequest.category, }); }} allowSubmit={allowSubmit} @@ -1401,7 +1401,7 @@ export function CreateTaskForm({ const pickupPerformAction = desc.phases[0].activity.description.activities[1].description.description; setRequestLabel({ - category: taskRequest.description.category, + task_identifier: taskRequest.description.category, pickup: pickupPerformAction.pickup_lot, cart_id: pickupPerformAction.cart_id, destination: desc.phases[1].activity.description.activities[0].description, @@ -1426,7 +1426,7 @@ export function CreateTaskForm({ const pickupPerformAction = desc.phases[0].activity.description.activities[1].description.description; setRequestLabel({ - category: taskRequest.description.category, + task_identifier: taskRequest.description.category, pickup: pickupPerformAction.pickup_zone, cart_id: pickupPerformAction.cart_id, destination: desc.phases[1].activity.description.activities[0].description, diff --git a/packages/react-components/lib/tasks/task-request-label.tsx b/packages/react-components/lib/tasks/task-request-label.tsx new file mode 100644 index 000000000..0173d54ad --- /dev/null +++ b/packages/react-components/lib/tasks/task-request-label.tsx @@ -0,0 +1,212 @@ +// This file was generated using https://app.quicktype.io/ +// Avoid modifying this file directly. + +// To parse this data: +// +// import { Convert, TaskRequestLabel } from "./file"; +// +// const taskRequestLabel = Convert.toTaskRequestLabel(json); +// +// These functions will throw an error if the JSON doesn't +// match the expected interface, even if the JSON is valid. + +export interface TaskRequestLabel { + task_identifier: string; + unix_millis_warn_time?: number; + pickup?: string; + destination?: string; + cart_id?: string; +} + +// Converts JSON strings to/from your types +// and asserts the results of JSON.parse at runtime +export class Convert { + public static toTaskRequestLabel(json: string): TaskRequestLabel { + return cast(JSON.parse(json), r('TaskRequestLabel')); + } + + public static taskRequestLabelToJson(value: TaskRequestLabel): string { + return JSON.stringify(uncast(value, r('TaskRequestLabel')), null, 2); + } +} + +function invalidValue(typ: any, val: any, key: any, parent: any = ''): never { + const prettyTyp = prettyTypeName(typ); + const parentText = parent ? ` on ${parent}` : ''; + const keyText = key ? ` for key "${key}"` : ''; + throw Error( + `Invalid value${keyText}${parentText}. Expected ${prettyTyp} but got ${JSON.stringify(val)}`, + ); +} + +function prettyTypeName(typ: any): string { + if (Array.isArray(typ)) { + if (typ.length === 2 && typ[0] === undefined) { + return `an optional ${prettyTypeName(typ[1])}`; + } else { + return `one of [${typ + .map((a) => { + return prettyTypeName(a); + }) + .join(', ')}]`; + } + } else if (typeof typ === 'object' && typ.literal !== undefined) { + return typ.literal; + } else { + return typeof typ; + } +} + +function jsonToJSProps(typ: any): any { + if (typ.jsonToJS === undefined) { + const map: any = {}; + typ.props.forEach((p: any) => (map[p.json] = { key: p.js, typ: p.typ })); + typ.jsonToJS = map; + } + return typ.jsonToJS; +} + +function jsToJSONProps(typ: any): any { + if (typ.jsToJSON === undefined) { + const map: any = {}; + typ.props.forEach((p: any) => (map[p.js] = { key: p.json, typ: p.typ })); + typ.jsToJSON = map; + } + return typ.jsToJSON; +} + +function transform(val: any, typ: any, getProps: any, key: any = '', parent: any = ''): any { + function transformPrimitive(typ: string, val: any): any { + if (typeof typ === typeof val) return val; + return invalidValue(typ, val, key, parent); + } + + function transformUnion(typs: any[], val: any): any { + // val must validate against one typ in typs + const l = typs.length; + for (let i = 0; i < l; i++) { + const typ = typs[i]; + try { + return transform(val, typ, getProps); + } catch (_) {} + } + return invalidValue(typs, val, key, parent); + } + + function transformEnum(cases: string[], val: any): any { + if (cases.indexOf(val) !== -1) return val; + return invalidValue( + cases.map((a) => { + return l(a); + }), + val, + key, + parent, + ); + } + + function transformArray(typ: any, val: any): any { + // val must be an array with no invalid elements + if (!Array.isArray(val)) return invalidValue(l('array'), val, key, parent); + return val.map((el) => transform(el, typ, getProps)); + } + + function transformDate(val: any): any { + if (val === null) { + return null; + } + const d = new Date(val); + if (isNaN(d.valueOf())) { + return invalidValue(l('Date'), val, key, parent); + } + return d; + } + + function transformObject(props: { [k: string]: any }, additional: any, val: any): any { + if (val === null || typeof val !== 'object' || Array.isArray(val)) { + return invalidValue(l(ref || 'object'), val, key, parent); + } + const result: any = {}; + Object.getOwnPropertyNames(props).forEach((key) => { + const prop = props[key]; + const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined; + result[prop.key] = transform(v, prop.typ, getProps, key, ref); + }); + Object.getOwnPropertyNames(val).forEach((key) => { + if (!Object.prototype.hasOwnProperty.call(props, key)) { + result[key] = transform(val[key], additional, getProps, key, ref); + } + }); + return result; + } + + if (typ === 'any') return val; + if (typ === null) { + if (val === null) return val; + return invalidValue(typ, val, key, parent); + } + if (typ === false) return invalidValue(typ, val, key, parent); + let ref: any = undefined; + while (typeof typ === 'object' && typ.ref !== undefined) { + ref = typ.ref; + typ = typeMap[typ.ref]; + } + if (Array.isArray(typ)) return transformEnum(typ, val); + if (typeof typ === 'object') { + return typ.hasOwnProperty('unionMembers') + ? transformUnion(typ.unionMembers, val) + : typ.hasOwnProperty('arrayItems') + ? transformArray(typ.arrayItems, val) + : typ.hasOwnProperty('props') + ? transformObject(getProps(typ), typ.additional, val) + : invalidValue(typ, val, key, parent); + } + // Numbers can be parsed by Date but shouldn't be. + if (typ === Date && typeof val !== 'number') return transformDate(val); + return transformPrimitive(typ, val); +} + +function cast(val: any, typ: any): T { + return transform(val, typ, jsonToJSProps); +} + +function uncast(val: T, typ: any): any { + return transform(val, typ, jsToJSONProps); +} + +function l(typ: any) { + return { literal: typ }; +} + +function a(typ: any) { + return { arrayItems: typ }; +} + +function u(...typs: any[]) { + return { unionMembers: typs }; +} + +function o(props: any[], additional: any) { + return { props, additional }; +} + +function m(additional: any) { + return { props: [], additional }; +} + +function r(name: string) { + return { ref: name }; +} + +const typeMap: any = { + TaskRequestLabel: o( + [ + { json: 'task_identifier', js: 'task_identifier', typ: '' }, + { json: 'unix_millis_warn_time', js: 'unix_millis_warn_time', typ: u(undefined, 0) }, + { json: 'pickup', js: 'pickup', typ: u(undefined, '') }, + { json: 'destination', js: 'destination', typ: u(undefined, '') }, + { json: 'cart_id', js: 'cart_id', typ: u(undefined, '') }, + ], + false, + ), +}; diff --git a/packages/react-components/lib/tasks/utils.ts b/packages/react-components/lib/tasks/utils.ts index b40050be2..707fd3f47 100644 --- a/packages/react-components/lib/tasks/utils.ts +++ b/packages/react-components/lib/tasks/utils.ts @@ -1,5 +1,6 @@ import { TaskType as RmfTaskType } from 'rmf-models'; import type { TaskState, TaskRequest } from 'api-client'; +import { Convert, TaskRequestLabel } from './task-request-label'; export function taskTypeToStr(taskType: number): string { switch (taskType) { @@ -20,24 +21,13 @@ export function taskTypeToStr(taskType: number): string { } } -export interface TaskRequestLabel { - category?: string; - unix_millis_warn_time?: number; - pickup?: string; - destination?: string; - cart_id?: string; -} - export function parseTaskRequestLabel(taskState: TaskState): TaskRequestLabel | null { let requestLabel: TaskRequestLabel | null = null; if (taskState.booking.labels) { for (const label of taskState.booking.labels) { try { - const parsedLabel: TaskRequestLabel = JSON.parse(label); - if (parsedLabel) { - requestLabel = parsedLabel; - break; - } + requestLabel = Convert.toTaskRequestLabel(label); + break; } catch (e) { continue; } From 440dc2cc0339e931378bf3857449354bac6a8150 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 29 Apr 2024 09:58:43 +0800 Subject: [PATCH 15/37] Made fields all optional, nested in a description object, regenerated api-client Signed-off-by: Aaron Chong --- packages/api-client/lib/openapi/api.ts | 141 ++++++++++++ packages/api-client/lib/version.ts | 2 +- packages/api-client/schema/index.ts | 47 ++++ .../api_server/models/task_request_label.py | 15 +- .../api_server/routes/tasks/tasks.py | 20 ++ packages/react-components/lib/tasks/index.ts | 1 + .../lib/tasks/task-request-label.tsx | 217 +----------------- 7 files changed, 232 insertions(+), 211 deletions(-) diff --git a/packages/api-client/lib/openapi/api.ts b/packages/api-client/lib/openapi/api.ts index cda60e41f..7536dc6fa 100644 --- a/packages/api-client/lib/openapi/api.ts +++ b/packages/api-client/lib/openapi/api.ts @@ -2863,6 +2863,56 @@ export interface TaskRequest { */ unix_millis_warn_time?: number; } +/** + * + * @export + * @interface TaskRequestLabel + */ +export interface TaskRequestLabel { + /** + * + * @type {TaskRequestLabelDescription} + * @memberof TaskRequestLabel + */ + description: TaskRequestLabelDescription; +} +/** + * + * @export + * @interface TaskRequestLabelDescription + */ +export interface TaskRequestLabelDescription { + /** + * + * @type {string} + * @memberof TaskRequestLabelDescription + */ + task_name?: string; + /** + * + * @type {string} + * @memberof TaskRequestLabelDescription + */ + unix_millis_warn_time?: string; + /** + * + * @type {string} + * @memberof TaskRequestLabelDescription + */ + pickup?: string; + /** + * + * @type {string} + * @memberof TaskRequestLabelDescription + */ + destination?: string; + /** + * + * @type {string} + * @memberof TaskRequestLabelDescription + */ + cart_id?: string; +} /** * * @export @@ -8562,6 +8612,47 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * + * @summary Get Task Request Label + * @param {string} taskId task_id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTaskRequestLabelTasksTaskIdRequestLabelGet: async ( + taskId: string, + options: AxiosRequestConfig = {}, + ): Promise => { + // verify required parameter 'taskId' is not null or undefined + assertParamExists('getTaskRequestLabelTasksTaskIdRequestLabelGet', 'taskId', taskId); + const localVarPath = `/tasks/{task_id}/request_label`.replace( + `{${'task_id'}}`, + encodeURIComponent(String(taskId)), + ); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary Get Task Request @@ -9645,6 +9736,24 @@ export const TasksApiFp = function (configuration?: Configuration) { ); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary Get Task Request Label + * @param {string} taskId task_id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTaskRequestLabelTasksTaskIdRequestLabelGet( + taskId: string, + options?: AxiosRequestConfig, + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = + await localVarAxiosParamCreator.getTaskRequestLabelTasksTaskIdRequestLabelGet( + taskId, + options, + ); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Get Task Request @@ -10149,6 +10258,21 @@ export const TasksApiFactory = function ( .getTaskLogTasksTaskIdLogGet(taskId, between, options) .then((request) => request(axios, basePath)); }, + /** + * + * @summary Get Task Request Label + * @param {string} taskId task_id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTaskRequestLabelTasksTaskIdRequestLabelGet( + taskId: string, + options?: any, + ): AxiosPromise { + return localVarFp + .getTaskRequestLabelTasksTaskIdRequestLabelGet(taskId, options) + .then((request) => request(axios, basePath)); + }, /** * * @summary Get Task Request @@ -10592,6 +10716,23 @@ export class TasksApi extends BaseAPI { .then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Get Task Request Label + * @param {string} taskId task_id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public getTaskRequestLabelTasksTaskIdRequestLabelGet( + taskId: string, + options?: AxiosRequestConfig, + ) { + return TasksApiFp(this.configuration) + .getTaskRequestLabelTasksTaskIdRequestLabelGet(taskId, options) + .then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Get Task Request diff --git a/packages/api-client/lib/version.ts b/packages/api-client/lib/version.ts index cdbc061aa..d5a99cf40 100644 --- a/packages/api-client/lib/version.ts +++ b/packages/api-client/lib/version.ts @@ -3,6 +3,6 @@ import { version as rmfModelVer } from 'rmf-models'; export const version = { rmfModels: rmfModelVer, - rmfServer: '4febac0944ca42150e94e4b9366e1e6d792ab29e', + rmfServer: 'd4c7f52a4650490d5bc1e51753ff2125d2edeffd', openapiGenerator: '6.2.1', }; diff --git a/packages/api-client/schema/index.ts b/packages/api-client/schema/index.ts index a8cc01ab0..284f41ab0 100644 --- a/packages/api-client/schema/index.ts +++ b/packages/api-client/schema/index.ts @@ -671,6 +671,36 @@ export default { }, }, }, + '/tasks/{task_id}/request_label': { + get: { + tags: ['Tasks'], + summary: 'Get Task Request Label', + operationId: 'get_task_request_label_tasks__task_id__request_label_get', + parameters: [ + { + description: 'task_id', + required: true, + schema: { title: 'Task Id', type: 'string', description: 'task_id' }, + name: 'task_id', + in: 'path', + }, + ], + responses: { + '200': { + description: 'Successful Response', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/TaskRequestLabel' } }, + }, + }, + '422': { + description: 'Validation Error', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/HTTPValidationError' } }, + }, + }, + }, + }, + }, '/tasks': { get: { tags: ['Tasks'], @@ -3840,6 +3870,23 @@ export default { }, }, }, + TaskRequestLabel: { + title: 'TaskRequestLabel', + required: ['description'], + type: 'object', + properties: { description: { $ref: '#/components/schemas/TaskRequestLabelDescription' } }, + }, + TaskRequestLabelDescription: { + title: 'TaskRequestLabelDescription', + type: 'object', + properties: { + task_name: { title: 'Task Name', type: 'string' }, + unix_millis_warn_time: { title: 'Unix Millis Warn Time', type: 'string' }, + pickup: { title: 'Pickup', type: 'string' }, + destination: { title: 'Destination', type: 'string' }, + cart_id: { title: 'Cart Id', type: 'string' }, + }, + }, TaskResumeRequest: { title: 'TaskResumeRequest', type: 'object', diff --git a/packages/api-server/api_server/models/task_request_label.py b/packages/api-server/api_server/models/task_request_label.py index 1ec838c15..52c7801f4 100644 --- a/packages/api-server/api_server/models/task_request_label.py +++ b/packages/api-server/api_server/models/task_request_label.py @@ -7,13 +7,24 @@ # populated by the dashboard. Any changes to either side will require syncing. -class TaskRequestLabel(BaseModel): - task_identifier: str +class TaskRequestLabelDescription(BaseModel): + task_name: Optional[str] unix_millis_warn_time: Optional[str] pickup: Optional[str] destination: Optional[str] cart_id: Optional[str] + @staticmethod + def from_json_string(json_str: str) -> Optional["TaskRequestLabelDescription"]: + try: + return TaskRequestLabelDescription.parse_raw(json_str) + except pydantic.error_wrappers.ValidationError: + return None + + +class TaskRequestLabel(BaseModel): + description: TaskRequestLabelDescription + @staticmethod def from_json_string(json_str: str) -> Optional["TaskRequestLabel"]: try: diff --git a/packages/api-server/api_server/routes/tasks/tasks.py b/packages/api-server/api_server/routes/tasks/tasks.py index cffe00a14..e9e0b6082 100644 --- a/packages/api-server/api_server/routes/tasks/tasks.py +++ b/packages/api-server/api_server/routes/tasks/tasks.py @@ -83,6 +83,26 @@ async def query_task_requests( return return_requests +@router.get("/{task_id}/request_label", response_model=mdl.TaskRequestLabel) +async def get_task_request_label( + task_repo: TaskRepository = Depends(TaskRepository), + task_id: str = Path(..., description="task_id"), +): + request = await task_repo.get_task_request(task_id) + if request is None: + raise HTTPException(status_code=404) + + if request.labels is not None: + for label in request.labels: + if len(label) == 0: + continue + + request_label = mdl.TaskRequestLabel.from_json_string(label) + if request_label is not None: + return request_label + raise HTTPException(status_code=404) + + @router.get("", response_model=List[mdl.TaskState]) async def query_task_states( task_repo: TaskRepository = Depends(TaskRepository), diff --git a/packages/react-components/lib/tasks/index.ts b/packages/react-components/lib/tasks/index.ts index 6cc17baaf..34539a4a3 100644 --- a/packages/react-components/lib/tasks/index.ts +++ b/packages/react-components/lib/tasks/index.ts @@ -1,6 +1,7 @@ export * from './create-task'; export * from './task-info'; export * from './task-logs'; +export * from './task-request-label'; export * from './task-table'; export * from './task-timeline'; export * from './task-table-datagrid'; diff --git a/packages/react-components/lib/tasks/task-request-label.tsx b/packages/react-components/lib/tasks/task-request-label.tsx index 0173d54ad..9e61f83fd 100644 --- a/packages/react-components/lib/tasks/task-request-label.tsx +++ b/packages/react-components/lib/tasks/task-request-label.tsx @@ -1,212 +1,13 @@ -// This file was generated using https://app.quicktype.io/ -// Avoid modifying this file directly. - -// To parse this data: -// -// import { Convert, TaskRequestLabel } from "./file"; -// -// const taskRequestLabel = Convert.toTaskRequestLabel(json); -// -// These functions will throw an error if the JSON doesn't -// match the expected interface, even if the JSON is valid. +import schema from 'api-client/dist/schema'; export interface TaskRequestLabel { - task_identifier: string; - unix_millis_warn_time?: number; - pickup?: string; - destination?: string; - cart_id?: string; -} - -// Converts JSON strings to/from your types -// and asserts the results of JSON.parse at runtime -export class Convert { - public static toTaskRequestLabel(json: string): TaskRequestLabel { - return cast(JSON.parse(json), r('TaskRequestLabel')); - } - - public static taskRequestLabelToJson(value: TaskRequestLabel): string { - return JSON.stringify(uncast(value, r('TaskRequestLabel')), null, 2); - } -} - -function invalidValue(typ: any, val: any, key: any, parent: any = ''): never { - const prettyTyp = prettyTypeName(typ); - const parentText = parent ? ` on ${parent}` : ''; - const keyText = key ? ` for key "${key}"` : ''; - throw Error( - `Invalid value${keyText}${parentText}. Expected ${prettyTyp} but got ${JSON.stringify(val)}`, - ); -} - -function prettyTypeName(typ: any): string { - if (Array.isArray(typ)) { - if (typ.length === 2 && typ[0] === undefined) { - return `an optional ${prettyTypeName(typ[1])}`; - } else { - return `one of [${typ - .map((a) => { - return prettyTypeName(a); - }) - .join(', ')}]`; - } - } else if (typeof typ === 'object' && typ.literal !== undefined) { - return typ.literal; - } else { - return typeof typ; - } -} - -function jsonToJSProps(typ: any): any { - if (typ.jsonToJS === undefined) { - const map: any = {}; - typ.props.forEach((p: any) => (map[p.json] = { key: p.js, typ: p.typ })); - typ.jsonToJS = map; - } - return typ.jsonToJS; -} - -function jsToJSONProps(typ: any): any { - if (typ.jsToJSON === undefined) { - const map: any = {}; - typ.props.forEach((p: any) => (map[p.js] = { key: p.json, typ: p.typ })); - typ.jsToJSON = map; - } - return typ.jsToJSON; -} - -function transform(val: any, typ: any, getProps: any, key: any = '', parent: any = ''): any { - function transformPrimitive(typ: string, val: any): any { - if (typeof typ === typeof val) return val; - return invalidValue(typ, val, key, parent); - } - - function transformUnion(typs: any[], val: any): any { - // val must validate against one typ in typs - const l = typs.length; - for (let i = 0; i < l; i++) { - const typ = typs[i]; - try { - return transform(val, typ, getProps); - } catch (_) {} - } - return invalidValue(typs, val, key, parent); - } - - function transformEnum(cases: string[], val: any): any { - if (cases.indexOf(val) !== -1) return val; - return invalidValue( - cases.map((a) => { - return l(a); - }), - val, - key, - parent, - ); - } - - function transformArray(typ: any, val: any): any { - // val must be an array with no invalid elements - if (!Array.isArray(val)) return invalidValue(l('array'), val, key, parent); - return val.map((el) => transform(el, typ, getProps)); - } - - function transformDate(val: any): any { - if (val === null) { - return null; - } - const d = new Date(val); - if (isNaN(d.valueOf())) { - return invalidValue(l('Date'), val, key, parent); - } - return d; - } - - function transformObject(props: { [k: string]: any }, additional: any, val: any): any { - if (val === null || typeof val !== 'object' || Array.isArray(val)) { - return invalidValue(l(ref || 'object'), val, key, parent); - } - const result: any = {}; - Object.getOwnPropertyNames(props).forEach((key) => { - const prop = props[key]; - const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined; - result[prop.key] = transform(v, prop.typ, getProps, key, ref); - }); - Object.getOwnPropertyNames(val).forEach((key) => { - if (!Object.prototype.hasOwnProperty.call(props, key)) { - result[key] = transform(val[key], additional, getProps, key, ref); - } - }); - return result; - } - - if (typ === 'any') return val; - if (typ === null) { - if (val === null) return val; - return invalidValue(typ, val, key, parent); - } - if (typ === false) return invalidValue(typ, val, key, parent); - let ref: any = undefined; - while (typeof typ === 'object' && typ.ref !== undefined) { - ref = typ.ref; - typ = typeMap[typ.ref]; - } - if (Array.isArray(typ)) return transformEnum(typ, val); - if (typeof typ === 'object') { - return typ.hasOwnProperty('unionMembers') - ? transformUnion(typ.unionMembers, val) - : typ.hasOwnProperty('arrayItems') - ? transformArray(typ.arrayItems, val) - : typ.hasOwnProperty('props') - ? transformObject(getProps(typ), typ.additional, val) - : invalidValue(typ, val, key, parent); - } - // Numbers can be parsed by Date but shouldn't be. - if (typ === Date && typeof val !== 'number') return transformDate(val); - return transformPrimitive(typ, val); -} - -function cast(val: any, typ: any): T { - return transform(val, typ, jsonToJSProps); -} - -function uncast(val: T, typ: any): any { - return transform(val, typ, jsToJSONProps); -} - -function l(typ: any) { - return { literal: typ }; -} - -function a(typ: any) { - return { arrayItems: typ }; -} - -function u(...typs: any[]) { - return { unionMembers: typs }; -} - -function o(props: any[], additional: any) { - return { props, additional }; -} - -function m(additional: any) { - return { props: [], additional }; -} - -function r(name: string) { - return { ref: name }; + description: { + task_name?: string; + unix_millis_warn_time?: number; + pickup?: string; + destination?: string; + cart_id?: string; + }; } -const typeMap: any = { - TaskRequestLabel: o( - [ - { json: 'task_identifier', js: 'task_identifier', typ: '' }, - { json: 'unix_millis_warn_time', js: 'unix_millis_warn_time', typ: u(undefined, 0) }, - { json: 'pickup', js: 'pickup', typ: u(undefined, '') }, - { json: 'destination', js: 'destination', typ: u(undefined, '') }, - { json: 'cart_id', js: 'cart_id', typ: u(undefined, '') }, - ], - false, - ), -}; +// const errIdx = obj.findIndex((req) => !ajv.validate(schema.components.schemas.TaskRequest, req)); From 0ec29e0bab497a66a601f214274dd91d3700b6f2 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 29 Apr 2024 14:57:51 +0800 Subject: [PATCH 16/37] Using Ajv properly, with basic json stringify and parse Signed-off-by: Aaron Chong --- .../api_server/repositories/tasks.py | 8 ++- .../src/components/tasks/task-summary.tsx | 24 ++++--- .../dashboard/src/components/tasks/utils.ts | 10 +-- .../lib/tasks/create-task.tsx | 64 +++++++++++++------ packages/react-components/lib/tasks/index.ts | 2 +- .../lib/tasks/task-request-label-utils.tsx | 54 ++++++++++++++++ .../lib/tasks/task-request-label.tsx | 13 ---- .../lib/tasks/task-table-datagrid.tsx | 14 ++-- packages/react-components/lib/tasks/utils.ts | 16 ----- packages/react-components/package.json | 1 + pnpm-lock.yaml | 3 + 11 files changed, 137 insertions(+), 72 deletions(-) create mode 100644 packages/react-components/lib/tasks/task-request-label-utils.tsx delete mode 100644 packages/react-components/lib/tasks/task-request-label.tsx diff --git a/packages/api-server/api_server/repositories/tasks.py b/packages/api-server/api_server/repositories/tasks.py index 542b7a806..6c3faa838 100644 --- a/packages/api-server/api_server/repositories/tasks.py +++ b/packages/api-server/api_server/repositories/tasks.py @@ -86,9 +86,13 @@ async def save_task_state(self, task_state: TaskState) -> None: "requester": task_state.booking.requester if task_state.booking.requester else None, - "pickup": request_label.pickup if request_label is not None else None, - "destination": request_label.destination + "pickup": request_label.description.pickup if request_label is not None + and request_label.description.pickup is not None + else None, + "destination": request_label.description.destination + if request_label is not None + and request_label.description.destination is not None else None, } diff --git a/packages/dashboard/src/components/tasks/task-summary.tsx b/packages/dashboard/src/components/tasks/task-summary.tsx index 5949690d3..be018add2 100644 --- a/packages/dashboard/src/components/tasks/task-summary.tsx +++ b/packages/dashboard/src/components/tasks/task-summary.tsx @@ -14,8 +14,12 @@ import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import { makeStyles, createStyles } from '@mui/styles'; -import { ApiServerModelsRmfApiTaskStateStatus as Status, TaskRequest, TaskState } from 'api-client'; -import { base, parseTaskRequestLabel, TaskRequestLabel } from 'react-components'; +import { + ApiServerModelsRmfApiTaskStateStatus as Status, + TaskRequestLabel, + TaskState, +} from 'api-client'; +import { base, getTaskRequestLabelFromTaskState } from 'react-components'; import { TaskInspector } from './task-inspector'; import { RmfAppContext } from '../rmf-app'; import { TaskCancelButton } from './task-cancellation'; @@ -80,7 +84,7 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { const [openTaskDetailsLogs, setOpenTaskDetailsLogs] = React.useState(false); const [taskState, setTaskState] = React.useState(null); - const [label, setLabel] = React.useState({}); + const [label, setLabel] = React.useState({ description: {} }); const [isOpen, setIsOpen] = React.useState(true); const taskProgress = React.useMemo(() => { @@ -107,11 +111,11 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { return; } const sub = rmf.getTaskStateObs(task.booking.id).subscribe((subscribedTask) => { - const requestLabel = parseTaskRequestLabel(subscribedTask); + const requestLabel = getTaskRequestLabelFromTaskState(subscribedTask); if (requestLabel) { setLabel(requestLabel); } else { - setLabel({}); + setLabel({ description: {} }); } setTaskState(subscribedTask); }); @@ -125,20 +129,20 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { value: taskState ? taskState.booking.id : 'n/a.', }, { - title: 'Category', - value: label.category ?? 'n/a', + title: 'Task name', + value: label.description.task_name ?? 'n/a', }, { title: 'Pickup', - value: label.pickup ?? 'n/a', + value: label.description.pickup ?? 'n/a', }, { title: 'Cart ID', - value: label.cart_id ?? 'n/a', + value: label.description.cart_id ?? 'n/a', }, { title: 'Dropoff', - value: label.destination ?? 'n/a', + value: label.description.destination ?? 'n/a', }, { title: 'Est. end time', diff --git a/packages/dashboard/src/components/tasks/utils.ts b/packages/dashboard/src/components/tasks/utils.ts index da5d83109..234e8f4dd 100644 --- a/packages/dashboard/src/components/tasks/utils.ts +++ b/packages/dashboard/src/components/tasks/utils.ts @@ -1,5 +1,5 @@ import { PostScheduledTaskRequest, TaskRequest, TaskState } from 'api-client'; -import { parseTaskRequestLabel, Schedule } from 'react-components'; +import { getTaskRequestLabelFromTaskState, Schedule } from 'react-components'; import schema from 'api-client/dist/schema'; import { ajv } from '../utils'; @@ -63,7 +63,7 @@ export function exportCsvMinimal(timestamp: Date, allTasks: TaskState[]) { ]; csvContent += keys.join(columnSeparator) + rowSeparator; allTasks.forEach((task) => { - let requestLabel = parseTaskRequestLabel(task); + let requestLabel = getTaskRequestLabelFromTaskState(task); const values = [ // Date @@ -73,9 +73,11 @@ export function exportCsvMinimal(timestamp: Date, allTasks: TaskState[]) { // Requester task.booking.requester ? task.booking.requester : 'n/a', // Pickup - requestLabel ? requestLabel.pickup : 'n/a', + requestLabel && requestLabel.description.pickup ? requestLabel.description.pickup : 'n/a', // Destination - requestLabel ? requestLabel.destination : 'n/a', + requestLabel && requestLabel.description.destination + ? requestLabel.description.destination + : 'n/a', // Robot task.assigned_to ? task.assigned_to.name : 'n/a', // Start Time diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index ecb4dbb17..fdb4ee844 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -38,12 +38,16 @@ import { useTheme, } from '@mui/material'; import { DatePicker, TimePicker, DateTimePicker } from '@mui/x-date-pickers'; -import type { TaskFavoritePydantic as TaskFavorite, TaskRequest } from 'api-client'; +import type { + TaskFavoritePydantic as TaskFavorite, + TaskRequest, + TaskRequestLabel, +} from 'api-client'; import React from 'react'; import { Loading } from '..'; import { ConfirmationDialog, ConfirmationDialogProps } from '../confirmation-dialog'; import { PositiveIntField } from '../form-inputs'; -import { TaskRequestLabel } from './task-request-label'; +import { serializeTaskRequestLabel } from './task-request-label-utils'; // A bunch of manually defined descriptions to avoid using `any`. export interface PatrolTaskDescription { @@ -1288,7 +1292,7 @@ export function CreateTaskForm({ const [taskRequest, setTaskRequest] = React.useState( () => requestTask ?? defaultTask(), ); - const [requestLabel, setRequestLabel] = React.useState(null); + const [requestLabel, setRequestLabel] = React.useState({ description: {} }); const [submitting, setSubmitting] = React.useState(false); const [formFullyFilled, setFormFullyFilled] = React.useState(requestTask !== undefined || false); @@ -1362,9 +1366,14 @@ export function CreateTaskForm({ patrolWaypoints={patrolWaypoints} onChange={(desc) => { handleTaskDescriptionChange('patrol', desc); - setRequestLabel({ - task_identifier: taskRequest.category, - destination: desc.places.at(-1), + setRequestLabel((prev) => { + return { + description: { + ...prev.description, + task_name: taskRequest.category, + destination: desc.places.at(-1), + }, + }; }); }} allowSubmit={allowSubmit} @@ -1376,8 +1385,13 @@ export function CreateTaskForm({ taskDesc={taskRequest.description as CustomComposeTaskDescription} onChange={(desc) => { handleCustomComposeTaskDescriptionChange(desc); - setRequestLabel({ - task_identifier: taskRequest.category, + setRequestLabel((prev) => { + return { + description: { + ...prev.description, + task_name: taskRequest.category, + }, + }; }); }} allowSubmit={allowSubmit} @@ -1400,11 +1414,16 @@ export function CreateTaskForm({ handleTaskDescriptionChange('compose', desc); const pickupPerformAction = desc.phases[0].activity.description.activities[1].description.description; - setRequestLabel({ - task_identifier: taskRequest.description.category, - pickup: pickupPerformAction.pickup_lot, - cart_id: pickupPerformAction.cart_id, - destination: desc.phases[1].activity.description.activities[0].description, + setRequestLabel((prev) => { + return { + description: { + ...prev.description, + task_name: taskRequest.description.category, + pickup: pickupPerformAction.pickup_lot, + cart_id: pickupPerformAction.cart_id, + destination: desc.phases[1].activity.description.activities[0].description, + }, + }; }); }} allowSubmit={allowSubmit} @@ -1425,11 +1444,16 @@ export function CreateTaskForm({ handleTaskDescriptionChange('compose', desc); const pickupPerformAction = desc.phases[0].activity.description.activities[1].description.description; - setRequestLabel({ - task_identifier: taskRequest.description.category, - pickup: pickupPerformAction.pickup_zone, - cart_id: pickupPerformAction.cart_id, - destination: desc.phases[1].activity.description.activities[0].description, + setRequestLabel((prev) => { + return { + description: { + ...prev.description, + task_name: taskRequest.description.category, + pickup: pickupPerformAction.pickup_zone, + cart_id: pickupPerformAction.cart_id, + destination: desc.phases[1].activity.description.activities[0].description, + }, + }; }); }} allowSubmit={allowSubmit} @@ -1526,8 +1550,10 @@ export function CreateTaskForm({ } try { - const labelString = JSON.stringify(requestLabel); + const labelString = serializeTaskRequestLabel(requestLabel); if (labelString) { + console.log('pushing label: '); + console.log(labelString); if (request.labels) { request.labels.push(labelString); } else { diff --git a/packages/react-components/lib/tasks/index.ts b/packages/react-components/lib/tasks/index.ts index 34539a4a3..afe9c0fdb 100644 --- a/packages/react-components/lib/tasks/index.ts +++ b/packages/react-components/lib/tasks/index.ts @@ -1,7 +1,7 @@ export * from './create-task'; export * from './task-info'; export * from './task-logs'; -export * from './task-request-label'; +export * from './task-request-label-utils'; export * from './task-table'; export * from './task-timeline'; export * from './task-table-datagrid'; diff --git a/packages/react-components/lib/tasks/task-request-label-utils.tsx b/packages/react-components/lib/tasks/task-request-label-utils.tsx new file mode 100644 index 000000000..5269c73b4 --- /dev/null +++ b/packages/react-components/lib/tasks/task-request-label-utils.tsx @@ -0,0 +1,54 @@ +import Ajv from 'ajv'; +import schema from 'api-client/dist/schema'; +import type { TaskState, TaskRequestLabel } from 'api-client'; + +// FIXME: AJV is duplicated here with dashboard, but this is only a temporary +// measure until we start using an external validation tool. +const ajv = new Ajv(); + +Object.entries(schema.components.schemas).forEach(([k, v]) => { + ajv.addSchema(v, `#/components/schemas/${k}`); +}); + +const validateTaskRequestLabel = ajv.compile(schema.components.schemas.TaskRequestLabel); + +export function serializeTaskRequestLabel(label: TaskRequestLabel): string { + return JSON.stringify(label); +} + +export function getTaskRequestLabelFromJsonString( + jsonString: string, +): TaskRequestLabel | undefined { + try { + // Validate first before parsing again into the interface + const validated = validateTaskRequestLabel(JSON.parse(jsonString)); + if (validated) { + const parsedLabel: TaskRequestLabel = JSON.parse(jsonString); + return parsedLabel; + } + } catch (e) { + console.error(`Failed to parse TaskRequestLabel: ${(e as Error).message}`); + return undefined; + } + + console.error(`Failed to validate TaskRequestLabel`); + return undefined; +} + +export function getTaskRequestLabelFromTaskState(taskState: TaskState): TaskRequestLabel | null { + let requestLabel: TaskRequestLabel | null = null; + if (taskState.booking.labels) { + for (const label of taskState.booking.labels) { + try { + const parsedLabel = getTaskRequestLabelFromJsonString(label); + if (parsedLabel) { + requestLabel = parsedLabel; + break; + } + } catch (e) { + continue; + } + } + } + return requestLabel; +} diff --git a/packages/react-components/lib/tasks/task-request-label.tsx b/packages/react-components/lib/tasks/task-request-label.tsx deleted file mode 100644 index 9e61f83fd..000000000 --- a/packages/react-components/lib/tasks/task-request-label.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import schema from 'api-client/dist/schema'; - -export interface TaskRequestLabel { - description: { - task_name?: string; - unix_millis_warn_time?: number; - pickup?: string; - destination?: string; - cart_id?: string; - }; -} - -// const errIdx = obj.findIndex((req) => !ajv.validate(schema.components.schemas.TaskRequest, req)); diff --git a/packages/react-components/lib/tasks/task-table-datagrid.tsx b/packages/react-components/lib/tasks/task-table-datagrid.tsx index 925ce36cf..b7e3b7a35 100644 --- a/packages/react-components/lib/tasks/task-table-datagrid.tsx +++ b/packages/react-components/lib/tasks/task-table-datagrid.tsx @@ -15,7 +15,7 @@ import { styled, Stack, Typography, Tooltip, useMediaQuery, SxProps, Theme } fro import * as React from 'react'; import { TaskState, ApiServerModelsRmfApiTaskStateStatus as Status } from 'api-client'; import { InsertInvitation as ScheduleIcon, Person as UserIcon } from '@mui/icons-material/'; -import { parseTaskRequestLabel } from './utils'; +import { getTaskRequestLabelFromTaskState } from './task-request-label-utils'; const classes = { taskActiveCell: 'MuiDataGrid-cell-active-cell', @@ -184,9 +184,9 @@ export function TaskDataGridTable({ width: 150, editable: false, valueGetter: (params: GridValueGetterParams) => { - const requestLabel = parseTaskRequestLabel(params.row); - if (requestLabel && requestLabel.pickup) { - return requestLabel.pickup; + const requestLabel = getTaskRequestLabelFromTaskState(params.row); + if (requestLabel && requestLabel.description.pickup) { + return requestLabel.description.pickup; } return 'n/a'; }, @@ -200,9 +200,9 @@ export function TaskDataGridTable({ width: 150, editable: false, valueGetter: (params: GridValueGetterParams) => { - const requestLabel = parseTaskRequestLabel(params.row); - if (requestLabel && requestLabel.destination) { - return requestLabel.destination; + const requestLabel = getTaskRequestLabelFromTaskState(params.row); + if (requestLabel && requestLabel.description.destination) { + return requestLabel.description.destination; } return 'n/a'; }, diff --git a/packages/react-components/lib/tasks/utils.ts b/packages/react-components/lib/tasks/utils.ts index 707fd3f47..1ec734fc5 100644 --- a/packages/react-components/lib/tasks/utils.ts +++ b/packages/react-components/lib/tasks/utils.ts @@ -1,6 +1,5 @@ import { TaskType as RmfTaskType } from 'rmf-models'; import type { TaskState, TaskRequest } from 'api-client'; -import { Convert, TaskRequestLabel } from './task-request-label'; export function taskTypeToStr(taskType: number): string { switch (taskType) { @@ -21,21 +20,6 @@ export function taskTypeToStr(taskType: number): string { } } -export function parseTaskRequestLabel(taskState: TaskState): TaskRequestLabel | null { - let requestLabel: TaskRequestLabel | null = null; - if (taskState.booking.labels) { - for (const label of taskState.booking.labels) { - try { - requestLabel = Convert.toTaskRequestLabel(label); - break; - } catch (e) { - continue; - } - } - } - return requestLabel; -} - function parsePhaseDetail(phases: TaskState['phases'], category?: string) { if (phases) { if (category === 'Loop') { diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 7b0787731..5707be2b3 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -35,6 +35,7 @@ "@types/react-leaflet": "^2.5.2", "@types/shallowequal": "^1.1.1", "@types/three": "^0.156.0", + "ajv": "^8.10.0", "api-client": "workspace:*", "clsx": "^1.1.1", "crc": "^3.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f179d8f8..4ef95e963 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -388,6 +388,9 @@ importers: '@types/three': specifier: ^0.156.0 version: 0.156.0 + ajv: + specifier: ^8.10.0 + version: 8.11.0 api-client: specifier: workspace:* version: link:../api-client From e78c8685138183b4743776c71ff2c1d990ea52f5 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 29 Apr 2024 16:28:21 +0800 Subject: [PATCH 17/37] Renaming to task booking label, with the endpoint getting the label from the state, instead of the request Signed-off-by: Aaron Chong --- .../api-server/api_server/models/__init__.py | 2 +- ...request_label.py => task_booking_label.py} | 16 ++++---- .../api_server/repositories/tasks.py | 22 +++++----- .../api_server/routes/tasks/tasks.py | 40 +++++++++---------- .../api_server/routes/tasks/test_tasks.py | 15 ++++++- .../api-server/api_server/test/test_data.py | 21 ++++++++++ 6 files changed, 75 insertions(+), 41 deletions(-) rename packages/api-server/api_server/models/{task_request_label.py => task_booking_label.py} (59%) diff --git a/packages/api-server/api_server/models/__init__.py b/packages/api-server/api_server/models/__init__.py index 3f22e7665..db2e63956 100644 --- a/packages/api-server/api_server/models/__init__.py +++ b/packages/api-server/api_server/models/__init__.py @@ -50,5 +50,5 @@ from .rmf_api.task_state_update import TaskStateUpdate from .rmf_api.undo_skip_phase_request import UndoPhaseSkipRequest from .rmf_api.undo_skip_phase_response import UndoPhaseSkipResponse -from .task_request_label import * +from .task_booking_label import * from .user import * diff --git a/packages/api-server/api_server/models/task_request_label.py b/packages/api-server/api_server/models/task_booking_label.py similarity index 59% rename from packages/api-server/api_server/models/task_request_label.py rename to packages/api-server/api_server/models/task_booking_label.py index 52c7801f4..36e4abb44 100644 --- a/packages/api-server/api_server/models/task_request_label.py +++ b/packages/api-server/api_server/models/task_booking_label.py @@ -7,27 +7,27 @@ # populated by the dashboard. Any changes to either side will require syncing. -class TaskRequestLabelDescription(BaseModel): +class TaskBookingLabelDescription(BaseModel): task_name: Optional[str] - unix_millis_warn_time: Optional[str] + unix_millis_warn_time: Optional[int] pickup: Optional[str] destination: Optional[str] cart_id: Optional[str] @staticmethod - def from_json_string(json_str: str) -> Optional["TaskRequestLabelDescription"]: + def from_json_string(json_str: str) -> Optional["TaskBookingLabelDescription"]: try: - return TaskRequestLabelDescription.parse_raw(json_str) + return TaskBookingLabelDescription.parse_raw(json_str) except pydantic.error_wrappers.ValidationError: return None -class TaskRequestLabel(BaseModel): - description: TaskRequestLabelDescription +class TaskBookingLabel(BaseModel): + description: TaskBookingLabelDescription @staticmethod - def from_json_string(json_str: str) -> Optional["TaskRequestLabel"]: + def from_json_string(json_str: str) -> Optional["TaskBookingLabel"]: try: - return TaskRequestLabel.parse_raw(json_str) + return TaskBookingLabel.parse_raw(json_str) except pydantic.error_wrappers.ValidationError: return None diff --git a/packages/api-server/api_server/repositories/tasks.py b/packages/api-server/api_server/repositories/tasks.py index 6c3faa838..75aae9ad3 100644 --- a/packages/api-server/api_server/repositories/tasks.py +++ b/packages/api-server/api_server/repositories/tasks.py @@ -14,9 +14,9 @@ LogEntry, Pagination, Phases, + TaskBookingLabel, TaskEventLog, TaskRequest, - TaskRequestLabel, TaskState, User, ) @@ -60,12 +60,12 @@ async def query_task_requests(self, task_ids: List[str]) -> List[DbTaskRequest]: async def save_task_state(self, task_state: TaskState) -> None: labels = task_state.booking.labels - request_label = None + booking_label = None if labels is not None: for l in labels: - validated_request_label = TaskRequestLabel.from_json_string(l) - if validated_request_label is not None: - request_label = validated_request_label + validated_booking_label = TaskBookingLabel.from_json_string(l) + if validated_booking_label is not None: + booking_label = validated_booking_label break task_state_dict = { @@ -86,13 +86,13 @@ async def save_task_state(self, task_state: TaskState) -> None: "requester": task_state.booking.requester if task_state.booking.requester else None, - "pickup": request_label.description.pickup - if request_label is not None - and request_label.description.pickup is not None + "pickup": booking_label.description.pickup + if booking_label is not None + and booking_label.description.pickup is not None else None, - "destination": request_label.description.destination - if request_label is not None - and request_label.description.destination is not None + "destination": booking_label.description.destination + if booking_label is not None + and booking_label.description.destination is not None else None, } diff --git a/packages/api-server/api_server/routes/tasks/tasks.py b/packages/api-server/api_server/routes/tasks/tasks.py index e9e0b6082..b9549382d 100644 --- a/packages/api-server/api_server/routes/tasks/tasks.py +++ b/packages/api-server/api_server/routes/tasks/tasks.py @@ -83,26 +83,6 @@ async def query_task_requests( return return_requests -@router.get("/{task_id}/request_label", response_model=mdl.TaskRequestLabel) -async def get_task_request_label( - task_repo: TaskRepository = Depends(TaskRepository), - task_id: str = Path(..., description="task_id"), -): - request = await task_repo.get_task_request(task_id) - if request is None: - raise HTTPException(status_code=404) - - if request.labels is not None: - for label in request.labels: - if len(label) == 0: - continue - - request_label = mdl.TaskRequestLabel.from_json_string(label) - if request_label is not None: - return request_label - raise HTTPException(status_code=404) - - @router.get("", response_model=List[mdl.TaskState]) async def query_task_states( task_repo: TaskRepository = Depends(TaskRepository), @@ -196,6 +176,26 @@ async def sub_task_state(req: SubscriptionRequest, task_id: str): return obs +@router.get("/{task_id}/booking_label", response_model=mdl.TaskBookingLabel) +async def get_task_booking_label( + task_repo: TaskRepository = Depends(TaskRepository), + task_id: str = Path(..., description="task_id"), +): + state = await task_repo.get_task_state(task_id) + if state is None: + raise HTTPException(status_code=404) + + if state.booking.labels is not None: + for label in state.booking.labels: + if len(label) == 0: + continue + + booking_label = mdl.TaskBookingLabel.from_json_string(label) + if booking_label is not None: + return booking_label + raise HTTPException(status_code=404) + + @router.get("/{task_id}/log", response_model=mdl.TaskEventLog) async def get_task_log( task_repo: TaskRepository = Depends(TaskRepository), diff --git a/packages/api-server/api_server/routes/tasks/test_tasks.py b/packages/api-server/api_server/routes/tasks/test_tasks.py index 5dee13e0a..ffa78e9db 100644 --- a/packages/api-server/api_server/routes/tasks/test_tasks.py +++ b/packages/api-server/api_server/routes/tasks/test_tasks.py @@ -3,7 +3,12 @@ from api_server import models as mdl from api_server.rmf_io import tasks_service -from api_server.test import AppFixture, make_task_log, make_task_state +from api_server.test import ( + AppFixture, + make_task_booking_label, + make_task_log, + make_task_state, +) class TestTasksRoute(AppFixture): @@ -51,6 +56,14 @@ def test_sub_task_state(self): state = next(gen) self.assertEqual(task_id, state.booking.id) # type: ignore + def test_get_task_booking_label(self): + resp = self.client.get(f"/tasks/{self.task_states[0].booking.id}/booking_label") + self.assertEqual(200, resp.status_code) + self.assertEqual( + make_task_booking_label(), + mdl.TaskBookingLabel(**resp.json()), + ) + def test_get_task_log(self): resp = self.client.get( f"/tasks/{self.task_logs[0].task_id}/log?between=0,1636388414500" diff --git a/packages/api-server/api_server/test/test_data.py b/packages/api-server/api_server/test/test_data.py index 8224329a5..21b028a9d 100644 --- a/packages/api-server/api_server/test/test_data.py +++ b/packages/api-server/api_server/test/test_data.py @@ -22,6 +22,8 @@ Lift, LiftState, RobotState, + TaskBookingLabel, + TaskBookingLabelDescription, TaskEventLog, TaskState, ) @@ -125,6 +127,18 @@ def make_fleet_log() -> FleetLog: return FleetLog(name=str(uuid4()), log=[], robots={}) +def make_task_booking_label() -> TaskBookingLabel: + return TaskBookingLabel( + description=TaskBookingLabelDescription( + task_name="Multi-Delivery", + unix_millis_warn_time=1636388400000, + pickup="Kitchen", + destination="room_203", + cart_id="soda", + ) + ) + + def make_task_state(task_id: str = "test_task") -> TaskState: # from https://raw.githubusercontent.com/open-rmf/rmf_api_msgs/960b286d9849fc716a3043b8e1f5fb341bdf5778/rmf_api_msgs/samples/task_state/multi_dropoff_delivery.json sample_task = json.loads( @@ -424,6 +438,13 @@ def make_task_state(task_id: str = "test_task") -> TaskState: """ ) sample_task["booking"]["id"] = task_id + + booking_labels = [ + "dummy_label_1", + "dummy_label_2", + make_task_booking_label().json(), + ] + sample_task["booking"]["labels"] = booking_labels return TaskState(**sample_task) From 136f7e2743658e47584915442b1754104de52cd6 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 29 Apr 2024 16:35:25 +0800 Subject: [PATCH 18/37] Generated API client after rename Signed-off-by: Aaron Chong --- packages/api-client/lib/openapi/api.ts | 212 ++++++++++++------------- packages/api-client/lib/version.ts | 2 +- packages/api-client/schema/index.ts | 94 +++++------ 3 files changed, 154 insertions(+), 154 deletions(-) diff --git a/packages/api-client/lib/openapi/api.ts b/packages/api-client/lib/openapi/api.ts index 7536dc6fa..d562cd848 100644 --- a/packages/api-client/lib/openapi/api.ts +++ b/packages/api-client/lib/openapi/api.ts @@ -2425,6 +2425,56 @@ export interface Task { */ description_schema?: object; } +/** + * + * @export + * @interface TaskBookingLabel + */ +export interface TaskBookingLabel { + /** + * + * @type {TaskBookingLabelDescription} + * @memberof TaskBookingLabel + */ + description: TaskBookingLabelDescription; +} +/** + * + * @export + * @interface TaskBookingLabelDescription + */ +export interface TaskBookingLabelDescription { + /** + * + * @type {string} + * @memberof TaskBookingLabelDescription + */ + task_name?: string; + /** + * + * @type {number} + * @memberof TaskBookingLabelDescription + */ + unix_millis_warn_time?: number; + /** + * + * @type {string} + * @memberof TaskBookingLabelDescription + */ + pickup?: string; + /** + * + * @type {string} + * @memberof TaskBookingLabelDescription + */ + destination?: string; + /** + * + * @type {string} + * @memberof TaskBookingLabelDescription + */ + cart_id?: string; +} /** * Response to a request to cancel a task * @export @@ -2863,56 +2913,6 @@ export interface TaskRequest { */ unix_millis_warn_time?: number; } -/** - * - * @export - * @interface TaskRequestLabel - */ -export interface TaskRequestLabel { - /** - * - * @type {TaskRequestLabelDescription} - * @memberof TaskRequestLabel - */ - description: TaskRequestLabelDescription; -} -/** - * - * @export - * @interface TaskRequestLabelDescription - */ -export interface TaskRequestLabelDescription { - /** - * - * @type {string} - * @memberof TaskRequestLabelDescription - */ - task_name?: string; - /** - * - * @type {string} - * @memberof TaskRequestLabelDescription - */ - unix_millis_warn_time?: string; - /** - * - * @type {string} - * @memberof TaskRequestLabelDescription - */ - pickup?: string; - /** - * - * @type {string} - * @memberof TaskRequestLabelDescription - */ - destination?: string; - /** - * - * @type {string} - * @memberof TaskRequestLabelDescription - */ - cart_id?: string; -} /** * * @export @@ -8566,21 +8566,19 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration }; }, /** - * Available in socket.io - * @summary Get Task Log + * + * @summary Get Task Booking Label * @param {string} taskId task_id - * @param {string} [between] The period of time to fetch, in unix millis. This can be either a comma separated string or a string prefixed with \'-\' to fetch the last X millis. Example: \"1000,2000\" - Fetches logs between unix millis 1000 and 2000. \"-60000\" - Fetches logs in the last minute. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTaskLogTasksTaskIdLogGet: async ( + getTaskBookingLabelTasksTaskIdBookingLabelGet: async ( taskId: string, - between?: string, options: AxiosRequestConfig = {}, ): Promise => { // verify required parameter 'taskId' is not null or undefined - assertParamExists('getTaskLogTasksTaskIdLogGet', 'taskId', taskId); - const localVarPath = `/tasks/{task_id}/log`.replace( + assertParamExists('getTaskBookingLabelTasksTaskIdBookingLabelGet', 'taskId', taskId); + const localVarPath = `/tasks/{task_id}/booking_label`.replace( `{${'task_id'}}`, encodeURIComponent(String(taskId)), ); @@ -8595,10 +8593,6 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - if (between !== undefined) { - localVarQueryParameter['between'] = between; - } - setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = { @@ -8613,19 +8607,21 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration }; }, /** - * - * @summary Get Task Request Label + * Available in socket.io + * @summary Get Task Log * @param {string} taskId task_id + * @param {string} [between] The period of time to fetch, in unix millis. This can be either a comma separated string or a string prefixed with \'-\' to fetch the last X millis. Example: \"1000,2000\" - Fetches logs between unix millis 1000 and 2000. \"-60000\" - Fetches logs in the last minute. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTaskRequestLabelTasksTaskIdRequestLabelGet: async ( + getTaskLogTasksTaskIdLogGet: async ( taskId: string, + between?: string, options: AxiosRequestConfig = {}, ): Promise => { // verify required parameter 'taskId' is not null or undefined - assertParamExists('getTaskRequestLabelTasksTaskIdRequestLabelGet', 'taskId', taskId); - const localVarPath = `/tasks/{task_id}/request_label`.replace( + assertParamExists('getTaskLogTasksTaskIdLogGet', 'taskId', taskId); + const localVarPath = `/tasks/{task_id}/log`.replace( `{${'task_id'}}`, encodeURIComponent(String(taskId)), ); @@ -8640,6 +8636,10 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + if (between !== undefined) { + localVarQueryParameter['between'] = between; + } + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = { @@ -9716,6 +9716,24 @@ export const TasksApiFp = function (configuration?: Configuration) { ); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary Get Task Booking Label + * @param {string} taskId task_id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTaskBookingLabelTasksTaskIdBookingLabelGet( + taskId: string, + options?: AxiosRequestConfig, + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = + await localVarAxiosParamCreator.getTaskBookingLabelTasksTaskIdBookingLabelGet( + taskId, + options, + ); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * Available in socket.io * @summary Get Task Log @@ -9736,24 +9754,6 @@ export const TasksApiFp = function (configuration?: Configuration) { ); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, - /** - * - * @summary Get Task Request Label - * @param {string} taskId task_id - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async getTaskRequestLabelTasksTaskIdRequestLabelGet( - taskId: string, - options?: AxiosRequestConfig, - ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = - await localVarAxiosParamCreator.getTaskRequestLabelTasksTaskIdRequestLabelGet( - taskId, - options, - ); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * * @summary Get Task Request @@ -10242,35 +10242,35 @@ export const TasksApiFactory = function ( .then((request) => request(axios, basePath)); }, /** - * Available in socket.io - * @summary Get Task Log + * + * @summary Get Task Booking Label * @param {string} taskId task_id - * @param {string} [between] The period of time to fetch, in unix millis. This can be either a comma separated string or a string prefixed with \'-\' to fetch the last X millis. Example: \"1000,2000\" - Fetches logs between unix millis 1000 and 2000. \"-60000\" - Fetches logs in the last minute. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTaskLogTasksTaskIdLogGet( + getTaskBookingLabelTasksTaskIdBookingLabelGet( taskId: string, - between?: string, options?: any, - ): AxiosPromise { + ): AxiosPromise { return localVarFp - .getTaskLogTasksTaskIdLogGet(taskId, between, options) + .getTaskBookingLabelTasksTaskIdBookingLabelGet(taskId, options) .then((request) => request(axios, basePath)); }, /** - * - * @summary Get Task Request Label + * Available in socket.io + * @summary Get Task Log * @param {string} taskId task_id + * @param {string} [between] The period of time to fetch, in unix millis. This can be either a comma separated string or a string prefixed with \'-\' to fetch the last X millis. Example: \"1000,2000\" - Fetches logs between unix millis 1000 and 2000. \"-60000\" - Fetches logs in the last minute. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTaskRequestLabelTasksTaskIdRequestLabelGet( + getTaskLogTasksTaskIdLogGet( taskId: string, + between?: string, options?: any, - ): AxiosPromise { + ): AxiosPromise { return localVarFp - .getTaskRequestLabelTasksTaskIdRequestLabelGet(taskId, options) + .getTaskLogTasksTaskIdLogGet(taskId, between, options) .then((request) => request(axios, basePath)); }, /** @@ -10698,38 +10698,38 @@ export class TasksApi extends BaseAPI { } /** - * Available in socket.io - * @summary Get Task Log + * + * @summary Get Task Booking Label * @param {string} taskId task_id - * @param {string} [between] The period of time to fetch, in unix millis. This can be either a comma separated string or a string prefixed with \'-\' to fetch the last X millis. Example: \"1000,2000\" - Fetches logs between unix millis 1000 and 2000. \"-60000\" - Fetches logs in the last minute. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof TasksApi */ - public getTaskLogTasksTaskIdLogGet( + public getTaskBookingLabelTasksTaskIdBookingLabelGet( taskId: string, - between?: string, options?: AxiosRequestConfig, ) { return TasksApiFp(this.configuration) - .getTaskLogTasksTaskIdLogGet(taskId, between, options) + .getTaskBookingLabelTasksTaskIdBookingLabelGet(taskId, options) .then((request) => request(this.axios, this.basePath)); } /** - * - * @summary Get Task Request Label + * Available in socket.io + * @summary Get Task Log * @param {string} taskId task_id + * @param {string} [between] The period of time to fetch, in unix millis. This can be either a comma separated string or a string prefixed with \'-\' to fetch the last X millis. Example: \"1000,2000\" - Fetches logs between unix millis 1000 and 2000. \"-60000\" - Fetches logs in the last minute. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof TasksApi */ - public getTaskRequestLabelTasksTaskIdRequestLabelGet( + public getTaskLogTasksTaskIdLogGet( taskId: string, + between?: string, options?: AxiosRequestConfig, ) { return TasksApiFp(this.configuration) - .getTaskRequestLabelTasksTaskIdRequestLabelGet(taskId, options) + .getTaskLogTasksTaskIdLogGet(taskId, between, options) .then((request) => request(this.axios, this.basePath)); } diff --git a/packages/api-client/lib/version.ts b/packages/api-client/lib/version.ts index d5a99cf40..fa3fd8f02 100644 --- a/packages/api-client/lib/version.ts +++ b/packages/api-client/lib/version.ts @@ -3,6 +3,6 @@ import { version as rmfModelVer } from 'rmf-models'; export const version = { rmfModels: rmfModelVer, - rmfServer: 'd4c7f52a4650490d5bc1e51753ff2125d2edeffd', + rmfServer: 'e78c8685138183b4743776c71ff2c1d990ea52f5', openapiGenerator: '6.2.1', }; diff --git a/packages/api-client/schema/index.ts b/packages/api-client/schema/index.ts index 284f41ab0..26d54df74 100644 --- a/packages/api-client/schema/index.ts +++ b/packages/api-client/schema/index.ts @@ -671,36 +671,6 @@ export default { }, }, }, - '/tasks/{task_id}/request_label': { - get: { - tags: ['Tasks'], - summary: 'Get Task Request Label', - operationId: 'get_task_request_label_tasks__task_id__request_label_get', - parameters: [ - { - description: 'task_id', - required: true, - schema: { title: 'Task Id', type: 'string', description: 'task_id' }, - name: 'task_id', - in: 'path', - }, - ], - responses: { - '200': { - description: 'Successful Response', - content: { - 'application/json': { schema: { $ref: '#/components/schemas/TaskRequestLabel' } }, - }, - }, - '422': { - description: 'Validation Error', - content: { - 'application/json': { schema: { $ref: '#/components/schemas/HTTPValidationError' } }, - }, - }, - }, - }, - }, '/tasks': { get: { tags: ['Tasks'], @@ -913,6 +883,36 @@ export default { }, }, }, + '/tasks/{task_id}/booking_label': { + get: { + tags: ['Tasks'], + summary: 'Get Task Booking Label', + operationId: 'get_task_booking_label_tasks__task_id__booking_label_get', + parameters: [ + { + description: 'task_id', + required: true, + schema: { title: 'Task Id', type: 'string', description: 'task_id' }, + name: 'task_id', + in: 'path', + }, + ], + responses: { + '200': { + description: 'Successful Response', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/TaskBookingLabel' } }, + }, + }, + '422': { + description: 'Validation Error', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/HTTPValidationError' } }, + }, + }, + }, + }, + }, '/tasks/{task_id}/log': { get: { tags: ['Tasks'], @@ -3634,6 +3634,23 @@ export default { }, }, }, + TaskBookingLabel: { + title: 'TaskBookingLabel', + required: ['description'], + type: 'object', + properties: { description: { $ref: '#/components/schemas/TaskBookingLabelDescription' } }, + }, + TaskBookingLabelDescription: { + title: 'TaskBookingLabelDescription', + type: 'object', + properties: { + task_name: { title: 'Task Name', type: 'string' }, + unix_millis_warn_time: { title: 'Unix Millis Warn Time', type: 'integer' }, + pickup: { title: 'Pickup', type: 'string' }, + destination: { title: 'Destination', type: 'string' }, + cart_id: { title: 'Cart Id', type: 'string' }, + }, + }, TaskCancelResponse: { title: 'TaskCancelResponse', allOf: [{ $ref: '#/components/schemas/SimpleResponse' }], @@ -3870,23 +3887,6 @@ export default { }, }, }, - TaskRequestLabel: { - title: 'TaskRequestLabel', - required: ['description'], - type: 'object', - properties: { description: { $ref: '#/components/schemas/TaskRequestLabelDescription' } }, - }, - TaskRequestLabelDescription: { - title: 'TaskRequestLabelDescription', - type: 'object', - properties: { - task_name: { title: 'Task Name', type: 'string' }, - unix_millis_warn_time: { title: 'Unix Millis Warn Time', type: 'string' }, - pickup: { title: 'Pickup', type: 'string' }, - destination: { title: 'Destination', type: 'string' }, - cart_id: { title: 'Cart Id', type: 'string' }, - }, - }, TaskResumeRequest: { title: 'TaskResumeRequest', type: 'object', From 5ea73fa997b91797312a0eb0bd02abe6c517a0bf Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 29 Apr 2024 16:35:44 +0800 Subject: [PATCH 19/37] Changes to dashboard and react components after rename Signed-off-by: Aaron Chong --- .../src/components/tasks/task-summary.tsx | 8 +++---- .../dashboard/src/components/tasks/utils.ts | 4 ++-- .../lib/tasks/create-task.tsx | 8 +++---- packages/react-components/lib/tasks/index.ts | 2 +- ...utils.tsx => task-booking-label-utils.tsx} | 24 +++++++++---------- .../lib/tasks/task-table-datagrid.tsx | 6 ++--- 6 files changed, 26 insertions(+), 26 deletions(-) rename packages/react-components/lib/tasks/{task-request-label-utils.tsx => task-booking-label-utils.tsx} (54%) diff --git a/packages/dashboard/src/components/tasks/task-summary.tsx b/packages/dashboard/src/components/tasks/task-summary.tsx index be018add2..2c38f1c1c 100644 --- a/packages/dashboard/src/components/tasks/task-summary.tsx +++ b/packages/dashboard/src/components/tasks/task-summary.tsx @@ -16,10 +16,10 @@ import DialogTitle from '@mui/material/DialogTitle'; import { makeStyles, createStyles } from '@mui/styles'; import { ApiServerModelsRmfApiTaskStateStatus as Status, - TaskRequestLabel, + TaskBookingLabel, TaskState, } from 'api-client'; -import { base, getTaskRequestLabelFromTaskState } from 'react-components'; +import { base, getTaskBookingLabelFromTaskState } from 'react-components'; import { TaskInspector } from './task-inspector'; import { RmfAppContext } from '../rmf-app'; import { TaskCancelButton } from './task-cancellation'; @@ -84,7 +84,7 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { const [openTaskDetailsLogs, setOpenTaskDetailsLogs] = React.useState(false); const [taskState, setTaskState] = React.useState(null); - const [label, setLabel] = React.useState({ description: {} }); + const [label, setLabel] = React.useState({ description: {} }); const [isOpen, setIsOpen] = React.useState(true); const taskProgress = React.useMemo(() => { @@ -111,7 +111,7 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { return; } const sub = rmf.getTaskStateObs(task.booking.id).subscribe((subscribedTask) => { - const requestLabel = getTaskRequestLabelFromTaskState(subscribedTask); + const requestLabel = getTaskBookingLabelFromTaskState(subscribedTask); if (requestLabel) { setLabel(requestLabel); } else { diff --git a/packages/dashboard/src/components/tasks/utils.ts b/packages/dashboard/src/components/tasks/utils.ts index 234e8f4dd..0303c7ff2 100644 --- a/packages/dashboard/src/components/tasks/utils.ts +++ b/packages/dashboard/src/components/tasks/utils.ts @@ -1,5 +1,5 @@ import { PostScheduledTaskRequest, TaskRequest, TaskState } from 'api-client'; -import { getTaskRequestLabelFromTaskState, Schedule } from 'react-components'; +import { getTaskBookingLabelFromTaskState, Schedule } from 'react-components'; import schema from 'api-client/dist/schema'; import { ajv } from '../utils'; @@ -63,7 +63,7 @@ export function exportCsvMinimal(timestamp: Date, allTasks: TaskState[]) { ]; csvContent += keys.join(columnSeparator) + rowSeparator; allTasks.forEach((task) => { - let requestLabel = getTaskRequestLabelFromTaskState(task); + let requestLabel = getTaskBookingLabelFromTaskState(task); const values = [ // Date diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index fdb4ee844..7dcf83486 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -39,15 +39,15 @@ import { } from '@mui/material'; import { DatePicker, TimePicker, DateTimePicker } from '@mui/x-date-pickers'; import type { + TaskBookingLabel, TaskFavoritePydantic as TaskFavorite, TaskRequest, - TaskRequestLabel, } from 'api-client'; import React from 'react'; import { Loading } from '..'; import { ConfirmationDialog, ConfirmationDialogProps } from '../confirmation-dialog'; import { PositiveIntField } from '../form-inputs'; -import { serializeTaskRequestLabel } from './task-request-label-utils'; +import { serializeTaskBookingLabel } from './task-booking-label-utils'; // A bunch of manually defined descriptions to avoid using `any`. export interface PatrolTaskDescription { @@ -1292,7 +1292,7 @@ export function CreateTaskForm({ const [taskRequest, setTaskRequest] = React.useState( () => requestTask ?? defaultTask(), ); - const [requestLabel, setRequestLabel] = React.useState({ description: {} }); + const [requestLabel, setRequestLabel] = React.useState({ description: {} }); const [submitting, setSubmitting] = React.useState(false); const [formFullyFilled, setFormFullyFilled] = React.useState(requestTask !== undefined || false); @@ -1550,7 +1550,7 @@ export function CreateTaskForm({ } try { - const labelString = serializeTaskRequestLabel(requestLabel); + const labelString = serializeTaskBookingLabel(requestLabel); if (labelString) { console.log('pushing label: '); console.log(labelString); diff --git a/packages/react-components/lib/tasks/index.ts b/packages/react-components/lib/tasks/index.ts index afe9c0fdb..2214ea0ea 100644 --- a/packages/react-components/lib/tasks/index.ts +++ b/packages/react-components/lib/tasks/index.ts @@ -1,7 +1,7 @@ export * from './create-task'; export * from './task-info'; export * from './task-logs'; -export * from './task-request-label-utils'; +export * from './task-booking-label-utils'; export * from './task-table'; export * from './task-timeline'; export * from './task-table-datagrid'; diff --git a/packages/react-components/lib/tasks/task-request-label-utils.tsx b/packages/react-components/lib/tasks/task-booking-label-utils.tsx similarity index 54% rename from packages/react-components/lib/tasks/task-request-label-utils.tsx rename to packages/react-components/lib/tasks/task-booking-label-utils.tsx index 5269c73b4..e50ff074f 100644 --- a/packages/react-components/lib/tasks/task-request-label-utils.tsx +++ b/packages/react-components/lib/tasks/task-booking-label-utils.tsx @@ -1,6 +1,6 @@ import Ajv from 'ajv'; import schema from 'api-client/dist/schema'; -import type { TaskState, TaskRequestLabel } from 'api-client'; +import type { TaskBookingLabel, TaskState } from 'api-client'; // FIXME: AJV is duplicated here with dashboard, but this is only a temporary // measure until we start using an external validation tool. @@ -10,37 +10,37 @@ Object.entries(schema.components.schemas).forEach(([k, v]) => { ajv.addSchema(v, `#/components/schemas/${k}`); }); -const validateTaskRequestLabel = ajv.compile(schema.components.schemas.TaskRequestLabel); +const validateTaskBookingLabel = ajv.compile(schema.components.schemas.TaskBookingLabel); -export function serializeTaskRequestLabel(label: TaskRequestLabel): string { +export function serializeTaskBookingLabel(label: TaskBookingLabel): string { return JSON.stringify(label); } -export function getTaskRequestLabelFromJsonString( +export function getTaskBookingLabelFromJsonString( jsonString: string, -): TaskRequestLabel | undefined { +): TaskBookingLabel | undefined { try { // Validate first before parsing again into the interface - const validated = validateTaskRequestLabel(JSON.parse(jsonString)); + const validated = validateTaskBookingLabel(JSON.parse(jsonString)); if (validated) { - const parsedLabel: TaskRequestLabel = JSON.parse(jsonString); + const parsedLabel: TaskBookingLabel = JSON.parse(jsonString); return parsedLabel; } } catch (e) { - console.error(`Failed to parse TaskRequestLabel: ${(e as Error).message}`); + console.error(`Failed to parse TaskBookingLabel: ${(e as Error).message}`); return undefined; } - console.error(`Failed to validate TaskRequestLabel`); + console.error(`Failed to validate TaskBookingLabel`); return undefined; } -export function getTaskRequestLabelFromTaskState(taskState: TaskState): TaskRequestLabel | null { - let requestLabel: TaskRequestLabel | null = null; +export function getTaskBookingLabelFromTaskState(taskState: TaskState): TaskBookingLabel | null { + let requestLabel: TaskBookingLabel | null = null; if (taskState.booking.labels) { for (const label of taskState.booking.labels) { try { - const parsedLabel = getTaskRequestLabelFromJsonString(label); + const parsedLabel = getTaskBookingLabelFromJsonString(label); if (parsedLabel) { requestLabel = parsedLabel; break; diff --git a/packages/react-components/lib/tasks/task-table-datagrid.tsx b/packages/react-components/lib/tasks/task-table-datagrid.tsx index b7e3b7a35..d63c3cc12 100644 --- a/packages/react-components/lib/tasks/task-table-datagrid.tsx +++ b/packages/react-components/lib/tasks/task-table-datagrid.tsx @@ -15,7 +15,7 @@ import { styled, Stack, Typography, Tooltip, useMediaQuery, SxProps, Theme } fro import * as React from 'react'; import { TaskState, ApiServerModelsRmfApiTaskStateStatus as Status } from 'api-client'; import { InsertInvitation as ScheduleIcon, Person as UserIcon } from '@mui/icons-material/'; -import { getTaskRequestLabelFromTaskState } from './task-request-label-utils'; +import { getTaskBookingLabelFromTaskState } from './task-booking-label-utils'; const classes = { taskActiveCell: 'MuiDataGrid-cell-active-cell', @@ -184,7 +184,7 @@ export function TaskDataGridTable({ width: 150, editable: false, valueGetter: (params: GridValueGetterParams) => { - const requestLabel = getTaskRequestLabelFromTaskState(params.row); + const requestLabel = getTaskBookingLabelFromTaskState(params.row); if (requestLabel && requestLabel.description.pickup) { return requestLabel.description.pickup; } @@ -200,7 +200,7 @@ export function TaskDataGridTable({ width: 150, editable: false, valueGetter: (params: GridValueGetterParams) => { - const requestLabel = getTaskRequestLabelFromTaskState(params.row); + const requestLabel = getTaskBookingLabelFromTaskState(params.row); if (requestLabel && requestLabel.description.destination) { return requestLabel.description.destination; } From 9fe336dd30c5ff3d0ad5a830bc0b00383429c15f Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 29 Apr 2024 16:53:29 +0800 Subject: [PATCH 20/37] Refactoring and moving ajx instance to react-components Signed-off-by: Aaron Chong --- packages/dashboard/package.json | 1 - packages/dashboard/src/components/tasks/utils.ts | 3 +-- packages/dashboard/src/components/utils.ts | 8 -------- .../lib/tasks/task-booking-label-utils.tsx | 10 +--------- packages/react-components/lib/utils/index.ts | 1 + packages/react-components/lib/utils/schema-utils.ts | 8 ++++++++ pnpm-lock.yaml | 3 --- 7 files changed, 11 insertions(+), 23 deletions(-) create mode 100644 packages/react-components/lib/utils/schema-utils.ts diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index f18b2cc6b..4464f6499 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -59,7 +59,6 @@ "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", "@types/three": "^0.156.0", - "ajv": "^8.10.0", "api-client": "workspace:*", "axios": "^0.21.1", "date-fns": "^2.21.3", diff --git a/packages/dashboard/src/components/tasks/utils.ts b/packages/dashboard/src/components/tasks/utils.ts index 0303c7ff2..2b165ac2e 100644 --- a/packages/dashboard/src/components/tasks/utils.ts +++ b/packages/dashboard/src/components/tasks/utils.ts @@ -1,7 +1,6 @@ import { PostScheduledTaskRequest, TaskRequest, TaskState } from 'api-client'; -import { getTaskBookingLabelFromTaskState, Schedule } from 'react-components'; +import { ajv, getTaskBookingLabelFromTaskState, Schedule } from 'react-components'; import schema from 'api-client/dist/schema'; -import { ajv } from '../utils'; export function parseTasksFile(contents: string): TaskRequest[] { const obj = JSON.parse(contents) as unknown[]; diff --git a/packages/dashboard/src/components/utils.ts b/packages/dashboard/src/components/utils.ts index cdb1d6cb4..ac280c665 100644 --- a/packages/dashboard/src/components/utils.ts +++ b/packages/dashboard/src/components/utils.ts @@ -1,13 +1,5 @@ -import Ajv from 'ajv'; -import schema from 'api-client/schema'; import { AxiosError } from 'axios'; export function getApiErrorMessage(error: unknown): string { return (error as AxiosError).response?.data.detail || ''; } - -export const ajv = new Ajv(); - -Object.entries(schema.components.schemas).forEach(([k, v]) => { - ajv.addSchema(v, `#/components/schemas/${k}`); -}); diff --git a/packages/react-components/lib/tasks/task-booking-label-utils.tsx b/packages/react-components/lib/tasks/task-booking-label-utils.tsx index e50ff074f..bfd4b0150 100644 --- a/packages/react-components/lib/tasks/task-booking-label-utils.tsx +++ b/packages/react-components/lib/tasks/task-booking-label-utils.tsx @@ -1,15 +1,7 @@ -import Ajv from 'ajv'; +import { ajv } from '../utils/schema-utils'; import schema from 'api-client/dist/schema'; import type { TaskBookingLabel, TaskState } from 'api-client'; -// FIXME: AJV is duplicated here with dashboard, but this is only a temporary -// measure until we start using an external validation tool. -const ajv = new Ajv(); - -Object.entries(schema.components.schemas).forEach(([k, v]) => { - ajv.addSchema(v, `#/components/schemas/${k}`); -}); - const validateTaskBookingLabel = ajv.compile(schema.components.schemas.TaskBookingLabel); export function serializeTaskBookingLabel(label: TaskBookingLabel): string { diff --git a/packages/react-components/lib/utils/index.ts b/packages/react-components/lib/utils/index.ts index 237b1f493..754cf481a 100644 --- a/packages/react-components/lib/utils/index.ts +++ b/packages/react-components/lib/utils/index.ts @@ -2,3 +2,4 @@ export * from './geometry'; export * from './health'; export * from './item-table'; export * from './misc'; +export * from './schema-utils'; diff --git a/packages/react-components/lib/utils/schema-utils.ts b/packages/react-components/lib/utils/schema-utils.ts new file mode 100644 index 000000000..e91d91097 --- /dev/null +++ b/packages/react-components/lib/utils/schema-utils.ts @@ -0,0 +1,8 @@ +import Ajv from 'ajv'; +import schema from 'api-client/schema'; + +export const ajv = new Ajv(); + +Object.entries(schema.components.schemas).forEach(([k, v]) => { + ajv.addSchema(v, `#/components/schemas/${k}`); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ef95e963..f9942c907 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,9 +150,6 @@ importers: '@types/three': specifier: ^0.156.0 version: 0.156.0 - ajv: - specifier: ^8.10.0 - version: 8.11.0 api-client: specifier: workspace:* version: link:../api-client From 2327f5b59b607de78e6e3247dd5f65cbcfc9c466 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 29 Apr 2024 17:05:07 +0800 Subject: [PATCH 21/37] Updated migration scipt to use TaskBookingLabel and task_name Signed-off-by: Aaron Chong --- .../api-server/migrations/migrate_db_912.py | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/api-server/migrations/migrate_db_912.py b/packages/api-server/migrations/migrate_db_912.py index b4faf17de..710a9327b 100644 --- a/packages/api-server/migrations/migrate_db_912.py +++ b/packages/api-server/migrations/migrate_db_912.py @@ -6,12 +6,12 @@ import api_server.models.tortoise_models as ttm from api_server.app_config import app_config, load_config -from api_server.models import TaskRequest, TaskRequestLabel, TaskState +from api_server.models import TaskBookingLabel, TaskRequest, TaskState -# NOTE: This script is for migrating TaskState in an existing database to work -# with https://github.com/open-rmf/rmf-web/pull/912. +# NOTE: This script is for migrating TaskState and ScheduledTask in an existing +# database to work with https://github.com/open-rmf/rmf-web/pull/912. # Before migration: -# - Pickup, destination, cart ID, task identifier information will be unavailable +# - Pickup, destination, cart ID, task name information will be unavailable # on the Task Queue Table on the dashboard, as we no longer gather those # fields from the TaskRequest # After migration: @@ -19,10 +19,10 @@ # dependent on TaskRequest to fill out those fields. It gathers those fields # from the json string in TaskState.booking.labels. # This script performs the following: -# - Construct TaskRequestLabel from its TaskRequest if it is available. +# - Construct TaskBookingLabel from its TaskRequest if it is available. # - Update the respective TaskState.data json TaskState.booking.labels field -# with the newly constructed TaskRequestLabel json string. -# - Update ScheduledTask to use labels too +# with the newly constructed TaskBookingLabel json string. +# - Update the requests in ScheduledTask to use labels too app_config = load_config( @@ -33,13 +33,13 @@ ) -def parse_task_identifier(task_request: TaskRequest) -> Optional[str]: - identifier = None +def parse_task_name(task_request: TaskRequest) -> Optional[str]: + name = None if task_request.category.lower() == "patrol": - identifier = "Patrol" + name = "Patrol" elif task_request.description and task_request.description["category"]: - identifier = task_request.description["category"] - return identifier + name = task_request.description["category"] + return name def parse_pickup(task_request: TaskRequest) -> Optional[str]: @@ -161,18 +161,18 @@ async def migrate(): continue request_model = TaskRequest(**request.request) - # Construct TaskRequestLabel based on TaskRequest + # Construct TaskBookingLabel based on TaskRequest pickup = parse_pickup(request_model) destination = parse_destination(request_model) - # If the task identifier could not be parsed, we skip migrating this TaskState - task_identifier = parse_task_identifier(request_model) - if task_identifier is None: + # If the task name could not be parsed, we skip migrating this TaskState + task_name = parse_task_name(request_model) + if task_name is None: failed_task_states_count += 1 continue - label = TaskRequestLabel( - task_identifier=task_identifier, + label = TaskBookingLabel( + task_name=task_name, unix_millis_warn_time=None, pickup=pickup, destination=destination, @@ -211,16 +211,16 @@ async def migrate(): ) print(task_request) - task_identifier = parse_task_identifier(task_request) - if task_identifier is None: + task_name = parse_task_name(task_request) + if task_name is None: failed_scheduled_task_count += 1 continue - # Construct TaskRequestLabel based on TaskRequest + # Construct TaskBookingLabel based on TaskRequest pickup = parse_pickup(task_request) destination = parse_destination(task_request) - label = TaskRequestLabel( - task_identifier=task_identifier, + label = TaskBookingLabel( + task_name=task_name, unix_millis_warn_time=None, pickup=pickup, destination=destination, From 71f5de97526bbbcf259d64ba86bde64f986247b4 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 29 Apr 2024 19:08:19 +0800 Subject: [PATCH 22/37] Handles editing scheduled task Signed-off-by: Aaron Chong --- .../lib/tasks/create-task.tsx | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 7dcf83486..0ff22f8eb 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -47,7 +47,10 @@ import React from 'react'; import { Loading } from '..'; import { ConfirmationDialog, ConfirmationDialogProps } from '../confirmation-dialog'; import { PositiveIntField } from '../form-inputs'; -import { serializeTaskBookingLabel } from './task-booking-label-utils'; +import { + getTaskBookingLabelFromJsonString, + serializeTaskBookingLabel, +} from './task-booking-label-utils'; // A bunch of manually defined descriptions to avoid using `any`. export interface PatrolTaskDescription { @@ -1292,7 +1295,18 @@ export function CreateTaskForm({ const [taskRequest, setTaskRequest] = React.useState( () => requestTask ?? defaultTask(), ); - const [requestLabel, setRequestLabel] = React.useState({ description: {} }); + + let bookingLabel: TaskBookingLabel = { description: {} }; + if (requestTask && requestTask.labels) { + for (const label of requestTask.labels) { + const parsedLabel = getTaskBookingLabelFromJsonString(label); + if (parsedLabel) { + bookingLabel = parsedLabel; + } + } + } + const [requestBookingLabel, setRequestBookingLabel] = + React.useState(bookingLabel); const [submitting, setSubmitting] = React.useState(false); const [formFullyFilled, setFormFullyFilled] = React.useState(requestTask !== undefined || false); @@ -1366,7 +1380,7 @@ export function CreateTaskForm({ patrolWaypoints={patrolWaypoints} onChange={(desc) => { handleTaskDescriptionChange('patrol', desc); - setRequestLabel((prev) => { + setRequestBookingLabel((prev) => { return { description: { ...prev.description, @@ -1385,7 +1399,7 @@ export function CreateTaskForm({ taskDesc={taskRequest.description as CustomComposeTaskDescription} onChange={(desc) => { handleCustomComposeTaskDescriptionChange(desc); - setRequestLabel((prev) => { + setRequestBookingLabel((prev) => { return { description: { ...prev.description, @@ -1414,7 +1428,7 @@ export function CreateTaskForm({ handleTaskDescriptionChange('compose', desc); const pickupPerformAction = desc.phases[0].activity.description.activities[1].description.description; - setRequestLabel((prev) => { + setRequestBookingLabel((prev) => { return { description: { ...prev.description, @@ -1444,7 +1458,7 @@ export function CreateTaskForm({ handleTaskDescriptionChange('compose', desc); const pickupPerformAction = desc.phases[0].activity.description.activities[1].description.description; - setRequestLabel((prev) => { + setRequestBookingLabel((prev) => { return { description: { ...prev.description, @@ -1466,6 +1480,7 @@ export function CreateTaskForm({ const handleTaskTypeChange = (ev: React.ChangeEvent) => { const newType = ev.target.value; setTaskType(newType); + setRequestBookingLabel({ description: { task_name: newType } }); if (newType === 'custom_compose') { taskRequest.category = 'custom_compose'; @@ -1550,15 +1565,9 @@ export function CreateTaskForm({ } try { - const labelString = serializeTaskBookingLabel(requestLabel); + const labelString = serializeTaskBookingLabel(requestBookingLabel); if (labelString) { - console.log('pushing label: '); - console.log(labelString); - if (request.labels) { - request.labels.push(labelString); - } else { - request.labels = [labelString]; - } + request.labels = [labelString]; } } catch (e) { console.error('Failed to generate string for task request label'); From bf45cf1944891330fe6c663aaa5bcd81f58ee8fa Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Tue, 30 Apr 2024 13:11:19 +0800 Subject: [PATCH 23/37] clean up task favorite models Signed-off-by: Aaron Chong --- packages/api-client/lib/openapi/api.ts | 120 +++++------------- packages/api-client/lib/version.ts | 2 +- packages/api-client/schema/index.ts | 37 +----- .../api-server/api_server/models/__init__.py | 1 + .../api_server/models/task_favorite.py | 14 ++ .../models/tortoise_models/__init__.py | 1 - .../models/tortoise_models/tasks.py | 5 +- .../api_server/routes/tasks/favorite_tasks.py | 23 +--- packages/dashboard/src/components/appbar.tsx | 2 +- .../lib/tasks/create-task.tsx | 8 +- 10 files changed, 67 insertions(+), 146 deletions(-) create mode 100644 packages/api-server/api_server/models/task_favorite.py diff --git a/packages/api-client/lib/openapi/api.ts b/packages/api-client/lib/openapi/api.ts index d562cd848..cda5cff5b 100644 --- a/packages/api-client/lib/openapi/api.ts +++ b/packages/api-client/lib/openapi/api.ts @@ -431,55 +431,6 @@ export interface ApiServerModelsTortoiseModelsScheduledTaskScheduledTaskSchedule */ at?: string | null; } -/** - * - * @export - * @interface ApiServerModelsTortoiseModelsTasksTaskFavoriteLeaf - */ -export interface ApiServerModelsTortoiseModelsTasksTaskFavoriteLeaf { - /** - * - * @type {string} - * @memberof ApiServerModelsTortoiseModelsTasksTaskFavoriteLeaf - */ - id: string; - /** - * - * @type {string} - * @memberof ApiServerModelsTortoiseModelsTasksTaskFavoriteLeaf - */ - name: string; - /** - * - * @type {string} - * @memberof ApiServerModelsTortoiseModelsTasksTaskFavoriteLeaf - */ - unix_millis_earliest_start_time?: string | null; - /** - * - * @type {any} - * @memberof ApiServerModelsTortoiseModelsTasksTaskFavoriteLeaf - */ - priority?: any; - /** - * - * @type {string} - * @memberof ApiServerModelsTortoiseModelsTasksTaskFavoriteLeaf - */ - category: string; - /** - * - * @type {any} - * @memberof ApiServerModelsTortoiseModelsTasksTaskFavoriteLeaf - */ - description?: any; - /** - * - * @type {string} - * @memberof ApiServerModelsTortoiseModelsTasksTaskFavoriteLeaf - */ - user: string; -} /** * Which agent (robot) is the task assigned to * @export @@ -2657,51 +2608,57 @@ export interface TaskEventLog { /** * * @export - * @interface TaskFavoritePydantic + * @interface TaskFavorite */ -export interface TaskFavoritePydantic { +export interface TaskFavorite { /** * * @type {string} - * @memberof TaskFavoritePydantic + * @memberof TaskFavorite */ id: string; /** * * @type {string} - * @memberof TaskFavoritePydantic + * @memberof TaskFavorite */ name: string; /** * * @type {number} - * @memberof TaskFavoritePydantic + * @memberof TaskFavorite */ unix_millis_earliest_start_time: number; /** * * @type {object} - * @memberof TaskFavoritePydantic + * @memberof TaskFavorite */ priority?: object; /** * * @type {string} - * @memberof TaskFavoritePydantic + * @memberof TaskFavorite */ category: string; /** * * @type {object} - * @memberof TaskFavoritePydantic + * @memberof TaskFavorite */ description?: object; /** * * @type {string} - * @memberof TaskFavoritePydantic + * @memberof TaskFavorite */ user: string; + /** + * + * @type {Array} + * @memberof TaskFavorite + */ + labels?: Array; } /** * @@ -8885,20 +8842,16 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration /** * * @summary Post Favorite Task - * @param {TaskFavoritePydantic} taskFavoritePydantic + * @param {TaskFavorite} taskFavorite * @param {*} [options] Override http request option. * @throws {RequiredError} */ postFavoriteTaskFavoriteTasksPost: async ( - taskFavoritePydantic: TaskFavoritePydantic, + taskFavorite: TaskFavorite, options: AxiosRequestConfig = {}, ): Promise => { - // verify required parameter 'taskFavoritePydantic' is not null or undefined - assertParamExists( - 'postFavoriteTaskFavoriteTasksPost', - 'taskFavoritePydantic', - taskFavoritePydantic, - ); + // verify required parameter 'taskFavorite' is not null or undefined + assertParamExists('postFavoriteTaskFavoriteTasksPost', 'taskFavorite', taskFavorite); const localVarPath = `/favorite_tasks`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -8921,7 +8874,7 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration ...options.headers, }; localVarRequestOptions.data = serializeDataIfNeeded( - taskFavoritePydantic, + taskFavorite, localVarRequestOptions, configuration, ); @@ -9654,9 +9607,7 @@ export const TasksApiFp = function (configuration?: Configuration) { */ async getFavoritesTasksFavoriteTasksGet( options?: AxiosRequestConfig, - ): Promise< - (axios?: AxiosInstance, basePath?: string) => AxiosPromise> - > { + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.getFavoritesTasksFavoriteTasksGet( options, ); @@ -9846,21 +9797,16 @@ export const TasksApiFp = function (configuration?: Configuration) { /** * * @summary Post Favorite Task - * @param {TaskFavoritePydantic} taskFavoritePydantic + * @param {TaskFavorite} taskFavorite * @param {*} [options] Override http request option. * @throws {RequiredError} */ async postFavoriteTaskFavoriteTasksPost( - taskFavoritePydantic: TaskFavoritePydantic, + taskFavorite: TaskFavorite, options?: AxiosRequestConfig, - ): Promise< - ( - axios?: AxiosInstance, - basePath?: string, - ) => AxiosPromise - > { + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.postFavoriteTaskFavoriteTasksPost( - taskFavoritePydantic, + taskFavorite, options, ); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); @@ -10191,7 +10137,7 @@ export const TasksApiFactory = function ( * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getFavoritesTasksFavoriteTasksGet(options?: any): AxiosPromise> { + getFavoritesTasksFavoriteTasksGet(options?: any): AxiosPromise> { return localVarFp .getFavoritesTasksFavoriteTasksGet(options) .then((request) => request(axios, basePath)); @@ -10345,16 +10291,16 @@ export const TasksApiFactory = function ( /** * * @summary Post Favorite Task - * @param {TaskFavoritePydantic} taskFavoritePydantic + * @param {TaskFavorite} taskFavorite * @param {*} [options] Override http request option. * @throws {RequiredError} */ postFavoriteTaskFavoriteTasksPost( - taskFavoritePydantic: TaskFavoritePydantic, + taskFavorite: TaskFavorite, options?: any, - ): AxiosPromise { + ): AxiosPromise { return localVarFp - .postFavoriteTaskFavoriteTasksPost(taskFavoritePydantic, options) + .postFavoriteTaskFavoriteTasksPost(taskFavorite, options) .then((request) => request(axios, basePath)); }, /** @@ -10815,17 +10761,17 @@ export class TasksApi extends BaseAPI { /** * * @summary Post Favorite Task - * @param {TaskFavoritePydantic} taskFavoritePydantic + * @param {TaskFavorite} taskFavorite * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof TasksApi */ public postFavoriteTaskFavoriteTasksPost( - taskFavoritePydantic: TaskFavoritePydantic, + taskFavorite: TaskFavorite, options?: AxiosRequestConfig, ) { return TasksApiFp(this.configuration) - .postFavoriteTaskFavoriteTasksPost(taskFavoritePydantic, options) + .postFavoriteTaskFavoriteTasksPost(taskFavorite, options) .then((request) => request(this.axios, this.basePath)); } diff --git a/packages/api-client/lib/version.ts b/packages/api-client/lib/version.ts index fa3fd8f02..9fa05261e 100644 --- a/packages/api-client/lib/version.ts +++ b/packages/api-client/lib/version.ts @@ -3,6 +3,6 @@ import { version as rmfModelVer } from 'rmf-models'; export const version = { rmfModels: rmfModelVer, - rmfServer: 'e78c8685138183b4743776c71ff2c1d990ea52f5', + rmfServer: '4c9eb01b568dd3b5e98d46fd3571d2e5b173179e', openapiGenerator: '6.2.1', }; diff --git a/packages/api-client/schema/index.ts b/packages/api-client/schema/index.ts index 26d54df74..92f72e087 100644 --- a/packages/api-client/schema/index.ts +++ b/packages/api-client/schema/index.ts @@ -1552,7 +1552,7 @@ export default { schema: { title: 'Response Get Favorites Tasks Favorite Tasks Get', type: 'array', - items: { $ref: '#/components/schemas/TaskFavoritePydantic' }, + items: { $ref: '#/components/schemas/TaskFavorite' }, }, }, }, @@ -1565,20 +1565,14 @@ export default { operationId: 'post_favorite_task_favorite_tasks_post', requestBody: { content: { - 'application/json': { schema: { $ref: '#/components/schemas/TaskFavoritePydantic' } }, + 'application/json': { schema: { $ref: '#/components/schemas/TaskFavorite' } }, }, required: true, }, responses: { '200': { description: 'Successful Response', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/api_server.models.tortoise_models.tasks.TaskFavorite.leaf', - }, - }, - }, + content: { 'application/json': { schema: {} } }, }, '422': { description: 'Validation Error', @@ -3734,8 +3728,8 @@ export default { }, additionalProperties: false, }, - TaskFavoritePydantic: { - title: 'TaskFavoritePydantic', + TaskFavorite: { + title: 'TaskFavorite', required: ['id', 'name', 'unix_millis_earliest_start_time', 'category', 'user'], type: 'object', properties: { @@ -3749,6 +3743,7 @@ export default { category: { title: 'Category', type: 'string' }, description: { title: 'Description', type: 'object' }, user: { title: 'User', type: 'string' }, + labels: { title: 'Labels', type: 'array', items: { type: 'string' } }, }, }, TaskInterruptionRequest: { @@ -4263,26 +4258,6 @@ export default { $ref: '#/components/schemas/api_server.models.tortoise_models.scheduled_task.ScheduledTask', }, }, - 'api_server.models.tortoise_models.tasks.TaskFavorite.leaf': { - title: 'TaskFavorite', - required: ['id', 'name', 'category', 'user'], - type: 'object', - properties: { - id: { title: 'Id', maxLength: 255, type: 'string' }, - name: { title: 'Name', maxLength: 255, type: 'string' }, - unix_millis_earliest_start_time: { - title: 'Unix Millis Earliest Start Time', - type: 'string', - format: 'date-time', - nullable: true, - }, - priority: { title: 'Priority' }, - category: { title: 'Category', maxLength: 255, type: 'string' }, - description: { title: 'Description' }, - user: { title: 'User', maxLength: 255, type: 'string' }, - }, - additionalProperties: false, - }, api_server__models__delivery_alerts__DeliveryAlert__Category: { title: 'Category', enum: ['missing', 'wrong', 'obstructed', 'cancelled'], diff --git a/packages/api-server/api_server/models/__init__.py b/packages/api-server/api_server/models/__init__.py index db2e63956..238fdf04b 100644 --- a/packages/api-server/api_server/models/__init__.py +++ b/packages/api-server/api_server/models/__init__.py @@ -51,4 +51,5 @@ from .rmf_api.undo_skip_phase_request import UndoPhaseSkipRequest from .rmf_api.undo_skip_phase_response import UndoPhaseSkipResponse from .task_booking_label import * +from .task_favorite import * from .user import * diff --git a/packages/api-server/api_server/models/task_favorite.py b/packages/api-server/api_server/models/task_favorite.py new file mode 100644 index 000000000..2714954fd --- /dev/null +++ b/packages/api-server/api_server/models/task_favorite.py @@ -0,0 +1,14 @@ +from typing import Dict, List + +from pydantic import BaseModel + + +class TaskFavorite(BaseModel): + id: str + name: str + unix_millis_earliest_start_time: int + priority: Dict | None + category: str + description: Dict | None + user: str + labels: List[str] | None diff --git a/packages/api-server/api_server/models/tortoise_models/__init__.py b/packages/api-server/api_server/models/tortoise_models/__init__.py index 301bed861..0028a5e78 100644 --- a/packages/api-server/api_server/models/tortoise_models/__init__.py +++ b/packages/api-server/api_server/models/tortoise_models/__init__.py @@ -26,7 +26,6 @@ TaskEventLogPhasesEventsLog, TaskEventLogPhasesLog, TaskFavorite, - TaskFavoritePydantic, TaskRequest, TaskState, ) diff --git a/packages/api-server/api_server/models/tortoise_models/tasks.py b/packages/api-server/api_server/models/tortoise_models/tasks.py index f36ac3fbc..d0156e8bb 100644 --- a/packages/api-server/api_server/models/tortoise_models/tasks.py +++ b/packages/api-server/api_server/models/tortoise_models/tasks.py @@ -1,4 +1,3 @@ -from tortoise.contrib.pydantic.creator import pydantic_model_creator from tortoise.fields import ( CharField, DatetimeField, @@ -80,6 +79,4 @@ class TaskFavorite(Model): category = CharField(255, null=False, index=True) description = JSONField() user = CharField(255, null=False, index=True) - - -TaskFavoritePydantic = pydantic_model_creator(TaskFavorite) + labels = JSONField(null=True, default=list) diff --git a/packages/api-server/api_server/routes/tasks/favorite_tasks.py b/packages/api-server/api_server/routes/tasks/favorite_tasks.py index d3dcabef5..42ca52271 100644 --- a/packages/api-server/api_server/routes/tasks/favorite_tasks.py +++ b/packages/api-server/api_server/routes/tasks/favorite_tasks.py @@ -3,30 +3,19 @@ from typing import Dict, List from fastapi import Depends, HTTPException -from pydantic import BaseModel from tortoise.exceptions import IntegrityError from api_server.authenticator import user_dep from api_server.fast_io import FastIORouter -from api_server.models import User +from api_server.models import TaskFavorite, User from api_server.models import tortoise_models as ttm router = FastIORouter(tags=["Tasks"]) -class TaskFavoritePydantic(BaseModel): - id: str - name: str - unix_millis_earliest_start_time: int - priority: Dict | None - category: str - description: Dict | None - user: str - - -@router.post("", response_model=ttm.TaskFavoritePydantic) +@router.post("") async def post_favorite_task( - request: TaskFavoritePydantic, + request: TaskFavorite, user: User = Depends(user_dep), ): try: @@ -40,6 +29,7 @@ async def post_favorite_task( "category": request.category, "description": request.description if request.description else None, "user": user.username, + "labels": request.labels, }, id=request.id if request.id != "" else uuid.uuid4(), ) @@ -47,13 +37,13 @@ async def post_favorite_task( raise HTTPException(422, str(e)) from e -@router.get("", response_model=List[TaskFavoritePydantic]) +@router.get("", response_model=List[TaskFavorite]) async def get_favorites_tasks( user: User = Depends(user_dep), ): favorites_tasks = await ttm.TaskFavorite.filter(user=user.username) return [ - TaskFavoritePydantic( + TaskFavorite( id=favorite_task.id, name=favorite_task.name, unix_millis_earliest_start_time=int( @@ -65,6 +55,7 @@ async def get_favorites_tasks( if favorite_task.description else None, user=user.username, + labels=favorite_task.labels, ) for favorite_task in favorites_tasks ] diff --git a/packages/dashboard/src/components/appbar.tsx b/packages/dashboard/src/components/appbar.tsx index df0a51218..fb5514703 100644 --- a/packages/dashboard/src/components/appbar.tsx +++ b/packages/dashboard/src/components/appbar.tsx @@ -31,7 +31,7 @@ import { } from '@mui/material'; import { ApiServerModelsTortoiseModelsAlertsAlertLeaf as Alert, - TaskFavoritePydantic as TaskFavorite, + TaskFavorite, TaskRequest, } from 'api-client'; import React from 'react'; diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 0ff22f8eb..4e2a2b32a 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -38,11 +38,7 @@ import { useTheme, } from '@mui/material'; import { DatePicker, TimePicker, DateTimePicker } from '@mui/x-date-pickers'; -import type { - TaskBookingLabel, - TaskFavoritePydantic as TaskFavorite, - TaskRequest, -} from 'api-client'; +import type { TaskBookingLabel, TaskFavorite, TaskRequest } from 'api-client'; import React from 'react'; import { Loading } from '..'; import { ConfirmationDialog, ConfirmationDialogProps } from '../confirmation-dialog'; @@ -1216,6 +1212,7 @@ const defaultFavoriteTask = (): TaskFavorite => { unix_millis_earliest_start_time: 0, priority: { type: 'binary', value: 0 }, user: '', + labels: [], }; }; @@ -1569,6 +1566,7 @@ export function CreateTaskForm({ if (labelString) { request.labels = [labelString]; } + console.log(`labels: ${request.labels}`); } catch (e) { console.error('Failed to generate string for task request label'); } From 37edc10c474a8c7839acff473f3f8197c5db9b90 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Tue, 30 Apr 2024 16:29:27 +0800 Subject: [PATCH 24/37] Adding label to favorite task before saving, setting booking label state when fav task is clicked Signed-off-by: Aaron Chong --- .../react-components/lib/tasks/create-task.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 4e2a2b32a..f6ec34a5f 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -1616,7 +1616,11 @@ export function CreateTaskForm({ } try { setSavingFavoriteTask(true); - await submitFavoriteTask(favoriteTaskBuffer); + + const favoriteTask = favoriteTaskBuffer; + favoriteTask.labels = [serializeTaskBookingLabel(requestBookingLabel)]; + + await submitFavoriteTask(favoriteTask); setSavingFavoriteTask(false); onSuccessFavoriteTask && onSuccessFavoriteTask( @@ -1696,6 +1700,16 @@ export function CreateTaskForm({ unix_millis_earliest_start_time: 0, priority: favoriteTask.priority, }); + let bookingLabel: TaskBookingLabel = { description: {} }; + if (favoriteTask.labels) { + for (const label of favoriteTask.labels) { + const parsedLabel = getTaskBookingLabelFromJsonString(label); + if (parsedLabel) { + bookingLabel = parsedLabel; + } + } + } + setRequestBookingLabel(bookingLabel); }} /> ); From f872e2280793a5bfd6581ed37c3d4bde7e9ed1f9 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Tue, 30 Apr 2024 16:39:09 +0800 Subject: [PATCH 25/37] Lint Signed-off-by: Aaron Chong --- .../api-server/api_server/routes/tasks/favorite_tasks.py | 2 +- packages/api-server/api_server/test/test_data.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/api-server/api_server/routes/tasks/favorite_tasks.py b/packages/api-server/api_server/routes/tasks/favorite_tasks.py index 42ca52271..6af718d30 100644 --- a/packages/api-server/api_server/routes/tasks/favorite_tasks.py +++ b/packages/api-server/api_server/routes/tasks/favorite_tasks.py @@ -1,6 +1,6 @@ import uuid from datetime import datetime -from typing import Dict, List +from typing import List from fastapi import Depends, HTTPException from tortoise.exceptions import IntegrityError diff --git a/packages/api-server/api_server/test/test_data.py b/packages/api-server/api_server/test/test_data.py index 21b028a9d..6ac37a302 100644 --- a/packages/api-server/api_server/test/test_data.py +++ b/packages/api-server/api_server/test/test_data.py @@ -25,9 +25,9 @@ TaskBookingLabel, TaskBookingLabelDescription, TaskEventLog, + TaskFavorite, TaskState, ) -from api_server.models import tortoise_models as ttm def make_door(name: str = "test_door") -> Door: @@ -450,7 +450,7 @@ def make_task_state(task_id: str = "test_task") -> TaskState: def make_task_favorite( favorite_task_id: str = "default_id", -) -> ttm.TaskFavoritePydantic: +) -> TaskFavorite: sample_favorite_task = json.loads( """ { @@ -479,7 +479,7 @@ def make_task_favorite( """ ) sample_favorite_task["id"] = favorite_task_id - return ttm.TaskFavoritePydantic(**sample_favorite_task) + return TaskFavorite(**sample_favorite_task) def make_task_log(task_id: str) -> TaskEventLog: From 6e3c9301f2d3c5f44ae8f08aad9d00d8ba60749d Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 6 May 2024 13:23:48 +0800 Subject: [PATCH 26/37] Added notes about TaskFavorite in migration script Signed-off-by: Aaron Chong --- packages/api-server/migrations/migrate_db_912.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/api-server/migrations/migrate_db_912.py b/packages/api-server/migrations/migrate_db_912.py index 710a9327b..d82fce227 100644 --- a/packages/api-server/migrations/migrate_db_912.py +++ b/packages/api-server/migrations/migrate_db_912.py @@ -23,6 +23,8 @@ # - Update the respective TaskState.data json TaskState.booking.labels field # with the newly constructed TaskBookingLabel json string. # - Update the requests in ScheduledTask to use labels too +# - TaskFavorite will not be migrated as the database model was not able to +# support it until rmf-web#912 app_config = load_config( From 24ddad501a37223a9fdb3720e1a1c9cd80cf1999 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 6 May 2024 15:38:17 +0800 Subject: [PATCH 27/37] Description of TaskBookingLabel Signed-off-by: Aaron Chong --- packages/api-client/lib/openapi/api.ts | 4 ++-- packages/api-client/lib/version.ts | 2 +- packages/api-client/schema/index.ts | 4 ++++ .../api_server/models/task_booking_label.py | 13 +++++++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/api-client/lib/openapi/api.ts b/packages/api-client/lib/openapi/api.ts index cda5cff5b..6afd8bdac 100644 --- a/packages/api-client/lib/openapi/api.ts +++ b/packages/api-client/lib/openapi/api.ts @@ -2377,7 +2377,7 @@ export interface Task { description_schema?: object; } /** - * + * This label is to be populated by any frontend during a task dispatch, by being added to TaskRequest.labels, which in turn populates TaskState.booking.labels, and can be used to display relevant information needed for any frontends. * @export * @interface TaskBookingLabel */ @@ -2390,7 +2390,7 @@ export interface TaskBookingLabel { description: TaskBookingLabelDescription; } /** - * + * This description holds several fields that could be useful for frontend dashboards when dispatching a task, to then be identified or rendered accordingly back on the same frontend. * @export * @interface TaskBookingLabelDescription */ diff --git a/packages/api-client/lib/version.ts b/packages/api-client/lib/version.ts index d9d3dac35..6b31a1d02 100644 --- a/packages/api-client/lib/version.ts +++ b/packages/api-client/lib/version.ts @@ -3,6 +3,6 @@ import { version as rmfModelVer } from 'rmf-models'; export const version = { rmfModels: rmfModelVer, - rmfServer: '6e3c9301f2d3c5f44ae8f08aad9d00d8ba60749d', + rmfServer: '3f9a874f93b41160098ebccd780c237e07589011', openapiGenerator: '6.2.1', }; diff --git a/packages/api-client/schema/index.ts b/packages/api-client/schema/index.ts index 92f72e087..293b094de 100644 --- a/packages/api-client/schema/index.ts +++ b/packages/api-client/schema/index.ts @@ -3633,6 +3633,8 @@ export default { required: ['description'], type: 'object', properties: { description: { $ref: '#/components/schemas/TaskBookingLabelDescription' } }, + description: + 'This label is to be populated by any frontend during a task dispatch, by\nbeing added to TaskRequest.labels, which in turn populates\nTaskState.booking.labels, and can be used to display relevant information\nneeded for any frontends.', }, TaskBookingLabelDescription: { title: 'TaskBookingLabelDescription', @@ -3644,6 +3646,8 @@ export default { destination: { title: 'Destination', type: 'string' }, cart_id: { title: 'Cart Id', type: 'string' }, }, + description: + 'This description holds several fields that could be useful for frontend\ndashboards when dispatching a task, to then be identified or rendered\naccordingly back on the same frontend.', }, TaskCancelResponse: { title: 'TaskCancelResponse', diff --git a/packages/api-server/api_server/models/task_booking_label.py b/packages/api-server/api_server/models/task_booking_label.py index 36e4abb44..8aa36b79f 100644 --- a/packages/api-server/api_server/models/task_booking_label.py +++ b/packages/api-server/api_server/models/task_booking_label.py @@ -8,6 +8,12 @@ class TaskBookingLabelDescription(BaseModel): + """ + This description holds several fields that could be useful for frontend + dashboards when dispatching a task, to then be identified or rendered + accordingly back on the same frontend. + """ + task_name: Optional[str] unix_millis_warn_time: Optional[int] pickup: Optional[str] @@ -23,6 +29,13 @@ def from_json_string(json_str: str) -> Optional["TaskBookingLabelDescription"]: class TaskBookingLabel(BaseModel): + """ + This label is to be populated by any frontend during a task dispatch, by + being added to TaskRequest.labels, which in turn populates + TaskState.booking.labels, and can be used to display relevant information + needed for any frontends. + """ + description: TaskBookingLabelDescription @staticmethod From 6c8dcf2f35cbb16350df4bef91c8c02d7f435d46 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Tue, 7 May 2024 15:44:50 +0800 Subject: [PATCH 28/37] Fix migration script Signed-off-by: Aaron Chong --- .../api-server/migrations/migrate_db_912.py | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/packages/api-server/migrations/migrate_db_912.py b/packages/api-server/migrations/migrate_db_912.py index d82fce227..37702ce4b 100644 --- a/packages/api-server/migrations/migrate_db_912.py +++ b/packages/api-server/migrations/migrate_db_912.py @@ -6,7 +6,12 @@ import api_server.models.tortoise_models as ttm from api_server.app_config import app_config, load_config -from api_server.models import TaskBookingLabel, TaskRequest, TaskState +from api_server.models import ( + TaskBookingLabel, + TaskBookingLabelDescription, + TaskRequest, + TaskState, +) # NOTE: This script is for migrating TaskState and ScheduledTask in an existing # database to work with https://github.com/open-rmf/rmf-web/pull/912. @@ -173,25 +178,30 @@ async def migrate(): failed_task_states_count += 1 continue - label = TaskBookingLabel( + label_description = TaskBookingLabelDescription( task_name=task_name, unix_millis_warn_time=None, pickup=pickup, destination=destination, cart_id=parse_cart_id(request_model), ) - print(label) + label = TaskBookingLabel(description=label_description) + # print(label) # Update data json if state_model.booking.labels is None: - state_model.booking.labels = [label.json()] + state_model.booking.labels = [ + label.json(exclude_none=True, separators=(",", ":")) + ] else: - state_model.booking.labels.append(label.json()) - print(state_model) + state_model.booking.labels.append( + label.json(exclude_none=True, separators=(",", ":")) + ) + # print(state_model) state.update_from_dict( { - "data": state_model.json(), + "data": state_model.json(exclude_none=True, separators=(",", ":")), "pickup": pickup, "destination": destination, } @@ -211,7 +221,7 @@ async def migrate(): task_request = TaskRequest( **scheduled_task_model.task_request # pyright: ignore[reportGeneralTypeIssues] ) - print(task_request) + # print(task_request) task_name = parse_task_name(task_request) if task_name is None: @@ -221,24 +231,33 @@ async def migrate(): # Construct TaskBookingLabel based on TaskRequest pickup = parse_pickup(task_request) destination = parse_destination(task_request) - label = TaskBookingLabel( + label_description = TaskBookingLabelDescription( task_name=task_name, unix_millis_warn_time=None, pickup=pickup, destination=destination, cart_id=parse_cart_id(task_request), ) - print(label) + label = TaskBookingLabel(description=label_description) + # print(label) # Update TaskRequest if task_request.labels is None: - task_request.labels = [label.json()] + task_request.labels = [label.json(exclude_none=True, separators=(",", ":"))] else: - task_request.labels.append(label.json()) - print(task_request) + task_request.labels.append( + label.json(exclude_none=True, separators=(",", ":")) + ) + # print(task_request) # Update ScheduledTask - scheduled_task.update_from_dict({"task_request": task_request.json()}) + scheduled_task.update_from_dict( + { + "task_request": task_request.json( + exclude_none=True, separators=(",", ":") + ) + } + ) await scheduled_task.save() await Tortoise.close_connections() From 963092f421e430f5c30d27b0be43d38c9b1e9222 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Thu, 9 May 2024 13:54:40 +0800 Subject: [PATCH 29/37] task_definition_id for TaskFavorite Signed-off-by: Aaron Chong --- packages/api-client/lib/openapi/api.ts | 4 ++-- packages/api-client/lib/version.ts | 2 +- packages/api-client/schema/index.ts | 11 ++++++++-- .../api_server/models/task_favorite.py | 4 ++-- .../models/tortoise_models/tasks.py | 2 +- .../api_server/routes/tasks/favorite_tasks.py | 20 ++++++++++--------- .../api-server/api_server/test/test_data.py | 3 ++- .../api_server/test/test_fixtures.py | 1 + 8 files changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/api-client/lib/openapi/api.ts b/packages/api-client/lib/openapi/api.ts index 6afd8bdac..beaf6cad0 100644 --- a/packages/api-client/lib/openapi/api.ts +++ b/packages/api-client/lib/openapi/api.ts @@ -2655,10 +2655,10 @@ export interface TaskFavorite { user: string; /** * - * @type {Array} + * @type {string} * @memberof TaskFavorite */ - labels?: Array; + task_definition_id: string; } /** * diff --git a/packages/api-client/lib/version.ts b/packages/api-client/lib/version.ts index 6b31a1d02..15ba780f3 100644 --- a/packages/api-client/lib/version.ts +++ b/packages/api-client/lib/version.ts @@ -3,6 +3,6 @@ import { version as rmfModelVer } from 'rmf-models'; export const version = { rmfModels: rmfModelVer, - rmfServer: '3f9a874f93b41160098ebccd780c237e07589011', + rmfServer: '7cdc0df3da518c2f6d4b9e5b6eb0ef756d2f8f73', openapiGenerator: '6.2.1', }; diff --git a/packages/api-client/schema/index.ts b/packages/api-client/schema/index.ts index 293b094de..33c09bbdb 100644 --- a/packages/api-client/schema/index.ts +++ b/packages/api-client/schema/index.ts @@ -3734,7 +3734,14 @@ export default { }, TaskFavorite: { title: 'TaskFavorite', - required: ['id', 'name', 'unix_millis_earliest_start_time', 'category', 'user'], + required: [ + 'id', + 'name', + 'unix_millis_earliest_start_time', + 'category', + 'user', + 'task_definition_id', + ], type: 'object', properties: { id: { title: 'Id', type: 'string' }, @@ -3747,7 +3754,7 @@ export default { category: { title: 'Category', type: 'string' }, description: { title: 'Description', type: 'object' }, user: { title: 'User', type: 'string' }, - labels: { title: 'Labels', type: 'array', items: { type: 'string' } }, + task_definition_id: { title: 'Task Definition Id', type: 'string' }, }, }, TaskInterruptionRequest: { diff --git a/packages/api-server/api_server/models/task_favorite.py b/packages/api-server/api_server/models/task_favorite.py index 2714954fd..174dee4f9 100644 --- a/packages/api-server/api_server/models/task_favorite.py +++ b/packages/api-server/api_server/models/task_favorite.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict from pydantic import BaseModel @@ -11,4 +11,4 @@ class TaskFavorite(BaseModel): category: str description: Dict | None user: str - labels: List[str] | None + task_definition_id: str diff --git a/packages/api-server/api_server/models/tortoise_models/tasks.py b/packages/api-server/api_server/models/tortoise_models/tasks.py index d0156e8bb..a3c28394f 100644 --- a/packages/api-server/api_server/models/tortoise_models/tasks.py +++ b/packages/api-server/api_server/models/tortoise_models/tasks.py @@ -79,4 +79,4 @@ class TaskFavorite(Model): category = CharField(255, null=False, index=True) description = JSONField() user = CharField(255, null=False, index=True) - labels = JSONField(null=True, default=list) + task_definition_id = CharField(255, null=True, index=True) diff --git a/packages/api-server/api_server/routes/tasks/favorite_tasks.py b/packages/api-server/api_server/routes/tasks/favorite_tasks.py index 6af718d30..66b069a18 100644 --- a/packages/api-server/api_server/routes/tasks/favorite_tasks.py +++ b/packages/api-server/api_server/routes/tasks/favorite_tasks.py @@ -15,23 +15,25 @@ @router.post("") async def post_favorite_task( - request: TaskFavorite, + favorite_task: TaskFavorite, user: User = Depends(user_dep), ): try: await ttm.TaskFavorite.update_or_create( { - "name": request.name, + "name": favorite_task.name, "unix_millis_earliest_start_time": datetime.fromtimestamp( - request.unix_millis_earliest_start_time / 1000 + favorite_task.unix_millis_earliest_start_time / 1000 ), - "priority": request.priority if request.priority else None, - "category": request.category, - "description": request.description if request.description else None, + "priority": favorite_task.priority if favorite_task.priority else None, + "category": favorite_task.category, + "description": favorite_task.description + if favorite_task.description + else None, "user": user.username, - "labels": request.labels, + "task_definition_id": favorite_task.task_definition_id, }, - id=request.id if request.id != "" else uuid.uuid4(), + id=favorite_task.id if favorite_task.id != "" else uuid.uuid4(), ) except IntegrityError as e: raise HTTPException(422, str(e)) from e @@ -55,7 +57,7 @@ async def get_favorites_tasks( if favorite_task.description else None, user=user.username, - labels=favorite_task.labels, + task_definition_id=favorite_task.task_definition_id, ) for favorite_task in favorites_tasks ] diff --git a/packages/api-server/api_server/test/test_data.py b/packages/api-server/api_server/test/test_data.py index 6ac37a302..dcb6a56de 100644 --- a/packages/api-server/api_server/test/test_data.py +++ b/packages/api-server/api_server/test/test_data.py @@ -474,7 +474,8 @@ def make_task_favorite( "payload":"" } }, - "user":"stub" + "user":"stub", + "task_definition_id": "delivery" } """ ) diff --git a/packages/api-server/api_server/test/test_fixtures.py b/packages/api-server/api_server/test/test_fixtures.py index 2280702de..32a2e6d2f 100644 --- a/packages/api-server/api_server/test/test_fixtures.py +++ b/packages/api-server/api_server/test/test_fixtures.py @@ -176,6 +176,7 @@ def post_favorite_task(self): "category": "clean", "description": {"type": "", "zone": ""}, "user": "", + "task_definition_id": "", } return self.client.post( "/favorite_tasks", From 22dada0b4efa216e71c82ce471f2cf528c58f295 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Thu, 9 May 2024 17:17:43 +0800 Subject: [PATCH 30/37] Use mandatory task_definition_id instead of task_name Signed-off-by: Aaron Chong --- packages/api-client/lib/openapi/api.ts | 2 +- packages/api-client/lib/version.ts | 2 +- packages/api-client/schema/index.ts | 3 +- .../api_server/models/task_booking_label.py | 2 +- .../api-server/api_server/test/test_data.py | 2 +- .../api-server/migrations/migrate_db_912.py | 23 +++++--- .../lib/tasks/create-task.tsx | 57 ++++++++++++++----- 7 files changed, 63 insertions(+), 28 deletions(-) diff --git a/packages/api-client/lib/openapi/api.ts b/packages/api-client/lib/openapi/api.ts index beaf6cad0..2a6aa1ece 100644 --- a/packages/api-client/lib/openapi/api.ts +++ b/packages/api-client/lib/openapi/api.ts @@ -2400,7 +2400,7 @@ export interface TaskBookingLabelDescription { * @type {string} * @memberof TaskBookingLabelDescription */ - task_name?: string; + task_definition_id: string; /** * * @type {number} diff --git a/packages/api-client/lib/version.ts b/packages/api-client/lib/version.ts index 15ba780f3..5d34f9598 100644 --- a/packages/api-client/lib/version.ts +++ b/packages/api-client/lib/version.ts @@ -3,6 +3,6 @@ import { version as rmfModelVer } from 'rmf-models'; export const version = { rmfModels: rmfModelVer, - rmfServer: '7cdc0df3da518c2f6d4b9e5b6eb0ef756d2f8f73', + rmfServer: '963092f421e430f5c30d27b0be43d38c9b1e9222', openapiGenerator: '6.2.1', }; diff --git a/packages/api-client/schema/index.ts b/packages/api-client/schema/index.ts index 33c09bbdb..cd47ed0a8 100644 --- a/packages/api-client/schema/index.ts +++ b/packages/api-client/schema/index.ts @@ -3638,9 +3638,10 @@ export default { }, TaskBookingLabelDescription: { title: 'TaskBookingLabelDescription', + required: ['task_definition_id'], type: 'object', properties: { - task_name: { title: 'Task Name', type: 'string' }, + task_definition_id: { title: 'Task Definition Id', type: 'string' }, unix_millis_warn_time: { title: 'Unix Millis Warn Time', type: 'integer' }, pickup: { title: 'Pickup', type: 'string' }, destination: { title: 'Destination', type: 'string' }, diff --git a/packages/api-server/api_server/models/task_booking_label.py b/packages/api-server/api_server/models/task_booking_label.py index 8aa36b79f..1e59d77ab 100644 --- a/packages/api-server/api_server/models/task_booking_label.py +++ b/packages/api-server/api_server/models/task_booking_label.py @@ -14,7 +14,7 @@ class TaskBookingLabelDescription(BaseModel): accordingly back on the same frontend. """ - task_name: Optional[str] + task_definition_id: str unix_millis_warn_time: Optional[int] pickup: Optional[str] destination: Optional[str] diff --git a/packages/api-server/api_server/test/test_data.py b/packages/api-server/api_server/test/test_data.py index dcb6a56de..9a1fc0597 100644 --- a/packages/api-server/api_server/test/test_data.py +++ b/packages/api-server/api_server/test/test_data.py @@ -130,7 +130,7 @@ def make_fleet_log() -> FleetLog: def make_task_booking_label() -> TaskBookingLabel: return TaskBookingLabel( description=TaskBookingLabelDescription( - task_name="Multi-Delivery", + task_definition_id="multi-delivery", unix_millis_warn_time=1636388400000, pickup="Kitchen", destination="room_203", diff --git a/packages/api-server/migrations/migrate_db_912.py b/packages/api-server/migrations/migrate_db_912.py index 37702ce4b..f7598e4ce 100644 --- a/packages/api-server/migrations/migrate_db_912.py +++ b/packages/api-server/migrations/migrate_db_912.py @@ -40,7 +40,13 @@ ) -def parse_task_name(task_request: TaskRequest) -> Optional[str]: +def parse_task_definition_id(task_request: TaskRequest) -> Optional[str]: + """ + Although not IDs per-se, these names are used to identify which task + definition to use when rendering on the dashboard. While these IDs are + going to be static, in the next update, the underlying display name will be + configurable at build time. + """ name = None if task_request.category.lower() == "patrol": name = "Patrol" @@ -172,14 +178,15 @@ async def migrate(): pickup = parse_pickup(request_model) destination = parse_destination(request_model) - # If the task name could not be parsed, we skip migrating this TaskState - task_name = parse_task_name(request_model) - if task_name is None: + # If the task definition id could not be parsed, we skip migrating this + # TaskState + task_definition_id = parse_task_definition_id(request_model) + if task_definition_id is None: failed_task_states_count += 1 continue label_description = TaskBookingLabelDescription( - task_name=task_name, + task_definition_id=task_definition_id, unix_millis_warn_time=None, pickup=pickup, destination=destination, @@ -223,8 +230,8 @@ async def migrate(): ) # print(task_request) - task_name = parse_task_name(task_request) - if task_name is None: + task_definition_id = parse_task_definition_id(task_request) + if task_definition_id is None: failed_scheduled_task_count += 1 continue @@ -232,7 +239,7 @@ async def migrate(): pickup = parse_pickup(task_request) destination = parse_destination(task_request) label_description = TaskBookingLabelDescription( - task_name=task_name, + task_definition_id=task_definition_id, unix_millis_warn_time=None, pickup=pickup, destination=destination, diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index f6ec34a5f..b49d33296 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -48,6 +48,36 @@ import { serializeTaskBookingLabel, } from './task-booking-label-utils'; +interface TaskDefinition { + task_definition_id: string; + task_display_name: string; +} + +const TaskDefinitions: Record = { + delivery_pickup: { + task_definition_id: 'delivery_pickup', + task_display_name: 'Delivery - 1:1', + }, + delivery_sequential_lot_pickup: { + task_definition_id: 'delivery_sequential_lot_pickup', + task_display_name: 'Delivery - Sequential lot pick up', + }, + delivery_area_pickup: { + task_definition_id: 'delivery_area_pickup', + task_display_name: 'Delivery - Area pick up', + }, + patrol: { + task_definition_id: 'patrol', + task_display_name: 'Patrol', + }, + custom_compose: { + task_definition_id: 'custom_compose', + task_display_name: 'Custom Compose Task', + }, +}; + +const DefaultTaskDefinitionId = 'custom_compose'; + // A bunch of manually defined descriptions to avoid using `any`. export interface PatrolTaskDescription { places: string[]; @@ -1212,7 +1242,7 @@ const defaultFavoriteTask = (): TaskFavorite => { unix_millis_earliest_start_time: 0, priority: { type: 'binary', value: 0 }, user: '', - labels: [], + task_definition_id: DefaultTaskDefinitionId, }; }; @@ -1293,7 +1323,9 @@ export function CreateTaskForm({ () => requestTask ?? defaultTask(), ); - let bookingLabel: TaskBookingLabel = { description: {} }; + let bookingLabel: TaskBookingLabel = { + description: { task_definition_id: DefaultTaskDefinitionId }, + }; if (requestTask && requestTask.labels) { for (const label of requestTask.labels) { const parsedLabel = getTaskBookingLabelFromJsonString(label); @@ -1381,7 +1413,7 @@ export function CreateTaskForm({ return { description: { ...prev.description, - task_name: taskRequest.category, + task_definition_id: taskRequest.category, destination: desc.places.at(-1), }, }; @@ -1477,7 +1509,7 @@ export function CreateTaskForm({ const handleTaskTypeChange = (ev: React.ChangeEvent) => { const newType = ev.target.value; setTaskType(newType); - setRequestBookingLabel({ description: { task_name: newType } }); + setRequestBookingLabel({ description: { task_definition_id: newType } }); if (newType === 'custom_compose') { taskRequest.category = 'custom_compose'; @@ -1618,7 +1650,7 @@ export function CreateTaskForm({ setSavingFavoriteTask(true); const favoriteTask = favoriteTaskBuffer; - favoriteTask.labels = [serializeTaskBookingLabel(requestBookingLabel)]; + favoriteTask.task_definition_id = requestBookingLabel.description.task_definition_id; await submitFavoriteTask(favoriteTask); setSavingFavoriteTask(false); @@ -1700,16 +1732,11 @@ export function CreateTaskForm({ unix_millis_earliest_start_time: 0, priority: favoriteTask.priority, }); - let bookingLabel: TaskBookingLabel = { description: {} }; - if (favoriteTask.labels) { - for (const label of favoriteTask.labels) { - const parsedLabel = getTaskBookingLabelFromJsonString(label); - if (parsedLabel) { - bookingLabel = parsedLabel; - } - } - } - setRequestBookingLabel(bookingLabel); + setRequestBookingLabel({ + description: { + task_definition_id: favoriteTask.task_definition_id, + }, + }); }} /> ); From 98741b14ceca74208ca98e4bb0c3ca9e41ca1e3c Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Fri, 10 May 2024 11:07:23 +0800 Subject: [PATCH 31/37] Generate label only before dispatching Signed-off-by: Aaron Chong --- .../api-server/migrations/migrate_db_912.py | 6 +- .../src/components/tasks/task-summary.tsx | 14 +- .../lib/tasks/create-task.tsx | 308 +++++++++--------- 3 files changed, 173 insertions(+), 155 deletions(-) diff --git a/packages/api-server/migrations/migrate_db_912.py b/packages/api-server/migrations/migrate_db_912.py index f7598e4ce..1722ce1ae 100644 --- a/packages/api-server/migrations/migrate_db_912.py +++ b/packages/api-server/migrations/migrate_db_912.py @@ -16,9 +16,9 @@ # NOTE: This script is for migrating TaskState and ScheduledTask in an existing # database to work with https://github.com/open-rmf/rmf-web/pull/912. # Before migration: -# - Pickup, destination, cart ID, task name information will be unavailable -# on the Task Queue Table on the dashboard, as we no longer gather those -# fields from the TaskRequest +# - Pickup, destination, cart ID, task definition id information will be +# unavailable on the Task Queue Table on the dashboard, as we no longer gather +# those fields from the TaskRequest # After migration: # - Dashboard will behave the same as before #912, however it is no longer # dependent on TaskRequest to fill out those fields. It gathers those fields diff --git a/packages/dashboard/src/components/tasks/task-summary.tsx b/packages/dashboard/src/components/tasks/task-summary.tsx index 2c38f1c1c..288ed81ed 100644 --- a/packages/dashboard/src/components/tasks/task-summary.tsx +++ b/packages/dashboard/src/components/tasks/task-summary.tsx @@ -84,7 +84,7 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { const [openTaskDetailsLogs, setOpenTaskDetailsLogs] = React.useState(false); const [taskState, setTaskState] = React.useState(null); - const [label, setLabel] = React.useState({ description: {} }); + const [label, setLabel] = React.useState(null); const [isOpen, setIsOpen] = React.useState(true); const taskProgress = React.useMemo(() => { @@ -115,7 +115,7 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { if (requestLabel) { setLabel(requestLabel); } else { - setLabel({ description: {} }); + setLabel(null); } setTaskState(subscribedTask); }); @@ -129,20 +129,20 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { value: taskState ? taskState.booking.id : 'n/a.', }, { - title: 'Task name', - value: label.description.task_name ?? 'n/a', + title: 'Task definition ID', + value: label?.description.task_definition_id ?? 'n/a', }, { title: 'Pickup', - value: label.description.pickup ?? 'n/a', + value: label?.description.pickup ?? 'n/a', }, { title: 'Cart ID', - value: label.description.cart_id ?? 'n/a', + value: label?.description.cart_id ?? 'n/a', }, { title: 'Dropoff', - value: label.description.destination ?? 'n/a', + value: label?.description.destination ?? 'n/a', }, { title: 'Est. end time', diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index b49d33296..173f7156a 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -43,17 +43,14 @@ import React from 'react'; import { Loading } from '..'; import { ConfirmationDialog, ConfirmationDialogProps } from '../confirmation-dialog'; import { PositiveIntField } from '../form-inputs'; -import { - getTaskBookingLabelFromJsonString, - serializeTaskBookingLabel, -} from './task-booking-label-utils'; +import { serializeTaskBookingLabel } from './task-booking-label-utils'; interface TaskDefinition { task_definition_id: string; task_display_name: string; } -const TaskDefinitions: Record = { +const SupportedTaskDefinitionsMap: Record = { delivery_pickup: { task_definition_id: 'delivery_pickup', task_display_name: 'Delivery - 1:1', @@ -76,8 +73,79 @@ const TaskDefinitions: Record = { }, }; +// If no task definition id is found in a past task (scheduled or favorite) const DefaultTaskDefinitionId = 'custom_compose'; +// FIXME: This is the order of the task type dropdown, and will be migrated out +// as a build-time configuration in a subsequent patch. +const SupportedTaskDefinitions: TaskDefinition[] = [ + { + task_definition_id: 'delivery_pickup', + task_display_name: 'Delivery - 1:1', + }, + { + task_definition_id: 'delivery_sequential_lot_pickup', + task_display_name: 'Delivery - Sequential lot pick up', + }, + { + task_definition_id: 'delivery_area_pickup', + task_display_name: 'Delivery - Area pick up', + }, + { + task_definition_id: 'patrol', + task_display_name: 'Patrol', + }, + { + task_definition_id: 'custom_compose', + task_display_name: 'Custom Compose Task', + }, +]; + +function makeDeliveryTaskBookingLabel(task_description: DeliveryTaskDescription): TaskBookingLabel { + const pickupDescription = + task_description.phases[0].activity.description.activities[1].description.description; + return { + description: { + task_definition_id: task_description.category, + pickup: pickupDescription.pickup_lot, + destination: task_description.phases[1].activity.description.activities[0].description, + cart_id: pickupDescription.cart_id, + }, + }; +} + +function makeDeliveryCustomTaskBookingLabel( + task_description: DeliveryCustomTaskDescription, +): TaskBookingLabel { + const pickupDescription = + task_description.phases[0].activity.description.activities[1].description.description; + return { + description: { + task_definition_id: task_description.category, + pickup: pickupDescription.pickup_zone, + destination: task_description.phases[1].activity.description.activities[0].description, + cart_id: pickupDescription.cart_id, + }, + }; +} + +function makePatrolTaskBookingLabel(task_description: PatrolTaskDescription): TaskBookingLabel { + return { + description: { + task_definition_id: 'patrol', + destination: task_description.places[task_description.places.length - 1], + }, + }; +} + +function makeCustomComposeTaskBookingLabel(): TaskBookingLabel { + return { + description: { + task_definition_id: 'custom_compose', + }, + }; +} + // A bunch of manually defined descriptions to avoid using `any`. export interface PatrolTaskDescription { places: string[]; @@ -1318,25 +1386,13 @@ export function CreateTaskForm({ const [favoriteTaskTitleError, setFavoriteTaskTitleError] = React.useState(false); const [savingFavoriteTask, setSavingFavoriteTask] = React.useState(false); - const [taskType, setTaskType] = React.useState(undefined); + const [taskDefinitionId, setTaskDefinitionId] = React.useState( + SupportedTaskDefinitions[0].task_definition_id, + ); const [taskRequest, setTaskRequest] = React.useState( () => requestTask ?? defaultTask(), ); - let bookingLabel: TaskBookingLabel = { - description: { task_definition_id: DefaultTaskDefinitionId }, - }; - if (requestTask && requestTask.labels) { - for (const label of requestTask.labels) { - const parsedLabel = getTaskBookingLabelFromJsonString(label); - if (parsedLabel) { - bookingLabel = parsedLabel; - } - } - } - const [requestBookingLabel, setRequestBookingLabel] = - React.useState(bookingLabel); - const [submitting, setSubmitting] = React.useState(false); const [formFullyFilled, setFormFullyFilled] = React.useState(requestTask !== undefined || false); const [openSchedulingDialog, setOpenSchedulingDialog] = React.useState(false); @@ -1407,18 +1463,7 @@ export function CreateTaskForm({ { - handleTaskDescriptionChange('patrol', desc); - setRequestBookingLabel((prev) => { - return { - description: { - ...prev.description, - task_definition_id: taskRequest.category, - destination: desc.places.at(-1), - }, - }; - }); - }} + onChange={(desc) => handleTaskDescriptionChange('patrol', desc)} allowSubmit={allowSubmit} /> ); @@ -1426,17 +1471,7 @@ export function CreateTaskForm({ return ( { - handleCustomComposeTaskDescriptionChange(desc); - setRequestBookingLabel((prev) => { - return { - description: { - ...prev.description, - task_name: taskRequest.category, - }, - }; - }); - }} + onChange={(desc) => handleCustomComposeTaskDescriptionChange(desc)} allowSubmit={allowSubmit} /> ); @@ -1457,17 +1492,6 @@ export function CreateTaskForm({ handleTaskDescriptionChange('compose', desc); const pickupPerformAction = desc.phases[0].activity.description.activities[1].description.description; - setRequestBookingLabel((prev) => { - return { - description: { - ...prev.description, - task_name: taskRequest.description.category, - pickup: pickupPerformAction.pickup_lot, - cart_id: pickupPerformAction.cart_id, - destination: desc.phases[1].activity.description.activities[0].description, - }, - }; - }); }} allowSubmit={allowSubmit} /> @@ -1487,17 +1511,6 @@ export function CreateTaskForm({ handleTaskDescriptionChange('compose', desc); const pickupPerformAction = desc.phases[0].activity.description.activities[1].description.description; - setRequestBookingLabel((prev) => { - return { - description: { - ...prev.description, - task_name: taskRequest.description.category, - pickup: pickupPerformAction.pickup_zone, - cart_id: pickupPerformAction.cart_id, - destination: desc.phases[1].activity.description.activities[0].description, - }, - }; - }); }} allowSubmit={allowSubmit} /> @@ -1508,8 +1521,7 @@ export function CreateTaskForm({ }; const handleTaskTypeChange = (ev: React.ChangeEvent) => { const newType = ev.target.value; - setTaskType(newType); - setRequestBookingLabel({ description: { task_definition_id: newType } }); + setTaskDefinitionId(newType); if (newType === 'custom_compose') { taskRequest.category = 'custom_compose'; @@ -1544,44 +1556,56 @@ export function CreateTaskForm({ request.requester = requester; request.unix_millis_request_time = Date.now(); - if ( - taskType === 'delivery_pickup' || - taskType === 'delivery_sequential_lot_pickup' || - taskType === 'delivery_area_pickup' - ) { - const goToOneOfThePlaces: GoToOneOfThePlacesActivity = { - category: 'go_to_place', - description: { - one_of: emergencyLots.map((placeName) => { - return { - waypoint: placeName, - }; - }), - constraints: [ - { - category: 'prefer_same_map', - description: '', - }, - ], - }, - }; - - // FIXME: there should not be any statically defined duration estimates as - // it makes assumptions of the deployments. - const deliveryDropoff: DropoffActivity = { - category: 'perform_action', - description: { - unix_millis_action_duration_estimate: 60000, - category: 'delivery_dropoff', - description: {}, - }, - }; - const onCancelDropoff: OnCancelDropoff = { - category: 'sequence', - description: [goToOneOfThePlaces, deliveryDropoff], - }; - request.description.phases[1].on_cancel = [onCancelDropoff]; - } else if (taskType === 'custom_compose') { + // if ( + // taskDefinition === 'delivery_pickup' || + // taskDefinition === 'delivery_sequential_lot_pickup' || + // taskDefinition === 'delivery_area_pickup' + // ) { + // const goToOneOfThePlaces: GoToOneOfThePlacesActivity = { + // category: 'go_to_place', + // description: { + // one_of: emergencyLots.map((placeName) => { + // return { + // waypoint: placeName, + // }; + // }), + // constraints: [ + // { + // category: 'prefer_same_map', + // description: '', + // }, + // ], + // }, + // }; + + // // FIXME: there should not be any statically defined duration estimates as + // // it makes assumptions of the deployments. + // const deliveryDropoff: DropoffActivity = { + // category: 'perform_action', + // description: { + // unix_millis_action_duration_estimate: 60000, + // category: 'delivery_dropoff', + // description: {}, + // }, + // }; + // const onCancelDropoff: OnCancelDropoff = { + // category: 'sequence', + // description: [goToOneOfThePlaces, deliveryDropoff], + // }; + // request.description.phases[1].on_cancel = [onCancelDropoff]; + // } else if (taskDefinition === 'custom_compose') { + // try { + // const obj = JSON.parse(request.description); + // request.category = 'compose'; + // request.description = obj; + // } catch (e) { + // console.error('Invalid custom compose task description'); + // onFail && onFail(e as Error, [request]); + // return; + // } + // } + + if (taskDefinitionId === 'custom_compose') { try { const obj = JSON.parse(request.description); request.category = 'compose'; @@ -1593,7 +1617,33 @@ export function CreateTaskForm({ } } + // Generate booking label for each task try { + let requestBookingLabel: TaskBookingLabel | null = null; + switch (taskDefinitionId) { + case 'delivery_pickup': + requestBookingLabel = makeDeliveryTaskBookingLabel(request.description); + break; + case 'delivery_sequential_lot_pickup': + case 'delivery_area_pickup': + requestBookingLabel = makeDeliveryCustomTaskBookingLabel(request.description); + break; + case 'patrol': + requestBookingLabel = makePatrolTaskBookingLabel(request.description); + break; + case 'custom_compose': + requestBookingLabel = makeCustomComposeTaskBookingLabel(); + break; + } + + if (!requestBookingLabel) { + const error = Error( + `Failed to generate booking label for task request of definition ID: ${taskDefinitionId}`, + ); + onFail && onFail(error, [request]); + return; + } + const labelString = serializeTaskBookingLabel(requestBookingLabel); if (labelString) { request.labels = [labelString]; @@ -1650,7 +1700,7 @@ export function CreateTaskForm({ setSavingFavoriteTask(true); const favoriteTask = favoriteTaskBuffer; - favoriteTask.task_definition_id = requestBookingLabel.description.task_definition_id; + favoriteTask.task_definition_id = taskDefinitionId ?? DefaultTaskDefinitionId; await submitFavoriteTask(favoriteTask); setSavingFavoriteTask(false); @@ -1732,11 +1782,7 @@ export function CreateTaskForm({ unix_millis_earliest_start_time: 0, priority: favoriteTask.priority, }); - setRequestBookingLabel({ - description: { - task_definition_id: favoriteTask.task_definition_id, - }, - }); + setTaskDefinitionId(favoriteTask.task_definition_id); }} /> ); @@ -1773,39 +1819,11 @@ export function CreateTaskForm({ }} InputLabelProps={{ style: { fontSize: isScreenHeightLessThan800 ? 16 : 20 } }} > - - Delivery - 1:1 - - - Delivery - Sequential lot pick up - - - Delivery - Area pick up - - - Patrol - - Custom Compose Task + {SupportedTaskDefinitions.map((taskDefinition) => ( + + {taskDefinition.task_display_name} + + ))} @@ -1896,7 +1914,7 @@ export function CreateTaskForm({ setOpenFavoriteDialog(true); }} style={{ marginTop: theme.spacing(2), marginBottom: theme.spacing(2) }} - disabled={taskType === 'custom_compose'} + disabled={taskDefinitionId === 'custom_compose'} > {callToUpdateFavoriteTask ? `Confirm edits` : 'Save as a favorite task'} From 305791b03b29e6b6a3575d899f3fe198483c2c51 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Fri, 10 May 2024 17:23:31 +0800 Subject: [PATCH 32/37] Clean up of emergency lots usage in config Signed-off-by: Aaron Chong --- packages/dashboard/src/components/appbar.tsx | 1 - .../src/managers/resource-manager.ts | 3 -- .../lib/tasks/create-task.tsx | 51 ------------------- 3 files changed, 55 deletions(-) diff --git a/packages/dashboard/src/components/appbar.tsx b/packages/dashboard/src/components/appbar.tsx index 1d1fa8673..76a3f5224 100644 --- a/packages/dashboard/src/components/appbar.tsx +++ b/packages/dashboard/src/components/appbar.tsx @@ -615,7 +615,6 @@ export const AppBar = React.memo(({ extraToolbarItems }: AppBarProps): React.Rea cleaningZones={cleaningZoneNames} pickupZones={resourceManager?.pickupZones} cartIds={resourceManager?.cartIds} - emergencyLots={resourceManager?.emergencyLots} pickupPoints={pickupPoints} dropoffPoints={dropoffPoints} favoritesTasks={favoritesTasks} diff --git a/packages/dashboard/src/managers/resource-manager.ts b/packages/dashboard/src/managers/resource-manager.ts index 2f872d855..4227326a4 100644 --- a/packages/dashboard/src/managers/resource-manager.ts +++ b/packages/dashboard/src/managers/resource-manager.ts @@ -18,7 +18,6 @@ export interface ResourceConfigurationsType { attributionPrefix?: string; cartIds?: string[]; loggedInDisplayLevel?: string; - emergencyLots?: string[]; } export default class ResourceManager { @@ -33,7 +32,6 @@ export default class ResourceManager { attributionPrefix?: string; cartIds?: string[]; loggedInDisplayLevel?: string; - emergencyLots?: string[]; /** * Gets the default resource manager using the embedded resource file (aka "assets/resources/main.json"). @@ -73,7 +71,6 @@ export default class ResourceManager { this.attributionPrefix = resources.attributionPrefix || 'OSRC-SG'; this.cartIds = resources.cartIds || []; this.loggedInDisplayLevel = resources.loggedInDisplayLevel; - this.emergencyLots = resources.emergencyLots || []; } } diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 173f7156a..887c97c41 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -1325,7 +1325,6 @@ export interface CreateTaskFormProps patrolWaypoints?: string[]; pickupZones?: string[]; cartIds?: string[]; - emergencyLots?: string[]; pickupPoints?: Record; dropoffPoints?: Record; favoritesTasks?: TaskFavorite[]; @@ -1352,7 +1351,6 @@ export function CreateTaskForm({ patrolWaypoints = [], pickupZones = [], cartIds = [], - emergencyLots = [], pickupPoints = {}, dropoffPoints = {}, favoritesTasks = [], @@ -1556,55 +1554,6 @@ export function CreateTaskForm({ request.requester = requester; request.unix_millis_request_time = Date.now(); - // if ( - // taskDefinition === 'delivery_pickup' || - // taskDefinition === 'delivery_sequential_lot_pickup' || - // taskDefinition === 'delivery_area_pickup' - // ) { - // const goToOneOfThePlaces: GoToOneOfThePlacesActivity = { - // category: 'go_to_place', - // description: { - // one_of: emergencyLots.map((placeName) => { - // return { - // waypoint: placeName, - // }; - // }), - // constraints: [ - // { - // category: 'prefer_same_map', - // description: '', - // }, - // ], - // }, - // }; - - // // FIXME: there should not be any statically defined duration estimates as - // // it makes assumptions of the deployments. - // const deliveryDropoff: DropoffActivity = { - // category: 'perform_action', - // description: { - // unix_millis_action_duration_estimate: 60000, - // category: 'delivery_dropoff', - // description: {}, - // }, - // }; - // const onCancelDropoff: OnCancelDropoff = { - // category: 'sequence', - // description: [goToOneOfThePlaces, deliveryDropoff], - // }; - // request.description.phases[1].on_cancel = [onCancelDropoff]; - // } else if (taskDefinition === 'custom_compose') { - // try { - // const obj = JSON.parse(request.description); - // request.category = 'compose'; - // request.description = obj; - // } catch (e) { - // console.error('Invalid custom compose task description'); - // onFail && onFail(e as Error, [request]); - // return; - // } - // } - if (taskDefinitionId === 'custom_compose') { try { const obj = JSON.parse(request.description); From fe4936035a4bacca0a3e849c203e8d8376615c9e Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Fri, 10 May 2024 17:27:00 +0800 Subject: [PATCH 33/37] Removed unused map Signed-off-by: Aaron Chong --- .../lib/tasks/create-task.tsx | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 887c97c41..346ee555b 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -50,29 +50,6 @@ interface TaskDefinition { task_display_name: string; } -const SupportedTaskDefinitionsMap: Record = { - delivery_pickup: { - task_definition_id: 'delivery_pickup', - task_display_name: 'Delivery - 1:1', - }, - delivery_sequential_lot_pickup: { - task_definition_id: 'delivery_sequential_lot_pickup', - task_display_name: 'Delivery - Sequential lot pick up', - }, - delivery_area_pickup: { - task_definition_id: 'delivery_area_pickup', - task_display_name: 'Delivery - Area pick up', - }, - patrol: { - task_definition_id: 'patrol', - task_display_name: 'Patrol', - }, - custom_compose: { - task_definition_id: 'custom_compose', - task_display_name: 'Custom Compose Task', - }, -}; - // If no task definition id is found in a past task (scheduled or favorite) const DefaultTaskDefinitionId = 'custom_compose'; From 2c2d5d26594c5f954e05632da817eaebf7060bc5 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Fri, 10 May 2024 17:37:48 +0800 Subject: [PATCH 34/37] Lint Signed-off-by: Aaron Chong --- packages/react-components/lib/tasks/create-task.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 346ee555b..fc62722c0 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -1746,7 +1746,10 @@ export function CreateTaskForm({ InputLabelProps={{ style: { fontSize: isScreenHeightLessThan800 ? 16 : 20 } }} > {SupportedTaskDefinitions.map((taskDefinition) => ( - + {taskDefinition.task_display_name} ))} From 82422f4dc51a7a443965ec3acd7914d556b434a1 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Fri, 17 May 2024 14:58:58 +0800 Subject: [PATCH 35/37] New generic TaskLabel tortoise model, to handle more custom label fields for sorting and filtering Signed-off-by: Aaron Chong --- .../models/tortoise_models/__init__.py | 1 + .../models/tortoise_models/tasks.py | 9 +++ .../api_server/repositories/tasks.py | 61 ++++++++++++------- .../api_server/routes/tasks/tasks.py | 29 +++++++-- .../api-server/migrations/migrate_db_912.py | 23 +++++-- 5 files changed, 90 insertions(+), 33 deletions(-) diff --git a/packages/api-server/api_server/models/tortoise_models/__init__.py b/packages/api-server/api_server/models/tortoise_models/__init__.py index 0028a5e78..384dd978d 100644 --- a/packages/api-server/api_server/models/tortoise_models/__init__.py +++ b/packages/api-server/api_server/models/tortoise_models/__init__.py @@ -26,6 +26,7 @@ TaskEventLogPhasesEventsLog, TaskEventLogPhasesLog, TaskFavorite, + TaskLabel, TaskRequest, TaskState, ) diff --git a/packages/api-server/api_server/models/tortoise_models/tasks.py b/packages/api-server/api_server/models/tortoise_models/tasks.py index a3c28394f..3111fc95a 100644 --- a/packages/api-server/api_server/models/tortoise_models/tasks.py +++ b/packages/api-server/api_server/models/tortoise_models/tasks.py @@ -1,4 +1,5 @@ from tortoise.fields import ( + BigIntField, CharField, DatetimeField, ForeignKeyField, @@ -29,6 +30,14 @@ class TaskState(Model): unix_millis_warn_time = DatetimeField(null=True, index=True) pickup = CharField(255, null=True, index=True) destination = CharField(255, null=True, index=True) + labels = ReverseRelation["TaskLabel"] + + +class TaskLabel(Model): + state = ForeignKeyField("models.TaskState", null=True, related_name="labels") + label_name = CharField(255, null=False, index=True) + label_value_str = CharField(255, null=True, index=True) + label_value_num = BigIntField(null=True, index=True) class TaskEventLog(Model): diff --git a/packages/api-server/api_server/repositories/tasks.py b/packages/api-server/api_server/repositories/tasks.py index 3b3872e25..0bb993788 100644 --- a/packages/api-server/api_server/repositories/tasks.py +++ b/packages/api-server/api_server/repositories/tasks.py @@ -59,15 +59,6 @@ async def query_task_requests(self, task_ids: List[str]) -> List[DbTaskRequest]: raise HTTPException(422, str(e)) from e async def save_task_state(self, task_state: TaskState) -> None: - labels = task_state.booking.labels - booking_label = None - if labels is not None: - for l in labels: - validated_booking_label = TaskBookingLabel.from_json_string(l) - if validated_booking_label is not None: - booking_label = validated_booking_label - break - task_state_dict = { "data": task_state.json(), "category": task_state.category.__root__ if task_state.category else None, @@ -86,23 +77,10 @@ async def save_task_state(self, task_state: TaskState) -> None: "requester": task_state.booking.requester if task_state.booking.requester else None, - "pickup": booking_label.description.pickup - if booking_label is not None - and booking_label.description.pickup is not None - else None, - "destination": booking_label.description.destination - if booking_label is not None - and booking_label.description.destination is not None - else None, } - if task_state.unix_millis_warn_time is not None: - task_state_dict["unix_millis_warn_time"] = datetime.fromtimestamp( - task_state.unix_millis_warn_time / 1000 - ) - try: - await ttm.TaskState.update_or_create( + state, created = await ttm.TaskState.update_or_create( task_state_dict, id_=task_state.booking.id ) except Exception as e: # pylint: disable=W0703 @@ -119,6 +97,43 @@ async def save_task_state(self, task_state: TaskState) -> None: self.logger.error( f"Failed to save task state of id [{task_state.booking.id}] [{e}]" ) + return + + if not created: + return + + # Save the labels that we want + labels = task_state.booking.labels + booking_label = None + if labels is not None: + for l in labels: + validated_booking_label = TaskBookingLabel.from_json_string(l) + if validated_booking_label is not None: + booking_label = validated_booking_label + break + if booking_label is None: + return + + # Here we generate the labels required for server-side sorting and + # filtering. + if booking_label.description.pickup is not None: + await ttm.TaskLabel.create( + state=state, + label_name="pickup", + label_value_str=booking_label.description.pickup, + ) + if booking_label.description.destination is not None: + await ttm.TaskLabel.create( + state=state, + label_name="destination", + label_value_str=booking_label.description.destination, + ) + if booking_label.description.unix_millis_warn_time is not None: + await ttm.TaskLabel.create( + state=state, + label_name="unix_millis_warn_time", + label_value_num=booking_label.description.unix_millis_warn_time, + ) async def query_task_states( self, query: QuerySet[DbTaskState], pagination: Optional[Pagination] = None diff --git a/packages/api-server/api_server/routes/tasks/tasks.py b/packages/api-server/api_server/routes/tasks/tasks.py index b9549382d..2a8030c91 100644 --- a/packages/api-server/api_server/routes/tasks/tasks.py +++ b/packages/api-server/api_server/routes/tasks/tasks.py @@ -126,10 +126,6 @@ async def query_task_states( filters["unix_millis_request_time__lte"] = request_time_between[1] if requester is not None: filters["requester__in"] = requester.split(",") - if pickup is not None: - filters["pickup__in"] = pickup.split(",") - if destination is not None: - filters["destination__in"] = destination.split(",") if assigned_to is not None: filters["assigned_to__in"] = assigned_to.split(",") if start_time_between is not None: @@ -146,6 +142,31 @@ async def query_task_states( continue filters["status__in"].append(mdl.Status(status_string)) + # NOTE: in order to perform filtering based on the values in labels, a + # filter on the label_name will need to be applied as well as a filter on + # the label_value. + if pickup is not None: + filters["labels__label_name"] = "pickup" + filters["labels__label_value_str__in"] = pickup.split(",") + if destination is not None: + filters["labels__label_name"] = "destination" + filters["labels__label_value_str__in"] = destination.split(",") + + # NOTE: In order to perform sorting based on the values in labels, a filter + # on the label_name has to be performed first. A side-effect of this would + # be that states that do not contain this field will not be returned. + if pagination.order_by is not None: + labels_fields = ["pickup", "destination"] + new_order = pagination.order_by + for field in labels_fields: + if field in pagination.order_by: + filters["labels__label_name"] = field + new_order = pagination.order_by.replace( + field, "labels__label_value_str" + ) + break + pagination.order_by = new_order + return await task_repo.query_task_states(DbTaskState.filter(**filters), pagination) diff --git a/packages/api-server/migrations/migrate_db_912.py b/packages/api-server/migrations/migrate_db_912.py index 1722ce1ae..968196b7a 100644 --- a/packages/api-server/migrations/migrate_db_912.py +++ b/packages/api-server/migrations/migrate_db_912.py @@ -18,11 +18,17 @@ # Before migration: # - Pickup, destination, cart ID, task definition id information will be # unavailable on the Task Queue Table on the dashboard, as we no longer gather -# those fields from the TaskRequest +# those fields from the TaskRequest. +# - TaskState database model contains optional CharFields for pickup and +# destination, to facilitate server-side sorting and filtering. # After migration: # - Dashboard will behave the same as before #912, however it is no longer # dependent on TaskRequest to fill out those fields. It gathers those fields # from the json string in TaskState.booking.labels. +# - In the database, we create a new generic key-value pair model, that allow +# us to encode all this information and tie them to a task state, and be used +# for sorting and filtering, using reverse relations, as opposed to fully +# filled out columns for TaskState. # This script performs the following: # - Construct TaskBookingLabel from its TaskRequest if it is available. # - Update the respective TaskState.data json TaskState.booking.labels field @@ -206,12 +212,17 @@ async def migrate(): ) # print(state_model) + if pickup is not None: + await ttm.TaskLabel.create( + state.state, label_name="pickup", label_value_str=pickup + ) + if destination is not None: + await ttm.TaskLabel.create( + state.state, label_name="destination", label_value_str=destination + ) + state.update_from_dict( - { - "data": state_model.json(exclude_none=True, separators=(",", ":")), - "pickup": pickup, - "destination": destination, - } + {"data": state_model.json(exclude_none=True, separators=(",", ":"))} ) await state.save() From bb9520201f72a6e22d07dd404211665701b8d33a Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Fri, 17 May 2024 15:35:27 +0800 Subject: [PATCH 36/37] Fix migration script Signed-off-by: Aaron Chong --- packages/api-server/migrations/migrate_db_912.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api-server/migrations/migrate_db_912.py b/packages/api-server/migrations/migrate_db_912.py index 968196b7a..c4923eaa3 100644 --- a/packages/api-server/migrations/migrate_db_912.py +++ b/packages/api-server/migrations/migrate_db_912.py @@ -214,11 +214,11 @@ async def migrate(): if pickup is not None: await ttm.TaskLabel.create( - state.state, label_name="pickup", label_value_str=pickup + state=state, label_name="pickup", label_value_str=pickup ) if destination is not None: await ttm.TaskLabel.create( - state.state, label_name="destination", label_value_str=destination + state=state, label_name="destination", label_value_str=destination ) state.update_from_dict( From 7c2d8efa27ad3edacc438dec24e756bda3c2baa9 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Mon, 20 May 2024 15:03:20 +0800 Subject: [PATCH 37/37] Added float field, and more comments on the steps of label creation and saving Signed-off-by: Aaron Chong --- packages/api-server/api_server/models/tortoise_models/tasks.py | 2 ++ packages/api-server/api_server/repositories/tasks.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/api-server/api_server/models/tortoise_models/tasks.py b/packages/api-server/api_server/models/tortoise_models/tasks.py index 3111fc95a..6e3f3e665 100644 --- a/packages/api-server/api_server/models/tortoise_models/tasks.py +++ b/packages/api-server/api_server/models/tortoise_models/tasks.py @@ -2,6 +2,7 @@ BigIntField, CharField, DatetimeField, + FloatField, ForeignKeyField, ForeignKeyRelation, JSONField, @@ -38,6 +39,7 @@ class TaskLabel(Model): label_name = CharField(255, null=False, index=True) label_value_str = CharField(255, null=True, index=True) label_value_num = BigIntField(null=True, index=True) + label_value_float = FloatField(null=True, index=True) class TaskEventLog(Model): diff --git a/packages/api-server/api_server/repositories/tasks.py b/packages/api-server/api_server/repositories/tasks.py index 0bb993788..c857abfea 100644 --- a/packages/api-server/api_server/repositories/tasks.py +++ b/packages/api-server/api_server/repositories/tasks.py @@ -99,10 +99,11 @@ async def save_task_state(self, task_state: TaskState) -> None: ) return + # Since this is updating an existing task state, we are done if not created: return - # Save the labels that we want + # Labels are created and saved when a new task state is first received labels = task_state.booking.labels booking_label = None if labels is not None: