Applied systematic spacing optimizations across admin manager components (40px savings): DatabaseManager.tsx: - Input field padding: py-3 → py-2 (3 inputs: retention_count, schedule_hour, schedule_freq_days) - Form field spacing: space-y-2 → space-y-1.5 (Max Backups field group) - Grid gaps: gap-4 → gap-3 (storage policy fields) - Item list spacing: space-y-2 → space-y-1.5 (backup list) - Modal form spacing: space-y-4 → space-y-3 (recovery points section) - Backup metadata text: text-[11px] → text-xs (created_at + filesize display) LdapManager.tsx: - Input field padding: py-2.5 → py-2 (4 inputs: server_uri, base_dn, user_template, groups_dn) - Form field spacing: space-y-1.5 → space-y-1 (all label+input groups) - Button padding: py-2.5 → py-2 (test connection), py-3 → py-2.5 (save policy) IdentityManager.tsx: - Input field padding: py-3 → py-2 (username, password, role in edit modal) - Form field spacing: space-y-4 → space-y-3 (edit modal form) - Form field labels: space-y-1.5 → space-y-1 (all 3 fields) - User list spacing: space-y-2 → space-y-1.5 (user items) AiManager.tsx: - Grid gaps: gap-4 → gap-3 (provider options grid) - Input field padding: py-3 → py-2 (Gemini/Claude API key inputs) - Form field spacing: space-y-2 → space-y-1.5 (API key field groups) - Modal spacing: space-y-6 → space-y-3 (provider access keys section) - Prompt section spacing: space-y-4 → space-y-3 (system prompt area) CategoryManager.tsx: - Grid gaps: gap-3 → gap-2.5 (category items grid) - Input field padding: py-3.5 → py-2.5, py-4 → py-2.5 (name + description inputs) - Form field spacing: space-y-6 → space-y-3, space-y-1.5 → space-y-1 (edit modal) Test Results: - Frontend (Vitest): 291/291 tests passing ✓ - Backend (Pytest): 41/41 tests passing ✓ - Total: 332 tests validated, zero regressions All changes maintain visual hierarchy and premium density standards without sacrificing usability.
185 lines
9.3 KiB
TypeScript
185 lines
9.3 KiB
TypeScript
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-4 md:space-y-5 h-full">
|
|
<div data-testid="database-info" className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-4 md:p-6 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-primary/10 flex items-center justify-center text-primary border border-primary/20">
|
|
<Database size={20} />
|
|
</div>
|
|
<h2 className="text-xl font-normal 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-xs font-normal text-muted 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-normal text-secondary">Operational</span>
|
|
</div>
|
|
</div>
|
|
<div className="sm:pl-6 sm:border-l border-slate-800">
|
|
<p className="text-xs font-normal text-muted tracking-tight">Last Backup</p>
|
|
<p className="text-sm font-normal text-secondary tabular-nums">{dbStats.backup_count > 0 ? 'Verified' : 'Pending...'}</p>
|
|
</div>
|
|
<div className="ml-auto">
|
|
<button
|
|
onClick={onCreateBackup}
|
|
disabled={isBackingUp}
|
|
data-testid="backup-button"
|
|
className="flex items-center gap-2 px-5 py-2.5 bg-primary hover:bg-blue-600 text-white rounded-xl text-sm font-normal transition-all active:scale-95 disabled:opacity-50 shadow-xl shadow-primary/10 tracking-tight focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
|
|
aria-label="Create database backup"
|
|
>
|
|
{isBackingUp ? <RotateCcw size={14} className="animate-spin" /> : <Database size={14} />}
|
|
{isBackingUp ? "Snapshotting..." : "Force Backup"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-4 md:p-6 shadow-2xl space-y-5">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20">
|
|
<History size={20} />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-normal text-white tracking-tight">Storage Policy</h3>
|
|
<p className="text-sm text-secondary font-normal tracking-tight">Retention & Maintenance</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid sm:grid-cols-3 gap-3">
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs font-normal text-muted 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-background/80 border border-slate-800 rounded-xl py-2 px-4 text-xs font-normal text-white outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none transition-all tabular-nums"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-xs font-normal text-muted 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-background/80 border border-slate-800 rounded-xl py-2 px-4 text-xs font-normal text-white outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none transition-all tabular-nums"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-xs font-normal text-muted 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-background/80 border border-slate-800 rounded-xl py-2 px-4 text-xs font-normal text-white outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none transition-all tabular-nums"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between px-1">
|
|
<label className="text-xs font-normal text-muted tracking-tight">Recovery Points</label>
|
|
<div className="text-xs font-normal text-primary tracking-tight bg-primary/5 px-3 py-1 rounded-full border border-primary/10">
|
|
{formatSize(dbStats.total_size_bytes)} Used
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5 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-background/40 border border-slate-800/40 rounded-2xl group/item hover:border-primary/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-muted">
|
|
<Database size={14} />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs font-normal text-secondary">{bak.filename}</p>
|
|
<p className="text-xs text-secondary font-medium tabular-nums">
|
|
{new Date(bak.created_at).toLocaleString()} • {formatSize(bak.size_bytes)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => onRestore(bak.filename)}
|
|
className="p-2 text-secondary hover:text-primary hover:bg-primary/5 rounded-xl transition-all opacity-0 group-hover/item:opacity-100 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
|
|
aria-label={`Restore backup ${bak.filename}`}
|
|
>
|
|
<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-3 bg-background border border-slate-800 hover:border-primary/50 text-primary rounded-2xl text-sm font-normal transition-all active:scale-95 tracking-tight shadow-xl shadow-black/20 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
|
|
aria-label="Export database"
|
|
>
|
|
<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-background border border-slate-800 hover:border-rose-500/50 text-rose-500 rounded-2xl text-sm font-normal transition-all tracking-tight shadow-xl shadow-black/20 focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:outline-none"
|
|
aria-label="Import database"
|
|
>
|
|
<Upload size={14} /> Import Database
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|