- New AdminPageNav component with horizontal navigation - Links to Hierarchie / Kataloge / Wiki-Import - Integrated in all 3 admin pages - Uses lucide-react icons (TreePine, FolderTree, Download) - Active state tracking via useLocation - Mobile-friendly with flexbox layout Navigation flow: /admin/hierarchy → /admin/catalogs → /admin/mediawiki-import
536 lines
20 KiB
JavaScript
536 lines
20 KiB
JavaScript
import React, { useState, useEffect } from 'react'
|
||
import api from '../utils/api'
|
||
import AdminPageNav from '../components/AdminPageNav'
|
||
|
||
export default function MediaWikiImportPage() {
|
||
const [activeTab, setActiveTab] = useState('preview')
|
||
const [loading, setLoading] = useState(false)
|
||
const [error, setError] = useState(null)
|
||
|
||
// Preview Tab State
|
||
const [previewCategory, setPreviewCategory] = useState('Übungen')
|
||
const [previewType, setPreviewType] = useState('exercise')
|
||
const [previewLimit, setPreviewLimit] = useState(10)
|
||
const [previewData, setPreviewData] = useState(null)
|
||
|
||
// Execute Tab State
|
||
const [executeCategory, setExecuteCategory] = useState('Übungen')
|
||
const [executeType, setExecuteType] = useState('exercise')
|
||
const [executeReimport, setExecuteReimport] = useState(false)
|
||
const [executeDryRun, setExecuteDryRun] = useState(false)
|
||
const [executeLimit, setExecuteLimit] = useState(null)
|
||
const [currentImport, setCurrentImport] = useState(null)
|
||
const [pollingInterval, setPollingInterval] = useState(null)
|
||
|
||
// History Tab State
|
||
const [logs, setLogs] = useState([])
|
||
|
||
// Load logs on mount and tab switch
|
||
useEffect(() => {
|
||
if (activeTab === 'history') {
|
||
loadLogs()
|
||
}
|
||
}, [activeTab])
|
||
|
||
// Polling cleanup
|
||
useEffect(() => {
|
||
return () => {
|
||
if (pollingInterval) {
|
||
clearInterval(pollingInterval)
|
||
}
|
||
}
|
||
}, [pollingInterval])
|
||
|
||
const loadLogs = async () => {
|
||
try {
|
||
setLoading(true)
|
||
const data = await api.listMediaWikiImportLogs()
|
||
setLogs(data)
|
||
} catch (err) {
|
||
setError(err.message)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const handlePreview = async () => {
|
||
try {
|
||
setLoading(true)
|
||
setError(null)
|
||
const data = await api.previewMediaWikiImport(previewCategory, previewType, previewLimit)
|
||
setPreviewData(data)
|
||
} catch (err) {
|
||
setError(err.message)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleExecute = async () => {
|
||
try {
|
||
setLoading(true)
|
||
setError(null)
|
||
const result = await api.executeMediaWikiImport({
|
||
category: executeCategory,
|
||
import_type: executeType,
|
||
reimport_existing: executeReimport,
|
||
dry_run: executeDryRun,
|
||
limit: executeLimit || null
|
||
})
|
||
setCurrentImport(result)
|
||
|
||
// Start polling
|
||
const interval = setInterval(async () => {
|
||
try {
|
||
const status = await api.getMediaWikiImportStatus(result.log_id)
|
||
setCurrentImport(status)
|
||
|
||
if (status.import_status === 'completed' || status.import_status === 'failed') {
|
||
clearInterval(interval)
|
||
setPollingInterval(null)
|
||
setLoading(false)
|
||
}
|
||
} catch (err) {
|
||
console.error('Polling error:', err)
|
||
}
|
||
}, 2000)
|
||
|
||
setPollingInterval(interval)
|
||
} catch (err) {
|
||
setError(err.message)
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
|
||
<AdminPageNav />
|
||
|
||
<h1>MediaWiki Import (Semantic MediaWiki)</h1>
|
||
<p style={{ color: 'var(--text2)', marginBottom: '24px' }}>
|
||
Importiere Übungen, Fähigkeiten und Methoden aus karatetrainer.net
|
||
</p>
|
||
|
||
{/* Tabs */}
|
||
<div style={{ borderBottom: '2px solid var(--border)', marginBottom: '24px' }}>
|
||
<div style={{ display: 'flex', gap: '8px' }}>
|
||
{['preview', 'execute', 'history'].map(tab => (
|
||
<button
|
||
key={tab}
|
||
onClick={() => setActiveTab(tab)}
|
||
style={{
|
||
padding: '12px 24px',
|
||
background: activeTab === tab ? 'var(--accent)' : 'transparent',
|
||
color: activeTab === tab ? 'white' : 'var(--text1)',
|
||
border: 'none',
|
||
borderBottom: activeTab === tab ? '2px solid var(--accent)' : '2px solid transparent',
|
||
cursor: 'pointer',
|
||
fontSize: '16px',
|
||
fontWeight: activeTab === tab ? 'bold' : 'normal',
|
||
transition: 'all 0.2s'
|
||
}}
|
||
>
|
||
{tab === 'preview' && '👁️ Vorschau'}
|
||
{tab === 'execute' && '▶️ Ausführen'}
|
||
{tab === 'history' && '📜 Historie'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Error Display */}
|
||
{error && (
|
||
<div style={{
|
||
padding: '16px',
|
||
background: '#FEE',
|
||
border: '1px solid #C00',
|
||
borderRadius: '8px',
|
||
marginBottom: '20px'
|
||
}}>
|
||
<strong>Fehler:</strong> {error}
|
||
</div>
|
||
)}
|
||
|
||
{/* Preview Tab */}
|
||
{activeTab === 'preview' && (
|
||
<div>
|
||
<div style={{ background: 'var(--surface)', padding: '20px', borderRadius: '12px', marginBottom: '20px' }}>
|
||
<h2>Import-Vorschau</h2>
|
||
<div style={{ display: 'grid', gap: '16px', marginTop: '16px' }}>
|
||
<div>
|
||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
|
||
Kategorie
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={previewCategory}
|
||
onChange={(e) => setPreviewCategory(e.target.value)}
|
||
placeholder="Übungen"
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
fontSize: '16px',
|
||
border: '1px solid var(--border)',
|
||
borderRadius: '8px'
|
||
}}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
|
||
Import-Typ
|
||
</label>
|
||
<select
|
||
value={previewType}
|
||
onChange={(e) => setPreviewType(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
fontSize: '16px',
|
||
border: '1px solid var(--border)',
|
||
borderRadius: '8px'
|
||
}}
|
||
>
|
||
<option value="exercise">Übungen</option>
|
||
<option value="skill">Fähigkeiten</option>
|
||
<option value="method">Methoden</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
|
||
Limit
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={previewLimit}
|
||
onChange={(e) => setPreviewLimit(parseInt(e.target.value) || 10)}
|
||
min="1"
|
||
max="100"
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
fontSize: '16px',
|
||
border: '1px solid var(--border)',
|
||
borderRadius: '8px'
|
||
}}
|
||
/>
|
||
</div>
|
||
<button
|
||
onClick={handlePreview}
|
||
disabled={loading}
|
||
style={{
|
||
padding: '16px',
|
||
background: 'var(--accent)',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '8px',
|
||
fontSize: '16px',
|
||
fontWeight: 'bold',
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
opacity: loading ? 0.6 : 1
|
||
}}
|
||
>
|
||
{loading ? 'Lade...' : '👁️ Vorschau laden'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Preview Results */}
|
||
{previewData && (
|
||
<div>
|
||
<h3>
|
||
{previewData.total_found} Einträge gefunden in Kategorie "{previewData.category}"
|
||
</h3>
|
||
<div style={{ marginTop: '16px', display: 'grid', gap: '12px' }}>
|
||
{previewData.preview.map((item, idx) => (
|
||
<details
|
||
key={idx}
|
||
style={{
|
||
background: 'var(--surface)',
|
||
padding: '16px',
|
||
borderRadius: '8px',
|
||
border: `2px solid ${item.errors.length > 0 ? '#C00' : item.warnings.length > 0 ? '#F90' : 'var(--border)'}`
|
||
}}
|
||
>
|
||
<summary style={{ cursor: 'pointer', fontWeight: 'bold', fontSize: '16px' }}>
|
||
{item.already_imported && '✅ '}
|
||
{item.wiki_page_title}
|
||
{item.errors.length > 0 && ' ❌'}
|
||
{item.warnings.length > 0 && ' ⚠️'}
|
||
</summary>
|
||
<div style={{ marginTop: '12px', fontSize: '14px', color: 'var(--text2)' }}>
|
||
{item.already_imported && (
|
||
<p><strong>Bereits importiert:</strong> {new Date(item.last_imported_at).toLocaleString('de-DE')}</p>
|
||
)}
|
||
{item.warnings.length > 0 && (
|
||
<div style={{ background: '#FFC', padding: '8px', borderRadius: '4px', marginTop: '8px' }}>
|
||
<strong>Warnungen:</strong>
|
||
<ul>{item.warnings.map((w, i) => <li key={i}>{w}</li>)}</ul>
|
||
</div>
|
||
)}
|
||
{item.errors.length > 0 && (
|
||
<div style={{ background: '#FEE', padding: '8px', borderRadius: '4px', marginTop: '8px' }}>
|
||
<strong>Fehler:</strong>
|
||
<ul>{item.errors.map((e, i) => <li key={i}>{e}</li>)}</ul>
|
||
</div>
|
||
)}
|
||
<details style={{ marginTop: '8px' }}>
|
||
<summary style={{ cursor: 'pointer', color: 'var(--accent)' }}>
|
||
Gemappte Felder anzeigen
|
||
</summary>
|
||
<pre style={{
|
||
background: 'var(--bg)',
|
||
padding: '12px',
|
||
borderRadius: '4px',
|
||
overflow: 'auto',
|
||
fontSize: '12px',
|
||
marginTop: '8px'
|
||
}}>
|
||
{JSON.stringify(item.mapped_fields, null, 2)}
|
||
</pre>
|
||
</details>
|
||
</div>
|
||
</details>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Execute Tab */}
|
||
{activeTab === 'execute' && (
|
||
<div>
|
||
<div style={{ background: 'var(--surface)', padding: '20px', borderRadius: '12px', marginBottom: '20px' }}>
|
||
<h2>Import ausführen</h2>
|
||
<div style={{ display: 'grid', gap: '16px', marginTop: '16px' }}>
|
||
<div>
|
||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
|
||
Kategorie
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={executeCategory}
|
||
onChange={(e) => setExecuteCategory(e.target.value)}
|
||
placeholder="Übungen"
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
fontSize: '16px',
|
||
border: '1px solid var(--border)',
|
||
borderRadius: '8px'
|
||
}}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
|
||
Import-Typ
|
||
</label>
|
||
<select
|
||
value={executeType}
|
||
onChange={(e) => setExecuteType(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
fontSize: '16px',
|
||
border: '1px solid var(--border)',
|
||
borderRadius: '8px'
|
||
}}
|
||
>
|
||
<option value="exercise">Übungen</option>
|
||
<option value="skill">Fähigkeiten</option>
|
||
<option value="method">Methoden</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||
<input
|
||
type="checkbox"
|
||
checked={executeReimport}
|
||
onChange={(e) => setExecuteReimport(e.target.checked)}
|
||
/>
|
||
<span>Existierende überschreiben (Reimport)</span>
|
||
</label>
|
||
</div>
|
||
<div>
|
||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||
<input
|
||
type="checkbox"
|
||
checked={executeDryRun}
|
||
onChange={(e) => setExecuteDryRun(e.target.checked)}
|
||
/>
|
||
<span>Dry-Run (nicht speichern)</span>
|
||
</label>
|
||
</div>
|
||
<div>
|
||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
|
||
Limit (optional)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={executeLimit || ''}
|
||
onChange={(e) => setExecuteLimit(e.target.value ? parseInt(e.target.value) : null)}
|
||
placeholder="Kein Limit"
|
||
min="1"
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
fontSize: '16px',
|
||
border: '1px solid var(--border)',
|
||
borderRadius: '8px'
|
||
}}
|
||
/>
|
||
</div>
|
||
<button
|
||
onClick={handleExecute}
|
||
disabled={loading}
|
||
style={{
|
||
padding: '16px',
|
||
background: executeDryRun ? 'var(--accent-dark)' : 'var(--accent)',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '8px',
|
||
fontSize: '16px',
|
||
fontWeight: 'bold',
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
opacity: loading ? 0.6 : 1
|
||
}}
|
||
>
|
||
{loading ? '⏳ Import läuft...' : executeDryRun ? '🧪 Dry-Run starten' : '▶️ Import starten'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Import Status */}
|
||
{currentImport && (
|
||
<div style={{
|
||
background: 'var(--surface)',
|
||
padding: '20px',
|
||
borderRadius: '12px',
|
||
border: `2px solid ${
|
||
currentImport.import_status === 'completed' ? '#0A0' :
|
||
currentImport.import_status === 'failed' ? '#C00' :
|
||
'var(--accent)'
|
||
}`
|
||
}}>
|
||
<h3>
|
||
{currentImport.import_status === 'running' && '⏳ Import läuft...'}
|
||
{currentImport.import_status === 'completed' && '✅ Import abgeschlossen'}
|
||
{currentImport.import_status === 'failed' && '❌ Import fehlgeschlagen'}
|
||
</h3>
|
||
<div style={{ marginTop: '16px', display: 'grid', gap: '8px', fontSize: '14px' }}>
|
||
<div><strong>Log ID:</strong> {currentImport.id}</div>
|
||
<div><strong>Kategorie:</strong> {currentImport.category}</div>
|
||
<div><strong>Typ:</strong> {currentImport.import_type}</div>
|
||
<div><strong>Total:</strong> {currentImport.items_total}</div>
|
||
<div style={{ color: '#0A0' }}><strong>✅ Importiert:</strong> {currentImport.items_imported}</div>
|
||
<div style={{ color: '#F90' }}><strong>⏭️ Übersprungen:</strong> {currentImport.items_skipped}</div>
|
||
<div style={{ color: '#C00' }}><strong>❌ Fehlgeschlagen:</strong> {currentImport.items_failed}</div>
|
||
{currentImport.started_at && (
|
||
<div><strong>Gestartet:</strong> {new Date(currentImport.started_at).toLocaleString('de-DE')}</div>
|
||
)}
|
||
{currentImport.finished_at && (
|
||
<div><strong>Beendet:</strong> {new Date(currentImport.finished_at).toLocaleString('de-DE')}</div>
|
||
)}
|
||
</div>
|
||
|
||
{currentImport.error_log && currentImport.error_log.length > 0 && (
|
||
<details style={{ marginTop: '16px' }}>
|
||
<summary style={{ cursor: 'pointer', color: '#C00', fontWeight: 'bold' }}>
|
||
❌ Fehler anzeigen ({currentImport.error_log.length})
|
||
</summary>
|
||
<div style={{
|
||
marginTop: '8px',
|
||
background: '#FEE',
|
||
padding: '12px',
|
||
borderRadius: '4px',
|
||
maxHeight: '300px',
|
||
overflow: 'auto'
|
||
}}>
|
||
{currentImport.error_log.map((err, idx) => (
|
||
<div key={idx} style={{ marginBottom: '8px', paddingBottom: '8px', borderBottom: '1px solid #CCC' }}>
|
||
<strong>{err.item}:</strong> {err.error}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</details>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* History Tab */}
|
||
{activeTab === 'history' && (
|
||
<div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||
<h2>Import-Historie</h2>
|
||
<button
|
||
onClick={loadLogs}
|
||
disabled={loading}
|
||
style={{
|
||
padding: '8px 16px',
|
||
background: 'var(--accent)',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '8px',
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
opacity: loading ? 0.6 : 1
|
||
}}
|
||
>
|
||
🔄 Aktualisieren
|
||
</button>
|
||
</div>
|
||
|
||
{logs.length === 0 && !loading && (
|
||
<p style={{ color: 'var(--text2)', textAlign: 'center', padding: '40px' }}>
|
||
Noch keine Imports durchgeführt
|
||
</p>
|
||
)}
|
||
|
||
<div style={{ display: 'grid', gap: '12px' }}>
|
||
{logs.map((log) => (
|
||
<div
|
||
key={log.id}
|
||
style={{
|
||
background: 'var(--surface)',
|
||
padding: '16px',
|
||
borderRadius: '8px',
|
||
border: `2px solid ${
|
||
log.import_status === 'completed' ? '#0A0' :
|
||
log.import_status === 'failed' ? '#C00' :
|
||
'var(--accent)'
|
||
}`
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
|
||
<div>
|
||
<div style={{ fontSize: '18px', fontWeight: 'bold', marginBottom: '8px' }}>
|
||
{log.import_status === 'completed' && '✅ '}
|
||
{log.import_status === 'failed' && '❌ '}
|
||
{log.import_status === 'running' && '⏳ '}
|
||
{log.category} ({log.import_type})
|
||
{log.dry_run && ' [Dry-Run]'}
|
||
</div>
|
||
<div style={{ fontSize: '14px', color: 'var(--text2)', display: 'grid', gap: '4px' }}>
|
||
<div>{new Date(log.started_at).toLocaleString('de-DE')}</div>
|
||
<div>
|
||
<span style={{ color: '#0A0' }}>✅ {log.items_imported}</span>
|
||
{' | '}
|
||
<span style={{ color: '#F90' }}>⏭️ {log.items_skipped}</span>
|
||
{' | '}
|
||
<span style={{ color: '#C00' }}>❌ {log.items_failed}</span>
|
||
{' | '}
|
||
<span>Total: {log.items_total}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|