UNPKG

vanta-auditor-tui

Version:

Beautiful terminal UI for exporting Vanta audit evidence with ZIP support and progress tracking

156 lines 7.36 kB
import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; import { Spinner } from '../lib/inkModules.js'; import figures from 'figures'; import { theme } from "../theme.js"; import { MultiLevelProgress } from "./AnimatedProgressBar.js"; export function DownloadProgress({ updates, gatherProgress }) { const [startTime] = useState(Date.now()); const [currentSpeed, setCurrentSpeed] = useState("0 KB/s"); const totals = useMemo(() => { // If we're still gathering, use that progress if (gatherProgress && gatherProgress.phase !== 'complete') { const discoveryProgress = gatherProgress.phase === 'discovering' ? 5 // Discovery phase is 0-5% : 10 + (gatherProgress.current / Math.max(1, gatherProgress.total)) * 5; // Processing is 10-15% return { total: gatherProgress.total, success: 0, failed: 0, skipped: 0, completed: 0, remaining: gatherProgress.total, progress: discoveryProgress, totalBytes: 0, phase: gatherProgress.phase, phaseMessage: gatherProgress.message }; } // Download phase (15-100%) const total = updates.at(-1)?.totalItems ?? updates.length; const success = updates.filter((u) => u.status === "success").length; const failed = updates.filter((u) => u.status === "failed").length; const skipped = updates.filter((u) => u.status === "skipped").length; const completed = success + failed + skipped; const remaining = total - completed; // Calculate weighted progress including partial downloads let weightedProgress = 0; const downloadingFiles = updates.filter(u => u.status === "downloading"); if (total > 0) { // Base progress from completed files (85% of the remaining progress) const baseProgress = (completed / total) * 85; // Add partial progress from currently downloading files let partialProgress = 0; downloadingFiles.forEach(file => { if (file.totalBytes && file.receivedBytes) { const fileProgress = file.receivedBytes / file.totalBytes; partialProgress += (fileProgress / total) * 85; } }); weightedProgress = 15 + baseProgress + partialProgress; // Start at 15% after discovery } // Calculate total size const totalBytes = updates.reduce((acc, u) => acc + (u.totalBytes || 0), 0); return { total, success, failed, skipped, completed, remaining, progress: weightedProgress, totalBytes, phase: 'downloading', phaseMessage: undefined }; }, [updates, gatherProgress]); // Get current downloading file const currentFile = useMemo(() => { const downloading = updates.filter(u => u.status === "downloading").pop(); if (!downloading) return null; const progress = downloading.totalBytes ? (downloading.receivedBytes || 0) / downloading.totalBytes * 100 : 0; const fileName = downloading.filePath.split('/').pop() || 'file'; const size = downloading.receivedBytes && downloading.totalBytes ? `${formatBytes(downloading.receivedBytes)}/${formatBytes(downloading.totalBytes)}` : undefined; return { fileName, progress, size }; }, [updates]); // Calculate download speed useEffect(() => { const elapsed = (Date.now() - startTime) / 1000; // seconds const totalReceived = updates.reduce((acc, u) => acc + (u.receivedBytes || 0), 0); const speed = elapsed > 0 ? totalReceived / elapsed : 0; setCurrentSpeed(formatSpeed(speed)); }, [updates, startTime]); // Determine current phase label const phaseLabel = totals.phase === 'discovering' ? '🔍 Discovering evidence...' : totals.phase === 'processing' ? '⚙️ Processing evidence items...' : '📥 Downloading files'; const currentLabel = totals.phaseMessage || (currentFile ? `Downloading: ${currentFile.fileName}` : phaseLabel); return (React.createElement(Box, { flexDirection: "column" }, React.createElement(Box, { marginBottom: 1 }, React.createElement(Text, { color: theme.colors.primary, bold: true }, phaseLabel)), React.createElement(MultiLevelProgress, { overall: totals.progress, current: currentFile?.progress || 0, label: currentLabel, stats: { downloaded: totals.completed, total: totals.total, speed: totals.phase === 'downloading' ? currentSpeed : undefined, eta: totals.remaining > 0 && totals.phase === 'downloading' ? `${totals.remaining} files remaining` : undefined } }), React.createElement(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 2 }, React.createElement(Text, { color: theme.colors.dim }, "Recent files:"), updates.slice(-3).map((u, idx) => (React.createElement(Box, { key: `${u.filePath}-${idx}`, paddingLeft: 2 }, React.createElement(Text, null, u.status === "downloading" && (React.createElement(Text, { color: theme.colors.primaryLight }, React.createElement(Spinner, { type: "dots" }))), u.status === "success" && (React.createElement(Text, { color: theme.colors.success }, figures.tick, " ")), u.status === "failed" && (React.createElement(Text, { color: theme.colors.error }, figures.cross, " ")), u.status === "skipped" && (React.createElement(Text, { color: theme.colors.warning }, figures.arrowUp, " ")), React.createElement(Text, { color: theme.colors.text }, truncate(u.filePath.split('/').pop() || '', 50))))))), totals.failed > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 2 }, React.createElement(Text, { color: theme.colors.error }, figures.warning, " ", totals.failed, " file(s) failed to download"))))); } function truncate(s, n) { if (s.length <= n) return s; return s.slice(0, n - 1) + "…"; } function formatBytes(bytes) { const units = ["B", "KB", "MB", "GB", "TB"]; let i = 0; let v = bytes; while (v >= 1024 && i < units.length - 1) { v /= 1024; i += 1; } return `${v.toFixed(1)} ${units[i]}`; } function formatSpeed(bytesPerSecond) { if (bytesPerSecond === 0) return "0 KB/s"; const kbps = bytesPerSecond / 1024; if (kbps < 1024) { return `${kbps.toFixed(1)} KB/s`; } const mbps = kbps / 1024; return `${mbps.toFixed(1)} MB/s`; } export default DownloadProgress; //# sourceMappingURL=DownloadProgress.js.map