sozsoft-platform/ui/src/views/setup/DatabaseSetup.tsx
2026-04-28 20:12:14 +03:00

281 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useRef, useState } from 'react'
import { APP_NAME } from '@/constants/app.constant'
import { getMigrateUrl } from '@/services/setup.service'
import { applicationConfigurationUrl } from '@/services/abpConfig.service'
interface LogLine {
level: 'info' | 'warn' | 'error' | 'success' | 'restart' | 'done'
message: string
}
type MigrationStatus = 'idle' | 'running' | 'success' | 'error' | 'restarting'
const levelClass: Record<string, string> = {
info: 'text-gray-300',
warn: 'text-yellow-400',
error: 'text-red-400',
success: 'text-green-400',
restart: 'text-blue-400',
done: 'text-blue-400',
}
/**
* Sunucu yeniden başlayana kadar ABP config endpoint'ini poll eder.
* Arka arkaya 2 başarılı yanıt alındığında onReady çağrılır.
* @param onReady Sunucu hazır olduğunda çağrılacak callback
* @param onAttempt Her denemede kaçıncı deneme olduğunu bildiren opsiyonel callback
*/
const pollUntilServerReady = (onReady: () => void, onAttempt?: (attempt: number) => void): (() => void) => {
const REQUIRED_SUCCESS = 2
let attempt = 0
let successCount = 0
let timerId: ReturnType<typeof setTimeout> | null = null
let cancelled = false
const tick = async () => {
if (cancelled) return
attempt++
onAttempt?.(attempt)
try {
const res = await fetch(
`${import.meta.env.VITE_API_URL ?? ''}${applicationConfigurationUrl(false)}`,
{ method: 'GET', headers: { Accept: 'application/json' }, cache: 'no-store' },
)
if (res.status === 200) {
try {
const json = await res.json()
if (json && typeof json.currentUser === 'object') {
successCount++
if (successCount >= REQUIRED_SUCCESS) {
onReady()
return
}
timerId = setTimeout(tick, 1000)
return
}
} catch { /* parse hatası */ }
}
successCount = 0
} catch {
successCount = 0
}
timerId = setTimeout(tick, 2000)
}
// İlk denemeden önce kısa bekleme (sunucunun kapanma süresi)
timerId = setTimeout(tick, 3000)
// İptal fonksiyonu döner
return () => {
cancelled = true
if (timerId) clearTimeout(timerId)
}
}
const DatabaseSetup = () => {
const [logs, setLogs] = useState<LogLine[]>([])
const [status, setStatus] = useState<MigrationStatus>('idle')
const [pollCountdown, setPollCountdown] = useState(0)
const logEndRef = useRef<HTMLDivElement>(null)
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// Auto-scroll to bottom when new logs arrive
useEffect(() => {
logEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [logs])
// Cleanup on component unmount
useEffect(() => {
return () => {
const ref = pollTimerRef.current as any
if (ref?.unref) ref.unref()
else if (pollTimerRef.current) clearTimeout(pollTimerRef.current as any)
}
}, [])
const addLog = (level: LogLine['level'], message: string) => {
setLogs((prev) => [...prev, { level, message }])
}
// ── Poll ABP config endpoint until server is ready ──────────────
const pollUntilReady = () => {
setStatus('restarting')
const cancel = pollUntilServerReady(
() => { window.location.href = '/' },
(attempt) => setPollCountdown(attempt),
)
pollTimerRef.current = { unref: cancel } as any
}
const startMigration = async () => {
if (status === 'running') return
setLogs([])
setStatus('running')
addLog('info', 'Starting migration...')
const url = getMigrateUrl()
const abortController = new AbortController()
const parseChunk = (chunk: string) => {
// SSE format: "data: {...}\n\n"
const lines = chunk.split('\n')
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed.startsWith('data:')) continue
const raw = trimmed.slice(5).trim()
if (!raw) continue
try {
const data = JSON.parse(raw) as { level?: string; message?: string }
const level = (data.level ?? 'info') as LogLine['level']
const message = data.message ?? raw
if (level === 'done') {
setStatus((prev) => {
if (prev === 'running') return 'error'
return prev
})
return
}
addLog(level, message)
if (level === 'success') {
setStatus('success')
} else if (level === 'error') {
setStatus('error')
} else if (level === 'restart') {
pollUntilReady()
}
} catch {
addLog('info', raw)
}
}
}
try {
const response = await fetch(url, {
method: 'POST',
headers: { Accept: 'text/event-stream' },
signal: abortController.signal,
})
if (!response.ok || !response.body) {
addLog('error', `Server responded with status ${response.status}`)
setStatus('error')
return
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = await reader.read()
if (done) break
parseChunk(decoder.decode(value, { stream: true }))
}
} catch (err: any) {
if (err?.name !== 'AbortError') {
setStatus((prev) => (prev === 'running' ? 'error' : prev))
addLog('error', 'Server connection lost or migration could not be completed.')
}
}
}
return (
<div className="min-h-screen bg-gray-900 flex flex-col items-center justify-center p-6 text-white">
{/* Header */}
<div className="mb-8 text-center">
<h1 className="text-3xl font-bold mb-2">{APP_NAME}</h1>
<p className="text-gray-400 text-lg">Initial Setup Creating Database</p>
</div>
{/* Status Card */}
<div className="w-full max-w-4xl bg-gray-800 rounded-xl shadow-2xl overflow-hidden">
{/* Card Header */}
<div className="flex items-center justify-between px-5 py-4 bg-gray-700 border-b border-gray-600">
<div className="flex items-center gap-3">
<StatusIndicator status={status} />
<span className="font-semibold text-sm tracking-wide">
{status === 'idle' && 'Ready to Setup'}
{status === 'running' && 'Migration Running...'}
{status === 'success' && 'Migration Completed ✓'}
{status === 'restarting' && `Server Restarting... (attempt ${pollCountdown})`}
{status === 'error' && 'Migration Failed ✗'}
</span>
</div>
<span className="text-xs text-gray-400">{logs.length} log lines</span>
</div>
{/* Log Terminal */}
<div className="h-96 overflow-y-auto bg-gray-950 px-4 py-3 font-mono text-xs leading-relaxed">
{logs.length === 0 ? (
<p className="text-gray-600 mt-2">Logs will appear here when migration starts...</p>
) : (
logs.map((line, i) => (
<div key={i} className={`${levelClass[line.level] ?? 'text-gray-300'} mb-0.5`}>
<span className="text-gray-600 mr-2 select-none">{String(i + 1).padStart(4, '0')}</span>
{line.message}
</div>
))
)}
<div ref={logEndRef} />
</div>
{/* Action Area */}
<div className="flex items-center justify-between px-5 py-4 bg-gray-700 border-t border-gray-600">
<div className="text-xs text-gray-400">
{status === 'idle' && 'Database not found. Press the button to start migration.'}
{status === 'running' && 'Please wait, migration is in progress...'}
{status === 'success' && 'Migration completed. Server is restarting...'}
{status === 'restarting' &&
'Waiting for application server to be ready, you will be redirected automatically...'}
{status === 'error' &&
'Review the errors in the logs, fix the issue, and try again.'}
</div>
<div className="flex gap-3">
{(status === 'idle' || status === 'error') && (
<button
onClick={startMigration}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-colors"
>
{status === 'error' ? 'Retry' : 'Start Setup'}
</button>
)}
{status === 'restarting' && (
<div className="flex items-center gap-2 text-blue-400 text-sm">
<span className="inline-block w-3 h-3 rounded-full bg-blue-400 animate-ping" />
Waiting...
</div>
)}
</div>
</div>
</div>
<p className="mt-6 text-xs text-gray-600">
This page is only visible when the database does not exist.
</p>
</div>
)
}
// ─── Status Indicator ────────────────────────────────────────────────────────
const StatusIndicator = ({ status }: { status: MigrationStatus }) => {
const cls: Record<MigrationStatus, string> = {
idle: 'bg-gray-500',
running: 'bg-yellow-400 animate-pulse',
success: 'bg-green-500',
restarting: 'bg-blue-400 animate-ping',
error: 'bg-red-500',
}
return <span className={`inline-block w-3 h-3 rounded-full ${cls[status]}`} />
}
export default DatabaseSetup