wrongmove/crawler/frontend/src/components/TaskIndicator.tsx

167 lines
5.8 KiB
TypeScript
Raw Normal View History

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 { useEffect, useState } from 'react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
import { Button } from './ui/button';
import { Loader2, CheckCircle2, XCircle, X } from 'lucide-react';
interface TaskIndicatorProps {
taskID: string | null;
onTaskCancelled?: () => void;
}
export function TaskIndicator({ taskID, onTaskCancelled }: TaskIndicatorProps) {
const [user, setUser] = useState<User | null>(null);
const [progressPercentage, setProgressPercentage] = useState<number>(0);
const [taskStatus, setTaskStatus] = useState<TaskStatus | null>(null);
const [isCancelling, setIsCancelling] = useState(false);
useEffect(() => {
getUser().then(setUser);
}, []);
useEffect(() => {
if (!user || !taskID) {
setTaskStatus(null);
return;
}
// Reset state for new task
setTaskStatus(TaskStatus.PENDING);
setProgressPercentage(0);
const pollTaskStatus = async () => {
try {
const data = await fetchTaskStatus(user, taskID);
const status = data.status as TaskStatus;
setTaskStatus(status);
if (status === TaskStatus.SUCCESS) {
setProgressPercentage(100);
return true; // Stop polling
}
if (status === TaskStatus.FAILURE || status === TaskStatus.REVOKED) {
return true; // Stop polling
}
// Parse progress for in-progress tasks
if (data.result) {
try {
const parsedResult: TaskResult = JSON.parse(data.result);
setProgressPercentage(parsedResult.progress * 100);
} catch {
// Ignore parsing errors
}
}
return false; // Continue polling
} catch {
setTaskStatus(TaskStatus.FAILURE);
return true; // Stop polling on error
}
};
// Initial poll
pollTaskStatus();
const interval = setInterval(async () => {
const shouldStop = await pollTaskStatus();
if (shouldStop) {
clearInterval(interval);
}
}, POLLING_INTERVALS.TASK_STATUS_MS);
return () => clearInterval(interval);
}, [taskID, user]);
const handleCancel = async () => {
if (!user || !taskID || isCancelling) return;
setIsCancelling(true);
try {
const result = await cancelTask(user, taskID);
if (result.success) {
setTaskStatus(TaskStatus.REVOKED);
onTaskCancelled?.();
}
} catch {
// Ignore cancel errors
} finally {
setIsCancelling(false);
}
};
if (!taskID || !taskStatus) {
return null;
}
const isInProgress = taskStatus !== TaskStatus.SUCCESS &&
taskStatus !== TaskStatus.FAILURE &&
taskStatus !== TaskStatus.REVOKED;
const getStatusIcon = () => {
if (isInProgress) {
return <Loader2 className="h-3.5 w-3.5 animate-spin text-blue-500" />;
}
if (taskStatus === TaskStatus.SUCCESS) {
return <CheckCircle2 className="h-3.5 w-3.5 text-green-500" />;
}
return <XCircle className="h-3.5 w-3.5 text-red-500" />;
};
const getTooltipContent = () => {
if (isInProgress) {
return `Task running: ${Math.round(progressPercentage)}%`;
}
if (taskStatus === TaskStatus.SUCCESS) {
return 'Task completed successfully';
}
if (taskStatus === TaskStatus.REVOKED) {
return 'Task was cancelled';
}
return 'Task failed';
};
return (
<TooltipProvider>
<div className="flex items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5 cursor-default">
{getStatusIcon()}
<span className="text-xs text-muted-foreground hidden sm:inline">
{isInProgress ? `${Math.round(progressPercentage)}%` : taskStatus}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>{getTooltipContent()}</p>
<p className="text-xs text-muted-foreground mt-1">ID: {taskID.slice(0, 8)}...</p>
</TooltipContent>
</Tooltip>
{isInProgress && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={handleCancel}
disabled={isCancelling}
className="h-6 w-6 text-muted-foreground hover:text-destructive"
>
<X className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Cancel task</p>
</TooltipContent>
</Tooltip>
)}
</div>
</TooltipProvider>
);
}