diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index 34ad273e1..6ed061916 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -137,7 +137,7 @@ function getAvatarInitials(member: Membership): string { return "U"; } -const PAGE_SIZE = 10; +const PAGE_SIZE = 5; export default function TeamManagementPage() { const params = useParams(); @@ -271,7 +271,7 @@ export default function TeamManagementPage() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.3 }} - className="min-h-screen bg-background select-none" + className="bg-background select-none" >
@@ -335,7 +335,7 @@ export default function TeamManagementPage() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.3 }} - className="min-h-screen bg-background select-none" + className="bg-background select-none" >
@@ -356,9 +356,9 @@ export default function TeamManagementPage() { /> )}
-

- {members.length} {members.length === 1 ? "member" : "members"} -

+

+ {members.length} {members.length === 1 ? "member" : "members"} +

{/* Members & Invites Table */} @@ -569,7 +569,7 @@ function MemberRow({ - e.preventDefault()}> + e.preventDefault()} className="min-w-[180px] dark:bg-neutral-800 dark:border dark:border-neutral-700"> {canManageRoles && roles .filter((r) => r.name !== "Owner") @@ -612,7 +612,7 @@ function MemberRow({ )} - + router.push(`/dashboard/${searchSpaceId}/settings?section=team-roles`)} > @@ -707,7 +707,7 @@ function CreateInviteDialog({ return ( (v ? setOpen(true) : handleClose())}> - @@ -929,7 +929,7 @@ function AllInvitesDialog({
- diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 143508977..24eb5bc60 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -221,7 +221,7 @@ export const ConnectorIndicator: FC<{ hideTrigger?: boolean }> = ({ hideTrigger )} - + Manage Connectors {/* YouTube Crawler View - shown when adding YouTube videos */} {isYouTubeView && searchSpaceId ? ( diff --git a/surfsense_web/components/settings/roles-manager.tsx b/surfsense_web/components/settings/roles-manager.tsx index a10f243d2..42301f99f 100644 --- a/surfsense_web/components/settings/roles-manager.tsx +++ b/surfsense_web/components/settings/roles-manager.tsx @@ -4,8 +4,6 @@ import { useQuery } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; import { Bot, - Check, - Crown, Edit2, FileText, Globe, @@ -19,12 +17,11 @@ import { Logs, Settings, Shield, - ShieldCheck, Trash2, Users, } from "lucide-react"; import { motion } from "motion/react"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { createRoleMutationAtom, @@ -44,15 +41,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, @@ -167,25 +156,11 @@ const ACTION_LABELS: Record = { manage_roles: "Manage Roles", }; -const ACTION_DISPLAY: Record = { - create: { label: "Create", color: "text-emerald-600 bg-emerald-500/10" }, - read: { label: "View", color: "text-blue-600 bg-blue-500/10" }, - update: { label: "Edit", color: "text-amber-600 bg-amber-500/10" }, - delete: { label: "Delete", color: "text-red-600 bg-red-500/10" }, - invite: { label: "Invite", color: "text-violet-600 bg-violet-500/10" }, - view: { label: "View", color: "text-blue-600 bg-blue-500/10" }, - remove: { label: "Remove", color: "text-red-600 bg-red-500/10" }, - manage_roles: { - label: "Manage Roles", - color: "text-violet-600 bg-violet-500/10", - }, -}; const ROLE_PRESETS = { editor: { name: "Editor", - description: - "Can create, read, and update content, but cannot delete or manage team settings", + description: "Create, read, and edit content. No delete or admin access.", permissions: [ "documents:create", "documents:read", @@ -353,32 +328,24 @@ export function RolesManager({ searchSpaceId }: { searchSpaceId: number }) { // ============ Role Permissions Display ============ -function RolePermissionsDisplay({ +function RolePermissionsDialog({ permissions, + roleName, + children, }: { permissions: string[]; + roleName: string; + children: React.ReactNode; }) { - if (permissions.includes("*")) { - return ( -
-
- -
-
-

Full Access

-

- All permissions granted -

-
-
- ); - } + const isFullAccess = permissions.includes("*"); const grouped: Record = {}; - for (const perm of permissions) { - const [category, action] = perm.split(":"); - if (!grouped[category]) grouped[category] = []; - grouped[category].push(action); + if (!isFullAccess) { + for (const perm of permissions) { + const [category, action] = perm.split(":"); + if (!grouped[category]) grouped[category] = []; + grouped[category].push(action); + } } const sortedCategories = Object.keys(grouped).sort((a, b) => { @@ -392,78 +359,87 @@ function RolePermissionsDisplay({ return ( - + {children} - - - - Role Permissions + + + {roleName} — Permissions - {permissions.length} permissions across {categoryCount} categories + {isFullAccess + ? "This role has unrestricted access to all resources" + : `${permissions.length} permissions across ${categoryCount} categories`} - -
- {sortedCategories.map((category) => { - const actions = grouped[category]; - const config = CATEGORY_CONFIG[category] || { - label: category, - icon: FileText, - }; - const IconComponent = config.icon; - return ( -
-
- - - {config.label} - -
-
- {actions.map((action) => ( - - {ACTION_LABELS[action] || - action.replace(/_/g, " ")} - - ))} -
-
- ); - })} + {isFullAccess ? ( +
+
+ +
+
+

Full access

+

All permissions granted across every category

+
- + ) : ( + +
+ {sortedCategories.map((category) => { + const actions = grouped[category]; + const config = CATEGORY_CONFIG[category] || { + label: category, + icon: FileText, + }; + const IconComponent = config.icon; + return ( +
+
+ + + {config.label} + +
+
+ {actions.map((action) => ( + + {ACTION_LABELS[action] || + action.replace(/_/g, " ")} + + ))} +
+
+ ); + })} +
+
+ )}
); } +function PermissionsBadge({ permissions }: { permissions: string[] }) { + if (permissions.includes("*")) { + return ( +
+ Full access +
+ ); + } + return ( +
+ {permissions.length} permissions +
+ ); +} + // ============ Roles Content ============ function RolesContent({ @@ -506,6 +482,8 @@ function RolesContent({ ); } + const editingRole = editingRoleId !== null ? roles.find((r) => r.id === editingRoleId) : null; + return ( - {canCreate && !showCreateRole && ( - - - +
)} - {showCreateRole && ( - + + {editingRole && ( + { if (!open) setEditingRoleId(null); }} + role={editingRole} groupedPermissions={groupedPermissions} - onCreateRole={onCreateRole} - onCancel={() => setShowCreateRole(false)} + onUpdateRole={onUpdateRole} /> )} - {editingRoleId !== null && - (() => { - const roleToEdit = roles.find((r) => r.id === editingRoleId); - if (!roleToEdit) return null; - return ( - setEditingRoleId(null)} - /> - ); - })()} - -
- {roles.map((role, index) => ( +
+ {roles.map((role, index) => ( - - {role.is_system_role && ( -
- System Role -
- )} - -
-
-
- -
-
- - {role.name} - - {role.is_default && ( - - Default - - )} -
+ + - e.preventDefault() - } + onCloseAutoFocus={(e) => e.preventDefault()} > {canUpdate && ( - - setEditingRoleId(role.id) - } - > + setEditingRoleId(role.id)}> Edit Role @@ -641,9 +584,7 @@ function RolesContent({ - e.preventDefault() - } + onSelect={(e) => e.preventDefault()} > Delete Role @@ -651,28 +592,16 @@ function RolesContent({ - - Delete role? - + Delete role? - This will permanently - delete the " - {role.name}" role. - Members with this role - will lose their - permissions. + This will permanently delete the "{role.name}" role. + Members with this role will lose their permissions. - - Cancel - + Cancel - onDeleteRole( - role.id - ) - } + onClick={() => onDeleteRole(role.id)} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > Delete @@ -684,20 +613,10 @@ function RolesContent({ )} - )} -
- {role.description && ( - - {role.description} - +
)} -
- - - -
+ +
))}
@@ -776,7 +695,7 @@ function PermissionsEditor({
-
+
{sortedCategories.map((category) => { const config = CATEGORY_CONFIG[category] || { label: category, @@ -792,68 +711,29 @@ function PermissionsEditor({ return (
); })} @@ -959,16 +807,18 @@ function PermissionsEditor({ ); } -// ============ Create Role Section ============ +// ============ Create Role Dialog ============ -function CreateRoleSection({ +function CreateRoleDialog({ + open, + onOpenChange, groupedPermissions, onCreateRole, - onCancel, }: { + open: boolean; + onOpenChange: (open: boolean) => void; groupedPermissions: Record; onCreateRole: (data: CreateRoleRequest["data"]) => Promise; - onCancel: () => void; }) { const [creating, setCreating] = useState(false); const [name, setName] = useState(""); @@ -976,6 +826,14 @@ function CreateRoleSection({ const [selectedPermissions, setSelectedPermissions] = useState([]); const [isDefault, setIsDefault] = useState(false); + const handleClose = () => { + onOpenChange(false); + setName(""); + setDescription(""); + setSelectedPermissions([]); + setIsDefault(false); + }; + const handleCreate = async () => { if (!name.trim()) { toast.error("Please enter a role name"); @@ -990,7 +848,7 @@ function CreateRoleSection({ permissions: selectedPermissions, is_default: isDefault, }); - onCancel(); + handleClose(); } catch (error) { console.error("Failed to create role:", error); } finally { @@ -1041,175 +899,117 @@ function CreateRoleSection({ ); return ( - - - -
-
-
- -
-
- - Create Custom Role - - - Define permissions for a new role in this - search space - -
-
- -
-
- - {/* Quick Start with Presets */} -
- -
- {Object.entries(ROLE_PRESETS).map( - ([key, preset]) => ( + (v ? onOpenChange(true) : handleClose())}> + + + Create Custom Role + + Define permissions for a new role in this search space + + +
+
+
+ +
+ {Object.entries(ROLE_PRESETS).map(([key, preset]) => ( - ) - )} + ))} +
-
-
-
- - setName(e.target.value)} - /> +
+
+ + setName(e.target.value)} + /> +
+
+ + setDescription(e.target.value)} + /> +
-
- - setDescription(e.target.value)} - /> -
-
-
- - setIsDefault(checked === true) - } +
+ setIsDefault(checked === true)} + /> +
+ +

+ New members without a specific role will be assigned this role +

+
+
+ + -
- -

- New members without a specific role will be - assigned this role -

-
- - - -
- - -
- - - +
+
+ + +
+ +
); } -// ============ Edit Role Section ============ +// ============ Edit Role Dialog ============ -function EditRoleSection({ +function EditRoleDialog({ + open, + onOpenChange, role, groupedPermissions, onUpdateRole, - onCancel, }: { + open: boolean; + onOpenChange: (open: boolean) => void; role: Role; groupedPermissions: Record; onUpdateRole: ( @@ -1221,16 +1021,22 @@ function EditRoleSection({ is_default?: boolean; } ) => Promise; - onCancel: () => void; }) { const [saving, setSaving] = useState(false); const [name, setName] = useState(role.name); const [description, setDescription] = useState(role.description || ""); - const [selectedPermissions, setSelectedPermissions] = useState( - role.permissions - ); + const [selectedPermissions, setSelectedPermissions] = useState(role.permissions); const [isDefault, setIsDefault] = useState(role.is_default); + useEffect(() => { + if (open) { + setName(role.name); + setDescription(role.description || ""); + setSelectedPermissions(role.permissions); + setIsDefault(role.is_default); + } + }, [open, role]); + const handleSave = async () => { if (!name.trim()) { toast.error("Please enter a role name"); @@ -1246,7 +1052,7 @@ function EditRoleSection({ is_default: isDefault, }); toast.success("Role updated successfully"); - onCancel(); + onOpenChange(false); } catch (error) { console.error("Failed to update role:", error); toast.error("Failed to update role"); @@ -1285,112 +1091,77 @@ function EditRoleSection({ ); return ( - - - -
-
-
- + + + + Edit Role + + Modify permissions for "{role.name}" + + +
+
+
+
+ + setName(e.target.value)} + />
-
- - Edit Role - - - Modify permissions for "{role.name} - " - +
+ + setDescription(e.target.value)} + />
- -
- - -
-
- - setName(e.target.value)} - /> -
-
- - setDescription(e.target.value)} - /> -
-
-
- - setIsDefault(checked === true) - } +
+ setIsDefault(checked === true)} + /> +
+ +

+ New members without a specific role will be assigned this role +

+
+
+ + -
- -

- New members without a specific role will be - assigned this role -

-
- - - -
- - -
-
- - +
+
+ + +
+ +
); }