import { useState } from 'react'; import { Avatar, Badge, Button, Input, MonoText, Tag, Select, ConfirmDialog, Spinner, InlineEdit, useToast, } from '@cameleer/design-system'; import { useGroups, useGroup, useCreateGroup, useUpdateGroup, useDeleteGroup, useAssignRoleToGroup, useRemoveRoleFromGroup, useAddUserToGroup, useRemoveUserFromGroup, useUsers, useRoles, } from '../../api/queries/admin/rbac'; import styles from './UserManagement.module.css'; const BUILTIN_ADMINS_ID = '00000000-0000-0000-0000-000000000010'; export default function GroupsTab() { const [search, setSearch] = useState(''); const [selectedGroupId, setSelectedGroupId] = useState(null); const [showCreate, setShowCreate] = useState(false); const [newGroupName, setNewGroupName] = useState(''); const [newGroupParentId, setNewGroupParentId] = useState(''); const [deleteOpen, setDeleteOpen] = useState(false); const [addMemberUserId, setAddMemberUserId] = useState(''); const [addRoleId, setAddRoleId] = useState(''); const { toast } = useToast(); const { data: groups = [], isLoading: groupsLoading } = useGroups(); const { data: selectedGroup, isLoading: detailLoading } = useGroup(selectedGroupId); const { data: users = [] } = useUsers(); const { data: roles = [] } = useRoles(); const createGroup = useCreateGroup(); const updateGroup = useUpdateGroup(); const deleteGroup = useDeleteGroup(); const assignRoleToGroup = useAssignRoleToGroup(); const removeRoleFromGroup = useRemoveRoleFromGroup(); const addUserToGroup = useAddUserToGroup(); const removeUserFromGroup = useRemoveUserFromGroup(); const filteredGroups = groups.filter((g) => g.name.toLowerCase().includes(search.toLowerCase()) ); const parentOptions = [ { value: '', label: 'Top-level' }, ...groups.map((g) => ({ value: g.id, label: g.name })), ]; const parentName = (parentGroupId: string | null) => { if (!parentGroupId) return 'Top-level'; const parent = groups.find((g) => g.id === parentGroupId); return parent ? parent.name : parentGroupId; }; const handleCreate = async () => { const name = newGroupName.trim(); if (!name) return; try { await createGroup.mutateAsync({ name, parentGroupId: newGroupParentId || null, }); toast({ title: 'Group created', variant: 'success' }); setNewGroupName(''); setNewGroupParentId(''); setShowCreate(false); } catch { toast({ title: 'Failed to create group', variant: 'error' }); } }; const handleRename = async (newName: string) => { if (!selectedGroup) return; try { await updateGroup.mutateAsync({ id: selectedGroup.id, name: newName, parentGroupId: selectedGroup.parentGroupId, }); toast({ title: 'Group renamed', variant: 'success' }); } catch { toast({ title: 'Failed to rename group', variant: 'error' }); } }; const handleDelete = async () => { if (!selectedGroup) return; try { await deleteGroup.mutateAsync(selectedGroup.id); toast({ title: 'Group deleted', variant: 'success' }); setSelectedGroupId(null); setDeleteOpen(false); } catch { toast({ title: 'Failed to delete group', variant: 'error' }); } }; const handleAddMember = async () => { if (!selectedGroup || !addMemberUserId) return; try { await addUserToGroup.mutateAsync({ userId: addMemberUserId, groupId: selectedGroup.id, }); toast({ title: 'Member added', variant: 'success' }); setAddMemberUserId(''); } catch { toast({ title: 'Failed to add member', variant: 'error' }); } }; const handleRemoveMember = async (userId: string) => { if (!selectedGroup) return; try { await removeUserFromGroup.mutateAsync({ userId, groupId: selectedGroup.id }); toast({ title: 'Member removed', variant: 'success' }); } catch { toast({ title: 'Failed to remove member', variant: 'error' }); } }; const handleAddRole = async () => { if (!selectedGroup || !addRoleId) return; try { await assignRoleToGroup.mutateAsync({ groupId: selectedGroup.id, roleId: addRoleId, }); toast({ title: 'Role assigned', variant: 'success' }); setAddRoleId(''); } catch { toast({ title: 'Failed to assign role', variant: 'error' }); } }; const handleRemoveRole = async (roleId: string) => { if (!selectedGroup) return; try { await removeRoleFromGroup.mutateAsync({ groupId: selectedGroup.id, roleId }); toast({ title: 'Role removed', variant: 'success' }); } catch { toast({ title: 'Failed to remove role', variant: 'error' }); } }; const isBuiltinAdmins = selectedGroup?.id === BUILTIN_ADMINS_ID; // Build sets for quick lookup of already-assigned items const memberUserIds = new Set((selectedGroup?.members ?? []).map((m) => m.userId)); const assignedRoleIds = new Set((selectedGroup?.directRoles ?? []).map((r) => r.id)); const availableUsers = users.filter((u) => !memberUserIds.has(u.userId)); const availableRoles = roles.filter((r) => !assignedRoleIds.has(r.id)); return (
{/* Left pane */}
setSearch(e.target.value)} onClear={() => setSearch('')} />
{showCreate && (
setNewGroupName(e.target.value)} />
({ value: u.userId, label: u.displayName, })), ]} value={addMemberUserId} onChange={(e) => setAddMemberUserId(e.target.value)} />
{/* Assigned roles */}
Assigned Roles
{(selectedGroup.directRoles ?? []).map((role) => ( handleRemoveRole(role.id)} /> ))} {(selectedGroup.directRoles ?? []).length === 0 && ( No roles assigned )}
{(selectedGroup.effectiveRoles ?? []).length > (selectedGroup.directRoles ?? []).length && (
+ {(selectedGroup.effectiveRoles ?? []).length - (selectedGroup.directRoles ?? []).length}{' '} inherited role(s)
)}