remove browse, add image

This commit is contained in:
2026-05-28 11:01:47 -04:00
parent 1cbb887e1d
commit d239de257d
10 changed files with 44 additions and 278 deletions
+32 -21
View File
@@ -3,6 +3,7 @@ import { Settings } from 'lucide-react'
import { useStats } from './hooks/useStats'
import { useTests } from './hooks/useTests'
import { useConfig } from './hooks/useConfig'
import cgw453Image from './assets/CGW453.PNG'
import StatCard from './components/StatCard'
import CompletionBar from './components/CompletionBar'
import TimeDisplay from './components/TimeDisplay'
@@ -97,34 +98,44 @@ export default function App() {
{/* Stats row */}
{!statsLoading && stats && (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
<div className="flex flex-col gap-2">
<StatCard
label="Overall"
value={`${((stats.overall.completionRate ?? 0) * 100).toFixed(1)}%`}
sub={`${stats.overall.completed} / ${stats.overall.total} tests`}
accent="text-emerald-400"
<div className="grid grid-cols-1 lg:grid-cols-[220px_minmax(0,1fr)] gap-4">
<div className="lg:row-span-2 bg-slate-900 border border-slate-800 rounded-xl p-4 flex items-center justify-center min-h-[220px]">
<img
src={cgw453Image}
alt="CGW453 test object"
className="max-h-48 w-auto object-contain"
/>
<CompletionBar value={stats.overall.completionRate ?? 0} />
</div>
{stats.devices.map(d => (
<div key={d.name} className="flex flex-col gap-2">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<div className="flex flex-col gap-2">
<StatCard
label={d.name}
value={`${(d.completionRate * 100).toFixed(1)}%`}
sub={`${d.completed} / ${d.total}`}
detailsPosition="right"
details={['COE', 'P2P', 'P3P'].map((type) => {
const typeStats = d.byType?.[type] ?? { completed: 0, total: 0 }
return `${type}: ${typeStats.completed}/${typeStats.total} tests`
})}
label="Overall"
value={`${((stats.overall.completionRate ?? 0) * 100).toFixed(1)}%`}
sub={`${stats.overall.completed} / ${stats.overall.total} tests`}
accent="text-emerald-400"
/>
<CompletionBar value={d.completionRate} />
<CompletionBar value={stats.overall.completionRate ?? 0} />
</div>
))}
<div className="col-span-2 sm:col-span-1 lg:col-span-2">
{stats.devices.map(d => (
<div key={d.name} className="flex flex-col gap-2">
<StatCard
label={d.name}
value={`${(d.completionRate * 100).toFixed(1)}%`}
sub={`${d.completed} / ${d.total}`}
detailsPosition="right"
details={['COE', 'P2P', 'P3P'].map((type) => {
const typeStats = d.byType?.[type] ?? { completed: 0, total: 0 }
return `${type}: ${typeStats.completed}/${typeStats.total} tests`
})}
/>
<CompletionBar value={d.completionRate} />
</div>
))}
</div>
<div>
<TimeDisplay
elapsedSeconds={stats.timing.elapsedSeconds}
estimatedRemainingSeconds={stats.timing.estimatedRemainingSeconds}
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+8 -28
View File
@@ -1,5 +1,4 @@
import { useState, useEffect } from 'react'
import DirectoryBrowser from './DirectoryBrowser'
import { useConfig, useSaveConfig } from '../hooks/useConfig'
import { apiFetch } from '../lib/api'
@@ -14,7 +13,6 @@ export default function ConfigModal({ onClose }) {
const { mutate: save, isPending } = useSaveConfig()
const [form, setForm] = useState({})
const [browser, setBrowser] = useState(null) // 'target_dir' | 'results_dir' | null
const [scanResult, setScanResult] = useState(null) // { testCount, completedCount } | null
const [saveError, setSaveError] = useState(null)
const [isRescanning, setIsRescanning] = useState(false)
@@ -158,21 +156,13 @@ export default function ConfigModal({ onClose }) {
].map(({ key, label }) => (
<div key={key}>
<label className="text-slate-400 text-xs block mb-1">{label}</label>
<div className="flex gap-2">
<input
type="text"
value={form[key] ?? ''}
onChange={e => setForm(f => ({ ...f, [key]: e.target.value }))}
placeholder="C:\path\to\folder"
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={() => setBrowser(key)}
className="px-3 py-2 text-sm rounded-lg border border-slate-700 text-slate-300 hover:bg-slate-800 transition-colors whitespace-nowrap"
>
Browse
</button>
</div>
<input
type="text"
value={form[key] ?? ''}
onChange={e => setForm(f => ({ ...f, [key]: e.target.value }))}
placeholder="C:\path\to\folder"
className="w-full 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"
/>
</div>
))}
</div>
@@ -204,10 +194,7 @@ export default function ConfigModal({ onClose }) {
{/* SMB Credentials */}
<section>
<h3 className="text-slate-300 text-xs uppercase tracking-widest mb-1">SMB Credentials</h3>
<p className="text-slate-500 text-xs mb-3">
Optional credentials for network shares used by target/results directories.
</p>
<h3 className="text-slate-300 text-xs uppercase tracking-widest mb-1">Credentials for network shares</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<input
type="text"
@@ -270,13 +257,6 @@ export default function ConfigModal({ onClose }) {
</div>
</div>
</div>
{browser && (
<DirectoryBrowser
onSelect={path => setForm(f => ({ ...f, [browser]: path }))}
onClose={() => setBrowser(null)}
/>
)}
</>
)
}
@@ -1,139 +0,0 @@
import { useState } from 'react'
import { browse } from '../lib/api'
export default function DirectoryBrowser({ onSelect, onClose }) {
const [current, setCurrent] = useState(null) // null = roots
const [parent, setParent] = useState(null)
const [dirs, setDirs] = useState(null) // null = not loaded yet
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [manualPath, setManualPath] = useState('')
async function navigate(path) {
setLoading(true)
setError(null)
try {
const data = await browse(path)
setCurrent(data.path)
setParent(data.parent)
setDirs(data.dirs)
setManualPath(data.path ?? '')
} catch (e) {
setError(e.message)
} finally {
setLoading(false)
}
}
function goToManualPath() {
const path = manualPath.trim()
if (!path) {
navigate(null)
return
}
navigate(path)
}
// Load roots on first render
if (dirs === null && !loading && !error) {
navigate(null)
}
const normalizedCurrent = current ?? ''
const normalizedPath = normalizedCurrent.replace(/\\/g, '/')
const isUnixPath = normalizedCurrent.startsWith('/')
const breadcrumbs = normalizedPath ? normalizedPath.split('/').filter(Boolean) : []
function breadcrumbPathAt(index) {
const parts = breadcrumbs.slice(0, index + 1)
if (isUnixPath) {
return `/${parts.join('/')}`
}
return parts.join('\\') + (index === 0 ? '\\' : '')
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
<div className="bg-slate-900 border border-slate-700 rounded-xl w-full max-w-lg mx-4 flex flex-col shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-800">
<span className="text-slate-200 font-semibold text-sm">Browse Folder</span>
<button onClick={onClose} className="text-slate-400 hover:text-slate-200"></button>
</div>
{/* 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">Roots</button>
{breadcrumbs.map((part, i) => {
const path = breadcrumbPathAt(i)
return (
<span key={i} className="flex items-center gap-1">
<span>/</span>
<button
onClick={() => navigate(path)}
className="hover:text-slate-200 truncate max-w-[120px]"
title={path}
>
{part}
</button>
</span>
)
})}
</div>
{/* Directory list */}
<div className="overflow-y-auto max-h-64 divide-y divide-slate-800">
{loading && (
<p className="text-slate-500 text-sm text-center py-8">Loading</p>
)}
{error && (
<p className="text-red-400 text-sm text-center py-8">{error}</p>
)}
{!loading && !error && parent !== null && (
<button
onClick={() => navigate(parent)}
className="w-full text-left px-4 py-2.5 text-slate-400 hover:bg-slate-800 text-sm transition-colors"
>
..
</button>
)}
{!loading && !error && dirs?.map(d => (
<button
key={d.path}
onClick={() => navigate(d.path)}
className="w-full text-left px-4 py-2.5 text-slate-300 hover:bg-slate-800 text-sm transition-colors flex items-center gap-2"
>
<span className="text-slate-500">📁</span>
{d.name}
</button>
))}
{!loading && !error && dirs?.length === 0 && (
<p className="text-slate-500 text-sm text-center py-8">No subdirectories</p>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between gap-2 px-4 py-3 border-t border-slate-800">
<p className="text-xs text-slate-500 truncate flex-1" title={current ?? ''}>
{current ?? 'Select a folder'}
</p>
<div className="flex gap-2">
<button
onClick={onClose}
className="px-3 py-1.5 text-sm rounded-lg border border-slate-700 text-slate-300 hover:bg-slate-800 transition-colors"
>
Cancel
</button>
<button
disabled={!current}
onClick={() => { onSelect(current); onClose() }}
className="px-3 py-1.5 text-sm rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
Select This Folder
</button>
</div>
</div>
</div>
</div>
)
}
+1 -1
View File
@@ -52,7 +52,7 @@ export default function TimeDisplay({ elapsedSeconds, estimatedRemainingSeconds,
<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-emerald-400 mt-0.5">
<p className="text-2xl font-bold text-blue-400 mt-0.5">
{fmt(elapsedSeconds) ?? '—'}
</p>
</div>
-1
View File
@@ -21,4 +21,3 @@ export const getTests = (params = {}) => {
}
export const getConfig = () => apiFetch('/config')
export const saveConfig = (body) => apiFetch('/config', { method: 'POST', body: JSON.stringify(body) })
export const browse = (path) => apiFetch(`/browse${path ? `?path=${encodeURIComponent(path)}` : ''}`)