Bump version to 1.0.51
This commit is contained in:
@@ -28,18 +28,32 @@ import {
|
||||
Terminal,
|
||||
AlertCircle,
|
||||
Ban,
|
||||
ChevronDown,
|
||||
Activity,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { statusVariant, statusLabel, getErrorCodeInfo } from '@/lib/status';
|
||||
import { formatDuration } from '@/lib/utils';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface SSEProgress {
|
||||
fileBytes: number;
|
||||
pct: number;
|
||||
speedBps: number;
|
||||
etaSeconds: number;
|
||||
xfrDone: number;
|
||||
xfrTotal: number;
|
||||
}
|
||||
|
||||
interface SSEEvent {
|
||||
type: string;
|
||||
job_id: number;
|
||||
status?: string;
|
||||
line?: string;
|
||||
stream?: string;
|
||||
progress?: SSEProgress;
|
||||
totalBytes?: number;
|
||||
sentBytes?: number;
|
||||
}
|
||||
|
||||
export default function JobDetail() {
|
||||
@@ -56,6 +70,10 @@ export default function JobDetail() {
|
||||
const [cancelModal, setCancelModal] = useState(false);
|
||||
const [cancelReason, setCancelReason] = useState('');
|
||||
const [errorModal, setErrorModal] = useState(false);
|
||||
const [progress, setProgress] = useState<SSEProgress | null>(null);
|
||||
const [finalTotals, setFinalTotals] = useState<{ totalBytes: number; sentBytes: number } | null>(null);
|
||||
const [hasMoreLogs, setHasMoreLogs] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadJob();
|
||||
@@ -78,6 +96,13 @@ export default function JobDetail() {
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
if (evt.type === 'progress' && evt.progress) {
|
||||
setProgress(evt.progress);
|
||||
}
|
||||
if (evt.type === 'progress_total' && evt.totalBytes !== undefined && evt.sentBytes !== undefined) {
|
||||
setFinalTotals({ totalBytes: evt.totalBytes, sentBytes: evt.sentBytes });
|
||||
setProgress(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
return () => esRef.current?.close();
|
||||
@@ -109,8 +134,10 @@ export default function JobDetail() {
|
||||
)) ?? [];
|
||||
if (offset === 0) {
|
||||
setLogs(ls);
|
||||
setHasMoreLogs(ls.length === 1000);
|
||||
} else {
|
||||
setLogs(prev => [...prev, ...ls]);
|
||||
setHasMoreLogs(ls.length === 1000);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
@@ -321,6 +348,10 @@ export default function JobDetail() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(progress || finalTotals) && (
|
||||
<TransferProgress progress={progress} finalTotals={finalTotals} />
|
||||
)}
|
||||
|
||||
<Card className="flex flex-col min-h-0">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -378,6 +409,23 @@ export default function JobDetail() {
|
||||
</>
|
||||
)}
|
||||
<div ref={logEndRef} />
|
||||
{hasMoreLogs && (
|
||||
<div className="flex justify-center py-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
setLoadingMore(true);
|
||||
await loadLogs(logs.length);
|
||||
setLoadingMore(false);
|
||||
}}
|
||||
disabled={loadingMore}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
{loadingMore ? 'Loading...' : 'Load more'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -452,3 +500,86 @@ function LogLine({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const units = ['B', 'kB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
||||
}
|
||||
|
||||
function formatSpeed(bps: number): string {
|
||||
if (bps === 0) return '0 B/s';
|
||||
const units = ['B/s', 'kB/s', 'MB/s', 'GB/s'];
|
||||
const i = Math.floor(Math.log(bps) / Math.log(1000));
|
||||
return `${(bps / Math.pow(1000, i)).toFixed(1)} ${units[i]}`;
|
||||
}
|
||||
|
||||
function TransferProgress({
|
||||
progress,
|
||||
finalTotals,
|
||||
}: {
|
||||
progress: SSEProgress | null;
|
||||
finalTotals: { totalBytes: number; sentBytes: number } | null;
|
||||
}) {
|
||||
const globalPct = finalTotals && finalTotals.totalBytes > 0
|
||||
? Math.round((finalTotals.sentBytes / finalTotals.totalBytes) * 100)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-accent" />
|
||||
<span className="text-xs font-semibold text-fg-muted uppercase tracking-wider">
|
||||
Transfer Progress
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{progress && (
|
||||
<>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between text-xs text-fg-muted">
|
||||
<span>Current file</span>
|
||||
<span>{progress.pct}%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-accent transition-all duration-300 rounded-full"
|
||||
style={{ width: `${progress.pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-xs text-fg-muted">
|
||||
<span className="font-mono">
|
||||
xfr#{(progress.xfrDone).toLocaleString()}/{progress.xfrTotal > 0 ? progress.xfrTotal.toLocaleString() : '?'}
|
||||
</span>
|
||||
<span className="font-mono">
|
||||
{formatSpeed(progress.speedBps)}
|
||||
</span>
|
||||
<span className="font-mono">
|
||||
ETA {progress.etaSeconds > 0 ? `${Math.floor(progress.etaSeconds / 60)}m ${progress.etaSeconds % 60}s` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{finalTotals && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between text-xs text-fg-muted">
|
||||
<span>Total transferred</span>
|
||||
<span>{globalPct}% — {formatBytes(finalTotals.sentBytes)} / {formatBytes(finalTotals.totalBytes)}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-emerald-500 transition-all duration-300 rounded-full"
|
||||
style={{ width: `${globalPct ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user