fix: implement Phase 2 medium-impact spacing reductions
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.
This commit is contained in:
@@ -42,7 +42,7 @@ export default function AiManager({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<div className="grid sm:grid-cols-2 gap-3">
|
||||
{aiConfig?.providers?.map((p: any) => (
|
||||
<button
|
||||
key={p.id}
|
||||
@@ -83,7 +83,7 @@ export default function AiManager({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-primary/5 border border-primary/10 rounded-[2rem] p-6 space-y-6">
|
||||
<div className="bg-primary/5 border border-primary/10 rounded-[2rem] p-6 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-lg text-primary"><Lock size={14} /></div>
|
||||
@@ -101,7 +101,7 @@ export default function AiManager({
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-normal text-secondary tracking-tight ml-1">Gemini Api Key</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
@@ -110,7 +110,7 @@ export default function AiManager({
|
||||
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-background/80 border border-slate-800 rounded-xl py-3 px-4 text-xs text-white outline-none focus:border-primary/50 transition-all placeholder:text-secondary"
|
||||
className="flex-1 bg-background/80 border border-slate-800 rounded-xl py-2 px-4 text-xs text-white outline-none focus:border-primary/50 transition-all placeholder:text-secondary"
|
||||
/>
|
||||
<button
|
||||
onClick={() => onTestAiKey('gemini')}
|
||||
@@ -127,7 +127,7 @@ export default function AiManager({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-normal text-secondary tracking-tight ml-1">Claude Api Key</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
@@ -136,7 +136,7 @@ export default function AiManager({
|
||||
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-background/80 border border-slate-800 rounded-xl py-3 px-4 text-xs text-white outline-none focus:border-primary/50 transition-all placeholder:text-secondary"
|
||||
className="flex-1 bg-background/80 border border-slate-800 rounded-xl py-2 px-4 text-xs text-white outline-none focus:border-primary/50 transition-all placeholder:text-secondary"
|
||||
/>
|
||||
<button
|
||||
onClick={() => onTestAiKey('claude')}
|
||||
@@ -156,8 +156,8 @@ export default function AiManager({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<label className="text-sm font-normal text-secondary tracking-tight">System Prompt (Vision Extraction)</label>
|
||||
<button
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function CategoryManager({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div data-testid="category-list" className="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div data-testid="category-list" className="grid sm:grid-cols-2 lg:grid-cols-4 gap-2.5">
|
||||
{categories.map(cat => (
|
||||
<div key={cat.id} data-testid="category-item" className="p-4 bg-background/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">
|
||||
@@ -84,23 +84,23 @@ export default function CategoryManager({
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div data-testid="category-form" className="space-y-6">
|
||||
<div className="space-y-1.5">
|
||||
<div data-testid="category-form" className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-normal text-secondary tracking-tight ml-1">Group Identifier</label>
|
||||
<input
|
||||
data-testid="category-name-input"
|
||||
type="text"
|
||||
value={editCatForm.name}
|
||||
onChange={(e) => setEditCatForm({...editCatForm, name: e.target.value})}
|
||||
className="w-full bg-background border border-slate-800 rounded-2xl py-3.5 px-5 text-sm text-white focus:border-primary outline-none transition-all"
|
||||
className="w-full bg-background border border-slate-800 rounded-2xl py-2.5 px-5 text-sm text-white focus:border-primary outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-normal text-secondary tracking-tight ml-1">Strategic Description</label>
|
||||
<textarea
|
||||
<textarea
|
||||
value={editCatForm.description}
|
||||
onChange={(e) => setEditCatForm({...editCatForm, description: e.target.value})}
|
||||
className="w-full bg-background 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"
|
||||
className="w-full bg-background border border-slate-800 rounded-2xl py-2.5 px-5 text-sm text-white focus:border-primary outline-none transition-all h-32 resize-none"
|
||||
/>
|
||||
</div>
|
||||
<button data-testid="add-category-submit" onClick={onUpdateCategorySubmit} className="w-full bg-primary hover:bg-primary text-white font-normal py-4 rounded-2xl shadow-xl shadow-primary/20 active:scale-95 transition-all text-sm mb-4 tracking-tight">
|
||||
|
||||
@@ -82,46 +82,46 @@ export default function DatabaseManager({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<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"
|
||||
<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-3 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"
|
||||
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"
|
||||
<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-3 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"
|
||||
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"
|
||||
<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-3 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"
|
||||
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-4">
|
||||
<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">
|
||||
@@ -129,7 +129,7 @@ export default function DatabaseManager({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pr-2 overflow-y-auto max-h-[300px] scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
|
||||
<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">
|
||||
@@ -138,7 +138,7 @@ export default function DatabaseManager({
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-normal text-secondary">{bak.filename}</p>
|
||||
<p className="text-[11px] text-secondary font-medium tabular-nums">
|
||||
<p className="text-xs text-secondary font-medium tabular-nums">
|
||||
{new Date(bak.created_at).toLocaleString()} • {formatSize(bak.size_bytes)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function IdentityManager({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-2 pr-2 -mr-2 overflow-y-auto max-h-[400px] scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
|
||||
<div className="flex-1 space-y-1.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-background/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">
|
||||
@@ -98,32 +98,32 @@ export default function IdentityManager({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-normal text-secondary tracking-tight ml-1">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
<input
|
||||
type="text"
|
||||
value={editUserForm.username}
|
||||
onChange={(e) => setEditUserForm({ ...editUserForm, username: e.target.value })}
|
||||
className="w-full bg-background/80 border border-slate-800 rounded-2xl py-3 px-4 text-sm font-normal text-white outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-0 transition-all font-mono"
|
||||
className="w-full bg-background/80 border border-slate-800 rounded-2xl py-2 px-4 text-sm font-normal text-white outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-0 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-normal text-secondary tracking-tight ml-1">New Password (Leave Empty To Keep)</label>
|
||||
<input
|
||||
type="password"
|
||||
<input
|
||||
type="password"
|
||||
value={editUserForm.password}
|
||||
onChange={(e) => setEditUserForm({ ...editUserForm, password: e.target.value })}
|
||||
placeholder="********"
|
||||
className="w-full bg-background/80 border border-slate-800 rounded-2xl py-3 px-4 text-sm font-normal text-white outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-0 transition-all font-mono"
|
||||
className="w-full bg-background/80 border border-slate-800 rounded-2xl py-2 px-4 text-sm font-normal text-white outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-0 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-normal text-secondary tracking-tight ml-1">Role</label>
|
||||
<select
|
||||
<select
|
||||
value={editUserForm.role}
|
||||
onChange={(e) => setEditUserForm({ ...editUserForm, role: e.target.value })}
|
||||
className="w-full bg-background/80 border border-slate-800 rounded-2xl py-3 px-4 text-sm font-normal text-white outline-none focus:border-primary/50 transition-all appearance-none"
|
||||
className="w-full bg-background/80 border border-slate-800 rounded-2xl py-2 px-4 text-sm font-normal text-white outline-none focus:border-primary/50 transition-all appearance-none"
|
||||
>
|
||||
<option value="user">Standard User</option>
|
||||
<option value="admin">Administrator</option>
|
||||
|
||||
@@ -58,58 +58,58 @@ export default function LdapManager({
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-normal text-secondary tracking-tight ml-1">LDAP URI</label>
|
||||
<div className="relative">
|
||||
<Server className="absolute left-3.5 top-1/2 -translate-y-1/2 text-muted" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
<input
|
||||
type="text"
|
||||
placeholder="ldap://host:389"
|
||||
value={ldapConfig.server_uri}
|
||||
onChange={(e) => setLdapConfig({...ldapConfig, server_uri: e.target.value})}
|
||||
className="w-full bg-background/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-normal text-white outline-none focus:border-primary/50 transition-all font-mono"
|
||||
className="w-full bg-background/80 border border-slate-800 rounded-2xl py-2 pl-10 pr-4 text-xs font-normal text-white outline-none focus:border-primary/50 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-normal text-secondary tracking-tight ml-1">Context DN</label>
|
||||
<div className="relative">
|
||||
<Shield className="absolute left-3.5 top-1/2 -translate-y-1/2 text-muted" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
<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-background/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-normal text-white outline-none focus:border-primary/50 transition-all font-mono"
|
||||
className="w-full bg-background/80 border border-slate-800 rounded-2xl py-2 pl-10 pr-4 text-xs font-normal text-white outline-none focus:border-primary/50 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-normal text-secondary 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-muted" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
<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-background/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-normal text-white outline-none focus:border-primary/50 transition-all font-mono"
|
||||
className="w-full bg-background/80 border border-slate-800 rounded-2xl py-2 pl-10 pr-4 text-xs font-normal text-white outline-none focus:border-primary/50 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-normal text-secondary 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-muted" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
<input
|
||||
type="text"
|
||||
placeholder="ou=groups"
|
||||
value={ldapConfig.groups_dn}
|
||||
onChange={(e) => setLdapConfig({...ldapConfig, groups_dn: e.target.value})}
|
||||
className="w-full bg-background/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-normal text-white outline-none focus:border-primary/50 transition-all font-mono"
|
||||
className="w-full bg-background/80 border border-slate-800 rounded-2xl py-2 pl-10 pr-4 text-xs font-normal text-white outline-none focus:border-primary/50 transition-all font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,17 +145,17 @@ export default function LdapManager({
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button
|
||||
<button
|
||||
onClick={onTestLdap}
|
||||
disabled={testingLdap}
|
||||
className="flex-1 bg-primary hover:bg-primary text-white rounded-xl py-2.5 text-sm font-normal shadow-xl shadow-primary/10 transition-all active:scale-95 disabled:opacity-50 flex items-center justify-center gap-2 border border-primary/30"
|
||||
className="flex-1 bg-primary hover:bg-primary text-white rounded-xl py-2 text-sm font-normal shadow-xl shadow-primary/10 transition-all active:scale-95 disabled:opacity-50 flex items-center justify-center gap-2 border border-primary/30"
|
||||
>
|
||||
{testingLdap ? <RotateCcw size={14} className="animate-spin" /> : <Wifi size={14} />}
|
||||
Test Connection
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={onUpdateLdap}
|
||||
className="flex-[2] bg-primary hover:bg-primary text-white rounded-xl py-3 text-sm font-normal shadow-xl shadow-primary/10 transition-all active:scale-95 flex items-center justify-center gap-2 border border-primary/30 tracking-tight"
|
||||
className="flex-[2] bg-primary hover:bg-primary text-white rounded-xl py-2.5 text-sm font-normal shadow-xl shadow-primary/10 transition-all active:scale-95 flex items-center justify-center gap-2 border border-primary/30 tracking-tight"
|
||||
>
|
||||
<Shield size={14} /> Save LDAP Policy
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user