Feedback sweep: spinners on every async button, toasts for action outcomes, top activity bar, loading states on every page, terminal connecting overlay, dashboard save indicator, task pane pinned above the package list with auto-scroll and completion toast
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d08b246b5f
commit
724f30c02b
+33
-18
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { api, UpdatePackage, UpdatesStatus } from './api'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { api, TaskInfo, UpdatePackage, UpdatesStatus } from './api'
|
||||
import TaskOutput from './TaskOutput'
|
||||
import { Btn, Loading, Spinner, toast } from './ui'
|
||||
|
||||
function ago(ms?: number): string {
|
||||
if (!ms) return 'never'
|
||||
@@ -12,39 +13,52 @@ function ago(ms?: number): string {
|
||||
export default function Updates() {
|
||||
const [st, setSt] = useState<UpdatesStatus | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
const [reloading, setReloading] = useState(false)
|
||||
const [sel, setSel] = useState<Set<string>>(new Set())
|
||||
const [task, setTask] = useState<string | null>(null)
|
||||
const [taskRunning, setTaskRunning] = useState(false)
|
||||
const [open, setOpen] = useState<string | null>(null)
|
||||
const [changelog, setChangelog] = useState<Record<string, string>>({})
|
||||
const [busy, setBusy] = useState(false)
|
||||
const taskBox = useRef<HTMLDivElement>(null)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setReloading(true)
|
||||
try {
|
||||
const s = await api.updates()
|
||||
setSt(s); setErr('')
|
||||
if (s.running_task) setTask(s.running_task)
|
||||
if (s.running_task) { setTask(s.running_task); setTaskRunning(true) }
|
||||
setSel((prev) => new Set([...prev].filter((n) => s.packages.some((p) => p.name === n))))
|
||||
} catch (e) { setErr(e instanceof Error ? e.message : 'failed') }
|
||||
finally { setReloading(false) }
|
||||
}, [])
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
async function start(fn: () => Promise<{ id: string }>) {
|
||||
setBusy(true); setErr('')
|
||||
try { const t = await fn(); setTask(t.id) } catch (e) { setErr(e instanceof Error ? e.message : 'failed') } finally { setBusy(false) }
|
||||
async function start(fn: () => Promise<TaskInfo>) {
|
||||
setErr('')
|
||||
try {
|
||||
const t = await fn()
|
||||
setTask(t.id); setTaskRunning(true)
|
||||
window.setTimeout(() => taskBox.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }), 50)
|
||||
} catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') }
|
||||
}
|
||||
function done(t: TaskInfo) {
|
||||
setTaskRunning(false)
|
||||
const ok = t.exit_code === 0 && !t.error
|
||||
toast(`${t.title}: ${ok ? 'finished' : t.error || `exited ${t.exit_code}`}`, ok ? 'ok' : 'err')
|
||||
load()
|
||||
}
|
||||
async function showChangelog(p: UpdatePackage) {
|
||||
if (open === p.name) { setOpen(null); return }
|
||||
setOpen(p.name)
|
||||
if (!changelog[p.name]) {
|
||||
try { setChangelog((c) => ({ ...c, [p.name]: '…' })); const r = await api.changelog(p.name); setChangelog((c) => ({ ...c, [p.name]: r.text || '(empty)' })) }
|
||||
catch (e) { setChangelog((c) => ({ ...c, [p.name]: String(e) })) }
|
||||
try { const r = await api.changelog(p.name); setChangelog((c) => ({ ...c, [p.name]: r.text || '(empty)' })) }
|
||||
catch (e) { setChangelog((c) => ({ ...c, [p.name]: e instanceof Error ? e.message : 'unavailable' })) }
|
||||
}
|
||||
}
|
||||
const toggle = (n: string) => setSel((s) => { const x = new Set(s); if (x.has(n)) x.delete(n); else x.add(n); return x })
|
||||
|
||||
if (!st) return <div className="dash"><div className="center muted">{err || '…'}</div></div>
|
||||
const running = !!st.running_task || busy
|
||||
const canAct = st.can_upgrade && !running
|
||||
if (!st) return <div className="dash">{err ? <div className="error">{err}</div> : <Loading label="Checking packages…" />}</div>
|
||||
const canAct = st.can_upgrade && !taskRunning
|
||||
|
||||
return (
|
||||
<div className="dash updates">
|
||||
@@ -66,13 +80,16 @@ export default function Updates() {
|
||||
</div>
|
||||
|
||||
<div className="svc-actions">
|
||||
<button className="btn" disabled={!canAct} onClick={() => start(api.refreshUpdates)}>Refresh index</button>
|
||||
<button className="btn on" disabled={!canAct || st.packages.length === 0} onClick={() => window.confirm(`Upgrade all ${st.packages.length} packages?`) && start(() => api.upgrade([]))}>Upgrade all</button>
|
||||
<button className="btn" disabled={!canAct || sel.size === 0} onClick={() => start(() => api.upgrade([...sel]))}>Upgrade selected ({sel.size})</button>
|
||||
<Btn className="btn" disabled={!canAct} onClick={() => start(api.refreshUpdates)}>Refresh index</Btn>
|
||||
<Btn className="btn on" disabled={!canAct || st.packages.length === 0} confirm={`Upgrade all ${st.packages.length} packages?`} onClick={() => start(() => api.upgrade([]))}>Upgrade all</Btn>
|
||||
<Btn className="btn" disabled={!canAct || sel.size === 0} onClick={() => start(() => api.upgrade([...sel]))}>Upgrade selected ({sel.size})</Btn>
|
||||
<span className="spacer" />
|
||||
{st.packages.length > 0 && <button className="btn" onClick={() => setSel(sel.size === st.packages.length ? new Set() : new Set(st.packages.map((p) => p.name)))}>{sel.size === st.packages.length ? 'None' : 'All'}</button>}
|
||||
<button className="btn" title="reload" disabled={reloading} onClick={load}>{reloading ? <Spinner /> : '↻'}</button>
|
||||
</div>
|
||||
|
||||
<div ref={taskBox}>{task && <TaskOutput id={task} onDone={done} />}</div>
|
||||
|
||||
<div className="card pkgs">
|
||||
{st.packages.length === 0 && <div className="empty">Everything is up to date.</div>}
|
||||
{st.packages.map((p) => (
|
||||
@@ -85,12 +102,10 @@ export default function Updates() {
|
||||
</span>
|
||||
<span className="pkg-ver"><span className="muted">{p.current}</span> → {p.candidate}<span className="muted"> · {p.suite}</span></span>
|
||||
</div>
|
||||
{open === p.name && <pre className="run-out changelog">{changelog[p.name] ?? '…'}</pre>}
|
||||
{open === p.name && <pre className="run-out changelog">{changelog[p.name] ?? <span className="muted"><Spinner /> fetching changelog…</span>}</pre>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{task && <TaskOutput id={task} onDone={() => load()} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user