Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Direct dispatch to fleet or robot #1004

Merged
merged 23 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3213eea
Basic implementation of priority based on legacy implementation
aaronchongth Aug 15, 2024
caa8fd7
Use icons for favorite
aaronchongth Aug 16, 2024
f9b2efa
Fix warning time, use switch and made buttons nicer
aaronchongth Aug 16, 2024
2bdd4d3
To display priority
aaronchongth Aug 16, 2024
77950be
Lint
aaronchongth Aug 19, 2024
09d6e57
Merge branch 'main' into feat/labels-priority-schedule
aaronchongth Aug 19, 2024
a6a4e41
Helper function to handle null and undefined priority, clean up creation
aaronchongth Aug 20, 2024
0703b77
Use switch for priority, flip low priority icon
aaronchongth Aug 20, 2024
6a5ff5a
Remove getDefaultTaskPriorty
aaronchongth Aug 20, 2024
ce848a2
Basic implementation working, still with frontend errors
aaronchongth Aug 22, 2024
ca9ecf9
Fix component render sequence issues
aaronchongth Aug 30, 2024
a29954b
Lint
aaronchongth Aug 30, 2024
a310646
Merge branch 'main' into feature/fleet-robot-selection
aaronchongth Sep 3, 2024
cdf7fba
Fix capitalization, disable scheuling for robot dispatch, allow edit …
aaronchongth Sep 4, 2024
35f92a8
Comments and consolidate logs
aaronchongth Sep 4, 2024
d9c8fba
Slight tree view for robots, change dropdown size, use const enum
aaronchongth Sep 4, 2024
7ba2064
Refactor dispatch and schedule callbacks, fix edit schedule event, ad…
aaronchongth Sep 5, 2024
bfd018a
Merge branch 'main' into feature/fleet-robot-selection
aaronchongth Sep 5, 2024
ad263a0
Remove stale print
aaronchongth Sep 5, 2024
3692aa7
lint
aaronchongth Sep 5, 2024
4dece1e
Make task form more generically named, new callback for schedule editing
aaronchongth Sep 14, 2024
adb317f
Merge branch 'main' into feature/fleet-robot-selection
aaronchongth Sep 17, 2024
9c592af
Document props
aaronchongth Sep 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 28 additions & 36 deletions packages/dashboard/src/components/appbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,19 @@ import {
import { AlertRequest, FireAlarmTriggerState, TaskFavorite } from 'api-client';
import { formatDistance } from 'date-fns';
import React from 'react';
import { ConfirmationDialog, CreateTaskForm, CreateTaskFormProps } from 'react-components';
import { ConfirmationDialog, TaskForm, TaskFormProps } from 'react-components';
import { Subscription } from 'rxjs';

import { useAppController } from '../hooks/use-app-controller';
import { useAuthenticator } from '../hooks/use-authenticator';
import { useCreateTaskFormData } from '../hooks/use-create-task-form';
import { useTaskFormData } from '../hooks/use-create-task-form';
import { useResources } from '../hooks/use-resources';
import { useRmfApi } from '../hooks/use-rmf-api';
import { useSettings } from '../hooks/use-settings';
import { useTaskRegistry } from '../hooks/use-task-registry';
import { useUserProfile } from '../hooks/use-user-profile';
import { AppEvents } from './app-events';
import { toApiSchedule } from './tasks/utils';
import { dispatchTask, scheduleTask } from './tasks/utils';
import { DashboardThemes } from './theme';

export const APP_BAR_HEIGHT = '3.5rem';
Expand Down Expand Up @@ -116,8 +116,8 @@ export const AppBar = React.memo(
FireAlarmTriggerState | undefined
>(undefined);

const { waypointNames, pickupPoints, dropoffPoints, cleaningZoneNames } =
useCreateTaskFormData(rmfApi);
const { waypointNames, pickupPoints, dropoffPoints, cleaningZoneNames, fleets } =
useTaskFormData(rmfApi);
const username = profile.user.username;

async function handleLogout(): Promise<void> {
Expand Down Expand Up @@ -147,30 +147,23 @@ export const AppBar = React.memo(
return () => subs.forEach((s) => s.unsubscribe());
}, [rmfApi]);

const submitTasks = React.useCallback<Required<CreateTaskFormProps>['submitTasks']>(
async (taskRequests, schedule) => {
if (!schedule) {
await Promise.all(
taskRequests.map((request) => {
console.debug('submitTask:');
console.debug(request);
return rmfApi.tasksApi.postDispatchTaskTasksDispatchTaskPost({
type: 'dispatch_task_request',
request,
});
}),
);
} else {
const scheduleRequests = taskRequests.map((req) => {
console.debug('schedule task:');
console.debug(req);
console.debug(schedule);
return toApiSchedule(req, schedule);
});
await Promise.all(
scheduleRequests.map((req) => rmfApi.tasksApi.postScheduledTaskScheduledTasksPost(req)),
);
const dispatchTaskCallback = React.useCallback<Required<TaskFormProps>['onDispatchTask']>(
async (taskRequest, robotDispatchTarget) => {
if (!rmfApi) {
throw new Error('tasks api not available');
}
await dispatchTask(rmfApi, taskRequest, robotDispatchTarget);
AppEvents.refreshTaskApp.next();
},
[rmfApi],
);

const scheduleTaskCallback = React.useCallback<Required<TaskFormProps>['onScheduleTask']>(
async (taskRequest, schedule) => {
if (!rmfApi) {
throw new Error('tasks api not available');
}
await scheduleTask(rmfApi, taskRequest, schedule);
AppEvents.refreshTaskApp.next();
},
[rmfApi],
Expand All @@ -189,19 +182,15 @@ export const AppBar = React.memo(
return () => sub.unsubscribe();
}, [rmfApi]);

const submitFavoriteTask = React.useCallback<
Required<CreateTaskFormProps>['submitFavoriteTask']
>(
const submitFavoriteTask = React.useCallback<Required<TaskFormProps>['submitFavoriteTask']>(
async (taskFavoriteRequest) => {
await rmfApi.tasksApi.postFavoriteTaskFavoriteTasksPost(taskFavoriteRequest);
AppEvents.refreshFavoriteTasks.next();
},
[rmfApi],
);

const deleteFavoriteTask = React.useCallback<
Required<CreateTaskFormProps>['deleteFavoriteTask']
>(
const deleteFavoriteTask = React.useCallback<Required<TaskFormProps>['deleteFavoriteTask']>(
async (favoriteTask) => {
if (!favoriteTask.id) {
throw new Error('Id is needed');
Expand Down Expand Up @@ -464,8 +453,9 @@ export const AppBar = React.memo(
</Menu>

{openCreateTaskForm && (
<CreateTaskForm
<TaskForm
user={username ? username : 'unknown user'}
fleets={fleets}
tasksToDisplay={taskRegistry.taskDefinitions}
patrolWaypoints={waypointNames}
cleaningZones={cleaningZoneNames}
Expand All @@ -476,7 +466,9 @@ export const AppBar = React.memo(
favoritesTasks={favoritesTasks}
open={openCreateTaskForm}
onClose={() => setOpenCreateTaskForm(false)}
submitTasks={submitTasks}
onDispatchTask={dispatchTaskCallback}
onScheduleTask={scheduleTaskCallback}
onEditScheduleTask={undefined}
submitFavoriteTask={submitFavoriteTask}
deleteFavoriteTask={deleteFavoriteTask}
onSuccess={() => {
Expand Down
82 changes: 53 additions & 29 deletions packages/dashboard/src/components/tasks/task-schedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import { ScheduledTask, ScheduledTaskScheduleOutput as ApiSchedule } from 'api-c
import React from 'react';
import {
ConfirmationDialog,
CreateTaskForm,
CreateTaskFormProps,
EventEditDeletePopup,
Schedule,
TaskForm,
TaskFormProps,
} from 'react-components';

import { useAppController } from '../../hooks/use-app-controller';
import { useCreateTaskFormData } from '../../hooks/use-create-task-form';
import { useTaskFormData } from '../../hooks/use-create-task-form';
import { useRmfApi } from '../../hooks/use-rmf-api';
import { useTaskRegistry } from '../../hooks/use-task-registry';
import { useUserProfile } from '../../hooks/use-user-profile';
Expand All @@ -33,7 +33,7 @@ import {
scheduleWithSelectedDay,
toISOStringWithTimezone,
} from './task-schedule-utils';
import { toApiSchedule } from './utils';
import { dispatchTask, editScheduledTaskEvent, editScheduledTaskSchedule } from './utils';

enum EventScopes {
ALL = 'all',
Expand Down Expand Up @@ -71,8 +71,8 @@ export const TaskSchedule = () => {
const rmfApi = useRmfApi();
const { showAlert } = useAppController();

const { waypointNames, pickupPoints, dropoffPoints, cleaningZoneNames } =
useCreateTaskFormData(rmfApi);
const { waypointNames, pickupPoints, dropoffPoints, cleaningZoneNames, fleets } =
useTaskFormData(rmfApi);
const username = useUserProfile().user.username;
const taskRegistry = useTaskRegistry();
const [eventScope, setEventScope] = React.useState<string>(EventScopes.CURRENT);
Expand Down Expand Up @@ -191,30 +191,51 @@ export const TaskSchedule = () => {
);
};

const submitTasks = React.useCallback<Required<CreateTaskFormProps>['submitTasks']>(
async (taskRequests, schedule) => {
if (!schedule || !currentScheduleTask) {
throw new Error('No schedule or task selected for submission.');
const dispatchTaskCallback = React.useCallback<Required<TaskFormProps>['onDispatchTask']>(
async (taskRequest, robotDispatchTarget) => {
if (!rmfApi) {
throw new Error('tasks api not available');
}
await dispatchTask(rmfApi, taskRequest, robotDispatchTarget);
AppEvents.refreshTaskApp.next();
},
[rmfApi],
);

const scheduleRequests = taskRequests.map((req) => toApiSchedule(req, schedule));
const editScheduledTaskCallback = React.useCallback<
Required<TaskFormProps>['onEditScheduleTask']
>(
async (taskRequest, schedule) => {
if (!rmfApi) {
throw new Error('tasks api not available');
}

let exceptDate: string | undefined = undefined;
if (eventScope === EventScopes.CURRENT) {
exceptDate = toISOStringWithTimezone(exceptDateRef.current);
console.debug(`Editing schedule id ${currentScheduleTask.id}, event date ${exceptDate}`);
} else {
console.debug(`Editing schedule id ${currentScheduleTask.id}`);
if (!currentScheduleTask) {
throw new Error('No schedule task selected for submission.');
}

await Promise.all(
scheduleRequests.map((req) =>
rmfApi.tasksApi.updateScheduleTaskScheduledTasksTaskIdUpdatePost(
currentScheduleTask.id,
req,
exceptDate,
),
),
// Edit entire schedule
if (eventScope !== EventScopes.CURRENT) {
console.debug(
`Editing schedule id [${currentScheduleTask.id}] with new schedule: ${schedule}`,
);
await editScheduledTaskSchedule(rmfApi, taskRequest, schedule, currentScheduleTask.id);

setEventScope(EventScopes.CURRENT);
AppEvents.refreshTaskSchedule.next();
return;
}

// Edit a single event
console.debug(
`Editing schedule id [${currentScheduleTask.id}] event [${exceptDateRef.current}] with new schedule: ${schedule}`,
);
await editScheduledTaskEvent(
rmfApi,
taskRequest,
schedule,
exceptDateRef.current,
currentScheduleTask.id,
);

setEventScope(EventScopes.CURRENT);
Expand Down Expand Up @@ -316,22 +337,25 @@ export const TaskSchedule = () => {
onSelectedDateChange={setSelectedDate}
/>
{openCreateTaskForm && (
<CreateTaskForm
<TaskForm
user={username ? username : 'unknown user'}
fleets={fleets}
tasksToDisplay={taskRegistry.taskDefinitions}
patrolWaypoints={waypointNames}
cleaningZones={cleaningZoneNames}
pickupPoints={pickupPoints}
dropoffPoints={dropoffPoints}
open={openCreateTaskForm}
scheduleToEdit={scheduleToEdit}
requestTask={currentScheduleTask?.task_request}
schedule={scheduleToEdit}
taskRequest={currentScheduleTask?.task_request}
onClose={() => {
setOpenCreateTaskForm(false);
setEventScope(EventScopes.CURRENT);
AppEvents.refreshTaskSchedule.next();
}}
submitTasks={submitTasks}
onDispatchTask={dispatchTaskCallback}
onScheduleTask={undefined}
onEditScheduleTask={editScheduledTaskCallback}
onSuccess={() => {
setOpenCreateTaskForm(false);
showAlert('success', 'Successfully created task');
Expand Down
82 changes: 80 additions & 2 deletions packages/dashboard/src/components/tasks/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { PostScheduledTaskRequest, TaskRequest, TaskStateOutput as TaskState } from 'api-client';
import { getTaskBookingLabelFromTaskState, Schedule } from 'react-components';
import {
AddExceptDateRequest,
PostScheduledTaskRequest,
RobotTaskRequest,
TaskRequest,
TaskStateOutput as TaskState,
} from 'api-client';
import { getTaskBookingLabelFromTaskState, RobotDispatchTarget, Schedule } from 'react-components';

import { RmfApi } from '../../services/rmf-api';
import { toISOStringWithTimezone } from './task-schedule-utils';

export function exportCsvFull(timestamp: Date, allTasks: TaskState[]) {
const columnSeparator = ';';
Expand Down Expand Up @@ -115,3 +124,72 @@ export const toApiSchedule = (
schedules: apiSchedules,
};
};

export async function dispatchTask(
rmf: RmfApi,
taskRequest: TaskRequest,
robotDispatchTarget: RobotDispatchTarget | null,
) {
if (robotDispatchTarget) {
const robotTask: RobotTaskRequest = {
type: 'robot_task_request',
robot: robotDispatchTarget.robot,
fleet: robotDispatchTarget.fleet,
request: taskRequest,
};
console.debug(`dispatch robot task: ${robotTask}`);
await rmf.tasksApi.postRobotTaskTasksRobotTaskPost(robotTask);
return;
}

console.debug(`dispatch task: ${taskRequest}`);
await rmf.tasksApi.postDispatchTaskTasksDispatchTaskPost({
type: 'dispatch_task_request',
request: taskRequest,
});
}

export async function scheduleTask(rmf: RmfApi, taskRequest: TaskRequest, schedule: Schedule) {
console.debug(`schedule task: ${taskRequest}\nschedule: ${schedule}`);
const scheduleRequest = toApiSchedule(taskRequest, schedule);
await rmf.tasksApi.postScheduledTaskScheduledTasksPost(scheduleRequest);
}

export async function editScheduledTaskEvent(
rmf: RmfApi,
taskRequest: TaskRequest,
newEventSchedule: Schedule,
newEventDate: Date,
originalScheduleTaskId: number,
) {
const addExceptDateRequest: AddExceptDateRequest = {
except_date: toISOStringWithTimezone(newEventDate),
};
console.debug(
`Adding [${addExceptDateRequest.except_date}] to except date of scheduled task [${originalScheduleTaskId}]`,
);
await rmf.tasksApi.addExceptDateScheduledTasksTaskIdExceptDatePost(
originalScheduleTaskId,
addExceptDateRequest,
);

console.debug(
`creating new schedule for edited event: ${taskRequest}\nschedule: ${newEventSchedule}`,
);
const newScheduleRequest = toApiSchedule(taskRequest, newEventSchedule);
await rmf.tasksApi.postScheduledTaskScheduledTasksPost(newScheduleRequest);
}

export async function editScheduledTaskSchedule(
rmf: RmfApi,
taskRequest: TaskRequest,
newSchedule: Schedule,
scheduleTaskId: number,
) {
const scheduleRequest = toApiSchedule(taskRequest, newSchedule);

await rmf.tasksApi.updateScheduleTaskScheduledTasksTaskIdUpdatePost(
scheduleTaskId,
scheduleRequest,
);
}
17 changes: 15 additions & 2 deletions packages/dashboard/src/hooks/use-create-task-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { Subscription } from 'rxjs';

import { RmfApi } from '../services/rmf-api';

export const useCreateTaskFormData = (rmfApi: RmfApi | undefined) => {
export const useTaskFormData = (rmfApi: RmfApi | undefined) => {
const [waypointNames, setWaypointNames] = React.useState<string[]>([]);
const [cleaningZoneNames, setCleaningZoneNames] = React.useState<string[]>([]);
const [pickupPoints, setPickupPoints] = React.useState<Record<string, string>>({});
const [dropoffPoints, setDropoffPoints] = React.useState<Record<string, string>>({});
const [fleets, setFleets] = React.useState<Record<string, string[]>>({});

React.useEffect(() => {
if (!rmfApi) {
Expand Down Expand Up @@ -42,9 +43,21 @@ export const useCreateTaskFormData = (rmfApi: RmfApi | undefined) => {
setWaypointNames(waypointNames);
}),
);
subs.push(
rmfApi.fleetsObs.subscribe((fleetStates) => {
const result: Record<string, string[]> = {};
for (const fleet of fleetStates) {
if (!fleet.name || !fleet.robots) {
continue;
}
result[fleet.name] = Object.keys(fleet.robots);
}
setFleets(result);
}),
);

return () => subs.forEach((s) => s.unsubscribe());
}, [rmfApi]);

return { waypointNames, pickupPoints, dropoffPoints, cleaningZoneNames };
return { waypointNames, pickupPoints, dropoffPoints, cleaningZoneNames, fleets };
};
1 change: 1 addition & 0 deletions packages/dashboard/src/services/rmf-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ export class DefaultRmfApi implements RmfApi {
return this._ingestorStateObsStore[guid];
}

// NOTE: This only emits once and doesn't update when the fleet changes.
fleetsObs: Observable<FleetState[]>;
private _fleetStateObsStore: Record<string, Observable<FleetState>> = {};
getFleetStateObs(name: string): Observable<FleetState> {
Expand Down
Loading
Loading