1
0
cvsa/packages/ml_panel/src/components/TaskMonitor.tsx

211 lines
6.3 KiB
TypeScript

import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Progress } from "@/components/ui/progress";
import { Badge } from "@/components/ui/badge";
import { RefreshCw, Play, Pause, CheckCircle, XCircle, Clock } from "lucide-react";
import { apiClient } from "@/lib/api";
import type { TasksResponse } from "@/types/api";
import { Spinner } from "@/components/ui/spinner"
export function TaskMonitor() {
const [statusFilter, setStatusFilter] = useState<string>("all");
// Fetch tasks
const {
data: tasksData,
isLoading: tasksLoading,
refetch: refetchTasks
} = useQuery<TasksResponse>({
queryKey: ["tasks", statusFilter],
queryFn: () => {
const params = statusFilter === "all" ? {} : { status: statusFilter };
return apiClient.getTasks(params.status, 50);
},
refetchInterval: 5000 // Refresh every 5 seconds
});
const getStatusIcon = (status: string) => {
switch (status) {
case "running":
return <Play className="h-4 w-4 text-blue-500" />;
case "completed":
return <CheckCircle className="h-4 w-4 text-green-500" />;
case "failed":
return <XCircle className="h-4 w-4 text-red-500" />;
case "pending":
return <Clock className="h-4 w-4 text-yellow-500" />;
default:
return <Pause className="h-4 w-4 text-gray-500" />;
}
};
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case "running":
return "default";
case "completed":
return "secondary";
case "failed":
return "destructive";
case "pending":
return "outline";
default:
return "outline";
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString("en-US");
};
const formatDuration = (start: string, end?: string) => {
const startTime = new Date(start).getTime();
const endTime = end ? new Date(end).getTime() : Date.now();
const duration = Math.floor((endTime - startTime) / 1000);
if (duration < 60) return `${duration}s`;
if (duration < 3600) return `${Math.floor(duration / 60)}m ${duration % 60}s`;
return `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`;
};
if (tasksLoading) {
return (
<div className="flex items-center justify-center h-64">
<Spinner/>
</div>
);
}
return (
<div className="space-y-6">
{/* Controls */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Task Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="running">Running</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="failed">Failed</SelectItem>
</SelectContent>
</Select>
</div>
<Button variant="outline" size="sm" onClick={() => refetchTasks()}>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
{/* Tasks List */}
<div className="space-y-3">
{tasksData?.tasks && tasksData.tasks.length > 0 ? (
tasksData.tasks.map((task: any) => (
<Card key={task.task_id}>
<CardContent className="p-4">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center space-x-2">
{getStatusIcon(task.status)}
<span className="font-mono text-sm">
{task.task_id.slice(0, 8)}...
</span>
<Badge variant={getStatusBadgeVariant(task.status)}>
{task.status}
</Badge>
</div>
<div className="text-sm text-muted-foreground">
{formatDate(task.created_at)}
</div>
</div>
{task.progress && (
<div className="space-y-2 mb-3">
<div className="flex items-center justify-between text-sm">
<span>{task.progress.message}</span>
<span>{task.progress.percentage.toFixed(1)}%</span>
</div>
<Progress
value={task.progress.percentage}
className="h-2"
/>
<div className="text-xs text-muted-foreground">
Step {task.progress.completed_steps}/
{task.progress.total_steps}:{" "}
{task.progress.current_step}
</div>
</div>
)}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
{task.started_at && (
<div>
<span className="text-muted-foreground">Start Time:</span>
<br />
{formatDate(task.started_at)}
</div>
)}
{task.completed_at && (
<div>
<span className="text-muted-foreground">Complete Time:</span>
<br />
{formatDate(task.completed_at)}
</div>
)}
<div>
<span className="text-muted-foreground">Duration:</span>
<br />
{formatDuration(
task.started_at || task.created_at,
task.completed_at
)}
</div>
{task.result && (
<div>
<span className="text-muted-foreground">Result:</span>
<br />
{task.result.dataset_id
? `Dataset: ${task.result.dataset_id.slice(0, 8)}...`
: "None"}
</div>
)}
</div>
{task.error && (
<div className="mt-3 p-2 bg-red-50 border border-red-200 rounded text-sm text-red-700">
<strong>Error:</strong> {task.error}
</div>
)}
</CardContent>
</Card>
))
) : (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Clock className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium mb-2">No Tasks</h3>
<p className="text-sm text-muted-foreground text-center">
{statusFilter === "all"
? "No tasks found"
: `No ${statusFilter} tasks found`}
</p>
</CardContent>
</Card>
)}
</div>
</div>
);
}