refactor(admin): split document-crawler page.tsx into colocated components
Break 839-line page.tsx into _types.ts, _components/SourcesTab.tsx, JobsTab.tsx, DocumentsTab.tsx, ReportTab.tsx, and ComplianceRing.tsx. page.tsx is now 56 LOC (wiring only). No behavior changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { CrawlSource, api } from '../_types'
|
||||
|
||||
export function SourcesTab() {
|
||||
const [sources, setSources] = useState<CrawlSource[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [formName, setFormName] = useState('')
|
||||
const [formPath, setFormPath] = useState('')
|
||||
const [testResult, setTestResult] = useState<Record<string, string>>({})
|
||||
|
||||
const loadSources = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await api('sources')
|
||||
setSources(data || [])
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => { loadSources() }, [loadSources])
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!formName || !formPath) return
|
||||
await api('sources', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: formName, path: formPath }),
|
||||
})
|
||||
setFormName('')
|
||||
setFormPath('')
|
||||
setShowForm(false)
|
||||
loadSources()
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
await api(`sources/${id}`, { method: 'DELETE' })
|
||||
loadSources()
|
||||
}
|
||||
|
||||
const handleToggle = async (source: CrawlSource) => {
|
||||
await api(`sources/${source.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ enabled: !source.enabled }),
|
||||
})
|
||||
loadSources()
|
||||
}
|
||||
|
||||
const handleTest = async (id: string) => {
|
||||
setTestResult(prev => ({ ...prev, [id]: 'testing...' }))
|
||||
const result = await api(`sources/${id}/test`, { method: 'POST' })
|
||||
setTestResult(prev => ({ ...prev, [id]: result?.message || 'Fehler' }))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Crawl-Quellen</h2>
|
||||
<button
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
className="px-4 py-2 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
+ Neue Quelle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
||||
<input
|
||||
value={formName}
|
||||
onChange={e => setFormName(e.target.value)}
|
||||
placeholder="z.B. Compliance-Ordner"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Pfad (relativ zu /data/crawl)</label>
|
||||
<input
|
||||
value={formPath}
|
||||
onChange={e => setFormPath(e.target.value)}
|
||||
placeholder="z.B. compliance-docs"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={handleCreate} className="px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700">
|
||||
Erstellen
|
||||
</button>
|
||||
<button onClick={() => setShowForm(false)} className="px-4 py-2 text-gray-600 text-sm hover:text-gray-800">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-500">Laden...</div>
|
||||
) : sources.length === 0 ? (
|
||||
<div className="text-center py-12 text-gray-500 bg-white rounded-xl border border-gray-200">
|
||||
<p className="text-lg font-medium">Keine Quellen konfiguriert</p>
|
||||
<p className="text-sm mt-1">Erstellen Sie eine Crawl-Quelle um Dokumente zu scannen.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{sources.map(s => (
|
||||
<div key={s.id} className="bg-white rounded-xl border border-gray-200 p-5 flex items-center gap-4">
|
||||
<div className={`w-3 h-3 rounded-full ${s.enabled ? 'bg-green-500' : 'bg-gray-300'}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{s.name}</div>
|
||||
<div className="text-sm text-gray-500 truncate">{s.path}</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
Tiefe: {s.max_depth} | Formate: {(typeof s.file_extensions === 'string' ? JSON.parse(s.file_extensions) : s.file_extensions).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
{testResult[s.id] && (
|
||||
<span className="text-xs text-gray-500 bg-gray-50 px-2 py-1 rounded">{testResult[s.id]}</span>
|
||||
)}
|
||||
<button onClick={() => handleTest(s.id)} className="text-sm text-blue-600 hover:text-blue-800">Testen</button>
|
||||
<button onClick={() => handleToggle(s)} className="text-sm text-gray-600 hover:text-gray-800">
|
||||
{s.enabled ? 'Deaktivieren' : 'Aktivieren'}
|
||||
</button>
|
||||
<button onClick={() => handleDelete(s.id)} className="text-sm text-red-600 hover:text-red-800">Loeschen</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user