import React from 'react';
const IdentityGovernanceCard = ({ context }) => {
const {
collapsedSections,
setCollapsedSections,
activeAdminTab,
setActiveAdminTab,
editingGroup,
setEditingGroup,
sortedGroups,
loadGroups,
handleDeleteGroup,
handleSaveGroup,
filteredUsers,
userSearch,
setUserSearch,
loadUsers,
usersLoading,
handleGroupChange,
handleRoleToggle,
saving,
labelClass,
inputClass,
sectionClass,
allGroups,
allNodes,
allSkills,
allUsers,
config,
providerLists,
userProfile
} = context;
if (userProfile?.role !== 'admin') return null;
return (
<div className="bg-white dark:bg-gray-800 rounded-3xl shadow-xl overflow-hidden border border-gray-100 dark:border-gray-700 backdrop-blur-sm transition-all duration-300">
<div
onClick={() => setCollapsedSections(prev => ({ ...prev, identity: !prev.identity }))}
className="p-6 border-b border-gray-100 dark:border-gray-700 bg-gray-50/30 dark:bg-gray-800/50 cursor-pointer hover:bg-gray-100/50 dark:hover:bg-gray-700/50 transition-colors flex items-center justify-between group"
>
<div className="flex items-center gap-3">
<svg className={`w-6 h-6 text-indigo-500 transition-transform duration-300 ${collapsedSections.identity ? '-rotate-90' : 'rotate-0'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
<div>
<h2 className="text-xl font-black text-gray-900 dark:text-white flex items-center gap-3 uppercase tracking-tight">
Identity & Access Governance
</h2>
<p className="text-[10px] text-gray-400 mt-1 uppercase font-bold tracking-widest leading-none">Manage user groups, resource whitelists, and individual account policies</p>
</div>
</div>
<div className="bg-indigo-50 dark:bg-indigo-950/30 p-2 rounded-xl border border-indigo-100 dark:border-indigo-900/50 group-hover:scale-110 transition-transform duration-300">
<svg className={`w-4 h-4 text-indigo-600 dark:text-indigo-400 transition-transform duration-300 ${collapsedSections.identity ? 'rotate-180' : 'rotate-0'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M19 9l-7 7-7-7" /></svg>
</div>
</div>
{!collapsedSections.identity && (
<div className="animate-fade-in-down">
<div className="flex border-b border-gray-200 dark:border-gray-700 bg-gray-100/30 dark:bg-gray-800/80 overflow-x-auto no-scrollbar">
{[
{ id: 'groups', label: 'Security Groups', adminOnly: true },
{ id: 'users', label: 'User Roster', adminOnly: true }
].filter(t => !t.adminOnly || userProfile?.role === 'admin').map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => setActiveAdminTab(tab.id)}
className={`flex-1 min-w-[120px] py-4 text-[10px] font-black uppercase tracking-widest transition-all duration-200 focus:outline-none ${activeAdminTab === tab.id
? 'text-indigo-600 dark:text-indigo-400 border-b-2 border-indigo-600 dark:border-indigo-400 bg-white dark:bg-gray-800 shadow-sm z-10'
: 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100/50 dark:hover:bg-gray-700/30'
}`}
>
{tab.label}
</button>
))}
</div>
<div className="p-6 sm:p-10 bg-gradient-to-br from-white to-gray-50/50 dark:from-gray-800 dark:to-gray-900/50">
{/* Groups Management */}
{activeAdminTab === 'groups' && (
<div className={sectionClass}>
{!editingGroup ? (
<div className="animate-fade-in space-y-6">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
<div>
<h3 className="text-lg font-black text-gray-900 dark:text-white uppercase tracking-wider">Governed Policy Groups</h3>
<p className="text-[10px] text-gray-500 font-bold uppercase tracking-widest mt-1">Define resource whitelists for teams and users</p>
</div>
<button
onClick={() => {
const newPolicy = { llm: [], tts: [], stt: [], nodes: [], skills: [] };
setEditingGroup({ id: 'new', name: '', description: '', policy: newPolicy });
}}
className="px-6 py-2.5 bg-indigo-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-lg shadow-indigo-200 dark:shadow-none hover:bg-indigo-700 transition-all flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M12 4v16m8-8H4" /></svg>
Create Group
</button>
</div>
<div className="grid grid-cols-1 gap-4">
{sortedGroups.map(g => (
<div key={g.id} className="p-5 border border-gray-100 dark:border-gray-700 rounded-3xl bg-white dark:bg-gray-800/80 hover:border-indigo-200 dark:hover:border-indigo-800 transition-all group flex flex-col sm:flex-row items-center justify-between gap-4 shadow-sm hover:shadow-md">
<div className="flex items-center gap-4 flex-1">
<div className="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 border border-indigo-100 dark:border-indigo-900/50 font-black text-xl">
{g.name[0].toUpperCase()}
</div>
<div>
<div className="flex items-center gap-2">
<h4 className="text-sm font-black text-gray-900 dark:text-white uppercase tracking-tight">{g.id === 'ungrouped' ? 'Ungrouped (Default)' : g.name}</h4>
{g.id === 'ungrouped' && <span className="text-[8px] font-black uppercase bg-gray-100 dark:bg-gray-700 text-gray-500 px-1.5 py-0.5 rounded-full">System</span>}
</div>
<p className="text-xs text-gray-400 mt-1 line-clamp-1">{g.description || 'No description provided.'}</p>
<div className="flex items-center gap-3 mt-2">
{['llm', 'tts', 'stt', 'nodes', 'skills'].map(section => (
<div key={section} className="flex -space-x-1.5 overflow-hidden">
{(g.policy?.[section] || []).length > 0 ? (
(g.policy?.[section] || []).slice(0, 3).map(p => (
<div key={p} className={`w-5 h-5 rounded-full ${section === 'nodes' ? 'bg-emerald-100 dark:bg-emerald-900 text-emerald-600 dark:text-emerald-400' : (section === 'skills' ? 'bg-amber-100 dark:bg-amber-900 text-amber-600 dark:text-amber-400' : 'bg-indigo-100 dark:bg-indigo-900 text-indigo-600 dark:text-indigo-400')} border-2 border-white dark:border-gray-800 flex items-center justify-center text-[8px] font-bold`} title={p}>
{p[0].toUpperCase()}
</div>
))
) : (
<span className="text-[10px] text-gray-400 italic">None</span>
)}
{g.policy?.[section]?.length > 3 && (
<div className="w-5 h-5 rounded-full bg-gray-100 dark:bg-gray-700 border-2 border-white dark:border-gray-800 flex items-center justify-center text-[8px] font-bold text-gray-500">
+{g.policy?.[section].length - 3}
</div>
)}
</div>
))}
</div>
</div>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={() => setEditingGroup(g)}
className="p-2 text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /></svg>
</button>
{g.id !== 'ungrouped' && (
<button
type="button"
onClick={() => handleDeleteGroup?.(g.id)}
className="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button>
)}
</div>
</div>
))}
</div>
</div>
) : (
<div className="animate-fade-in space-y-6">
{/* (Group editing form - unchanged logic, just cleaner container) */}
<div className="flex items-center gap-4">
<button type="button" onClick={() => setEditingGroup(null)} className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
</button>
<h3 className="text-xl font-black text-gray-900 dark:text-white">
{editingGroup.id === 'new' ? 'New Group Policy' : `Edit: ${editingGroup.id === 'ungrouped' ? 'Standard / Guest Policy' : editingGroup.name}`}
</h3>
{editingGroup.id === 'ungrouped' && (
<span className="text-[10px] font-black uppercase tracking-widest text-amber-700 dark:text-amber-400 py-1 px-2.5 bg-amber-100/60 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-full flex items-center gap-1">
<svg className="w-3 x-3" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" /></svg>
System Group
</span>
)}
</div>
<div className="bg-gray-50/50 dark:bg-gray-900/30 p-6 rounded-3xl border border-gray-100 dark:border-gray-700 space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div>
<label className={labelClass}>Group Name</label>
<input
value={editingGroup.id === 'ungrouped' ? 'Ungrouped (System Default)' : editingGroup.name}
onChange={e => editingGroup.id !== 'ungrouped' && setEditingGroup({ ...editingGroup, name: e.target.value })}
readOnly={editingGroup.id === 'ungrouped'}
placeholder="Engineering, Designers, etc."
className={`${inputClass} ${editingGroup.id === 'ungrouped'
? 'opacity-60 cursor-not-allowed bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
: (editingGroup.name.trim() &&
allGroups.some(g => g.id !== editingGroup.id && g.name.toLowerCase() === editingGroup.name.trim().toLowerCase())
? '!border-red-400 dark:!border-red-600 !ring-red-300'
: '')
}`}
/>
{editingGroup.id === 'ungrouped' ? (
<p className="mt-1.5 text-xs text-amber-600 dark:text-amber-400 font-medium flex items-center gap-1">
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" /></svg>
System group name is locked. Only the access policy can be changed.
</p>
) : editingGroup.name.trim() &&
allGroups.some(g => g.id !== editingGroup.id && g.name.toLowerCase() === editingGroup.name.trim().toLowerCase()) && (
<p className="mt-1.5 text-xs font-semibold text-red-500 flex items-center gap-1">
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" /></svg>
A group with this name already exists
</p>
)}
</div>
<div>
<label className={labelClass}>Description</label>
<input
value={editingGroup.description || ''}
onChange={e => setEditingGroup({ ...editingGroup, description: e.target.value })}
placeholder="Short description of this group..."
className={inputClass}
/>
</div>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
<label className="text-sm font-black uppercase tracking-widest text-gray-500 mb-4 block">Provider Access Policy (Whitelists)</label>
<div className="grid grid-cols-1 gap-8">
{['llm', 'tts', 'stt', 'nodes', 'skills'].map(section => (
<div key={section} className="space-y-3">
<div className="flex items-center justify-between mb-2">
<span className="text-[10px] font-black uppercase tracking-widest text-gray-400">{section === 'nodes' ? 'Accessible Nodes' : `${section} Access`}</span>
<div className="flex gap-2">
<button type="button" onClick={() => {
let availableIds = [];
if (section === 'nodes') {
availableIds = allNodes.map(n => n.node_id);
} else if (section === 'skills') {
availableIds = allSkills.filter(s => !s.is_system).map(s => s.name);
} else {
availableIds = config[section]?.providers ? Object.keys(config[section].providers) : [];
}
setEditingGroup({ ...editingGroup, policy: { ...editingGroup.policy, [section]: availableIds } })
}} className="text-[10px] font-bold text-indigo-600 dark:text-indigo-400 hover:underline">Select All</button>
<button type="button" onClick={() => setEditingGroup({ ...editingGroup, policy: { ...editingGroup.policy, [section]: [] } })} className="text-[10px] font-bold text-red-600 dark:text-red-400 hover:underline">Clear</button>
</div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{(section === 'nodes' ? allNodes.map(n => ({ id: n.node_id, label: n.display_name })) :
(section === 'skills' ? allSkills.filter(s => !s.is_system).map(s => ({ id: s.name, label: s.name })) :
(config[section]?.providers ? Object.keys(config[section].providers) : []).map(pId => {
const baseType = pId.split('_')[0];
const baseDef = providerLists[section].find(ld => ld.id === baseType || ld.id === pId);
return { id: pId, label: baseDef ? (pId.includes('_') ? `${baseDef.label} (${pId.split('_').slice(1).join('_')})` : baseDef.label) : pId };
}))).map(item => {
const isChecked = (editingGroup.policy?.[section] || []).includes(item.id);
return (
<label key={item.id} className={`flex items-center gap-2 px-3 py-2 rounded-xl border cursor-pointer transition-all ${isChecked ? 'bg-indigo-50 dark:bg-indigo-900/30 border-indigo-200 dark:border-indigo-800' : 'border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/50'}`}>
<input
type="checkbox"
checked={isChecked}
onChange={(e) => {
const current = editingGroup.policy?.[section] || [];
const next = e.target.checked ? [...current, item.id] : current.filter(x => x !== item.id);
setEditingGroup({ ...editingGroup, policy: { ...editingGroup.policy, [section]: next } });
}}
className="rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500"
/>
<span className="text-[11px] font-bold text-gray-700 dark:text-gray-300 truncate" title={item.label}>{item.label}</span>
</label>
);
})}
</div>
{section === 'nodes' && allNodes.length === 0 && (
<p className="text-[10px] text-gray-400 italic">No agent nodes registered yet.</p>
)}
</div>
))}
</div>
<div className="flex justify-end gap-3 pt-4">
<button type="button" onClick={() => setEditingGroup(null)} className="px-6 py-2 rounded-xl text-sm font-bold text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">Cancel</button>
<button type="button" onClick={handleSaveGroup} disabled={saving || !editingGroup.name || allGroups.some(g => g.id !== editingGroup.id && g.name.toLowerCase() === editingGroup.name.trim().toLowerCase())} className="px-8 py-2 bg-indigo-600 text-white rounded-xl text-sm font-black uppercase tracking-widest shadow-lg shadow-indigo-200 dark:shadow-indigo-900/50 hover:bg-indigo-700 disabled:opacity-50 transition-all">
{saving ? 'Saving...' : 'Save Group'}
</button>
</div>
</div>
</div>
</div>
)}
</div>
)}
{/* Users Management */}
{activeAdminTab === 'users' && (
<div className={sectionClass}>
<div className="space-y-6">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
<h3 className="text-lg font-black flex items-center gap-3 text-gray-900 dark:text-white">
Active Roster
<span className="text-[10px] py-0.5 px-2 bg-gray-100 dark:bg-gray-700 text-gray-400 rounded-full">{filteredUsers.length}</span>
</h3>
<div className="flex items-center gap-3 w-full sm:w-auto">
<div className="relative flex-1 sm:min-w-[250px]">
<input
type="text"
value={userSearch}
onChange={e => setUserSearch(e.target.value)}
placeholder="Search by name, email..."
className="w-full text-xs p-2.5 pl-9 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl focus:ring-2 focus:ring-indigo-500 outline-none transition-all"
/>
<svg className="w-4 h-4 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
</div>
<button onClick={loadUsers} className="p-2.5 text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-xl transition-colors">
<svg className={`w-4 h-4 ${usersLoading ? 'animate-spin' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
</button>
</div>
</div>
<div className="overflow-hidden border border-gray-100 dark:border-gray-700 rounded-2xl shadow-sm bg-gray-50/50 dark:bg-gray-900/30">
<table className="w-full text-left border-collapse">
<thead className="bg-white dark:bg-gray-800">
<tr>
<th className="px-6 py-4 text-[9px] font-black uppercase tracking-widest text-gray-500">Member</th>
<th className="px-6 py-4 text-[9px] font-black uppercase tracking-widest text-gray-500">Policy Group</th>
<th className="px-6 py-4 text-[9px] font-black uppercase tracking-widest text-gray-500">Activity Auditing</th>
<th className="px-6 py-4 text-[9px] font-black uppercase tracking-widest text-gray-500 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{filteredUsers.map((u) => (
<tr key={u.id} className="hover:bg-white dark:hover:bg-gray-800/50 transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-[10px] font-black ${u.role === 'admin' ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300' : 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'}`}>
{(u.username || u.email || '?')[0].toUpperCase()}
</div>
<div>
<p className="text-xs font-black text-gray-900 dark:text-white truncate max-w-[150px]">{u.username || u.email}</p>
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-tight">{u.role}</p>
</div>
</div>
</td>
<td className="px-6 py-4">
<select
value={u.group_id || 'ungrouped'}
onChange={(e) => handleGroupChange(u.id, e.target.value)}
className="text-xs p-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg outline-none focus:ring-1 focus:ring-indigo-500 transition-all font-bold text-gray-600 dark:text-gray-300 min-w-[150px]"
>
{sortedGroups.map(g => (
<option key={g.id} value={g.id}>
{g.id === 'ungrouped' ? 'Standard / Guest' : g.name}
</option>
))}
</select>
</td>
<td className="px-6 py-4">
<div className="space-y-1">
<div className="flex items-center gap-1.5">
<span className="text-[9px] font-black uppercase text-gray-400">Join:</span>
<span className="text-[10px] font-bold text-gray-600 dark:text-gray-300">{new Date(u.created_at).toLocaleDateString()}</span>
</div>
<div className="flex items-center gap-1.5">
<span className="text-[9px] font-black uppercase text-gray-400">Last:</span>
<span className="text-[10px] font-bold text-indigo-600 dark:text-indigo-400">
{u.last_login_at ? new Date(u.last_login_at).toLocaleDateString() : 'Never'}
</span>
</div>
</div>
</td>
<td className="px-6 py-4 text-right">
<button
type="button"
onClick={(e) => { e.preventDefault(); handleRoleToggle(u); }}
className={`text-[9px] font-black uppercase tracking-widest transition-all ${u.role === 'admin'
? 'text-red-600 hover:text-red-700'
: 'text-indigo-600 hover:text-indigo-700'
}`}
>
{u.role === 'admin' ? 'Demote' : 'Promote'}
</button>
</td>
</tr>
))}
</tbody>
</table>
{allUsers.length === 0 && !usersLoading && (
<div className="p-12 text-center text-gray-400 italic text-sm">No other users found.</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
)}
</div>
);
};
export default IdentityGovernanceCard;