/** * PushProgressPanel -- real-time per-device push status display. * Polls the push status API every 3 seconds until all devices * reach a terminal state (committed/reverted/failed). */ import { useQuery } from '@tanstack/react-query' import { CheckCircle, XCircle, AlertTriangle, Loader2, Clock } from 'lucide-react' import { templatesApi } from '@/lib/templatesApi' import { cn } from '@/lib/utils' import { formatDateTime } from '@/lib/utils' import { DeviceLink } from '@/components/ui/device-link' interface PushProgressPanelProps { tenantId: string rolloutId: string onClose: () => void } const STATUS_CONFIG: Record = { pending: { icon: Clock, color: 'text-text-muted', label: 'Pending' }, pushing: { icon: Loader2, color: 'text-info', label: 'Pushing' }, committed: { icon: CheckCircle, color: 'text-success', label: 'Committed' }, reverted: { icon: AlertTriangle, color: 'text-warning', label: 'Reverted' }, failed: { icon: XCircle, color: 'text-error', label: 'Failed' }, } const TERMINAL_STATUSES = new Set(['committed', 'reverted', 'failed']) export function PushProgressPanel({ tenantId, rolloutId, onClose }: PushProgressPanelProps) { const { data } = useQuery({ queryKey: ['push-status', rolloutId], queryFn: () => templatesApi.pushStatus(tenantId, rolloutId), refetchInterval: (query) => { const jobs = query.state.data?.jobs ?? [] const allTerminal = jobs.length > 0 && jobs.every((j) => TERMINAL_STATUSES.has(j.status)) return allTerminal ? false : 3000 }, }) const jobs = data?.jobs ?? [] const total = jobs.length const committed = jobs.filter((j) => j.status === 'committed').length const failed = jobs.filter((j) => j.status === 'failed').length const reverted = jobs.filter((j) => j.status === 'reverted').length const pending = jobs.filter((j) => j.status === 'pending').length const allDone = jobs.length > 0 && jobs.every((j) => TERMINAL_STATUSES.has(j.status)) const hasFailed = failed > 0 || reverted > 0 // Progress percentage const completedCount = committed + failed + reverted const progressPct = total > 0 ? Math.round((completedCount / total) * 100) : 0 return (
{/* Summary */}
Push Progress
{completedCount} / {total} devices
{/* Progress bar */}
{/* Status message */} {allDone && !hasFailed && (
Push complete -- all {committed} devices configured successfully
)} {allDone && hasFailed && pending > 0 && (
Push paused -- {failed + reverted} device(s) failed/reverted.{' '} {pending} device(s) remain pending.
)} {allDone && hasFailed && pending === 0 && (
Push complete with errors -- {failed} failed, {reverted} reverted out of {total} devices.
)} {/* Per-device list */}
{jobs.map((job) => { const config = STATUS_CONFIG[job.status] ?? STATUS_CONFIG.pending const Icon = config.icon return (
{job.hostname}
{job.error_message && (
{job.error_message}
)}
{config.label} {job.completed_at && ( {formatDateTime(job.completed_at)} )}
) })}
{allDone && (
)}
) }