Skip to content

Commit

Permalink
Using Ajv properly, with basic json stringify and parse
Browse files Browse the repository at this point in the history
Signed-off-by: Aaron Chong <[email protected]>
  • Loading branch information
aaronchongth committed Apr 29, 2024
1 parent 440dc2c commit 0ec29e0
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 72 deletions.
8 changes: 6 additions & 2 deletions packages/api-server/api_server/repositories/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
24 changes: 14 additions & 10 deletions packages/dashboard/src/components/tasks/task-summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -80,7 +84,7 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => {

const [openTaskDetailsLogs, setOpenTaskDetailsLogs] = React.useState(false);
const [taskState, setTaskState] = React.useState<TaskState | null>(null);
const [label, setLabel] = React.useState<TaskRequestLabel>({});
const [label, setLabel] = React.useState<TaskRequestLabel>({ description: {} });
const [isOpen, setIsOpen] = React.useState(true);

const taskProgress = React.useMemo(() => {
Expand All @@ -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);
});
Expand All @@ -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',
Expand Down
10 changes: 6 additions & 4 deletions packages/dashboard/src/components/tasks/utils.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
64 changes: 45 additions & 19 deletions packages/react-components/lib/tasks/create-task.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -1288,7 +1292,7 @@ export function CreateTaskForm({
const [taskRequest, setTaskRequest] = React.useState<TaskRequest>(
() => requestTask ?? defaultTask(),
);
const [requestLabel, setRequestLabel] = React.useState<TaskRequestLabel | null>(null);
const [requestLabel, setRequestLabel] = React.useState<TaskRequestLabel>({ description: {} });

const [submitting, setSubmitting] = React.useState(false);
const [formFullyFilled, setFormFullyFilled] = React.useState(requestTask !== undefined || false);
Expand Down Expand Up @@ -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}
Expand All @@ -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}
Expand All @@ -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}
Expand All @@ -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}
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-components/lib/tasks/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
54 changes: 54 additions & 0 deletions packages/react-components/lib/tasks/task-request-label-utils.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 0 additions & 13 deletions packages/react-components/lib/tasks/task-request-label.tsx

This file was deleted.

14 changes: 7 additions & 7 deletions packages/react-components/lib/tasks/task-table-datagrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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';
},
Expand All @@ -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';
},
Expand Down
16 changes: 0 additions & 16 deletions packages/react-components/lib/tasks/utils.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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') {
Expand Down
1 change: 1 addition & 0 deletions packages/react-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0ec29e0

Please sign in to comment.