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 132906d9f..a0bc6be03 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -3,29 +3,38 @@ import { useQuery } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; import { + Bot, Calendar, Check, Clock, Copy, Crown, Edit2, + FileText, Hash, Link2, LinkIcon, Loader2, + Logs, + type LucideIcon, + MessageCircle, + MessageSquare, + Mic, MoreHorizontal, + Plug, Plus, RefreshCw, Search, + Settings, Shield, ShieldCheck, Trash2, - User, UserMinus, UserPlus, Users, } from "lucide-react"; import { motion } from "motion/react"; +import Image from "next/image"; import { useParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; @@ -512,6 +521,25 @@ export default function TeamManagementPage() { // ============ Members Tab ============ +// Helper function to get avatar initials +function getAvatarInitials(member: Membership): string { + // Try display name first + if (member.user_display_name) { + const parts = member.user_display_name.trim().split(/\s+/); + if (parts.length >= 2) { + return (parts[0][0] + parts[1][0]).toUpperCase(); + } + return member.user_display_name.slice(0, 2).toUpperCase(); + } + // Try email + if (member.user_email) { + const emailName = member.user_email.split("@")[0]; + return emailName.slice(0, 2).toUpperCase(); + } + // Fallback + return "U"; +} + function MembersTab({ members, roles, @@ -560,7 +588,7 @@ function MembersTab({
setSearchQuery(e.target.value)} className="pl-9" @@ -573,10 +601,30 @@ function MembersTab({ - Member - Role - Joined - Actions + +
+ + Member +
+
+ +
+ + Role +
+
+ +
+ + Joined +
+
+ +
+ + Actions +
+
@@ -601,19 +649,36 @@ function MembersTab({
-
- -
+ {member.user_avatar_url ? ( + + ) : ( +
+ + {getAvatarInitials(member)} + +
+ )} {member.is_owner && ( -
+
)}

- {member.user_email || "Unknown"} + {member.user_display_name || member.user_email || "Unknown"}

+ {member.user_display_name && member.user_email && ( +

+ {member.user_email} +

+ )} {member.is_owner && ( No role {roles.map((role) => ( -
- - {role.name} -
+ {role.name}
))} ) : ( - - - {member.role?.name || "No role"} - + + {member.role?.name || "No role"} + )} -
- + {new Date(member.joined_at).toLocaleDateString()} -
+
{canRemove && !member.is_owner && ( @@ -708,6 +768,130 @@ function MembersTab({ ); } +// ============ Role Permissions Display ============ + +const CATEGORY_CONFIG: Record = { + documents: { label: "Documents", icon: FileText, order: 1 }, + chats: { label: "Chats", icon: MessageSquare, order: 2 }, + comments: { label: "Comments", icon: MessageCircle, order: 3 }, + llm_configs: { label: "LLM Configs", icon: Bot, order: 4 }, + podcasts: { label: "Podcasts", icon: Mic, order: 5 }, + connectors: { label: "Connectors", icon: Plug, order: 6 }, + logs: { label: "Logs", icon: Logs, order: 7 }, + members: { label: "Members", icon: Users, order: 8 }, + roles: { label: "Roles", icon: Shield, order: 9 }, + settings: { label: "Settings", icon: Settings, order: 10 }, +}; + +const ACTION_LABELS: Record = { + create: "Create", + read: "Read", + update: "Update", + delete: "Delete", + invite: "Invite", + view: "View", + remove: "Remove", + manage_roles: "Manage Roles", +}; + +function RolePermissionsDisplay({ permissions }: { permissions: string[] }) { + if (permissions.includes("*")) { + return ( +
+
+ +
+
+

Full Access

+

All permissions granted

+
+
+ ); + } + + // Group permissions by category + const grouped: Record = {}; + for (const perm of permissions) { + const [category, action] = perm.split(":"); + if (!grouped[category]) grouped[category] = []; + grouped[category].push(action); + } + + // Sort categories by predefined order + const sortedCategories = Object.keys(grouped).sort((a, b) => { + const orderA = CATEGORY_CONFIG[a]?.order ?? 99; + const orderB = CATEGORY_CONFIG[b]?.order ?? 99; + return orderA - orderB; + }); + + const categoryCount = sortedCategories.length; + + return ( + + + + + + + + + Role Permissions + + + {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, " ")} + + ))} +
+
+ ); + })} +
+
+
+
+ ); +} + // ============ Roles Tab ============ function RolesTab({ @@ -852,32 +1036,7 @@ function RolesTab({ )} -
- -
- {role.permissions.includes("*") ? ( - - Full Access - - ) : ( - role.permissions.slice(0, 5).map((perm) => ( - - {perm.replace(":", " ")} - - )) - )} - {!role.permissions.includes("*") && role.permissions.length > 5 && ( - - +{role.permissions.length - 5} more - - )} -
-
+