import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { Button, Card, Col, Form, Input, InputNumber, message, Progress, Row, Segmented, Select, Space, Table, Tag, Tooltip, Typography } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import PageCard from '../components/PageCard'; import { tasksApi, type QueuedTask, type TaskQueueSettings } from '../api/tasks'; import { useI18n } from '../i18n'; import { SearchOutlined } from '@ant-design/icons'; type QueueStatus = QueuedTask['status']; const AUTO_REFRESH_INTERVAL = 5000; const TaskQueuePage = memo(function TaskQueuePage() { const { t } = useI18n(); const [messageApi, contextHolder] = message.useMessage(); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(false); const [keyword, setKeyword] = useState(''); const [statusFilter, setStatusFilter] = useState(['pending', 'running']); const [autoRefresh, setAutoRefresh] = useState<'on' | 'off'>('on'); const [settings, setSettings] = useState({ concurrency: 1, active_workers: 0 }); const [concurrencyDraft, setConcurrencyDraft] = useState(1); const [settingsLoading, setSettingsLoading] = useState(false); const [lastUpdated, setLastUpdated] = useState(null); const statusOptions = useMemo(() => ([ { label: t('Pending'), value: 'pending' }, { label: t('Running'), value: 'running' }, { label: t('Success'), value: 'success' }, { label: t('Failed'), value: 'failed' }, ]), [t]); const loadTasks = useCallback(async (withSpinner = false) => { if (withSpinner) setLoading(true); try { const data = await tasksApi.getQueue(); setTasks(data); setLastUpdated(Date.now()); } catch (err) { const msg = err instanceof Error && err.message ? err.message : t('Load failed'); messageApi.error(msg); } finally { if (withSpinner) setLoading(false); } }, [messageApi, t]); const loadSettings = useCallback(async () => { try { const data = await tasksApi.getQueueSettings(); setSettings(data); setConcurrencyDraft(data.concurrency); } catch (err) { const msg = err instanceof Error && err.message ? err.message : t('Load failed'); messageApi.error(msg); } }, [messageApi, t]); useEffect(() => { loadTasks(true).catch(() => {}); loadSettings().catch(() => {}); }, [loadTasks, loadSettings]); useEffect(() => { if (autoRefresh === 'off') return () => {}; const timer = window.setInterval(() => { loadTasks().catch(() => {}); }, AUTO_REFRESH_INTERVAL); return () => window.clearInterval(timer); }, [autoRefresh, loadTasks]); const metrics = useMemo(() => { const counts = { total: tasks.length, pending: 0, running: 0, success: 0, failed: 0, }; tasks.forEach(task => { counts[task.status] += 1; }); return counts; }, [tasks]); const filteredTasks = useMemo(() => { const normalizedKeyword = keyword.trim().toLowerCase(); return tasks.filter(task => { if (statusFilter.length > 0 && !statusFilter.includes(task.status)) return false; if (!normalizedKeyword) return true; const haystack = [ task.id, task.name, JSON.stringify(task.task_info ?? {}), task.meta ? JSON.stringify(task.meta) : '', task.error ?? '', ].join(' ').toLowerCase(); return haystack.includes(normalizedKeyword); }); }, [keyword, statusFilter, tasks]); const progressLabel = useCallback((task: QueuedTask) => { const percent = task.progress?.percent; const stage = task.progress?.stage; if (percent !== undefined && percent !== null) { const percentText = `${percent.toFixed(1)}%`; return stage ? `${percentText} ยท ${stage}` : percentText; } return stage ?? '--'; }, []); const columns: ColumnsType = useMemo(() => ([ { title: 'ID', dataIndex: 'id', width: 160, render: (id: string) => ( {id.slice(0, 8)} ), }, { title: t('Task Type'), dataIndex: 'name', sorter: (a, b) => a.name.localeCompare(b.name), width: 160, }, { title: t('Status'), dataIndex: 'status', width: 120, render: (status: QueueStatus) => { const colorMap: Record = { pending: 'default', running: 'processing', success: 'success', failed: 'error', }; const labelMap: Record = { pending: t('Pending'), running: t('Running'), success: t('Success'), failed: t('Failed'), }; return {labelMap[status]}; }, }, { title: t('Progress'), render: (_: unknown, record) => ( record.progress?.percent !== undefined && record.progress.percent !== null ? : {progressLabel(record)} ), }, { title: t('Details'), dataIndex: 'task_info', render: (_: unknown, record) => { const info = record.task_info ? JSON.stringify(record.task_info) : '--'; return ( {info} ); }, }, { title: t('Error'), dataIndex: 'error', render: (error: string | undefined) => error ? {error} : '--', }, ]), [progressLabel, t]); const handleSaveSettings = useCallback(async () => { setSettingsLoading(true); try { const next = await tasksApi.updateQueueSettings({ concurrency: concurrencyDraft }); setSettings(next); setConcurrencyDraft(next.concurrency); messageApi.success(t('Settings saved')); await loadTasks(); } catch (err) { const msg = err instanceof Error && err.message ? err.message : t('Operation failed'); messageApi.error(msg); } finally { setSettingsLoading(false); } }, [concurrencyDraft, loadTasks, messageApi, t]); const lastUpdatedText = useMemo(() => { if (!lastUpdated) return '--'; return new Date(lastUpdated).toLocaleTimeString(); }, [lastUpdated]); return ( <> {contextHolder} {t('Last updated at {time}', { time: lastUpdatedText })} setAutoRefresh(value as 'on' | 'off')} /> } > {[{ label: t('Total Tasks'), value: metrics.total, color: '#1677ff' }, { label: t('Running Tasks'), value: metrics.running, color: '#52c41a' }, { label: t('Waiting Tasks'), value: metrics.pending, color: '#faad14' }, { label: t('Failed Tasks'), value: metrics.failed, color: '#ff4d4f' }, { label: t('Active Workers'), value: settings.active_workers, color: '#722ed1' }].map((item) => ( {item.label} {item.value} ))}
} onChange={(e) => setKeyword(e.target.value)} style={{ width: 260 }} />