feat(admin): finalizing modular refactoring with testing suite and UI polish
This commit is contained in:
183
frontend/components/admin/AiManager.tsx
Normal file
183
frontend/components/admin/AiManager.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import React from 'react';
|
||||
import { Brain, Cpu, Zap, Lock, RotateCcw, Wifi, FileText, Shield } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface AiManagerProps {
|
||||
aiConfig: any;
|
||||
handleUpdateAiProvider: (provider: string) => void;
|
||||
aiKeys: { gemini: string; claude: string };
|
||||
setAiKeys: (keys: any) => void;
|
||||
isSavingKeys: boolean;
|
||||
onSaveAiKeys: () => void;
|
||||
isTestingKeys: { gemini: boolean; claude: boolean };
|
||||
onTestAiKey: (provider: 'gemini' | 'claude') => void;
|
||||
aiPrompt: string;
|
||||
setAiPrompt: (prompt: string) => void;
|
||||
isSavingPrompt: boolean;
|
||||
onUpdatePrompt: () => void;
|
||||
}
|
||||
|
||||
export default function AiManager({
|
||||
aiConfig,
|
||||
handleUpdateAiProvider,
|
||||
aiKeys,
|
||||
setAiKeys,
|
||||
isSavingKeys,
|
||||
onSaveAiKeys,
|
||||
isTestingKeys,
|
||||
onTestAiKey,
|
||||
aiPrompt,
|
||||
setAiPrompt,
|
||||
isSavingPrompt,
|
||||
onUpdatePrompt
|
||||
}: AiManagerProps) {
|
||||
return (
|
||||
<section className="bg-slate-900/40 backdrop-blur-xl border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl space-y-6 transition-all group/ai">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
||||
<Brain size={20} />
|
||||
</div>
|
||||
<h2 className="text-xl font-black text-white tracking-tight">AI Intelligence</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
{aiConfig?.providers?.map((p: any) => (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => handleUpdateAiProvider(p.id)}
|
||||
className={cn(
|
||||
"p-4 rounded-2xl border transition-all text-left flex items-center justify-between group",
|
||||
p.active
|
||||
? "bg-indigo-600 border-indigo-500 shadow-xl shadow-indigo-600/10"
|
||||
: "bg-slate-950/60 border-slate-800 hover:border-indigo-500/40"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
"w-8 h-8 rounded-lg flex items-center justify-center transition-colors",
|
||||
p.active ? "bg-white/20 text-white" : "bg-indigo-500/10 text-indigo-400 group-hover:bg-indigo-500/20"
|
||||
)}>
|
||||
{p.id === 'gemini' ? <Cpu size={16} /> : <Zap size={16} />}
|
||||
</div>
|
||||
<div>
|
||||
<p className={cn("text-xs font-black tracking-tight", p.active ? "text-white" : "text-slate-200")}>{p.name}</p>
|
||||
<p className={cn(
|
||||
"text-[9px] font-black mt-0.5 px-0.5 rounded",
|
||||
p.active
|
||||
? (p.configured ? "text-emerald-200" : "text-rose-100")
|
||||
: (p.configured ? "text-emerald-500" : "text-rose-500")
|
||||
)}>
|
||||
{p.configured ? "● Key Configured" : "○ Missing Api Key"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{p.active && (
|
||||
<div className="bg-white/20 px-2 py-0.5 rounded-full text-[8px] font-black text-white tracking-tighter">
|
||||
Active
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-indigo-500/5 border border-indigo-500/10 rounded-[2rem] p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-indigo-500/10 rounded-lg text-indigo-400"><Lock size={14} /></div>
|
||||
<h3 className="text-xs font-black text-slate-200 tracking-tight">Provider Access Keys</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onSaveAiKeys}
|
||||
disabled={isSavingKeys}
|
||||
className="px-6 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-[10px] font-black transition-all active:scale-95 shadow-xl shadow-indigo-600/10 tracking-tight border border-indigo-500/30 flex items-center gap-2"
|
||||
>
|
||||
{isSavingKeys ? <RotateCcw size={14} className="animate-spin" /> : <Lock size={14} />}
|
||||
{isSavingKeys ? "Storing..." : "Store Keys"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[9px] font-black text-slate-500 tracking-tight ml-1">Gemini Api Key</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="password"
|
||||
value={aiKeys.gemini}
|
||||
onChange={(e) => setAiKeys({...aiKeys, gemini: e.target.value})}
|
||||
placeholder={aiConfig?.providers?.find((p: any) => p.id === 'gemini')?.masked_key || "Enter Gemini Key..."}
|
||||
className="flex-1 bg-slate-950/80 border border-slate-800 rounded-xl py-3 px-4 text-xs text-white outline-none focus:border-indigo-500/50 transition-all placeholder:text-slate-600"
|
||||
/>
|
||||
<button
|
||||
onClick={() => onTestAiKey('gemini')}
|
||||
disabled={isTestingKeys.gemini}
|
||||
className={cn(
|
||||
"px-4 py-2 rounded-xl text-[9px] font-black tracking-tighter transition-all border shadow-lg flex items-center gap-1.5",
|
||||
isTestingKeys.gemini
|
||||
? "bg-slate-800 border-slate-700 text-slate-500 cursor-wait"
|
||||
: "bg-indigo-600 border-indigo-500 text-white hover:bg-indigo-500"
|
||||
)}
|
||||
>
|
||||
{isTestingKeys.gemini ? <RotateCcw size={12} className="animate-spin" /> : <Wifi size={12} />}
|
||||
{isTestingKeys.gemini ? "..." : "Test"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[9px] font-black text-slate-500 tracking-tight ml-1">Claude Api Key</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="password"
|
||||
value={aiKeys.claude}
|
||||
onChange={(e) => setAiKeys({...aiKeys, claude: e.target.value})}
|
||||
placeholder={aiConfig?.providers?.find((p: any) => p.id === 'claude')?.masked_key || "Enter Claude Key..."}
|
||||
className="flex-1 bg-slate-950/80 border border-slate-800 rounded-xl py-3 px-4 text-xs text-white outline-none focus:border-indigo-500/50 transition-all placeholder:text-slate-600"
|
||||
/>
|
||||
<button
|
||||
onClick={() => onTestAiKey('claude')}
|
||||
disabled={isTestingKeys.claude}
|
||||
className={cn(
|
||||
"px-4 py-2 rounded-xl text-[9px] font-black uppercase tracking-tighter transition-all border shadow-lg flex items-center gap-1.5",
|
||||
isTestingKeys.claude
|
||||
? "bg-slate-800 border-slate-700 text-slate-500 cursor-wait"
|
||||
: "bg-indigo-600 border-indigo-500 text-white hover:bg-indigo-500"
|
||||
)}
|
||||
>
|
||||
{isTestingKeys.claude ? <RotateCcw size={12} className="animate-spin" /> : <Wifi size={12} />}
|
||||
{isTestingKeys.claude ? "..." : "Test"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<label className="text-[10px] font-black text-slate-500 tracking-tight">System Prompt (Vision Extraction)</label>
|
||||
<button
|
||||
onClick={onUpdatePrompt}
|
||||
disabled={isSavingPrompt}
|
||||
className="px-6 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-[10px] font-black transition-all active:scale-95 shadow-xl shadow-indigo-600/10 tracking-tight border border-indigo-500/30 flex items-center gap-2"
|
||||
>
|
||||
{isSavingPrompt ? <RotateCcw size={14} className="animate-spin" /> : <FileText size={14} />}
|
||||
{isSavingPrompt ? "Saving..." : "Save Prompt"}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={aiPrompt}
|
||||
onChange={(e) => setAiPrompt(e.target.value)}
|
||||
className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl p-6 text-[11px] font-mono font-bold text-slate-300 leading-relaxed outline-none focus:border-purple-500/50 transition-all min-h-[200px] custom-scrollbar shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-purple-500/5 border border-purple-500/10 rounded-2xl p-4 flex gap-4 items-start">
|
||||
<div className="p-2 bg-purple-500/10 rounded-lg text-purple-400 shrink-0"><Shield size={14} /></div>
|
||||
<p className="text-[10px] font-bold text-slate-500 leading-relaxed">
|
||||
This prompt instructs the Vision AI core on label interpretation. Ensure it defines explicit mapping for technical attributes like <span className="text-purple-400">Item</span>, <span className="text-purple-400">Type</span>, and <span className="text-purple-400">Part Number</span> to avoid extraction null-pointers and ensure inventory data integrity.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
113
frontend/components/admin/CategoryManager.tsx
Normal file
113
frontend/components/admin/CategoryManager.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import { Layers, Plus, Edit2, Trash2, X } from 'lucide-react';
|
||||
|
||||
interface CategoryManagerProps {
|
||||
categories: any[];
|
||||
onAddCategory: () => void;
|
||||
onDeleteCategory: (id: number, name: string) => void;
|
||||
setEditingCategory: (cat: any) => void;
|
||||
setEditCatForm: (form: any) => void;
|
||||
editCatForm: any;
|
||||
editingCategory: any;
|
||||
onUpdateCategorySubmit: () => void;
|
||||
}
|
||||
|
||||
export default function CategoryManager({
|
||||
categories,
|
||||
onAddCategory,
|
||||
onDeleteCategory,
|
||||
setEditingCategory,
|
||||
setEditCatForm,
|
||||
editCatForm,
|
||||
editingCategory,
|
||||
onUpdateCategorySubmit
|
||||
}: CategoryManagerProps) {
|
||||
return (
|
||||
<section className="bg-slate-900/40 backdrop-blur-xl border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl space-y-6 transition-all group/categories">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
||||
<Layers size={20} />
|
||||
</div>
|
||||
<h2 className="text-xl font-black text-white tracking-tight">Category Groups</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onAddCategory}
|
||||
className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-500 text-white px-6 py-3 rounded-xl text-xs font-black transition-all shadow-xl shadow-indigo-600/10 active:scale-95 border border-indigo-500/30 tracking-tight"
|
||||
>
|
||||
<Plus size={14} /> New Group
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{categories.map(cat => (
|
||||
<div key={cat.id} className="p-4 bg-slate-950/40 border border-slate-800/50 rounded-2xl flex items-center justify-between group hover:border-primary/40 transition-all">
|
||||
<div className="min-w-0 pr-4">
|
||||
<p className="text-sm font-bold text-slate-200 group-hover:text-primary transition-colors">{cat.name}</p>
|
||||
<p className="text-[10px] text-slate-500 font-medium mt-0.5">{cat.description || 'General storage'}</p>
|
||||
</div>
|
||||
<div className="flex gap-1 shrink-0">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingCategory(cat);
|
||||
setEditCatForm({ name: cat.name, description: cat.description || '' });
|
||||
}}
|
||||
className="p-2 text-slate-600 hover:text-white hover:bg-slate-800 rounded-lg transition-all"
|
||||
>
|
||||
<Edit2 size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDeleteCategory(cat.id, cat.name)}
|
||||
className="p-2 text-slate-600 hover:text-red-500 hover:bg-red-500/5 rounded-lg transition-all"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Edit Category Modal */}
|
||||
{editingCategory && (
|
||||
<div className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-0 sm:p-4 bg-slate-950/90 backdrop-blur-xl animate-in fade-in duration-300">
|
||||
<div className="w-full max-w-lg bg-slate-900 border-x border-t sm:border border-slate-800 rounded-t-[2.5rem] sm:rounded-3xl p-6 sm:p-10 shadow-2xl space-y-8 overflow-hidden animate-in slide-in-from-bottom-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
||||
<Edit2 size={20} />
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-white tracking-tight">Modify Group</h3>
|
||||
</div>
|
||||
<button onClick={() => setEditingCategory(null)} className="p-3 hover:bg-slate-800 rounded-2xl text-slate-500 transition-colors border border-slate-800">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 tracking-tight ml-1">Group Identifier</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editCatForm.name}
|
||||
onChange={(e) => setEditCatForm({...editCatForm, name: e.target.value})}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3.5 px-5 text-sm text-white focus:border-primary outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 tracking-tight ml-1">Strategic Description</label>
|
||||
<textarea
|
||||
value={editCatForm.description}
|
||||
onChange={(e) => setEditCatForm({...editCatForm, description: e.target.value})}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-4 px-5 text-sm text-white focus:border-primary outline-none transition-all h-32 resize-none"
|
||||
/>
|
||||
</div>
|
||||
<button onClick={onUpdateCategorySubmit} className="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-black py-4 rounded-2xl shadow-xl shadow-indigo-600/20 active:scale-95 transition-all text-xs mb-4 tracking-tight">
|
||||
Update Asset Group
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
178
frontend/components/admin/DatabaseManager.tsx
Normal file
178
frontend/components/admin/DatabaseManager.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import React from 'react';
|
||||
import { Database, RotateCcw, Download, Upload, Zap, History, Clock } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface DatabaseManagerProps {
|
||||
dbStats: any;
|
||||
isBackingUp: boolean;
|
||||
onCreateBackup: () => void;
|
||||
dbSettings: any;
|
||||
onUpdateDbSettings: (settings: any) => void;
|
||||
backups: any[];
|
||||
onRestore: (filename: string) => void;
|
||||
onExport: () => void;
|
||||
onImport: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
export default function DatabaseManager({
|
||||
dbStats,
|
||||
isBackingUp,
|
||||
onCreateBackup,
|
||||
dbSettings,
|
||||
onUpdateDbSettings,
|
||||
backups,
|
||||
onRestore,
|
||||
onExport,
|
||||
onImport
|
||||
}: DatabaseManagerProps) {
|
||||
const formatSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 md:space-y-8 h-full">
|
||||
<div className="bg-slate-900/40 backdrop-blur-xl border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl overflow-hidden relative group transition-all">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
||||
<Database size={20} />
|
||||
</div>
|
||||
<h2 className="text-xl font-black text-white tracking-tight">System Integrity</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6">
|
||||
<div className="space-y-1 sm:pr-6">
|
||||
<p className="text-[10px] font-black text-slate-500 tracking-tight">Database Health</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.3)]" />
|
||||
<span className="text-sm font-bold text-slate-200">Operational</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sm:pl-6 sm:border-l border-slate-800">
|
||||
<p className="text-[10px] font-black text-slate-500 tracking-tight">Last Backup</p>
|
||||
<p className="text-sm font-bold text-slate-200 tabular-nums">{dbStats.backup_count > 0 ? 'Verified' : 'Pending...'}</p>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<button
|
||||
onClick={onCreateBackup}
|
||||
disabled={isBackingUp}
|
||||
className="flex items-center gap-2 px-5 py-3 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-[10px] font-black transition-all active:scale-95 disabled:opacity-50 shadow-xl shadow-indigo-600/10 tracking-tight"
|
||||
>
|
||||
{isBackingUp ? <RotateCcw size={14} className="animate-spin" /> : <Database size={14} />}
|
||||
{isBackingUp ? "Snapshotting..." : "Force Backup"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900/40 backdrop-blur-xl border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl space-y-8">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
||||
<History size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-black text-white tracking-tight">Storage Policy</h3>
|
||||
<p className="text-[10px] text-slate-500 font-bold tracking-tight">Retention & Maintenance</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-3 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[9px] font-black text-slate-500 tracking-tight ml-1 flex items-center gap-2">
|
||||
<History size={10} /> Max Backups
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={dbSettings.retention_count}
|
||||
onChange={(e) => onUpdateDbSettings({...dbSettings, retention_count: parseInt(e.target.value)})}
|
||||
className="w-full bg-slate-950/80 border border-slate-800 rounded-xl py-3 px-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all tabular-nums"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[9px] font-black text-slate-500 tracking-tight ml-1 flex items-center gap-2">
|
||||
<Clock size={10} /> Schedule (Hour)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0" max="23"
|
||||
value={dbSettings.schedule_hour}
|
||||
onChange={(e) => onUpdateDbSettings({...dbSettings, schedule_hour: parseInt(e.target.value)})}
|
||||
className="w-full bg-slate-950/80 border border-slate-800 rounded-xl py-3 px-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all tabular-nums"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[9px] font-black text-slate-500 tracking-tight ml-1 flex items-center gap-2">
|
||||
<Zap size={10} /> Frequency (Days)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={dbSettings.schedule_freq_days}
|
||||
onChange={(e) => onUpdateDbSettings({...dbSettings, schedule_freq_days: parseInt(e.target.value)})}
|
||||
className="w-full bg-slate-950/80 border border-slate-800 rounded-xl py-3 px-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all tabular-nums"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<label className="text-[10px] font-black text-slate-500 tracking-tight">Recovery Points</label>
|
||||
<div className="text-[10px] font-black text-indigo-400 tracking-tight bg-indigo-500/5 px-3 py-1 rounded-full border border-indigo-500/10">
|
||||
{formatSize(dbStats.total_size_bytes)} Used
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pr-2 overflow-y-auto max-h-[300px] scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
|
||||
{backups.map((bak: any) => (
|
||||
<div key={bak.filename} className="flex items-center justify-between p-3.5 bg-slate-950/40 border border-slate-800/40 rounded-2xl group/item hover:border-indigo-500/30 transition-all">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-slate-800 flex items-center justify-center text-slate-500">
|
||||
<Database size={14} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-slate-200">{bak.filename}</p>
|
||||
<p className="text-[9px] text-slate-500 font-bold tabular-nums">
|
||||
{new Date(bak.created_at).toLocaleString()} • {formatSize(bak.size_bytes)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onRestore(bak.filename)}
|
||||
className="p-2 text-slate-600 hover:text-indigo-400 hover:bg-indigo-500/5 rounded-xl transition-all opacity-0 group-hover/item:opacity-100"
|
||||
title="Restore this point"
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 pt-2">
|
||||
<button
|
||||
onClick={onExport}
|
||||
className="flex items-center justify-center gap-2 py-4 bg-slate-950 border border-slate-800 hover:border-indigo-500/50 text-indigo-400 rounded-2xl text-[10px] font-black transition-all active:scale-95 tracking-tight shadow-xl shadow-black/20"
|
||||
>
|
||||
<Download size={14} /> Export Database
|
||||
</button>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="file"
|
||||
accept=".db"
|
||||
onChange={onImport}
|
||||
className="absolute inset-0 opacity-0 cursor-pointer z-10"
|
||||
/>
|
||||
<button className="w-full flex items-center justify-center gap-2 py-4 bg-slate-950 border border-slate-800 hover:border-rose-500/50 text-rose-500 rounded-2xl text-[10px] font-black transition-all tracking-tight shadow-xl shadow-black/20">
|
||||
<Upload size={14} /> Import Database
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
136
frontend/components/admin/IdentityManager.tsx
Normal file
136
frontend/components/admin/IdentityManager.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import React from 'react';
|
||||
import { User, UserPlus, Shield, Edit2, Trash2, X } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface IdentityManagerProps {
|
||||
users: any[];
|
||||
onAddUser: () => void;
|
||||
onDeleteUser: (id: number, username: string) => void;
|
||||
editingUser: any | null;
|
||||
setEditingUser: (user: any | null) => void;
|
||||
editUserForm: any;
|
||||
setEditUserForm: (form: any) => void;
|
||||
onUpdateUserSubmit: () => void;
|
||||
}
|
||||
|
||||
export default function IdentityManager({
|
||||
users,
|
||||
onAddUser,
|
||||
onDeleteUser,
|
||||
editingUser,
|
||||
setEditingUser,
|
||||
editUserForm,
|
||||
setEditUserForm,
|
||||
onUpdateUserSubmit
|
||||
}: IdentityManagerProps) {
|
||||
return (
|
||||
<div className="bg-slate-900/40 backdrop-blur-xl border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 flex flex-col shadow-2xl transition-all group/identity h-full">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
||||
<User size={20} />
|
||||
</div>
|
||||
<h2 className="text-xl font-black text-white tracking-tight">Identity Management</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onAddUser}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-xs font-black transition-all shadow-xl shadow-indigo-600/10 active:scale-95 border border-indigo-500/30 tracking-tight"
|
||||
>
|
||||
<UserPlus size={16} /> Add User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-2.5 pr-2 -mr-2 overflow-y-auto max-h-[400px] scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
|
||||
{users.map((user) => (
|
||||
<div key={user.id} className="flex items-center justify-between p-3.5 bg-slate-950/40 border border-slate-800/40 rounded-2xl hover:border-primary/30 transition-all group">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className={cn(
|
||||
"w-9 h-9 rounded-xl flex items-center justify-center transition-colors shadow-inner shrink-0",
|
||||
user.role === 'admin' ? "bg-primary/10 text-primary border border-primary/20" : "bg-slate-800 text-slate-500 border border-slate-700"
|
||||
)}>
|
||||
{user.role === 'admin' ? <Shield size={16} /> : <User size={16} />}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-bold text-slate-200 truncate">{user.username}</p>
|
||||
<p className="text-[9px] text-slate-500 font-black tracking-tight capitalize">{user.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 ml-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingUser(user);
|
||||
setEditUserForm({ username: user.username, password: '', role: user.role });
|
||||
}}
|
||||
className="p-2 text-slate-600 hover:text-primary hover:bg-primary/5 rounded-xl transition-all"
|
||||
>
|
||||
<Edit2 size={16} />
|
||||
</button>
|
||||
{user.username !== 'Admin' && (
|
||||
<button
|
||||
onClick={() => onDeleteUser(user.id, user.username)}
|
||||
className="p-2 text-slate-600 hover:text-red-500 hover:bg-red-500/5 rounded-xl transition-all"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Edit User Modal */}
|
||||
{editingUser && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-sm animate-in fade-in duration-200">
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-[2.5rem] p-8 w-full max-w-md shadow-2xl">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-xl font-black text-white tracking-tight">Edit Identity</h3>
|
||||
<button onClick={() => setEditingUser(null)} className="p-2 text-slate-500 hover:text-white transition-colors">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 tracking-tight ml-1">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editUserForm.username}
|
||||
onChange={(e) => setEditUserForm({ ...editUserForm, username: e.target.value })}
|
||||
className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-3 px-4 text-sm font-bold text-white outline-none focus:border-primary/50 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 tracking-tight ml-1">New Password (Leave Empty To Keep)</label>
|
||||
<input
|
||||
type="password"
|
||||
value={editUserForm.password}
|
||||
onChange={(e) => setEditUserForm({ ...editUserForm, password: e.target.value })}
|
||||
placeholder="********"
|
||||
className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-3 px-4 text-sm font-bold text-white outline-none focus:border-primary/50 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 tracking-tight ml-1">Role</label>
|
||||
<select
|
||||
value={editUserForm.role}
|
||||
onChange={(e) => setEditUserForm({ ...editUserForm, role: e.target.value })}
|
||||
className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-3 px-4 text-sm font-bold text-white outline-none focus:border-primary/50 transition-all appearance-none"
|
||||
>
|
||||
<option value="user">Standard User</option>
|
||||
<option value="admin">Administrator</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onUpdateUserSubmit}
|
||||
className="w-full bg-primary hover:bg-primary/80 text-white rounded-2xl py-4 text-sm font-black transition-all active:scale-95 shadow-xl shadow-primary/20 tracking-tight mt-4"
|
||||
>
|
||||
Apply Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
165
frontend/components/admin/LdapManager.tsx
Normal file
165
frontend/components/admin/LdapManager.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React from 'react';
|
||||
import { Globe, Power, Server, Shield, User, Layers, Lock, Wifi, RotateCcw } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface LdapManagerProps {
|
||||
ldapConfig: any;
|
||||
setLdapConfig: (config: any) => void;
|
||||
testingLdap: boolean;
|
||||
onTestLdap: () => void;
|
||||
onUpdateLdap: () => void;
|
||||
}
|
||||
|
||||
export default function LdapManager({
|
||||
ldapConfig,
|
||||
setLdapConfig,
|
||||
testingLdap,
|
||||
onTestLdap,
|
||||
onUpdateLdap
|
||||
}: LdapManagerProps) {
|
||||
return (
|
||||
<div className="bg-slate-900/40 backdrop-blur-xl border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl space-y-6 transition-all group/ldap">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
||||
<Globe size={20} />
|
||||
</div>
|
||||
<h2 className="text-xl font-black text-white tracking-tight">Enterprise LDAP</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-slate-950/40 border border-slate-800/60 rounded-2xl group transition-all hover:border-indigo-500/30">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
"w-8 h-8 rounded-xl flex items-center justify-center transition-all shadow-inner",
|
||||
ldapConfig.ldap_enabled ? "bg-green-500/10 text-green-500" : "bg-slate-800 text-slate-500"
|
||||
)}>
|
||||
<Power size={14} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-slate-200 tracking-tight leading-none">Ldap Authorization</p>
|
||||
<p className={cn("text-[8px] font-bold mt-1", ldapConfig.ldap_enabled ? "text-green-500/80" : "text-slate-600")}>
|
||||
{ldapConfig.ldap_enabled ? "External Directory Active" : "Internal Authentication Only"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setLdapConfig({...ldapConfig, ldap_enabled: !ldapConfig.ldap_enabled})}
|
||||
className={cn(
|
||||
"w-10 h-5 rounded-full relative transition-all duration-300 shadow-inner border",
|
||||
ldapConfig.ldap_enabled ? "bg-indigo-600 border-indigo-500" : "bg-slate-800 border-slate-700"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"absolute top-0.5 w-3.5 h-3.5 bg-white rounded-full transition-all shadow-sm",
|
||||
ldapConfig.ldap_enabled ? "right-0.5" : "left-0.5"
|
||||
)} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 tracking-tight ml-1">Ldap Uri</label>
|
||||
<div className="relative">
|
||||
<Server className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-700" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="ldap://host:389"
|
||||
value={ldapConfig.server_uri}
|
||||
onChange={(e) => setLdapConfig({...ldapConfig, server_uri: e.target.value})}
|
||||
className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 tracking-tight ml-1">Context Dn</label>
|
||||
<div className="relative">
|
||||
<Shield className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-700" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="dc=example,dc=com"
|
||||
value={ldapConfig.base_dn}
|
||||
onChange={(e) => setLdapConfig({...ldapConfig, base_dn: e.target.value})}
|
||||
className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 tracking-tight ml-1">User Dn Template</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-700" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="uid={username},ou=people..."
|
||||
value={ldapConfig.user_template}
|
||||
onChange={(e) => setLdapConfig({...ldapConfig, user_template: e.target.value})}
|
||||
className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 tracking-tight ml-1">Groups Base Dn</label>
|
||||
<div className="relative">
|
||||
<Layers className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-700" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="ou=groups"
|
||||
value={ldapConfig.groups_dn}
|
||||
onChange={(e) => setLdapConfig({...ldapConfig, groups_dn: e.target.value})}
|
||||
className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-slate-950/40 border border-slate-800/60 rounded-2xl group transition-all hover:border-indigo-500/30">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
"w-8 h-8 rounded-xl flex items-center justify-center transition-all shadow-inner",
|
||||
ldapConfig.use_tls ? "bg-indigo-500/10 text-indigo-400" : "bg-slate-800 text-slate-500"
|
||||
)}>
|
||||
<Lock size={14} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-slate-200 tracking-tight leading-none">Use Tls</p>
|
||||
<p className={cn("text-[8px] font-bold mt-1", ldapConfig.use_tls ? "text-indigo-400/80" : "text-slate-600")}>
|
||||
{ldapConfig.use_tls ? "Encrypted Channel" : "Standard Link"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setLdapConfig({...ldapConfig, use_tls: !ldapConfig.use_tls})}
|
||||
className={cn(
|
||||
"w-10 h-5 rounded-full relative transition-all duration-300 shadow-inner border",
|
||||
ldapConfig.use_tls ? "bg-indigo-600 border-indigo-500" : "bg-slate-800 border-slate-700"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"absolute top-0.5 w-3.5 h-3.5 bg-white rounded-full transition-all shadow-sm",
|
||||
ldapConfig.use_tls ? "right-0.5" : "left-0.5"
|
||||
)} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button
|
||||
onClick={onTestLdap}
|
||||
disabled={testingLdap}
|
||||
className="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl py-3 text-xs font-black shadow-xl shadow-indigo-600/10 transition-all active:scale-95 disabled:opacity-50 flex items-center justify-center gap-2 border border-indigo-500/30"
|
||||
>
|
||||
{testingLdap ? <RotateCcw size={14} className="animate-spin" /> : <Wifi size={14} />}
|
||||
Test Connection
|
||||
</button>
|
||||
<button
|
||||
onClick={onUpdateLdap}
|
||||
className="flex-[2] bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl py-3 text-xs font-black shadow-xl shadow-indigo-600/10 transition-all active:scale-95 flex items-center justify-center gap-2 border border-indigo-500/30 tracking-tight"
|
||||
>
|
||||
<Shield size={14} /> Save LDAP Policy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user