import { useState, useMemo } from 'react'; import { Avatar, Badge, Button, Input, Select, MonoText, SectionHeader, Tag, InlineEdit, MultiSelect, ConfirmDialog, AlertDialog, SplitPane, EntityList, Spinner, useToast, } from '@cameleer/design-system'; import { useGroups, useGroup, useCreateGroup, useUpdateGroup, useDeleteGroup, useAssignRoleToGroup, useRemoveRoleFromGroup, useAddUserToGroup, useRemoveUserFromGroup, useUsers, useRoles, } from '../../api/queries/admin/rbac'; import type { GroupDetail } 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 { toast } = useToast(); const { data: groups = [], isLoading: groupsLoading } = useGroups(); const { data: users = [] } = useUsers(); const { data: roles = [] } = useRoles(); const [search, setSearch] = useState(''); const [selectedId, setSelectedId] = useState(null); const [creating, setCreating] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [removeRoleTarget, setRemoveRoleTarget] = useState(null); // Create form state const [newName, setNewName] = useState(''); const [newParent, setNewParent] = useState(''); // Detail query const { data: selectedGroup, isLoading: detailLoading } = useGroup(selectedId); // Mutations const createGroup = useCreateGroup(); const updateGroup = useUpdateGroup(); const deleteGroup = useDeleteGroup(); const assignRoleToGroup = useAssignRoleToGroup(); const removeRoleFromGroup = useRemoveRoleFromGroup(); const addUserToGroup = useAddUserToGroup(); const removeUserFromGroup = useRemoveUserFromGroup(); const filtered = useMemo(() => { if (!search) return groups; const q = search.toLowerCase(); return groups.filter((g) => g.name.toLowerCase().includes(q)); }, [groups, search]); const isBuiltinAdmins = selectedGroup?.id === BUILTIN_ADMINS_ID; const parentOptions = [ { value: '', label: 'Top-level' }, ...groups .filter((g) => g.id !== selectedId) .map((g) => ({ value: g.id, label: g.name })), ]; const duplicateGroupName = newName.trim() !== '' && groups.some( (g) => g.name.toLowerCase() === newName.trim().toLowerCase(), ); // Derived data for the detail pane const children = selectedGroup?.childGroups ?? []; const members = selectedGroup?.members ?? []; const parentGroup = selectedGroup?.parentGroupId ? groups.find((g) => g.id === selectedGroup.parentGroupId) : null; const memberUserIds = new Set(members.map((m) => m.userId)); const assignedRoleIds = new Set( (selectedGroup?.directRoles ?? []).map((r) => r.id), ); const availableRoles = roles .filter((r) => !assignedRoleIds.has(r.id)) .map((r) => ({ value: r.id, label: r.name })); const availableMembers = users .filter((u) => !memberUserIds.has(u.userId)) .map((u) => ({ value: u.userId, label: u.displayName })); function parentName(parentGroupId: string | null): string { if (!parentGroupId) return 'Top-level'; const parent = groups.find((g) => g.id === parentGroupId); return parent ? parent.name : parentGroupId; } async function handleCreate() { if (!newName.trim()) return; try { await createGroup.mutateAsync({ name: newName.trim(), parentGroupId: newParent || null, }); toast({ title: 'Group created', description: newName.trim(), variant: 'success' }); setCreating(false); setNewName(''); setNewParent(''); } catch { toast({ title: 'Failed to create group', variant: 'error' }); } } async function handleDelete() { if (!deleteTarget) return; try { await deleteGroup.mutateAsync(deleteTarget.id); toast({ title: 'Group deleted', description: deleteTarget.name, variant: 'warning', }); if (selectedId === deleteTarget.id) setSelectedId(null); setDeleteTarget(null); } catch { toast({ title: 'Failed to delete group', variant: 'error' }); setDeleteTarget(null); } } async function handleRename(newNameVal: string) { if (!selectedGroup) return; try { await updateGroup.mutateAsync({ id: selectedGroup.id, name: newNameVal, parentGroupId: selectedGroup.parentGroupId, }); toast({ title: 'Group renamed', variant: 'success' }); } catch { toast({ title: 'Failed to rename group', variant: 'error' }); } } async function handleRemoveMember(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' }); } } async function handleAddMembers(userIds: string[]) { if (!selectedGroup) return; for (const userId of userIds) { try { await addUserToGroup.mutateAsync({ userId, groupId: selectedGroup.id, }); toast({ title: 'Member added', variant: 'success' }); } catch { toast({ title: 'Failed to add member', variant: 'error' }); } } } async function handleAddRoles(roleIds: string[]) { if (!selectedGroup) return; for (const roleId of roleIds) { try { await assignRoleToGroup.mutateAsync({ groupId: selectedGroup.id, roleId, }); toast({ title: 'Role assigned', variant: 'success' }); } catch { toast({ title: 'Failed to assign role', variant: 'error' }); } } } async function handleRemoveRole(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' }); } } if (groupsLoading) return ; return ( <> {creating && (
setNewName(e.target.value)} /> {duplicateGroupName && ( Group name already exists )}