shinkan-jinkendo/frontend/src/pages/MediaWikiImportPage.jsx
Lars 89055ddbc4
Some checks failed
Deploy Development / deploy (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 5s
Test Suite / playwright-tests (push) Failing after 1m55s
feat: Admin page navigation with MediaWiki Import link
- 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
2026-04-24 17:03:13 +02:00

536 lines
20 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
}