From f31dce3250a895e187a8cd26aeece4cc3f14843e Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Tue, 28 Nov 2023 06:34:30 +0000 Subject: [PATCH] Export minimal with new columns Signed-off-by: Aaron Chong --- .../src/components/tasks/tasks-app.tsx | 24 +++- .../dashboard/src/components/tasks/utils.ts | 40 ++++-- packages/react-components/lib/tasks/index.ts | 1 + .../lib/tasks/task-table-datagrid.tsx | 116 ++---------------- packages/react-components/lib/tasks/utils.ts | 105 +++++++++++++++- 5 files changed, 165 insertions(+), 121 deletions(-) diff --git a/packages/dashboard/src/components/tasks/tasks-app.tsx b/packages/dashboard/src/components/tasks/tasks-app.tsx index 59f5c4f93..676f39ffb 100644 --- a/packages/dashboard/src/components/tasks/tasks-app.tsx +++ b/packages/dashboard/src/components/tasks/tasks-app.tsx @@ -242,14 +242,36 @@ export const TasksApp = React.memo( return allTasks; }; + const getAllTaskRequests = async (tasks: TaskState[]) => { + if (!rmf) { + return {}; + } + + const taskIds: string[] = tasks.map((task) => task.booking.id); + const taskIdsQuery = taskIds.join(','); + const taskRequests = (await rmf.tasksApi.queryTaskRequestsTasksRequestsGet(taskIdsQuery)) + .data; + + const taskRequestMap: Record = {}; + 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) => { const now = new Date(); const allTasks = await getAllTasks(now); + const allTaskRequests = await getAllTaskRequests(allTasks); if (!allTasks || !allTasks.length) { return; } if (minimal) { - downloadCsvMinimal(now, allTasks); + downloadCsvMinimal(now, allTasks, allTaskRequests); } else { downloadCsvFull(now, allTasks); } diff --git a/packages/dashboard/src/components/tasks/utils.ts b/packages/dashboard/src/components/tasks/utils.ts index 70bbb4d04..571712fc3 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 } from 'react-components'; +import { Schedule, parsePickup, parseDestination } from 'react-components'; import schema from 'api-client/dist/schema'; import { ajv } from '../utils'; @@ -46,40 +46,56 @@ export function downloadCsvFull(timestamp: Date, allTasks: TaskState[]) { }); } -export function downloadCsvMinimal(timestamp: Date, allTasks: TaskState[]) { +export function downloadCsvMinimal( + timestamp: Date, + allTasks: TaskState[], + taskRequestMap: Record, +) { const columnSeparator = ';'; const rowSeparator = '\n'; const keys = [ 'Date', 'Requester', - 'ID', 'Category', - 'Assignee', + 'Pickup', + 'Destination', + 'Robot', 'Start Time', 'End Time', 'State', ]; let csvContent = keys.join(columnSeparator) + rowSeparator; allTasks.forEach((task) => { + const request: TaskRequest | undefined = taskRequestMap[task.booking.id]; const values = [ + // Date task.booking.unix_millis_request_time ? `${new Date(task.booking.unix_millis_request_time).toLocaleDateString()}` - : 'unknown', - task.booking.requester ? task.booking.requester : 'unknown', - task.booking.id, - task.category ? task.category : 'unknown', - task.assigned_to ? task.assigned_to.name : 'unknown', + : 'n/a', + // Requester + task.booking.requester ? task.booking.requester : 'n/a', + // Category + task.category ? task.category : 'n/a', + // Pickup + parsePickup(task, request), + // Destination + parseDestination(task, request), + // Robot + task.assigned_to ? task.assigned_to.name : 'n/a', + // Start Time task.unix_millis_start_time ? `${new Date(task.unix_millis_start_time).toLocaleDateString()} ${new Date( task.unix_millis_start_time, ).toLocaleTimeString()}` - : 'unknown', + : 'n/a', + // End Time task.unix_millis_finish_time ? `${new Date(task.unix_millis_finish_time).toLocaleDateString()} ${new Date( task.unix_millis_finish_time, ).toLocaleTimeString()}` - : 'unknown', - task.status ? task.status : 'unknown', + : 'n/a', + // State + task.status ? task.status : 'n/a', ]; csvContent += values.join(columnSeparator) + rowSeparator; }); diff --git a/packages/react-components/lib/tasks/index.ts b/packages/react-components/lib/tasks/index.ts index 250227325..6cc17baaf 100644 --- a/packages/react-components/lib/tasks/index.ts +++ b/packages/react-components/lib/tasks/index.ts @@ -5,3 +5,4 @@ export * from './task-table'; export * from './task-timeline'; export * from './task-table-datagrid'; export * from './task-schedule-event-edit-delete-popup'; +export * from './utils'; diff --git a/packages/react-components/lib/tasks/task-table-datagrid.tsx b/packages/react-components/lib/tasks/task-table-datagrid.tsx index 14daa0e11..cb62070f8 100644 --- a/packages/react-components/lib/tasks/task-table-datagrid.tsx +++ b/packages/react-components/lib/tasks/task-table-datagrid.tsx @@ -15,6 +15,7 @@ import { styled, TextField, Stack, Typography, Tooltip, useMediaQuery } from '@m 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'; const classes = { taskActiveCell: 'MuiDataGrid-cell-active-cell', @@ -134,111 +135,6 @@ export function TaskDataGridTable({ (operator) => operator.value === 'equals', ); - const getPickup = (state: TaskState): string => { - const request: TaskRequest | undefined = tasks.requests[state.booking.id]; - if (request === undefined || request.category.toLowerCase() === 'patrol') { - return 'n/a'; - } - - // custom deliveries - const supportedDeliveries = [ - 'delivery_pickup', - 'delivery_sequential_lot_pickup', - 'delivery_area_pickup', - ]; - if ( - !request.description['category'] || - !supportedDeliveries.includes(request.description['category']) - ) { - return 'n/a'; - } - - // TODO(ac): use schemas - try { - const deliveryType: string = request.description['category']; - const perform_action_description = - request.description['phases'][0]['activity']['description']['activities'][1]['description'][ - 'description' - ]; - - switch (deliveryType) { - case 'delivery_pickup': { - const pickup_lot: string = perform_action_description['pickup_lot']; - return pickup_lot; - } - case 'delivery_sequential_lot_pickup': - case 'delivery_area_pickup': { - const pickup_zone: string = perform_action_description['pickup_zone']; - return pickup_zone; - } - default: - return 'n/a'; - } - } catch (e) { - console.error(`Failed to parse pickup lot/zone from task request: ${(e as Error).message}`); - } - - return 'n/a'; - }; - - const getDestination = (state: TaskState): string => { - const request: TaskRequest | undefined = tasks.requests[state.booking.id]; - if (request === undefined) { - return 'n/a'; - } - - // patrol - if ( - request.category.toLowerCase() === 'patrol' && - request.description['places'] !== undefined && - request.description['places'].length > 0 - ) { - return request.description['places'].at(-1); - } - - // custom deliveries - const supportedDeliveries = [ - 'delivery_pickup', - 'delivery_sequential_lot_pickup', - 'delivery_area_pickup', - ]; - if ( - !request.description['category'] || - !supportedDeliveries.includes((request.description['category'] as string).toLowerCase()) - ) { - return 'n/a'; - } - - // TODO(ac): use schemas - try { - const destination = - request.description['phases'][0]['activity']['description']['activities'][2]['description']; - return destination; - } catch (e) { - console.error(`Failed to parse destination from task request: ${(e as Error).message}`); - } - - // automated tasks that can only be parsed with state - if (state.category && state.category === 'Charge Battery') { - try { - const charge_phase = state['phases'] ? state['phases']['1'] : undefined; - const charge_events = charge_phase ? charge_phase.events : undefined; - const charge_event_name = charge_events ? charge_events['1'].name : undefined; - if (charge_event_name === undefined) { - console.error('Unable to parse charging event name.'); - return 'n/a'; - } - - const charging_place = charge_event_name?.split('[place:')[1].split(']')[0]; - return charging_place; - } catch (e) { - console.error(`Failed to parse charging point from task state: ${(e as Error).message}`); - } - } - - return 'n/a'; - }; - const getMinimalDateOperators = getGridDateOperators(true).filter( (operator) => operator.value === 'onOrAfter' || operator.value === 'onOrBefore', ); @@ -295,7 +191,10 @@ export function TaskDataGridTable({ headerName: 'Pickup', width: 150, editable: false, - valueGetter: (params: GridValueGetterParams) => getPickup(params.row), + valueGetter: (params: GridValueGetterParams) => { + const request: TaskRequest | undefined = tasks.requests[params.row.booking.id]; + return parsePickup(params.row, request); + }, flex: 1, filterOperators: getMinimalStringFilterOperators, filterable: true, @@ -305,7 +204,10 @@ export function TaskDataGridTable({ headerName: 'Destination', width: 150, editable: false, - valueGetter: (params: GridValueGetterParams) => getDestination(params.row), + valueGetter: (params: GridValueGetterParams) => { + const request: TaskRequest | undefined = tasks.requests[params.row.booking.id]; + return parseDestination(params.row, request); + }, flex: 1, filterOperators: getMinimalStringFilterOperators, filterable: true, diff --git a/packages/react-components/lib/tasks/utils.ts b/packages/react-components/lib/tasks/utils.ts index dd1f00363..9dd08b319 100644 --- a/packages/react-components/lib/tasks/utils.ts +++ b/packages/react-components/lib/tasks/utils.ts @@ -1,5 +1,5 @@ import { TaskType as RmfTaskType } from 'rmf-models'; -import type { TaskState } from 'api-client'; +import type { TaskState, TaskRequest } from 'api-client'; export function taskTypeToStr(taskType: number): string { switch (taskType) { @@ -46,3 +46,106 @@ export function parseTaskDetail( return {}; } } + +export function parsePickup(state: TaskState, request?: TaskRequest): string { + if (request === undefined || request.category.toLowerCase() === 'patrol') { + return 'n/a'; + } + + // custom deliveries + const supportedDeliveries = [ + 'delivery_pickup', + 'delivery_sequential_lot_pickup', + 'delivery_area_pickup', + ]; + if ( + !request.description['category'] || + !supportedDeliveries.includes(request.description['category']) + ) { + return 'n/a'; + } + + // TODO(ac): use schemas + try { + const deliveryType: string = request.description['category']; + const perform_action_description = + request.description['phases'][0]['activity']['description']['activities'][1]['description'][ + 'description' + ]; + + switch (deliveryType) { + case 'delivery_pickup': { + const pickup_lot: string = perform_action_description['pickup_lot']; + return pickup_lot; + } + case 'delivery_sequential_lot_pickup': + case 'delivery_area_pickup': { + const pickup_zone: string = perform_action_description['pickup_zone']; + return pickup_zone; + } + default: + return 'n/a'; + } + } catch (e) { + console.error(`Failed to parse pickup lot/zone from task request: ${(e as Error).message}`); + } + + return 'n/a'; +} + +export function parseDestination(state: TaskState, request: TaskRequest): string { + if (request === undefined) { + return 'n/a'; + } + + // patrol + if ( + request.category.toLowerCase() === 'patrol' && + request.description['places'] !== undefined && + request.description['places'].length > 0 + ) { + return request.description['places'].at(-1); + } + + // custom deliveries + const supportedDeliveries = [ + 'delivery_pickup', + 'delivery_sequential_lot_pickup', + 'delivery_area_pickup', + ]; + if ( + !request.description['category'] || + !supportedDeliveries.includes((request.description['category'] as string).toLowerCase()) + ) { + return 'n/a'; + } + + // TODO(ac): use schemas + try { + const destination = + request.description['phases'][0]['activity']['description']['activities'][2]['description']; + return destination; + } catch (e) { + console.error(`Failed to parse destination from task request: ${(e as Error).message}`); + } + + // automated tasks that can only be parsed with state + if (state.category && state.category === 'Charge Battery') { + try { + const charge_phase = state['phases'] ? state['phases']['1'] : undefined; + const charge_events = charge_phase ? charge_phase.events : undefined; + const charge_event_name = charge_events ? charge_events['1'].name : undefined; + if (charge_event_name === undefined) { + console.error('Unable to parse charging event name.'); + return 'n/a'; + } + + const charging_place = charge_event_name?.split('[place:')[1].split(']')[0]; + return charging_place; + } catch (e) { + console.error(`Failed to parse charging point from task state: ${(e as Error).message}`); + } + } + + return 'n/a'; +}