Build [v1.4.0] - Audit Dashboard & LDAP Restoration
This commit is contained in:
@@ -22,7 +22,11 @@ import {
|
||||
Wifi,
|
||||
WifiOff,
|
||||
Layers,
|
||||
ChevronDown
|
||||
ChevronDown,
|
||||
Clock,
|
||||
HardDrive,
|
||||
Download,
|
||||
RotateCcw
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -52,6 +56,12 @@ export default function AdminPage() {
|
||||
const [editingCategory, setEditingCategory] = useState<any | null>(null);
|
||||
const [editCatForm, setEditCatForm] = useState({ name: '', description: '' });
|
||||
|
||||
// DB Management State
|
||||
const [backups, setBackups] = useState<any[]>([]);
|
||||
const [dbStats, setDbStats] = useState({ backup_count: 0, total_size_bytes: 0 });
|
||||
const [dbSettings, setDbSettings] = useState({ retention_count: 10, schedule_hour: 3, schedule_freq_days: 1 });
|
||||
const [isBackingUp, setIsBackingUp] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
@@ -59,14 +69,20 @@ export default function AdminPage() {
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [u, c, l] = await Promise.all([
|
||||
const [u, c, l, b, s, st] = await Promise.all([
|
||||
inventoryApi.getUsers(),
|
||||
inventoryApi.getCategories(),
|
||||
inventoryApi.getLdapConfig()
|
||||
inventoryApi.getLdapConfig(),
|
||||
inventoryApi.getDbBackups(),
|
||||
inventoryApi.getDbStats(),
|
||||
inventoryApi.getDbSettings()
|
||||
]);
|
||||
setUsers(u);
|
||||
setCategories(c);
|
||||
if (l && l.server_uri) setLdapConfig(l);
|
||||
setBackups(b);
|
||||
setDbStats(s);
|
||||
setDbSettings(st);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to load admin data");
|
||||
@@ -161,44 +177,119 @@ export default function AdminPage() {
|
||||
window.location.href = '/login';
|
||||
};
|
||||
|
||||
const handleCreateBackup = async () => {
|
||||
setIsBackingUp(true);
|
||||
try {
|
||||
await inventoryApi.triggerBackup();
|
||||
toast.success("Snapshot created successfully");
|
||||
loadData();
|
||||
} catch (err) {
|
||||
toast.error("Backup failed");
|
||||
} finally {
|
||||
setIsBackingUp(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestore = async (filename: string) => {
|
||||
if (!confirm(`DANGEROUS: Restore database from ${filename}? Current data will be replaced. A rollback snapshot will be created automatically.`)) return;
|
||||
|
||||
const loadingToast = toast.loading("Restoring database...");
|
||||
try {
|
||||
await inventoryApi.restoreDatabase(filename);
|
||||
toast.success("Database restored! Reloading system...", { id: loadingToast });
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} catch (err) {
|
||||
toast.error("Restore failed", { id: loadingToast });
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateDbSettings = async (newSettings: any) => {
|
||||
try {
|
||||
await inventoryApi.updateDbSettings(newSettings);
|
||||
toast.success("System policy updated");
|
||||
setDbSettings(newSettings);
|
||||
} catch (err) {
|
||||
toast.error("Failed to update settings");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateLdap = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await inventoryApi.updateLdapConfig(ldapConfig);
|
||||
toast.success("Enterprise configuration updated");
|
||||
} catch (err) {
|
||||
toast.error("Failed to update LDAP config");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestLdap = async () => {
|
||||
setTestingLdap(true);
|
||||
try {
|
||||
const res = await inventoryApi.testLdapConnection(ldapConfig);
|
||||
if (res.status === 'success') {
|
||||
toast.success(res.message || "LDAP Connection Successful!");
|
||||
} else {
|
||||
toast.error(`LDAP Error: ${res.message || "Unknown error"}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error(`Connection failed: ${err.response?.data?.detail || err.message}`);
|
||||
} finally {
|
||||
setTestingLdap(false);
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<PageShell requireAdmin={true}>
|
||||
<main className="p-4 md:p-8 max-w-6xl mx-auto space-y-16">
|
||||
<header className="flex items-center gap-6">
|
||||
<div className="p-4 bg-primary/10 rounded-3xl text-primary border border-primary/20 shadow-xl shadow-primary/5">
|
||||
<Shield size={40} />
|
||||
<main className="p-4 md:p-8 max-w-7xl mx-auto space-y-16">
|
||||
<header className="flex items-center gap-5">
|
||||
<div className="p-4 bg-primary/10 rounded-[2rem] text-primary border border-primary/20 shadow-xl shadow-primary/5">
|
||||
<Shield size={32} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-4xl font-black tracking-tight text-white">System Admin</h1>
|
||||
<h1 className="text-3xl font-black tracking-tight text-white">System Admin</h1>
|
||||
<p className="text-xs text-slate-500 font-bold mt-1">Enterprise Control Center</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* User Management Section */}
|
||||
<section className="space-y-6">
|
||||
<section className="bg-slate-900/40 border border-slate-800/40 p-6 md:p-8 rounded-[2.5rem] md:rounded-[3rem] shadow-2xl space-y-8">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<User size={20} className="text-primary" />
|
||||
<h2 className="text-lg font-black text-white">User Accounts</h2>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-primary/10 rounded-2xl text-primary border border-primary/20">
|
||||
<User size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-black text-white">User Accounts</h2>
|
||||
<p className="text-xs text-slate-500 font-bold mt-1">Manage local and network synchronized identities</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddUser}
|
||||
className="flex items-center justify-center gap-2 bg-primary/10 hover:bg-primary text-primary hover:text-white font-black text-xs px-4 py-2 rounded-xl transition-all border border-primary/20 active:scale-95"
|
||||
className="flex items-center justify-center gap-2 bg-primary/10 hover:bg-primary text-primary hover:text-white font-black text-xs px-5 py-2.5 rounded-xl transition-all border border-primary/20 active:scale-95"
|
||||
>
|
||||
<UserPlus size={14} /> Add Local User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-12">
|
||||
<div className="space-y-10">
|
||||
{/* Local Users Group */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
|
||||
<h3 className="text-sm font-black text-white">Local Users</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
|
||||
<h3 className="text-sm font-black text-white">Local Users</h3>
|
||||
</div>
|
||||
<div className="bg-slate-900/40 border border-slate-800/50 rounded-3xl overflow-hidden shadow-xl divide-y divide-slate-800/50">
|
||||
<div className="bg-slate-950/40 border border-slate-800/50 rounded-2xl overflow-hidden shadow-xl divide-y divide-slate-800/50">
|
||||
{loading ? (
|
||||
<div className="p-8 text-center animate-pulse text-slate-600 text-xs font-black">Loading...</div>
|
||||
) : (
|
||||
@@ -207,33 +298,29 @@ export default function AdminPage() {
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={cn(
|
||||
"p-2.5 rounded-xl",
|
||||
u.role === 'admin' ? "bg-primary/10 text-primary border border-primary/20" : "bg-slate-800 text-slate-500 border border-slate-700"
|
||||
u.role === 'admin' ? "bg-primary/10 text-primary border border-primary/20" : "bg-slate-800 text-slate-500 border border-slate-800"
|
||||
)}>
|
||||
{u.role === 'admin' ? <Shield size={16} /> : <User size={16} />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-bold text-white group-hover:text-primary transition-colors">{u.username}</p>
|
||||
<span className="text-[8px] font-black text-slate-500 opacity-60 bg-slate-800/50 px-1.5 py-0.5 rounded-md border border-slate-700/50 font-mono tracking-tighter">{u.role}</span>
|
||||
</div>
|
||||
<p className="text-sm font-bold text-white group-hover:text-primary transition-colors">{u.username}</p>
|
||||
<span className="text-[8px] font-black text-slate-500 opacity-60 font-mono tracking-tighter">{u.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 transition-opacity pr-2">
|
||||
<div className="flex items-center gap-1 pr-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingUser(u);
|
||||
setEditUserForm({ username: u.username, password: '', role: u.role });
|
||||
}}
|
||||
title="Edit User"
|
||||
className="p-2.5 bg-slate-800/50 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-all border border-slate-700/50"
|
||||
className="p-2.5 bg-slate-800/50 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-all border border-slate-800"
|
||||
>
|
||||
<Edit2 size={14} />
|
||||
</button>
|
||||
{u.username !== 'Admin' && (
|
||||
<button
|
||||
onClick={() => handleDeleteUser(u.id, u.username)}
|
||||
title="Delete User"
|
||||
className="p-2.5 bg-rose-500/5 hover:bg-rose-500 text-rose-500 hover:text-white rounded-lg transition-all border border-rose-500/10"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
@@ -249,13 +336,11 @@ export default function AdminPage() {
|
||||
{/* enterprise Users Group */}
|
||||
{users.some(u => u.origin === 'ldap') && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-indigo-500" />
|
||||
<h3 className="text-sm font-black text-white">Enterprise Users (LDAP)</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-indigo-500" />
|
||||
<h3 className="text-sm font-black text-white">Enterprise Users (LDAP)</h3>
|
||||
</div>
|
||||
<div className="bg-indigo-500/5 border border-indigo-500/10 rounded-3xl overflow-hidden shadow-xl divide-y divide-indigo-500/10">
|
||||
<div className="bg-indigo-500/5 border border-indigo-500/10 rounded-2xl overflow-hidden shadow-xl divide-y divide-indigo-500/10">
|
||||
{users.filter(u => u.origin === 'ldap').map(u => (
|
||||
<div key={u.id} className="flex items-center justify-between p-4 hover:bg-indigo-500/10 transition-all group">
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -266,15 +351,15 @@ export default function AdminPage() {
|
||||
<Shield size={16} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-sm font-bold text-white">{u.username}</p>
|
||||
<span className="text-[8px] bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 px-1.5 py-0.5 rounded-md font-black">Network Sync</span>
|
||||
<span className="text-[8px] font-black text-slate-500 opacity-60 bg-slate-800/50 px-1.5 py-0.5 rounded-md border border-slate-700/50 font-mono tracking-tighter">{u.role}</span>
|
||||
<p className="text-sm font-bold text-white">{u.username}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[8px] bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 px-1.5 py-0.5 rounded-md font-black">Ldap Profile</span>
|
||||
<span className="text-[8px] font-black text-slate-500 opacity-60 font-mono tracking-tighter">{u.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs font-black text-indigo-400/50 pr-4 truncate max-w-[150px] italic">
|
||||
Read Only Profile
|
||||
<div className="text-[10px] font-black text-indigo-400/50 pr-4 italic">
|
||||
Externally Managed
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -284,22 +369,164 @@ export default function AdminPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Enterprise Integration (LDAP) Section */}
|
||||
<section className="bg-slate-900/40 border border-slate-800/40 p-6 md:p-8 rounded-[2.5rem] md:rounded-[3rem] shadow-2xl space-y-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6 px-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-indigo-500/10 rounded-2xl text-indigo-500 border border-indigo-500/20">
|
||||
<Globe size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-black text-white">Enterprise Integration</h2>
|
||||
<p className="text-xs text-slate-500 font-bold mt-1">Configure directory services and single sign-on synchronization</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 bg-slate-950/50 p-2 rounded-2xl border border-slate-800">
|
||||
<span className={cn("text-[10px] font-black px-3 transition-colors", ldapConfig.ldap_enabled ? "text-slate-600" : "text-rose-500")}>Offline Only</span>
|
||||
<button
|
||||
onClick={() => setLdapConfig({ ...ldapConfig, ldap_enabled: !ldapConfig.ldap_enabled })}
|
||||
className={cn(
|
||||
"relative inline-flex h-7 w-12 items-center rounded-full transition-all duration-300 outline-none",
|
||||
ldapConfig.ldap_enabled ? "bg-indigo-600" : "bg-slate-800"
|
||||
)}
|
||||
>
|
||||
<span className={cn(
|
||||
"inline-block h-5 w-5 transform rounded-full bg-white transition-transform duration-300",
|
||||
ldapConfig.ldap_enabled ? "translate-x-6" : "translate-x-1"
|
||||
)} />
|
||||
</button>
|
||||
<span className={cn("text-[10px] font-black px-3 transition-colors", ldapConfig.ldap_enabled ? "text-indigo-400" : "text-slate-600")}>LDAP Active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-8">
|
||||
<div className="space-y-6">
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 px-1">Server URI</label>
|
||||
<div className="relative flex items-center">
|
||||
<Server className="absolute left-4 text-slate-600" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="ldap://192.168.1.10:389"
|
||||
value={ldapConfig.server_uri}
|
||||
onChange={(e) => setLdapConfig({ ...ldapConfig, server_uri: e.target.value })}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3 pl-10 pr-4 text-sm text-white outline-none focus:border-indigo-500/30 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 px-1">Base DN</label>
|
||||
<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 border border-slate-800 rounded-2xl py-3 px-4 text-sm text-white outline-none focus:border-indigo-500/30 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 px-1">User Template (LDAP query path)</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="cn={username},ou=people,dc=example,dc=com"
|
||||
value={ldapConfig.user_template}
|
||||
onChange={(e) => setLdapConfig({ ...ldapConfig, user_template: e.target.value })}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3 px-4 text-sm text-white outline-none focus:border-indigo-500/30 transition-all font-mono"
|
||||
/>
|
||||
</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 px-1">Groups DN</label>
|
||||
<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 border border-slate-800 rounded-2xl py-3 px-4 text-sm text-white outline-none focus:border-indigo-500/30 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 px-1">Required Security Group</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="inventory_admins"
|
||||
value={ldapConfig.required_group}
|
||||
onChange={(e) => setLdapConfig({ ...ldapConfig, required_group: e.target.value })}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3 px-4 text-sm text-white outline-none focus:border-indigo-500/30 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 flex flex-col justify-between">
|
||||
<div className="bg-indigo-500/5 border border-indigo-500/10 rounded-3xl p-6 space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Wifi className={cn("transition-colors", ldapConfig.ldap_enabled ? "text-indigo-400" : "text-slate-700")} size={20} />
|
||||
<h3 className="text-sm font-black text-slate-300">Synchronization Shield</h3>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 leading-relaxed font-bold">
|
||||
Enabling LDAP will allow users from your network domain to log in using their enterprise credentials.
|
||||
Local account passwords will still function as a fail-safe backup.
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-[10px] font-black">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={cn("w-1.5 h-1.5 rounded-full", ldapConfig.use_tls ? "bg-green-500" : "bg-slate-800")} />
|
||||
<span className="text-slate-400">LDAPS / TLS</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setLdapConfig({ ...ldapConfig, use_tls: !ldapConfig.use_tls })}
|
||||
className="text-indigo-500 hover:text-indigo-400 underline decoration-indigo-500/30 font-bold"
|
||||
>
|
||||
Toggle Security Layer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
onClick={handleTestLdap}
|
||||
disabled={testingLdap}
|
||||
className="flex-1 bg-slate-950 border border-slate-800 hover:border-indigo-500/50 text-slate-400 hover:text-white font-black text-xs py-4 rounded-2xl transition-all active:scale-95 disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
>
|
||||
{testingLdap ? <div className="w-3 h-3 border-2 border-indigo-500 border-t-transparent animate-spin rounded-full" /> : <WifiOff size={14} />}
|
||||
Test Connection
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpdateLdap}
|
||||
className="flex-[1.5] bg-indigo-600 hover:bg-indigo-500 text-white font-black text-xs py-4 rounded-2xl shadow-xl shadow-indigo-500/20 transition-all active:scale-95"
|
||||
>
|
||||
Save Enterprise Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Category Management Section */}
|
||||
<section className="space-y-6">
|
||||
<section className="bg-slate-900/40 border border-slate-800/40 p-6 md:p-8 rounded-[2.5rem] md:rounded-[3rem] shadow-2xl space-y-8">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Layers size={20} className="text-primary" />
|
||||
<h2 className="text-lg font-black text-white">Category Groups</h2>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-primary/10 rounded-2xl text-primary border border-primary/20">
|
||||
<Layers size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-black text-white">Category Groups</h2>
|
||||
<p className="text-xs text-slate-500 font-bold mt-1">Classify inventory items into logical clusters</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddCategory}
|
||||
className="flex items-center justify-center gap-2 bg-primary/10 hover:bg-primary text-primary hover:text-white font-black text-xs px-4 py-2 rounded-xl transition-all border border-primary/20 active:scale-95"
|
||||
className="flex items-center justify-center gap-2 bg-primary/10 hover:bg-primary text-primary hover:text-white font-black text-xs px-5 py-2.5 rounded-xl transition-all border border-primary/20 active:scale-95"
|
||||
>
|
||||
<Plus size={14} /> Add New Group
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900/40 border border-slate-800/50 rounded-3xl overflow-hidden shadow-xl divide-y divide-slate-800/50">
|
||||
<div className="bg-slate-950/40 border border-slate-800/50 rounded-2xl overflow-hidden shadow-xl divide-y divide-slate-800/50">
|
||||
{loading ? (
|
||||
<div className="p-8 text-center animate-pulse text-slate-600 text-xs font-black">Loading Categories...</div>
|
||||
) : (
|
||||
@@ -311,26 +538,24 @@ export default function AdminPage() {
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-bold text-white group-hover:text-primary transition-colors truncate">{cat.name}</p>
|
||||
<p className="text-xs text-slate-500 font-medium truncate opacity-60">
|
||||
<p className="text-[10px] text-slate-500 font-medium truncate opacity-60">
|
||||
{cat.description || 'General Purpose Group'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 transition-opacity ml-4 pr-2">
|
||||
<div className="flex items-center gap-1 ml-4 pr-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingCategory(cat);
|
||||
setEditCatForm({ name: cat.name, description: cat.description || '' });
|
||||
}}
|
||||
title="Edit Category"
|
||||
className="p-2.5 bg-slate-800/50 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-all border border-slate-700/50"
|
||||
className="p-2.5 bg-slate-800/50 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-all border border-slate-800"
|
||||
>
|
||||
<Edit2 size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteCategory(cat.id, cat.name)}
|
||||
title="Delete Category"
|
||||
className="p-2.5 bg-rose-500/5 hover:bg-rose-500 text-rose-500 hover:text-white rounded-lg transition-all border border-rose-500/10"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
@@ -340,235 +565,172 @@ export default function AdminPage() {
|
||||
))
|
||||
)}
|
||||
{categories.length === 0 && !loading && (
|
||||
<div className="p-8 text-center text-slate-600 text-xs font-black italic">
|
||||
<div className="p-8 text-center text-slate-600 text-[10px] font-black italic">
|
||||
No categories defined
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* System Settings Section */}
|
||||
<div className="pt-8 border-t border-slate-900">
|
||||
<section className="p-8 bg-slate-900/50 rounded-[3rem] border border-slate-800 flex flex-col items-center text-center gap-6 shadow-inner">
|
||||
<div className="w-16 h-16 bg-primary/10 rounded-3xl flex items-center justify-center text-primary border border-primary/20 shadow-lg shadow-primary/5">
|
||||
<Database size={32} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-black text-white tracking-tight">System Integrity</h3>
|
||||
<p className="text-xs text-slate-500 mt-3 max-w-[280px] mx-auto leading-relaxed">
|
||||
Hybrid storage model active. Real-time synchronization between local memory and cloud-hosted SQLite.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 bg-slate-950 px-6 py-2.5 rounded-full border border-slate-800 shadow-2xl">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.5)]" />
|
||||
<span className="text-xs font-black text-green-500">Storage Online</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Enterprise LDAP Integration */}
|
||||
<section className="space-y-8 bg-slate-900/30 border border-slate-800/40 p-8 rounded-[3rem] shadow-2xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<Globe size={120} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6 px-2 relative z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-4 bg-indigo-500/10 rounded-2xl text-indigo-400 border border-indigo-500/20">
|
||||
<Server size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-black text-white">Enterprise Integration</h2>
|
||||
<p className="text-xs text-slate-500 font-bold mt-1">LLDAP / Active Directory Connectivity</p>
|
||||
</div>
|
||||
{/* System Integrity (Compacted and left-aligned) */}
|
||||
<section className="bg-slate-900/30 border border-slate-800/40 p-5 px-8 rounded-3xl flex items-center justify-between gap-6 shadow-sm">
|
||||
<div className="flex items-center gap-5 min-w-0 flex-1">
|
||||
<div className="w-10 h-10 bg-primary/10 rounded-xl flex items-center justify-center text-primary border border-primary/20 shrink-0">
|
||||
<Database size={20} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 bg-slate-950 p-2 rounded-2xl border border-slate-800">
|
||||
<span className="text-xs font-black text-slate-400 px-3">LDAP Status</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newConfig = { ...ldapConfig, ldap_enabled: !ldapConfig.ldap_enabled };
|
||||
setLdapConfig(newConfig);
|
||||
inventoryApi.updateLdapConfig(newConfig);
|
||||
toast.success(`LDAP ${newConfig.ldap_enabled ? 'Enabled' : 'Disabled'}`);
|
||||
}}
|
||||
className={cn(
|
||||
"px-6 py-2 rounded-xl text-xs font-black transition-all",
|
||||
ldapConfig.ldap_enabled ? "bg-green-500/10 text-green-500 border border-green-500/20" : "bg-slate-800 text-slate-500"
|
||||
)}
|
||||
>
|
||||
{ldapConfig.ldap_enabled ? 'Active' : 'Offline'}
|
||||
</button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-sm font-black text-white">System Integrity</h3>
|
||||
<p className="text-[10px] text-slate-500 mt-0.5 max-w-xl truncate">
|
||||
Hybrid storage model active. Real-time synchronization between local memory and cloud-hosted SQLite instance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-8 relative z-10">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 px-1">Server URI</label>
|
||||
<div className="relative group/input">
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-slate-600 group-focus-within/input:text-indigo-400 transition-colors">
|
||||
<Wifi size={16} />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={ldapConfig.server_uri || ''}
|
||||
onChange={(e) => setLdapConfig({ ...ldapConfig, server_uri: e.target.value })}
|
||||
placeholder="ldap://192.168.84.107:3890"
|
||||
className="w-full bg-slate-950 border border-slate-800 focus:border-indigo-500/50 rounded-2xl py-4 pl-12 pr-5 text-white outline-none transition-all font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 px-1">Base DN (Suffix)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={ldapConfig.base_dn || ''}
|
||||
onChange={(e) => setLdapConfig({ ...ldapConfig, base_dn: e.target.value })}
|
||||
placeholder="dc=example,dc=com"
|
||||
className="w-full bg-slate-950 border border-slate-800 focus:border-indigo-500/50 rounded-2xl py-4 px-5 text-white outline-none transition-all font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 px-1">User DN Template</label>
|
||||
<input
|
||||
type="text"
|
||||
value={ldapConfig.user_template || ''}
|
||||
onChange={(e) => setLdapConfig({ ...ldapConfig, user_template: e.target.value })}
|
||||
placeholder="uid={username},ou=people,dc=..."
|
||||
className="w-full bg-slate-950 border border-slate-800 focus:border-indigo-500/50 rounded-2xl py-4 px-5 text-white outline-none transition-all font-mono text-sm"
|
||||
/>
|
||||
<div className="flex items-center gap-6 divide-x divide-slate-800/50">
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-[8px] font-black text-slate-500 uppercase tracking-widest">Storage Status</span>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.3)]" />
|
||||
<span className="text-[10px] font-black text-green-500 tracking-tight">Online</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<label className="text-xs font-black text-slate-500">Role Mappings</label>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newMappings = [...(ldapConfig.role_mappings || []), { group: '', role: 'user' }];
|
||||
setLdapConfig({ ...ldapConfig, role_mappings: newMappings });
|
||||
}}
|
||||
className="text-xs font-black text-indigo-400 hover:text-indigo-300 transition-colors"
|
||||
>
|
||||
+ Add Mapping
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 max-h-[180px] overflow-y-auto pr-2 custom-scrollbar">
|
||||
{(ldapConfig.role_mappings || []).map((mapping: any, idx: number) => (
|
||||
<div key={idx} className="flex gap-2 items-center bg-slate-950/50 p-3 rounded-2xl border border-slate-800/50">
|
||||
<input
|
||||
type="text"
|
||||
value={mapping.group}
|
||||
onChange={(e) => {
|
||||
const newMappings = [...ldapConfig.role_mappings];
|
||||
newMappings[idx].group = e.target.value;
|
||||
setLdapConfig({ ...ldapConfig, role_mappings: newMappings });
|
||||
}}
|
||||
placeholder="Group CN (e.g. admins)"
|
||||
className="flex-1 bg-transparent text-xs font-mono text-white outline-none"
|
||||
/>
|
||||
<select
|
||||
value={mapping.role}
|
||||
onChange={(e) => {
|
||||
const newMappings = [...ldapConfig.role_mappings];
|
||||
newMappings[idx].role = e.target.value;
|
||||
setLdapConfig({ ...ldapConfig, role_mappings: newMappings });
|
||||
}}
|
||||
className="bg-slate-900 text-xs font-bold text-slate-400 px-2 py-1 rounded-lg border border-slate-700 outline-none"
|
||||
>
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newMappings = ldapConfig.role_mappings.filter((_: any, i: number) => i !== idx);
|
||||
setLdapConfig({ ...ldapConfig, role_mappings: newMappings });
|
||||
}}
|
||||
className="text-slate-600 hover:text-red-400 p-1"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{(ldapConfig.role_mappings || []).length === 0 && (
|
||||
<div className="text-center py-4 border border-dashed border-slate-800 rounded-2xl text-xs text-slate-600">
|
||||
No roles mapped
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-slate-950 rounded-3xl border border-slate-800 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Lock size={16} className="text-slate-500" />
|
||||
<span className="text-sm font-bold text-slate-300">Encrypted Connection (TLS)</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setLdapConfig({ ...ldapConfig, use_tls: !ldapConfig.use_tls })}
|
||||
className={cn(
|
||||
"w-12 h-6 rounded-full relative transition-all",
|
||||
ldapConfig.use_tls ? "bg-indigo-500" : "bg-slate-800"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"absolute top-1 w-4 h-4 bg-white rounded-full transition-all",
|
||||
ldapConfig.use_tls ? "right-1" : "left-1"
|
||||
)} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-slate-900 flex gap-4">
|
||||
<button
|
||||
onClick={async () => {
|
||||
setTestingLdap(true);
|
||||
try {
|
||||
const res = await inventoryApi.testLdapConnection(ldapConfig);
|
||||
if (res.status === 'success') {
|
||||
toast.success("Connection Successful!");
|
||||
} else {
|
||||
toast.error(res.message);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error("Network Error during test");
|
||||
} finally {
|
||||
setTestingLdap(false);
|
||||
}
|
||||
}}
|
||||
disabled={testingLdap}
|
||||
className="flex-1 bg-slate-900 hover:bg-slate-800 text-xs font-black py-4 rounded-xl border border-slate-800 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
{testingLdap ? <div className="w-4 h-4 border-2 border-indigo-400 border-t-transparent animate-spin rounded-full" /> : <Wifi size={14} />}
|
||||
Test Link
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={async () => {
|
||||
await inventoryApi.updateLdapConfig(ldapConfig);
|
||||
toast.success("Settings applied");
|
||||
}}
|
||||
className="flex-1 bg-indigo-600 hover:bg-indigo-500 shadow-lg shadow-indigo-900/20 text-white text-xs font-black py-4 rounded-xl transition-all"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 p-4 bg-indigo-500/5 rounded-2xl border border-indigo-500/10">
|
||||
<AlertTriangle size={16} className="text-indigo-400 shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-indigo-300/60 leading-normal italic">
|
||||
LLDAP usually serves LDAP on port 3890. Ensure your firewall allows traffic on this port between the inventory server and 192.168.84.107.
|
||||
</p>
|
||||
<div className="flex flex-col items-end pl-6">
|
||||
<span className="text-[8px] font-black text-slate-500 uppercase tracking-widest">Local Archives</span>
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
<span className="text-[10px] font-black text-white">{dbStats.backup_count} Files</span>
|
||||
<span className="text-[10px] font-black text-primary/60">{formatSize(dbStats.total_size_bytes)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Database & Continuity Card */}
|
||||
<section className="bg-slate-900/40 border border-slate-800/40 p-6 md:p-8 rounded-[2.5rem] md:rounded-[3rem] shadow-2xl space-y-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6 px-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-amber-500/10 rounded-2xl text-amber-500 border border-amber-500/20">
|
||||
<HardDrive size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-black text-white">Database & Continuity</h2>
|
||||
<p className="text-xs text-slate-500 font-bold mt-1">Snapshot management and disaster recovery tools</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCreateBackup}
|
||||
disabled={isBackingUp}
|
||||
className="group flex items-center justify-center gap-2 bg-amber-500/10 hover:bg-amber-500 text-amber-500 hover:text-white font-black text-xs px-6 py-3 rounded-xl transition-all border border-amber-500/20 active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
{isBackingUp ? (
|
||||
<div className="w-3 h-3 border-2 border-current border-t-transparent animate-spin rounded-full" />
|
||||
) : (
|
||||
<Download size={14} className="group-hover:-translate-y-0.5 transition-transform" />
|
||||
)}
|
||||
Create Manual Backup
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
{/* Scheduling Configuration */}
|
||||
<div className="lg:col-span-1 space-y-6">
|
||||
<div className="p-6 bg-slate-950/40 border border-slate-800/50 rounded-3xl space-y-6">
|
||||
<div className="flex items-center gap-2 text-amber-500/80">
|
||||
<Clock size={16} />
|
||||
<span className="text-xs font-black">Retention & Automation</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 px-1">Retention Limit (Max Files)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={dbSettings.retention_count}
|
||||
onChange={(e) => handleUpdateDbSettings({ ...dbSettings, retention_count: parseInt(e.target.value) || 1 })}
|
||||
className="w-full bg-slate-900 border border-slate-800 rounded-xl py-2 px-4 text-sm text-white outline-none focus:border-amber-500/30 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 px-1">Backup Hour</label>
|
||||
<select
|
||||
value={dbSettings.schedule_hour}
|
||||
onChange={(e) => handleUpdateDbSettings({ ...dbSettings, schedule_hour: parseInt(e.target.value) })}
|
||||
className="w-full bg-slate-900 border border-slate-800 rounded-xl py-2 px-3 text-sm text-white outline-none focus:border-amber-500/30 transition-all appearance-none"
|
||||
>
|
||||
{Array.from({ length: 24 }).map((_, i) => (
|
||||
<option key={i} value={i}>{String(i).padStart(2, '0')}:00</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-500 px-1">Frequency (Days)</label>
|
||||
<select
|
||||
value={dbSettings.schedule_freq_days}
|
||||
onChange={(e) => handleUpdateDbSettings({ ...dbSettings, schedule_freq_days: parseInt(e.target.value) })}
|
||||
className="w-full bg-slate-900 border border-slate-800 rounded-xl py-2 px-3 text-sm text-white outline-none focus:border-amber-500/30 transition-all appearance-none"
|
||||
>
|
||||
<option value={1}>Daily</option>
|
||||
<option value={2}>Every 2 days</option>
|
||||
<option value={3}>Every 3 days</option>
|
||||
<option value={5}>Every 5 days</option>
|
||||
<option value={7}>Weekly</option>
|
||||
<option value={14}>Bi-weekly</option>
|
||||
<option value={30}>Monthly</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<p className="text-[10px] text-slate-500 italic leading-relaxed px-1">
|
||||
Automated backups are stored in <code className="text-amber-500/60 font-mono">/data/backups</code>. Restoring data will overwrite the current live primary database.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backup History List */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-slate-950/40 border border-slate-800/50 rounded-3xl overflow-hidden shadow-xl flex flex-col h-full max-h-[400px]">
|
||||
<div className="px-5 py-3 border-b border-slate-800/50 bg-slate-900/20 flex items-center justify-between">
|
||||
<span className="text-xs font-black text-slate-400">Available Snapshots</span>
|
||||
<span className="text-[10px] font-bold text-slate-500">Sorted by newest</span>
|
||||
</div>
|
||||
<div className="overflow-y-auto flex-1 divide-y divide-slate-800/50 custom-scrollbar">
|
||||
{backups.length === 0 ? (
|
||||
<div className="p-12 text-center text-slate-600 italic text-xs font-bold">No backups available on disk</div>
|
||||
) : (
|
||||
backups.map((b) => (
|
||||
<div key={b.filename} className="flex items-center justify-between p-4 hover:bg-slate-800/20 transition-all group">
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
<div className="p-2.5 bg-slate-800 rounded-xl text-slate-500 group-hover:text-amber-500 transition-colors shrink-0">
|
||||
<History size={16} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-bold text-slate-200 truncate">{b.filename}</p>
|
||||
<div className="flex items-center gap-3 mt-0.5">
|
||||
<span className="text-[10px] font-black text-slate-500">{new Date(b.created_at).toLocaleString()}</span>
|
||||
<span className="text-[10px] font-black text-amber-500/40">{formatSize(b.size_bytes)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleRestore(b.filename)}
|
||||
className="flex items-center gap-2 bg-slate-800 hover:bg-amber-600 text-slate-400 hover:text-white px-3 py-2 rounded-xl text-[10px] font-black transition-all border border-slate-700 hover:border-amber-500"
|
||||
>
|
||||
<RotateCcw size={12} />
|
||||
Restore
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
{/* Edit User Modal */}
|
||||
{editingUser && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-950/90 backdrop-blur-xl animate-in fade-in duration-300">
|
||||
|
||||
Reference in New Issue
Block a user