diff --git a/packages/dashboard/src/components/app-base.tsx b/packages/dashboard/src/components/app-base.tsx index c40420773..0e76dc200 100644 --- a/packages/dashboard/src/components/app-base.tsx +++ b/packages/dashboard/src/components/app-base.tsx @@ -1,6 +1,8 @@ import { Alert, AlertProps, + Backdrop, + CircularProgress, createTheme, CssBaseline, GlobalStyles, @@ -14,6 +16,7 @@ import { loadSettings, saveSettings, Settings, ThemeMode } from '../settings'; import { AppController, AppControllerContext, SettingsContext } from './app-contexts'; import AppBar from './appbar'; import { AlertStore } from './alert-store'; +import { AppEvents } from './app-events'; import { DeliveryAlertStore } from './delivery-alert-store'; const DefaultAlertDuration = 2000; @@ -39,6 +42,7 @@ export function AppBase({ children }: React.PropsWithChildren<{}>): JSX.Element const [alertMessage, setAlertMessage] = React.useState(''); const [alertDuration, setAlertDuration] = React.useState(DefaultAlertDuration); const [extraAppbarIcons, setExtraAppbarIcons] = React.useState(null); + const [openLoadingBackdrop, setOpenLoadingBackdrop] = React.useState(false); const theme = React.useMemo(() => { switch (settings.themeMode) { @@ -70,10 +74,25 @@ export function AppBase({ children }: React.PropsWithChildren<{}>): JSX.Element [updateSettings], ); + React.useEffect(() => { + const sub = AppEvents.loadingBackdrop.subscribe((value) => { + setOpenLoadingBackdrop(value); + }); + return () => sub.unsubscribe(); + }, []); + return ( {settings.themeMode === ThemeMode.RmfDark && } + {openLoadingBackdrop && ( + theme.zIndex.drawer + 1 }} + open={openLoadingBackdrop} + > + + + )} diff --git a/packages/dashboard/src/components/app-events.ts b/packages/dashboard/src/components/app-events.ts index 72626d905..585979224 100644 --- a/packages/dashboard/src/components/app-events.ts +++ b/packages/dashboard/src/components/app-events.ts @@ -30,4 +30,5 @@ export const AppEvents = { levelSelect: new BehaviorSubject(null), justLoggedIn: new BehaviorSubject(false), resetCamera: new Subject<[x: number, y: number, z: number, zoom: number]>(), + loadingBackdrop: new Subject(), }; diff --git a/packages/dashboard/src/components/tasks/tasks-app.tsx b/packages/dashboard/src/components/tasks/tasks-app.tsx index b66e466d1..6645a448c 100644 --- a/packages/dashboard/src/components/tasks/tasks-app.tsx +++ b/packages/dashboard/src/components/tasks/tasks-app.tsx @@ -2,7 +2,7 @@ import DownloadIcon from '@mui/icons-material/Download'; import RefreshIcon from '@mui/icons-material/Refresh'; import { Box, - IconButton, + Button, Menu, MenuItem, styled, @@ -28,9 +28,10 @@ import { MicroAppProps } from '../micro-app'; import { RmfAppContext } from '../rmf-app'; import { TaskSchedule } from './task-schedule'; import { TaskSummary } from './task-summary'; -import { downloadCsvFull, downloadCsvMinimal } from './utils'; +import { exportCsvFull, exportCsvMinimal } from './utils'; const RefreshTaskQueueTableInterval = 15000; +const QueryLimit = 100; enum TaskTablePanel { QueueTable = 0, @@ -240,64 +241,89 @@ export const TasksApp = React.memo( selectedPanelIndex, ]); - const getAllTasks = async (timestamp: Date) => { + const getPastMonthTasks = async (timestamp: Date) => { if (!rmf) { return []; } - const resp = await rmf.tasksApi.queryTaskStatesTasksGet( - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - `0,${timestamp.getTime()}`, - undefined, - undefined, - undefined, - undefined, - '-unix_millis_start_time', - undefined, - ); - const allTasks = resp.data as TaskState[]; + const currentMillis = timestamp.getTime(); + const oneMonthMillis = 31 * 24 * 60 * 60 * 1000; + const allTasks: TaskState[] = []; + let queries: TaskState[] = []; + let queryIndex = 0; + do { + queries = ( + await rmf.tasksApi.queryTaskStatesTasksGet( + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + `${currentMillis - oneMonthMillis},${currentMillis}`, + undefined, + QueryLimit, + queryIndex * QueryLimit, + '-unix_millis_start_time', + undefined, + ) + ).data; + if (queries.length === 0) { + break; + } + + allTasks.push(...queries); + queryIndex += 1; + } while (queries.length !== 0); return allTasks; }; - const getAllTaskRequests = async (tasks: TaskState[]) => { + const getPastMonthTaskRequests = 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]; + 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; } - ++requestIndex; } return taskRequestMap; }; const exportTasksToCsv = async (minimal: boolean) => { + AppEvents.loadingBackdrop.next(true); const now = new Date(); - const allTasks = await getAllTasks(now); - const allTaskRequests = await getAllTaskRequests(allTasks); - if (!allTasks || !allTasks.length) { + const pastMonthTasks = await getPastMonthTasks(now); + + if (!pastMonthTasks || !pastMonthTasks.length) { return; } if (minimal) { - downloadCsvMinimal(now, allTasks, allTaskRequests); + // 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); } else { - downloadCsvFull(now, allTasks); + exportCsvFull(now, pastMonthTasks); } + AppEvents.loadingBackdrop.next(false); }; const [anchorExportElement, setAnchorExportElement] = React.useState( @@ -324,18 +350,27 @@ export const TasksApp = React.memo( toolbar={
- - + { - handleCloseExportMenu(); exportTasksToCsv(true); + handleCloseExportMenu(); }} disableRipple > @@ -357,8 +392,8 @@ export const TasksApp = React.memo( { - handleCloseExportMenu(); exportTasksToCsv(false); + handleCloseExportMenu(); }} disableRipple > @@ -366,16 +401,27 @@ export const TasksApp = React.memo(
- - +
} diff --git a/packages/dashboard/src/components/tasks/utils.ts b/packages/dashboard/src/components/tasks/utils.ts index b28de38bb..93d74a9f5 100644 --- a/packages/dashboard/src/components/tasks/utils.ts +++ b/packages/dashboard/src/components/tasks/utils.ts @@ -17,7 +17,7 @@ export function parseTasksFile(contents: string): TaskRequest[] { return obj; } -export function downloadCsvFull(timestamp: Date, allTasks: TaskState[]) { +export function exportCsvFull(timestamp: Date, allTasks: TaskState[]) { const columnSeparator = ';'; const rowSeparator = '\n'; let csvContent = `sep=${columnSeparator}` + rowSeparator; @@ -47,7 +47,7 @@ export function downloadCsvFull(timestamp: Date, allTasks: TaskState[]) { }); } -export function downloadCsvMinimal( +export function exportCsvMinimal( timestamp: Date, allTasks: TaskState[], taskRequestMap: Record,