import { useState } from 'react';
import { Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { useTrackedMutation } from '../hooks/useTrackedMutation';
import { getJobs, cancelJob, approveRenewal, rejectRenewal } from '../api/client';
import PageHeader from '../components/PageHeader';
import DataTable from '../components/DataTable';
import type { Column } from '../components/DataTable';
import StatusBadge from '../components/StatusBadge';
import ErrorState from '../components/ErrorState';
import { formatDateTime } from '../api/utils';
import type { Job } from '../api/types';
function RejectModal({ job, onClose, onReject }: { job: Job; onClose: () => void; onReject: (reason: string) => void }) {
const [reason, setReason] = useState('');
return (
e.stopPropagation()}>
Reject Job
Rejecting job {job.id} for certificate {job.certificate_id}
);
}
function VerificationBadge({ status }: { status?: string }) {
if (!status) return —;
const styles: Record = {
success: 'bg-emerald-100 text-emerald-700',
failed: 'bg-red-100 text-red-700',
pending: 'bg-yellow-100 text-yellow-700',
skipped: 'bg-gray-100 text-gray-600',
};
const labels: Record = {
success: 'Verified',
failed: 'Failed',
pending: 'Pending',
skipped: 'Skipped',
};
return (
{labels[status] || status}
);
}
export default function JobsPage() {
const [statusFilter, setStatusFilter] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [rejectingJob, setRejectingJob] = useState(null);
const params: Record = {};
if (statusFilter) params.status = statusFilter;
if (typeFilter) params.type = typeFilter;
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['jobs', params],
queryFn: () => getJobs(params),
refetchInterval: 10000,
});
const cancelMutation = useTrackedMutation({
mutationFn: cancelJob,
invalidates: [['jobs']],
});
const approveMutation = useTrackedMutation({
mutationFn: approveRenewal,
invalidates: [['jobs']],
});
const rejectMutation = useTrackedMutation({
mutationFn: ({ id, reason }: { id: string; reason: string }) => rejectRenewal(id, reason),
invalidates: [['jobs']],
onSuccess: () => {
setRejectingJob(null);
},
});
const awaitingCount = data?.data?.filter(j => j.status === 'AwaitingApproval').length || 0;
const columns: Column[] = [
{
key: 'id',
label: 'Job',
render: (j) => (
e.stopPropagation()}>
{j.id}
{j.type}
),
},
{ key: 'status', label: 'Status', render: (j) => },
{ key: 'cert', label: 'Certificate', render: (j) => {j.certificate_id} },
{
key: 'agent',
label: 'Agent',
render: (j) => j.agent_id ? (
e.stopPropagation()}>
{j.agent_id}
) : (
—
),
},
{
key: 'attempts',
label: 'Attempts',
render: (j) => {j.attempts}/{j.max_attempts},
},
{
key: 'error',
label: 'Error',
render: (j) => j.status === 'Failed' && j.last_error ? (
{j.last_error.length > 80 ? j.last_error.substring(0, 80) + '...' : j.last_error}
) : —,
},
{ key: 'scheduled', label: 'Scheduled', render: (j) => {formatDateTime(j.scheduled_at)} },
{ key: 'completed', label: 'Completed', render: (j) => {formatDateTime(j.completed_at)} },
{
key: 'verification',
label: 'Verification',
render: (j) => j.type === 'Deployment' ? : —,
},
{
key: 'actions',
label: '',
render: (j) => (
{j.status === 'AwaitingApproval' && (
<>
>
)}
{(j.status === 'Pending' || j.status === 'Running') && (
)}
),
},
];
return (
<>
{/* Pending approval banner */}
{awaitingCount > 0 && (
{awaitingCount} job{awaitingCount !== 1 ? 's' : ''} awaiting approval
{statusFilter !== 'AwaitingApproval' && (
)}
)}
{error ? (
refetch()} />
) : (
)}
{rejectingJob && (
setRejectingJob(null)}
onReject={(reason) => rejectMutation.mutate({ id: rejectingJob.id, reason })}
/>
)}
>
);
}