Compact request activity into a live summary and modal history
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import './latest-activity.css'
|
||||
|
||||
type Event = { id: string; service: string; state: string; message: string; started_at?: string; finished_at?: string }
|
||||
type Operation = { id: string; label: string; status: string; events: Event[] }
|
||||
|
||||
export default function LatestActivity({ operation, besideDownload, onDismiss }: {
|
||||
operation: Operation; besideDownload: boolean; onDismiss: () => void
|
||||
}) {
|
||||
const dialog = useRef<HTMLDialogElement>(null)
|
||||
const trigger = useRef<HTMLButtonElement>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
const latest = [...operation.events].sort((a, b) =>
|
||||
(a.finished_at ?? a.started_at ?? '').localeCompare(b.finished_at ?? b.started_at ?? '')
|
||||
).at(-1)
|
||||
const status = operation.status === 'running' ? 'Working' : operation.status === 'complete' ? 'Finished' : 'Needs attention'
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const element = dialog.current
|
||||
element?.showModal()
|
||||
const previous = document.body.style.overflow
|
||||
document.body.style.overflow = 'hidden'
|
||||
return () => {
|
||||
element?.close()
|
||||
document.body.style.overflow = previous
|
||||
trigger.current?.focus()
|
||||
}
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
<div className={`request-overview-block latest-activity ${besideDownload ? 'beside-download' : 'full-row'}`}>
|
||||
<button ref={trigger} type="button" className="latest-activity-trigger" onClick={() => setOpen(true)} aria-haspopup="dialog" aria-expanded={open}>
|
||||
<span className="latest-activity-heading"><span className="request-overview-label">Latest activity</span><span className={`latest-activity-badge is-${operation.status}`}>{status}</span></span>
|
||||
<span className="latest-activity-message" role="status">{latest?.message || 'Getting ready to check your request…'}</span>
|
||||
<span className="latest-activity-more">View all activity ({operation.events.length}) <span aria-hidden="true">↗</span></span>
|
||||
</button>
|
||||
<dialog ref={dialog} className="activity-dialog" aria-labelledby="activity-dialog-title" onCancel={() => setOpen(false)} onClose={() => setOpen(false)} onClick={(event) => { if (event.target === event.currentTarget) setOpen(false) }}>
|
||||
<div className="activity-dialog-content">
|
||||
<header>
|
||||
<div><span className="request-overview-label">Activity details</span><h2 id="activity-dialog-title">{operation.label}</h2><small>{status} · {operation.events.length} updates</small></div>
|
||||
<button type="button" onClick={() => setOpen(false)} autoFocus>Close</button>
|
||||
</header>
|
||||
<ol className="activity-dialog-events" aria-label="All activity, oldest first">
|
||||
{operation.events.map((event) => <li key={event.id} className={`is-${event.state}`}>
|
||||
<span className="activity-event-state">{event.state === 'active' ? 'Working' : event.state === 'error' ? 'Needs attention' : 'Done'}</span>
|
||||
<div><strong>{event.service}</strong><p>{event.message}</p></div>
|
||||
</li>)}
|
||||
</ol>
|
||||
{operation.status !== 'running' && <footer><button type="button" onClick={() => { setOpen(false); onDismiss() }}>Dismiss activity</button></footer>}
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user