Spaces:
Sleeping
Sleeping
File size: 4,223 Bytes
68af3c5 9dfccd9 68af3c5 9dfccd9 68af3c5 9dfccd9 68af3c5 9dfccd9 68af3c5 9dfccd9 68af3c5 9dfccd9 68af3c5 9dfccd9 68af3c5 9dfccd9 68af3c5 9dfccd9 68af3c5 9dfccd9 68af3c5 9dfccd9 68af3c5 9dfccd9 68af3c5 9dfccd9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | import { useState, useEffect, useRef } from 'react'
import { apiFetch, ApiError } from '@/lib/http'
import { useUIStore } from '@/stores/uiStore'
import { cn } from '@/lib/utils'
type JobStatus = 'pending' | 'running' | 'completed' | 'failed'
interface Props {
source: 'jira' | 'confluence'
sourceKey: string
label: string
lastSynced?: string
}
const STATUS_LABEL: Record<JobStatus, string> = {
pending: 'Queued…',
running: 'Syncing…',
completed: 'Sync complete',
failed: 'Sync failed',
}
const STATUS_COLOR: Record<JobStatus, string> = {
pending: 'text-stone-400',
running: 'text-blue-500',
completed: 'text-green-600',
failed: 'text-red-500',
}
export function SyncTrigger({ source, sourceKey, label, lastSynced }: Props) {
const [taskId, setTaskId] = useState<string | null>(null)
const [jobStatus, setJobStatus] = useState<JobStatus | null>(null)
const [loading, setLoading] = useState(false)
const addToast = useUIStore((s) => s.addToast)
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
// Poll job status until terminal state
useEffect(() => {
if (!taskId) return
if (pollRef.current) clearInterval(pollRef.current)
const poll = async () => {
try {
const res = await apiFetch(`/ingest/jobs/${taskId}`)
const data = await res.json() as { status: JobStatus }
setJobStatus(data.status)
if (data.status === 'completed' || data.status === 'failed') {
clearInterval(pollRef.current!)
pollRef.current = null
addToast({
type: data.status === 'completed' ? 'success' : 'error',
message: data.status === 'completed'
? `${label} sync finished`
: `${label} sync failed`,
})
}
} catch { /* ignore poll errors */ }
}
poll() // immediate first check
pollRef.current = setInterval(poll, 4000)
return () => { if (pollRef.current) clearInterval(pollRef.current) }
}, [taskId]) // eslint-disable-line react-hooks/exhaustive-deps
const trigger = async () => {
setLoading(true)
setTaskId(null)
setJobStatus(null)
try {
const path = source === 'jira'
? `/jira/sync/${sourceKey}`
: `/confluence/sync/${sourceKey}`
const res = await apiFetch(path, { method: 'POST' })
const data = await res.json() as { task_id: string }
setTaskId(data.task_id)
setJobStatus('pending')
addToast({ type: 'success', message: `${label} sync queued` })
} catch (err) {
if (!(err instanceof ApiError)) {
addToast({ type: 'error', message: `Failed to trigger ${label} sync` })
}
} finally {
setLoading(false)
}
}
const isBusy = loading || jobStatus === 'pending' || jobStatus === 'running'
return (
<div className="flex items-center justify-between rounded-lg border border-surface-subtle p-4">
<div>
<p className="text-sm font-medium">{label}</p>
{lastSynced && !jobStatus && (
<p className="text-xs text-stone-500">Last synced {lastSynced}</p>
)}
{jobStatus && (
<div className="mt-1 flex items-center gap-1.5">
{(jobStatus === 'pending' || jobStatus === 'running') && (
<span className="inline-block h-2.5 w-2.5 animate-spin rounded-full border-2 border-blue-300 border-t-blue-600" />
)}
<p className={cn('text-xs font-medium', STATUS_COLOR[jobStatus])}>
{STATUS_LABEL[jobStatus]}
</p>
</div>
)}
{taskId && (
<p className="mt-0.5 font-mono text-[10px] text-stone-400">
Task {taskId.slice(0, 8)}…
</p>
)}
</div>
<button
onClick={trigger}
disabled={isBusy}
className={cn(
'rounded-lg px-3 py-1.5 text-xs font-medium transition-colors',
isBusy
? 'cursor-not-allowed bg-stone-100 text-stone-400 dark:bg-stone-800'
: 'bg-brand text-white hover:bg-brand-dark',
)}
>
{loading ? 'Queuing…' : isBusy ? 'Syncing…' : 'Sync now'}
</button>
</div>
)
}
|