import { useQuery } from '@tanstack/react-query';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
Activity,
Play,
Pause,
RotateCcw,
Terminal,
CheckCircle,
AlertCircle,
Clock,
ChevronDown,
ChevronRight,
Copy,
Filter,
X,
RefreshCw,
} from 'lucide-react';
import {
jobsService,
JobHistoryEntry,
JobStatus,
JOB_LABELS,
getJobDataSummary,
getJobDataDetails,
getJobStatus,
} from '../services/jobsService';
import { formatDistanceToNow, format } from 'date-fns';
/* ─────────────────────────────────────────── Filter state ─────────────────────────────────────────── */
interface Filters {
status: JobStatus | 'all';
type: string;
}
const INITIAL_FILTERS: Filters = { status: 'all', type: 'all' };
/* ─────────────────────────────────────────── Helpers ─────────────────────────────────────────────── */
const JOB_TYPES = Object.keys(JOB_LABELS);
function statusBgColor(status: JobStatus): string {
switch (status) {
case 'running':
return 'bg-accent/10 border-accent/25';
case 'failed':
return 'bg-red-400/10 border-red-400/25';
case 'completed':
return 'bg-green-400/10 border-green-400/25';
}
}
function formatDuration(ms: number): string {
const seconds = Math.floor(ms / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainSec = seconds % 60;
return `${minutes}m ${remainSec}s`;
}
/* ─────────────────────────────────────────── Sub-components ──────────────────────────────────────── */
function StatCard({ icon: Icon, label, value, color }: { icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>; label: string; value: number; color: string }) {
return (
{label}
{value.toLocaleString()}
);
}
function StatusBadge({ status }: { status: JobStatus }) {
const dotColor = status === 'running' ? 'bg-accent' : status === 'failed' ? 'bg-red-400' : 'bg-green-400';
const label = status === 'running' ? 'Running' : status === 'failed' ? 'Failed' : 'Completed';
return (
{label}
);
}
function ExpandedDetail({ label, value }: { label: string; value: string }) {
return (
{label}
{value || '—'}
);
}
/* ─────────────────────────────────────────── Job row ─────────────────────────────────────────────── */
function JobRow({
job,
isExpanded,
onToggle,
}: {
job: JobHistoryEntry;
isExpanded: boolean;
onToggle: () => void;
}) {
const status = getJobStatus(job);
const summary = getJobDataSummary(job.name, job.data);
const details = useMemo(() => getJobDataDetails(job.name, job.data), [job.name, job.data]);
const duration = job.finishedOn && job.timestamp ? job.finishedOn - job.timestamp : null;
const hasReturnValue = job.returnvalue !== undefined && job.returnvalue !== null;
return (
{/* ── collapsed row ── */}
{/* ── expanded detail ── */}
{isExpanded && (
{/* Job metadata */}
{job.finishedOn && (
)}
{duration !== null && (
)}
{/* Job-specific payload */}
{details.length > 0 && (
Payload
{details.map((d) => (
))}
)}
{/* Raw data (collapsible) */}
{Object.keys(job.data).length > 0 && (
)}
{/* Return value */}
{hasReturnValue &&
}
{/* Failed reason */}
{job.failedReason && (
)}
)}
);
}
/* ── Raw JSON expandable block ── */
function RawDataBlock({ label, data }: { label: string; data: unknown }) {
const [open, setOpen] = useState(false);
const text = JSON.stringify(data, null, 2);
return (
{open && (
{text}
)}
);
}
/* ─────────────────────────────────────────── Filter bar ──────────────────────────────────────────── */
function FilterBar({
filters,
onChange,
totalCount,
filteredCount,
}: {
filters: Filters;
onChange: (f: Filters) => void;
totalCount: number;
filteredCount: number;
}) {
const setStatus = (status: Filters['status']) => onChange({ ...filters, status });
const setType = (type: string) => onChange({ ...filters, type });
const hasActiveFilters = filters.status !== 'all' || filters.type !== 'all';
const statusOptions: { value: Filters['status']; label: string }[] = [
{ value: 'all', label: 'All' },
{ value: 'completed', label: 'Completed' },
{ value: 'failed', label: 'Failed' },
{ value: 'running', label: 'Running' },
];
const typeOptions = JOB_TYPES;
return (
{/* Status pills */}
{statusOptions.map((opt) => (
))}
{/* Type dropdown */}
{/* Result count */}
{filteredCount} / {totalCount} jobs
{/* Clear filters */}
{hasActiveFilters && (
)}
);
}
/* ─────────────────────────────────────────── Page ────────────────────────────────────────────────── */
export default function JobsPage() {
const [autoRefresh, setAutoRefresh] = useState(true);
const [selectedTab, setSelectedTab] = useState<'overview' | 'history'>('overview');
const [filters, setFilters] = useState(INITIAL_FILTERS);
const [expandedIds, setExpandedIds] = useState>(new Set());
const { data: stats, refetch: refetchStats } = useQuery({
queryKey: ['queueStats'],
queryFn: () => jobsService.getQueueStats(),
refetchInterval: autoRefresh ? 3000 : false,
staleTime: 1000,
});
const { data: history, refetch: refetchHistory } = useQuery({
queryKey: ['jobHistory'],
queryFn: () => jobsService.getJobHistory(200),
refetchInterval: autoRefresh ? 5000 : false,
staleTime: 2000,
});
useEffect(() => {
if (selectedTab === 'overview') refetchStats();
else refetchHistory();
}, [selectedTab, refetchStats, refetchHistory]);
const toggleExpanded = useCallback((id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
}, []);
// Filtered & sorted history
const filteredHistory = useMemo(() => {
if (!history) return [];
return history
.filter((job) => {
if (filters.status !== 'all' && getJobStatus(job) !== filters.status) return false;
if (filters.type !== 'all' && job.name !== filters.type) return false;
return true;
});
}, [history, filters]);
return (
{/* ── Header ── */}
{/* ── Tabs ── */}
{/* ── Content ── */}
{/* ── Overview tab ── */}
{selectedTab === 'overview' && stats && (
)}
{/* ── History tab ── */}
{selectedTab === 'history' && (
<>
{/* Filter bar */}
{history && history.length > 0 && (
)}
{/* Job list */}
{filteredHistory.length === 0 ? (
{history && history.length > 0
? 'No jobs match the current filters'
: 'No job history yet'}
{(history && history.length > 0) && (
)}
) : (
{filteredHistory.map((job) => (
toggleExpanded(job.id)}
/>
))}
)}
>
)}
);
}