Add first-run setup wizard and file browser for library creation
- On first start (no users in DB), show a setup page in the web UI to create the admin account instead of reading credentials from .env - New public endpoints: GET /api/setup/status, POST /api/setup - New admin endpoint: GET /api/filebrowser?path=... for directory listing - FileBrowser modal component with navigation, used in Admin > Libraries - Remove _seed_admin / _seed_default_library from server startup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
103
frontend/src/components/common/FileBrowser.tsx
Normal file
103
frontend/src/components/common/FileBrowser.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Folder, ChevronRight, ChevronUp, X, Check, Loader2 } from 'lucide-react'
|
||||
import api from '../../api/client'
|
||||
|
||||
interface Entry { name: string; path: string; isDir: boolean }
|
||||
|
||||
interface Props {
|
||||
initialPath?: string
|
||||
onSelect: (path: string) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function FileBrowser({ initialPath = '/', onSelect, onClose }: Props) {
|
||||
const [path, setPath] = useState(initialPath)
|
||||
const [entries, setEntries] = useState<Entry[]>([])
|
||||
const [parent, setParent] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const load = async (p: string) => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const r = await api.get('/api/filebrowser', { params: { path: p } })
|
||||
setPath(r.data.path)
|
||||
setParent(r.data.parent)
|
||||
setEntries(r.data.entries.filter((e: Entry) => e.isDir))
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Fehler beim Laden')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => { load(initialPath) }, [])
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/70 z-50 flex items-center justify-center p-4" onClick={onClose}>
|
||||
<div
|
||||
className="bg-surface border border-white/10 rounded-xl w-full max-w-lg shadow-2xl flex flex-col"
|
||||
style={{ maxHeight: '80vh' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10 flex-shrink-0">
|
||||
<h3 className="text-sm font-semibold text-white">Ordner auswählen</h3>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-white">
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Current path */}
|
||||
<div className="px-4 py-2 bg-white/5 border-b border-white/10 flex items-center gap-2 flex-shrink-0">
|
||||
{parent && (
|
||||
<button onClick={() => load(parent)} className="text-gray-400 hover:text-white flex-shrink-0">
|
||||
<ChevronUp size={16} />
|
||||
</button>
|
||||
)}
|
||||
<p className="text-xs text-gray-300 font-mono truncate">{path}</p>
|
||||
</div>
|
||||
|
||||
{/* Entry list */}
|
||||
<div className="overflow-y-auto flex-1">
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-10">
|
||||
<Loader2 size={24} className="text-primary animate-spin" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<p className="text-red-400 text-sm px-4 py-6">{error}</p>
|
||||
) : entries.length === 0 ? (
|
||||
<p className="text-gray-500 text-sm px-4 py-6">Keine Unterordner</p>
|
||||
) : (
|
||||
entries.map((e) => (
|
||||
<button
|
||||
key={e.path}
|
||||
onClick={() => load(e.path)}
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-white/5 text-gray-300 hover:text-white text-sm text-left transition-colors"
|
||||
>
|
||||
<Folder size={16} className="text-yellow-500 flex-shrink-0" />
|
||||
<span className="flex-1 truncate">{e.name}</span>
|
||||
<ChevronRight size={14} className="flex-shrink-0 text-gray-600" />
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-white/10 flex-shrink-0">
|
||||
<button onClick={onClose} className="text-gray-400 text-sm hover:text-white">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { onSelect(path); onClose() }}
|
||||
className="flex items-center gap-2 bg-primary text-black px-4 py-2 rounded-lg text-sm font-medium hover:bg-primary/80"
|
||||
>
|
||||
<Check size={14} />
|
||||
Auswählen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user