feat: SMW-Importer Frontend (Phase 2 complete)
Phase 2A: API Functions - 5 MediaWiki import functions in api.js - previewMediaWikiImport, executeMediaWikiImport - getMediaWikiImportStatus, listMediaWikiImportLogs - deleteMediaWikiImportReference Phase 2B: UI Component - MediaWikiImportPage.jsx (3-tab interface) - Preview Tab: Category selection, preview with accordions - Execute Tab: Import form with status polling - History Tab: Import log list with refresh Phase 2C: Routing - Added /admin/mediawiki-import route in App.jsx - Import and ProtectedRoute wrapper Issue: SMW-Importer Frontend (Option C from handover) Related: backend/routers/csv_import.py (MediaWiki endpoints)
This commit is contained in:
parent
a67cc5f812
commit
46d000d6b3
|
|
@ -13,6 +13,7 @@ import TrainingPlanningPage from './pages/TrainingPlanningPage'
|
||||||
import AdminCatalogsPage from './pages/AdminCatalogsPage'
|
import AdminCatalogsPage from './pages/AdminCatalogsPage'
|
||||||
import AdminHierarchyPage from './pages/AdminHierarchyPage'
|
import AdminHierarchyPage from './pages/AdminHierarchyPage'
|
||||||
import TrainerContextsPage from './pages/TrainerContextsPage'
|
import TrainerContextsPage from './pages/TrainerContextsPage'
|
||||||
|
import MediaWikiImportPage from './pages/MediaWikiImportPage'
|
||||||
import './app.css'
|
import './app.css'
|
||||||
|
|
||||||
// Bottom Navigation (Mobile)
|
// Bottom Navigation (Mobile)
|
||||||
|
|
@ -199,6 +200,14 @@ function AppRoutes() {
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/admin/mediawiki-import"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<MediaWikiImportPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/trainer-contexts"
|
path="/trainer-contexts"
|
||||||
element={
|
element={
|
||||||
|
|
|
||||||
532
frontend/src/pages/MediaWikiImportPage.jsx
Normal file
532
frontend/src/pages/MediaWikiImportPage.jsx
Normal file
|
|
@ -0,0 +1,532 @@
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import api from '../utils/api'
|
||||||
|
|
||||||
|
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' }}>
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -595,7 +595,26 @@ export const api = {
|
||||||
|
|
||||||
// System
|
// System
|
||||||
getVersion,
|
getVersion,
|
||||||
healthCheck
|
healthCheck,
|
||||||
|
|
||||||
|
// MediaWiki Import
|
||||||
|
previewMediaWikiImport: (category, importType = 'exercise', limit = 10) =>
|
||||||
|
request(`/api/import/mediawiki/preview?category=${encodeURIComponent(category)}&import_type=${importType}&limit=${limit}`),
|
||||||
|
|
||||||
|
executeMediaWikiImport: (data) =>
|
||||||
|
request('/api/import/mediawiki/execute', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}),
|
||||||
|
|
||||||
|
getMediaWikiImportStatus: (logId) =>
|
||||||
|
request(`/api/import/mediawiki/status/${logId}`),
|
||||||
|
|
||||||
|
listMediaWikiImportLogs: () =>
|
||||||
|
request('/api/import/mediawiki/logs'),
|
||||||
|
|
||||||
|
deleteMediaWikiImportReference: (refId) =>
|
||||||
|
request(`/api/import/mediawiki/references/${refId}`, { method: 'DELETE' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export default api
|
export default api
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user