このコミットが含まれているのは:
2026-10-26 21:24:32 +09:00
コミット 2563b3fef3
21個のファイルの変更、703行の追加、54行の削除
+130
ファイルの表示
@@ -0,0 +1,130 @@
import { useEffect, useRef, useState } from 'react'
import LocalisedText from '@/components/LocalisedText'
import { apiDelete, apiGet, apiPost, isApiError, startAPIDownload } from '@/lib/api'
import { useUITranslation } from '@/lib/useUITranslation'
import type { DialogueFormControls } from '@/lib/dialogues/useDialogue'
import type { LocaleCode } from '@/lib/locales'
export type CountsExportHandle = { cancel: () => Promise<boolean> }
type ExportStatus = {
status: 'starting' | 'queued' | 'running' | 'cancelling' | 'cancelled' | 'ready' | 'failed'
processed: number; total: number | null; token?: string }
const CountsExportProgress = (
{ payload, locale, handle, controls }: {
payload: Record<string, string>; locale: LocaleCode
handle: CountsExportHandle; controls: DialogueFormControls },
) => {
const { label, message } = useUITranslation ('counts')
const [state, setState] = useState<ExportStatus> ({
status: 'starting', processed: 0, total: null })
const [cancelError, setCancelError] = useState (false)
const terminal = useRef (false)
const cancelled = useRef (false)
const job = useRef<string | null> (null)
const creation = useRef<Promise<{ job: string }> | null> (null)
const downloaded = useRef (false)
useEffect (() => {
handle.cancel = async () => {
if (terminal.current) return true
cancelled.current = true
if (job.current != null)
{
try
{
setCancelError (false)
const result = await apiDelete<ExportStatus> (
`/counts/export/jobs/${encodeURIComponent (job.current)}`, { locale })
setState (result)
}
catch
{
setCancelError (true)
}
}
return false
}
}, [handle, locale])
useEffect (() => {
let active = true
let timer: ReturnType<typeof setTimeout> | undefined
const poll = async (token: string) => {
try
{
const response = await apiGet<ExportStatus> (
`/counts/export/jobs/${encodeURIComponent (token)}`, { locale })
if (!active) return
const result = (cancelled.current && response.status === 'ready')
? await apiDelete<ExportStatus> (
`/counts/export/jobs/${encodeURIComponent (token)}`, { locale }) : response
if (!active) return
setState (result)
terminal.current = ['ready', 'failed', 'cancelled'].includes (result.status)
if (result.status === 'ready' && result.token != null && !downloaded.current)
{
downloaded.current = true
startAPIDownload ('/counts/export', result.token, () => {
if (active) setState (previous => ({ ...previous, status: 'failed' }))
})
}
if (!terminal.current) timer = setTimeout (() => poll (token), 800)
}
catch (error)
{
if (active)
{
if (isApiError (error) && error.response?.status === 404)
{
terminal.current = true
setState (previous => ({ ...previous, status: 'failed' }))
return
}
setCancelError (true)
timer = setTimeout (() => poll (token), 2000)
}
}
}
creation.current ??= apiPost<{ job: string }> ('/counts/export/jobs', payload, { locale })
creation.current.then (async result => {
if (!active) return
job.current = result.job
if (cancelled.current) await handle.cancel ()
await poll (result.job)
}).catch (() => {
if (active)
{
terminal.current = true
setState (previous => ({ ...previous, status: 'failed' }))
}
})
return () => {
active = false
if (timer != null) clearTimeout (timer)
}
}, [handle, locale, payload])
useEffect (() => {
controls.setActions ([])
}, [controls])
const percent = state.total == null ? null : (state.total === 0 ? 100
: Math.min (100, Math.floor (state.processed / state.total * 100)))
const phase = message (`exportProgress.${state.status}`)
return <div className="space-y-3 py-2">
<p>{label ('exportProgress.format', {
format: message (`exportFormats.${payload.format}`).text })}</p>
<p role="status" aria-live="polite">
<LocalisedText language={phase.language}>{phase.text}</LocalisedText>
</p>
<progress className="h-3 w-full" max={100} value={percent ?? undefined}
aria-label={phase.text} lang={phase.language}/>
<p aria-live="polite">{percent == null
? label ('exportProgress.processed', { count: state.processed })
: label ('exportProgress.measured', {
processed: state.processed, total: state.total ?? 0, percent })}</p>
{cancelError && <p role="alert">{label ('common:fetchError')}</p>}
</div>
}
export default CountsExportProgress