Files
Magent/frontend/app/requests/[id]/LatestActivity.tsx
T
Assclaw e3332bec1f
Magent CI/CD / verify (push) Successful in 1m50s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Skipped
Explain search outcomes and provide a direct version selection action
2026-09-13 12:16:28 +12:00

81 lines
4.8 KiB
TypeScript

'use client'
import { useEffect, useRef, useState } from 'react'
import './latest-activity.css'
import { lockBodyScroll } from '../../lib/scrollLock'
type Event = { id: string; service: string; state: string; message: string; started_at?: string; finished_at?: string }
type Operation = { summary?: { title: string; message: string; next: string; action?: string }; 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(true)
useEffect(() => { setOpen(true) }, [operation.id])
// Events are appended in order. Client result events have no timestamp.
const latest = operation.events.at(-1)
const working = operation.status === 'running' || operation.status === 'searching'
const message = latest?.message ?? ''
const choosing = /[1-9]\d* releases? (found|shown)/i.test(message)
const sent = operation.status === 'complete' && /sent|accepted.*release/i.test(message) && /download|release|Sonarr|Radarr/i.test(message)
const interrupted = latest?.id === 'connection-error'
const status = operation.summary?.title ?? (working ? 'Working on it' : choosing ? 'Choose a download' : operation.status === 'complete' ? 'Done' : 'Needs your attention')
const currentStep = operation.summary?.message ?? (working
? /send release/i.test(operation.label) ? 'Sending your download...'
: /search/i.test(operation.label) ? 'Looking for a download...'
: latest?.service === 'Jellyfin' ? 'Checking if it is ready to watch...'
: latest?.service === 'qBittorrent' ? 'Checking your download...'
: 'Checking your request...'
: interrupted ? 'The connection was lost.'
: choosing ? 'The search is finished. Choose a version to download.'
: sent ? 'Your download has been sent.'
: operation.status === 'error' ? 'We could not finish this step.'
: 'This check is finished.')
const nextStep = operation.summary?.next ?? (working ? 'Please wait. You can close this box while we work.'
: interrupted ? 'Close this box and check the request before trying again.'
: choosing ? 'Close this box to see the available downloads.'
: sent ? 'You can close this box. The request will update when the download starts.'
: operation.status === 'error' ? 'Close this box to review the request and its available options.'
: 'Close this box to see the updated request status.')
const progress = <div className={`activity-process is-${working ? 'working' : operation.status}`} role="progressbar" aria-label={currentStep} aria-valuemin={0} aria-valuemax={100} aria-valuenow={working || operation.status === 'error' ? undefined : 100}><span /></div>
useEffect(() => {
if (!open) return
const element = dialog.current
element?.showModal()
const unlock = lockBodyScroll()
return () => {
element?.close()
unlock()
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">{working && <span className="activity-spinner" aria-hidden="true" />}{currentStep}</span>
{progress}
<span className="latest-activity-more">View progress</span>
</button>
<dialog ref={dialog} className="activity-dialog" aria-labelledby="activity-dialog-title" onCancel={() => setOpen(false)} onClose={() => setOpen(false)}>
<div className="activity-dialog-content">
<header>
<div><span className="request-overview-label">Request progress</span><h2 id="activity-dialog-title">{status}</h2></div>
<button type="button" onClick={() => setOpen(false)}>Close</button>
</header>
<div className="activity-current" role="status" aria-live="polite" aria-atomic="true">
<p className="activity-current-step">{working && <span className="activity-spinner" aria-hidden="true" />}{currentStep}</p>
{progress}
<p className="activity-next-step">{nextStep}</p>
</div>
{!working && <footer><button type="button" onClick={() => { setOpen(false); onDismiss() }}>{operation.summary?.action ?? 'Dismiss activity'}</button></footer>}
</div>
</dialog>
</div>
)
}