Add configurable scheduling, UI health/task indicators, and auto-load map with default filters

This commit is contained in:
Viktor Barzin 2026-02-01 17:28:37 +00:00
parent 1c8c3e4657
commit c7ac448f15
18 changed files with 2287 additions and 656 deletions

View file

@ -1,141 +1,128 @@
import { getUser } from '@/auth/authService';
import { POLLING_INTERVALS } from '@/constants';
import { fetchTaskStatus, cancelTask } from '@/services';
import { TaskStatus, type TaskResult } from '@/types';
import type { User } from 'oidc-client-ts';
import React, { useEffect, useState } from 'react';
import AlertError from './AlertError';
import { Spinner } from './Spinner';
import { HoverCard, HoverCardContent, HoverCardTrigger } from './ui/hover-card';
import { Progress } from './ui/progress';
import { Button } from './ui/button';
import { X } from 'lucide-react';
interface ModalProps {
interface ActiveQueryProps {
taskID: string | null;
onTaskCancelled?: () => void;
}
const fetchTaskStatusData = async (user: User, taskID: string) => {
const accessToken = user?.access_token;
const response = await fetch(`/api/task_status?task_id=${taskID}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`, // Pass the token
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch task status: ${response.status}`);
}
const data =
await response.json();
return data;
};
type TaskStatus = string
// enum TaskStatus {
// QUEUED = 'queued',
// PROCESSING = 'processing',
// COMPLETED = 'completed',
// FAILED = 'failed',
// }
// const taskStatusToProgress = (taskStatus: TaskStatus): number => {
// switch (taskStatus) {
// case TaskStatus.QUEUED:
// return 0.33; // Queued status
// case TaskStatus.PROCESSING:
// return 0.66; // Processing status
// case TaskStatus.COMPLETED:
// return 1.0; // Completed status
// default:
// throw new Error('Unknown task status: ' + status);
// }
// }
// const getTaskStatus = (status: string): TaskStatus => {
// switch (status.toLowerCase()) {
// case 'queued':
// return TaskStatus.QUEUED;
// case 'processing':
// return TaskStatus.PROCESSING;
// case 'completed':
// return TaskStatus.COMPLETED;
// case 'failed':
// return TaskStatus.FAILED;
// default:
// throw new Error('Unknown task status: ' + status);
// }
// };
const ActiveQuery: React.FC<ModalProps> = ({
taskID
}) => {
const ActiveQuery: React.FC<ActiveQueryProps> = ({ taskID, onTaskCancelled }) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
getUser().then(setUser);
}, []);
const [progressPercentage, setProgressPercentage] = useState<number>(0);
const [taskStatus, setTaskStatus] = useState<TaskStatus | null>("PENDING");
const [taskStatus, setTaskStatus] = useState<TaskStatus | null>(TaskStatus.PENDING);
const [lastUpdateTime, setLastUpdateTime] = useState<Date>(new Date());
const [fetchStatusError, setFetchStatusError] = useState<string | null>(null);
const [alertDialogIsOpen, setAlertDialogIsOpen] = useState(false);
const [isCancelling, setIsCancelling] = useState(false);
const fetchTaskStatus = async (interval: NodeJS.Timeout) => {
if (!user || !taskID) {
return;
}
let data = null
const handleCancelTask = async () => {
if (!user || !taskID || isCancelling) return;
setIsCancelling(true);
try {
data = await fetchTaskStatusData(user, taskID);
} catch (error: any) {
clearInterval(interval);
setTaskStatus("FAILURE")
setAlertDialogIsOpen(true)
if (error instanceof Error) {
setFetchStatusError(error.message)
const result = await cancelTask(user, taskID);
if (result.success) {
setTaskStatus(TaskStatus.REVOKED);
onTaskCancelled?.();
} else {
setFetchStatusError('Failed to update task status: ' + error.toString())
setFetchStatusError(result.message);
setAlertDialogIsOpen(true);
}
} catch (error) {
setFetchStatusError(error instanceof Error ? error.message : 'Failed to cancel task');
setAlertDialogIsOpen(true);
} finally {
setIsCancelling(false);
}
};
const pollTaskStatus = async (interval: NodeJS.Timeout) => {
if (!user || !taskID) {
return;
}
try {
const data = await fetchTaskStatus(user, taskID);
setLastUpdateTime(new Date());
const status = data.status as TaskStatus;
setTaskStatus(status);
if (status === TaskStatus.FAILURE || status === TaskStatus.REVOKED) {
clearInterval(interval);
setFetchStatusError('Task failed with status: ' + status);
setAlertDialogIsOpen(true);
return;
}
if (status === TaskStatus.SUCCESS) {
clearInterval(interval);
setProgressPercentage(100);
return;
}
// Only parse result for in-progress tasks
if (data.result) {
try {
const parsedResult: TaskResult = JSON.parse(data.result);
setProgressPercentage(parsedResult.progress * 100);
} catch {
// Result parsing failed, but task is still running - ignore
}
}
} catch (error) {
clearInterval(interval);
setTaskStatus(TaskStatus.FAILURE);
setAlertDialogIsOpen(true);
if (error instanceof Error) {
setFetchStatusError(error.message);
} else {
setFetchStatusError('Failed to update task status: ' + String(error));
}
}
if (!data) {
clearInterval(interval);
return;
}
setLastUpdateTime(new Date());
// const taskStatus = getTaskStatus(data.status);
const taskStatus = data.status;
setTaskStatus(taskStatus);
if (taskStatus === "FAILURE" || taskStatus === "REVOKED") {
clearInterval(interval);
throw new Error('Task failed. status: ' + taskStatus);
}
// const progress = taskStatusToProgress(taskStatus);
const parsedResult = JSON.parse(data.result)
setProgressPercentage(parsedResult.progress * 100);
if (taskStatus === "SUCCESS") {
clearInterval(interval);
return;
}
};
// fetch status periodically
// maybe move to ws one day
useEffect(() => {
const interval = setInterval
(() => fetchTaskStatus(interval), 5000); // every 5 seconds
const interval = setInterval(
() => pollTaskStatus(interval),
POLLING_INTERVALS.TASK_STATUS_MS
);
return () => clearInterval(interval);
}, [taskID]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [taskID, user]);
if (!taskID) {
return null;
}
const isInProgress = taskStatus &&
taskStatus !== TaskStatus.SUCCESS &&
taskStatus !== TaskStatus.FAILURE &&
taskStatus !== TaskStatus.REVOKED;
return (
<>
<div>
<div className="flex items-center gap-2 p-2 border-t bg-muted/50">
<HoverCard>
<HoverCardTrigger>
{taskStatus && <>Task status: {taskStatus} </>}
<Progress value={progressPercentage} />
{taskStatus && taskStatus !== 'SUCCESS' && taskStatus !== 'FAILURE' && taskStatus !== 'REVOKED' && <Spinner />}
<HoverCardTrigger className="flex-1">
<div className="flex items-center gap-2">
{taskStatus && <span className="text-sm">Task: {taskStatus}</span>}
{isInProgress && <Spinner />}
</div>
<Progress value={progressPercentage} className="mt-1" />
</HoverCardTrigger>
<HoverCardContent>
Task ID: {taskID}
@ -143,10 +130,22 @@ const ActiveQuery: React.FC<ModalProps> = ({
Last updated: {lastUpdateTime.toLocaleString()}
</HoverCardContent>
</HoverCard>
{isInProgress && (
<Button
variant="ghost"
size="sm"
onClick={handleCancelTask}
disabled={isCancelling}
className="h-8 px-2 text-destructive hover:text-destructive hover:bg-destructive/10"
>
<X className="h-4 w-4" />
<span className="ml-1 hidden sm:inline">Cancel</span>
</Button>
)}
</div>
<AlertError message={fetchStatusError} open={alertDialogIsOpen} setIsOpen={setAlertDialogIsOpen} />
</>
)
);
};
export default ActiveQuery;