'use client'; import { useMemo, useState } from 'react'; import { useTranslations } from 'next-intl'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { format } from 'date-fns'; import { Activity, ChevronLeft, ChevronRight, RefreshCw, RotateCcw, } from 'lucide-react'; import services from '@/lib/services'; import type { TaskExecution, TaskExecutionStatus, TaskMeta, } from '@/lib/services/admin'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { EmptyStateWithBorder } from '@/components/layout/empty'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from '@/components/ui/sheet'; import { Spinner } from '@/components/ui/spinner'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; const STATUS_LABELS_KEYS: Record = { pending: 'statusPending', running: 'statusRunning', succeeded: 'statusSucceeded', failed: 'statusFailed', }; const TRIGGER_LABELS_KEYS: Record = { system: 'triggerSystem', manual: 'triggerManual', retry: 'triggerRetry', schedule: 'triggerSchedule', }; const PAGE_SIZE = 10; function formatDateTime(value?: string | null) { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return format(date, 'yyyy-MM-dd HH:mm:ss'); } function formatDuration(duration: number) { if (!duration) return '-'; if (duration < 1000) return `${duration}ms`; return `${(duration / 1000).toFixed(2)}s`; } function statusVariant(status: TaskExecutionStatus) { if (status === 'failed') return 'destructive'; if (status === 'succeeded') return 'secondary'; return 'outline'; } export function TaskExecutionsManager() { const t = useTranslations('admin.tasks'); const queryClient = useQueryClient(); const [executionsPage, setExecutionsPage] = useState(1); const [executionStatus, setExecutionStatus] = useState< TaskExecutionStatus | 'all' >('all'); const [executionTaskType, setExecutionTaskType] = useState('all'); const [selectedExecutionId, setSelectedExecutionId] = useState( null, ); const [executionPreview, setExecutionPreview] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const taskTypesQuery = useQuery({ queryKey: ['admin', 'task-types'], queryFn: () => services.adminTask.getTaskTypes(), }); const executionsQuery = useQuery({ queryKey: [ 'admin', 'task-executions', executionsPage, executionStatus, executionTaskType, ], queryFn: () => services.adminTask.listTaskExecutions({ page: executionsPage, page_size: PAGE_SIZE, status: executionStatus === 'all' ? undefined : executionStatus, task_type: executionTaskType === 'all' ? undefined : executionTaskType, }), }); const detailQuery = useQuery({ queryKey: ['admin', 'task-execution', selectedExecutionId], queryFn: () => services.adminTask.getTaskExecution(selectedExecutionId!), enabled: detailOpen && !!selectedExecutionId, }); const retryMutation = useMutation({ mutationFn: (id: string) => services.adminTask.retryTaskExecution(id), onSuccess: (taskID) => { toast.success(t('retrySuccess'), { description: t('retrySuccessDesc', { taskId: taskID }), }); void queryClient.invalidateQueries({ queryKey: ['admin', 'task-executions'], }); }, onError: (err: Error) => { toast.error(t('retryFailed'), { description: err.message || t('unknownError'), }); }, }); const taskTypes: TaskMeta[] = taskTypesQuery.data ?? []; const executions = executionsQuery.data?.items ?? []; const executionsTotal = executionsQuery.data?.total ?? 0; const executionsLoading = executionsQuery.isPending || executionsQuery.isFetching; const executionsError = executionsQuery.isError ? executionsQuery.error : null; const selectedExecution = detailQuery.data ?? executionPreview; const detailLoading = detailQuery.isFetching; const totalPages = useMemo( () => Math.max(1, Math.ceil(executionsTotal / PAGE_SIZE)), [executionsTotal], ); const openExecutionDetail = (execution: TaskExecution) => { setExecutionPreview(execution); setSelectedExecutionId(execution.id); setDetailOpen(true); }; const handleStatusChange = (value: TaskExecutionStatus | 'all') => { setExecutionStatus(value); setExecutionsPage(1); }; const handleTaskTypeChange = (value: string) => { setExecutionTaskType(value); setExecutionsPage(1); }; return (

{executionsError ? (
void executionsQuery.refetch()} className='justify-center' />
) : executionsLoading && executions.length === 0 ? ( ) : executions.length === 0 ? ( ) : (
{t('colTask')} {t('colStatus')} {t('colTrigger')} {t('colRetry')} {t('colDuration')} {t('colResult')} {t('colCreatedAt')} {executions.map((execution) => ( openExecutionDetail(execution)} >
{execution.task_name || execution.task_type} {execution.task_id}
{t(STATUS_LABELS_KEYS[execution.status]) || execution.status} {t(TRIGGER_LABELS_KEYS[execution.triggered_by]) || execution.triggered_by} {execution.retry_count}/{execution.max_retry} {formatDuration(execution.duration)} {execution.error_message || execution.result || '-'} {formatDateTime(execution.created_at)}
))}
)}
{t('paginationInfo', { total: executionsTotal, current: executionsPage, totalPages, })}
{t('executionDetailTitle')} {selectedExecution?.task_name || selectedExecution?.task_type || t('executionRecord')}
{detailLoading && !selectedExecution ? ( ) : selectedExecution ? (
{t('detailStatus')}
{t(STATUS_LABELS_KEYS[selectedExecution.status]) || selectedExecution.status}
{t('detailTrigger')}
{t( TRIGGER_LABELS_KEYS[selectedExecution.triggered_by], ) || selectedExecution.triggered_by}
{t('detailRetryCount')}
{selectedExecution.retry_count}/ {selectedExecution.max_retry}
{t('detailDuration')}
{formatDuration(selectedExecution.duration)}
{selectedExecution.task_id}
{formatDateTime(selectedExecution.created_at)}
{formatDateTime(selectedExecution.started_at)}
{formatDateTime(selectedExecution.finished_at)}
{formatDateTime(selectedExecution.updated_at)}
{selectedExecution.result || '-'}
{selectedExecution.error_message && (
{selectedExecution.error_message}
)}
                    {selectedExecution.payload || '{}'}
                  
                    {selectedExecution.log || t('noLog')}
                  
) : ( )}
{selectedExecution && selectedExecution.status === 'failed' && selectedExecution.retryable && ( )}
); }