UI changes

This commit is contained in:
2026-05-27 10:29:26 -04:00
parent 1ba2aa39d8
commit d560b2e0b4
14 changed files with 237 additions and 28 deletions
+64 -3
View File
@@ -7,6 +7,8 @@ export default function DirectoryBrowser({ onSelect, onClose }) {
const [dirs, setDirs] = useState(null) // null = not loaded yet
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [manualPath, setManualPath] = useState('')
const [networkHost, setNetworkHost] = useState('')
async function navigate(path) {
setLoading(true)
@@ -16,6 +18,7 @@ export default function DirectoryBrowser({ onSelect, onClose }) {
setCurrent(data.path)
setParent(data.parent)
setDirs(data.dirs)
setManualPath(data.path ?? '')
} catch (e) {
setError(e.message)
} finally {
@@ -23,16 +26,40 @@ export default function DirectoryBrowser({ onSelect, onClose }) {
}
}
function goToManualPath() {
const path = manualPath.trim()
if (!path) {
navigate(null)
return
}
navigate(path)
}
function goToHost() {
const host = networkHost.trim()
if (!host) return
navigate(`\\\\${host}`)
}
// Load roots on first render
if (dirs === null && !loading && !error) {
navigate(null)
}
const breadcrumbs = current ? current.replace(/\\/g, '/').split('/').filter(Boolean) : []
const isUnixPath = !!current && current.startsWith('/')
const normalizedCurrent = current ?? ''
const isUnixPath = normalizedCurrent.startsWith('/')
const isUncPath = normalizedCurrent.startsWith('\\\\')
const breadcrumbs = normalizedCurrent
? (isUncPath
? normalizedCurrent.slice(2).split(/\\+/).filter(Boolean)
: normalizedCurrent.replace(/\\/g, '/').split('/').filter(Boolean))
: []
function breadcrumbPathAt(index) {
const parts = breadcrumbs.slice(0, index + 1)
if (isUncPath) {
return `\\\\${parts.join('\\')}`
}
if (isUnixPath) {
return `/${parts.join('/')}`
}
@@ -50,7 +77,7 @@ export default function DirectoryBrowser({ onSelect, onClose }) {
{/* Breadcrumb */}
<div className="px-4 py-2 border-b border-slate-800 flex items-center gap-1 text-xs text-slate-400 flex-wrap min-h-[36px]">
<button onClick={() => navigate(null)} className="hover:text-slate-200">Drives</button>
<button onClick={() => navigate(null)} className="hover:text-slate-200">Roots</button>
{breadcrumbs.map((part, i) => {
const path = breadcrumbPathAt(i)
return (
@@ -68,6 +95,40 @@ export default function DirectoryBrowser({ onSelect, onClose }) {
})}
</div>
{/* Jump controls */}
<div className="px-4 py-3 border-b border-slate-800 space-y-2">
<div className="flex gap-2">
<input
type="text"
value={manualPath}
onChange={e => setManualPath(e.target.value)}
placeholder="Path (e.g. C:\\data or \\\\192.168.1.10\\share)"
className="flex-1 bg-slate-800 border border-slate-700 text-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-blue-500"
/>
<button
onClick={goToManualPath}
className="px-3 py-2 text-sm rounded-lg border border-slate-700 text-slate-300 hover:bg-slate-800 transition-colors"
>
Go
</button>
</div>
<div className="flex gap-2">
<input
type="text"
value={networkHost}
onChange={e => setNetworkHost(e.target.value)}
placeholder="Network host/IP (e.g. 192.168.1.10)"
className="flex-1 bg-slate-800 border border-slate-700 text-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-blue-500"
/>
<button
onClick={goToHost}
className="px-3 py-2 text-sm rounded-lg border border-slate-700 text-slate-300 hover:bg-slate-800 transition-colors"
>
Open Host
</button>
</div>
</div>
{/* Directory list */}
<div className="overflow-y-auto max-h-64 divide-y divide-slate-800">
{loading && (
+1
View File
@@ -13,6 +13,7 @@ const DERIVED_FIELDS = [
{ key: 'channel', label: 'Channel' },
{ key: 'bandwidth', label: 'Bandwidth' },
{ key: 'direction', label: 'Direction' },
{ key: 'throttled', label: 'Throttled' },
]
function unique(tests, key) {
+32 -5
View File
@@ -1,9 +1,36 @@
export default function StatCard({ label, value, sub, accent }) {
export default function StatCard({ label, value, sub, accent, details, detailsPosition = 'below' }) {
const showDetails = details?.length > 0
const detailsOnRight = showDetails && detailsPosition === 'right'
return (
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col gap-1 min-w-0">
<p className="text-slate-400 text-xs uppercase tracking-widest truncate">{label}</p>
<p className={`text-3xl font-bold ${accent ?? 'text-slate-100'}`}>{value}</p>
{sub && <p className="text-slate-400 text-sm">{sub}</p>}
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4 min-w-0">
<div className={`flex ${detailsOnRight ? 'items-start justify-between gap-4' : 'flex-col gap-1'}`}>
<div className="min-w-0">
<p className="text-slate-400 text-xs uppercase tracking-widest truncate">{label}</p>
<p className={`text-3xl font-bold ${accent ?? 'text-slate-100'}`}>{value}</p>
{sub && <p className="text-slate-400 text-sm">{sub}</p>}
</div>
{detailsOnRight && (
<div className="space-y-0.5 text-right shrink-0">
{details.map((line) => (
<p key={line} className="text-slate-500 text-xs font-medium">
{line}
</p>
))}
</div>
)}
</div>
{showDetails && !detailsOnRight && (
<div className="mt-1 space-y-0.5">
{details.map((line) => (
<p key={line} className="text-slate-500 text-xs">
{line}
</p>
))}
</div>
)}
</div>
)
}
+1 -1
View File
@@ -1,5 +1,5 @@
export default function StatusBadge({ completed }) {
return completed
? <span className="inline-flex items-center gap-1 text-emerald-400 text-sm font-medium"> Done</span>
? <span className="inline-flex items-center gap-1 text-emerald-400 text-sm font-medium"> Completed</span>
: <span className="inline-flex items-center gap-1 text-slate-500 text-sm"> Pending</span>
}
+1
View File
@@ -127,6 +127,7 @@ export default function TestTable({ tests = [], isLoading }) {
<TagPill label="Band" value={test.band} />
<TagPill label="Channel" value={test.channel} />
<TagPill label="Bandwidth" value={test.bandwidth} />
<TagPill label="Throttle" value={test.throttled} />
<TagPill label="Direction" value={test.direction} />
<TagPill label="Elapsed Time" value={fmtDuration(test.duration_seconds)} />
</div>
+30 -12
View File
@@ -6,16 +6,33 @@ function fmt(seconds) {
return `${m}m`
}
function fmtDays(seconds) {
if (seconds == null) return null
const days = seconds / 57600 // 16 hours per day
return days < 1 ? `${(days * 24).toFixed(1)}h` : `${days.toFixed(1)}d (16h/day)`
function estimateDays(seconds) {
const s = Number(seconds)
if (!Number.isFinite(s) || s < 0) return null
let days = s / 57600 // 16 hours per day
const estDate = new Date(Date.now() + days * 24 * 3600 * 1000)
// Calculate how many weekends
let weekends = 0
for (let d = new Date(); d < estDate; d.setDate(d.getDate() + 1)) {
if (d.getDay() === 0 || d.getDay() === 6) {
weekends++
}
}
const weekdays = (s - weekends * 24 * 3600) / 57600
days = weekdays + weekends
return Number.isFinite(days) ? days : null
}
function fmtCompletionDate(seconds) {
if (seconds == null) return null
const days = seconds / 57600 // 16 hours per day
function fmtDaysLabel(days) {
if (days == null || !Number.isFinite(days)) return null
return days < 1 ? `${(days * 24).toFixed(1)}h` : `${days.toFixed(1)}d`
}
function fmtCompletionDate(days) {
if (days == null || !Number.isFinite(days)) return null
const date = new Date(Date.now() + days * 24 * 3600 * 1000)
if (Number.isNaN(date.getTime())) return null
return date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' })
}
@@ -26,15 +43,16 @@ export default function TimeDisplay({ elapsedSeconds, estimatedRemainingSeconds,
.map(([t]) => t)
: []
const days = fmtDays(estimatedRemainingSeconds)
const completionDate = fmtCompletionDate(estimatedRemainingSeconds)
const estimatedDays = estimateDays(estimatedRemainingSeconds)
const daysLabel = fmtDaysLabel(estimatedDays)
const completionDate = fmtCompletionDate(estimatedDays)
return (
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col gap-3">
<div className="flex flex-wrap gap-6">
<div>
<p className="text-slate-400 text-xs uppercase tracking-widest">Time Elapsed</p>
<p className="text-2xl font-bold text-slate-100 mt-0.5">
<p className="text-2xl font-bold text-emerald-400 mt-0.5">
{fmt(elapsedSeconds) ?? '—'}
</p>
</div>
@@ -43,8 +61,8 @@ export default function TimeDisplay({ elapsedSeconds, estimatedRemainingSeconds,
<p className={`text-2xl font-bold mt-0.5 ${estimatedRemainingSeconds != null ? 'text-slate-100' : 'text-amber-400'}`}>
{estimatedRemainingSeconds != null ? fmt(estimatedRemainingSeconds) : '—'}
</p>
{days && (
<p className="text-slate-400 text-xs mt-0.5">{days}</p>
{daysLabel && (
<p className="text-slate-400 text-xs mt-0.5">{daysLabel}</p>
)}
</div>
{completionDate && (