|
|
@@ -15,48 +15,14 @@ import {
|
|
|
Building2,
|
|
|
ChevronRight,
|
|
|
Bot,
|
|
|
- Blocks
|
|
|
+ Blocks,
|
|
|
+ ClipboardCheck
|
|
|
} from 'lucide-react';
|
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
|
import { useLanguage } from '../../../contexts/LanguageContext';
|
|
|
import { cn } from '../../utils/cn';
|
|
|
|
|
|
-interface TenantTreeNode {
|
|
|
- tenantId: string;
|
|
|
- role: string;
|
|
|
- tenant: {
|
|
|
- id: string;
|
|
|
- name: string;
|
|
|
- domain?: string;
|
|
|
- parentId?: string;
|
|
|
- };
|
|
|
- children?: TenantTreeNode[];
|
|
|
-}
|
|
|
-
|
|
|
-const buildTenantTree = (memberships: any[]): TenantTreeNode[] => {
|
|
|
- const map = new Map<string, TenantTreeNode>();
|
|
|
- const roots: TenantTreeNode[] = [];
|
|
|
-
|
|
|
- memberships.forEach(m => {
|
|
|
- map.set(m.tenantId, { ...m, children: [] });
|
|
|
- });
|
|
|
-
|
|
|
- memberships.forEach(m => {
|
|
|
- const node = map.get(m.tenantId)!;
|
|
|
- const parentId = (m.tenant as any)?.parentId;
|
|
|
- if (parentId && map.has(parentId)) {
|
|
|
- const parent = map.get(parentId)!;
|
|
|
- parent.children = parent.children || [];
|
|
|
- parent.children.push(node);
|
|
|
- } else {
|
|
|
- roots.push(node);
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- return roots;
|
|
|
-};
|
|
|
-
|
|
|
interface SidebarItemProps {
|
|
|
icon: React.ElementType;
|
|
|
label: string;
|
|
|
@@ -103,6 +69,57 @@ const WorkspaceLayout: React.FC = () => {
|
|
|
navigate(path);
|
|
|
};
|
|
|
|
|
|
+ const buildTenantTree = (memberships: typeof availableTenants) => {
|
|
|
+ const map = new Map<string, any>();
|
|
|
+ const roots: any[] = [];
|
|
|
+
|
|
|
+ memberships.forEach(m => {
|
|
|
+ map.set(m.tenantId, { ...m, children: [] });
|
|
|
+ });
|
|
|
+
|
|
|
+ memberships.forEach(m => {
|
|
|
+ const node = map.get(m.tenantId)!;
|
|
|
+ const parentId = m.tenant.parentId;
|
|
|
+ if (parentId && map.has(parentId)) {
|
|
|
+ const parent = map.get(parentId)!;
|
|
|
+ parent.children.push(node);
|
|
|
+ } else {
|
|
|
+ roots.push(node);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return roots;
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderTenantItem = (membership: any, depth = 0) => (
|
|
|
+ <React.Fragment key={membership.tenantId}>
|
|
|
+ <button
|
|
|
+ onClick={() => {
|
|
|
+ switchTenant(membership.tenantId);
|
|
|
+ setShowTenantMenu(false);
|
|
|
+ }}
|
|
|
+ className={cn(
|
|
|
+ "w-full text-left px-4 py-3 rounded-xl flex items-center justify-between group transition-colors",
|
|
|
+ activeTenant?.tenantId === membership.tenantId
|
|
|
+ ? "bg-blue-50 text-blue-700"
|
|
|
+ : "hover:bg-slate-50 text-slate-600"
|
|
|
+ )}
|
|
|
+ style={{ paddingLeft: `${depth * 1.5 + 1}rem` }}
|
|
|
+ >
|
|
|
+ <div className="flex flex-col min-w-0">
|
|
|
+ <span className="text-sm font-bold truncate">{membership.tenant.name}</span>
|
|
|
+ <span className="text-[10px] font-medium opacity-60 uppercase tracking-tight">{membership.role}</span>
|
|
|
+ </div>
|
|
|
+ {activeTenant?.tenantId === membership.tenantId && (
|
|
|
+ <div className="w-1.5 h-1.5 bg-blue-600 rounded-full shrink-0" />
|
|
|
+ )}
|
|
|
+ </button>
|
|
|
+ {membership.children?.map((child: any) => renderTenantItem(child, depth + 1))}
|
|
|
+ </React.Fragment>
|
|
|
+ );
|
|
|
+
|
|
|
+ const tenantTree = buildTenantTree(availableTenants);
|
|
|
+
|
|
|
return (
|
|
|
<div className="flex h-screen bg-[#FCFDFF] text-slate-900 font-sans selection:bg-blue-100 selection:text-blue-900">
|
|
|
|
|
|
@@ -131,7 +148,6 @@ const WorkspaceLayout: React.FC = () => {
|
|
|
isActive={location.pathname.startsWith('/chat')}
|
|
|
onClick={handleNavClick}
|
|
|
/>
|
|
|
-{/*
|
|
|
<SidebarItem
|
|
|
icon={Bot}
|
|
|
label={t('navAgent')}
|
|
|
@@ -139,14 +155,14 @@ const WorkspaceLayout: React.FC = () => {
|
|
|
isActive={location.pathname.startsWith('/agents')}
|
|
|
onClick={handleNavClick}
|
|
|
/>
|
|
|
- <SidebarItem
|
|
|
+ {/* <SidebarItem
|
|
|
icon={Blocks}
|
|
|
label={t('navPlugin')}
|
|
|
path="/plugins"
|
|
|
isActive={location.pathname.startsWith('/plugins')}
|
|
|
onClick={handleNavClick}
|
|
|
- />
|
|
|
- */}
|
|
|
+ /> */}
|
|
|
+
|
|
|
<SidebarItem
|
|
|
icon={Database}
|
|
|
label={t('navKnowledge')}
|
|
|
@@ -154,7 +170,6 @@ const WorkspaceLayout: React.FC = () => {
|
|
|
isActive={location.pathname === '/knowledge' || location.pathname.startsWith('/knowledge/')}
|
|
|
onClick={handleNavClick}
|
|
|
/>
|
|
|
- {/*
|
|
|
{(activeTenant?.features?.isNotebookEnabled ?? true) && (
|
|
|
<SidebarItem
|
|
|
icon={BookOpen}
|
|
|
@@ -164,7 +179,6 @@ const WorkspaceLayout: React.FC = () => {
|
|
|
onClick={handleNavClick}
|
|
|
/>
|
|
|
)}
|
|
|
- */}
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-0.5">
|
|
|
@@ -177,6 +191,16 @@ const WorkspaceLayout: React.FC = () => {
|
|
|
/>
|
|
|
</div>
|
|
|
</nav>
|
|
|
+
|
|
|
+ <div className="p-4 border-t border-slate-100/80 mt-auto">
|
|
|
+ <button
|
|
|
+ onClick={logout}
|
|
|
+ className="flex items-center gap-2 px-3 py-2 text-[13px] font-medium text-red-500 hover:bg-red-50 w-full rounded-lg transition-colors"
|
|
|
+ >
|
|
|
+ <LogOut size={14} />
|
|
|
+ {t('logout')}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
</aside>
|
|
|
|
|
|
{/* Main Content Area */}
|
|
|
@@ -215,49 +239,8 @@ const WorkspaceLayout: React.FC = () => {
|
|
|
<div className="p-3 border-b border-slate-100">
|
|
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-2">{t('navTenants')}</span>
|
|
|
</div>
|
|
|
- <div className="max-h-80 overflow-y-auto p-1">
|
|
|
- {(() => {
|
|
|
- const tree = buildTenantTree(availableTenants);
|
|
|
- const renderTenantItem = (node: TenantTreeNode, depth: number = 0) => {
|
|
|
- const isSelected = activeTenant?.tenantId === node.tenantId;
|
|
|
- return (
|
|
|
- <React.Fragment key={node.tenantId}>
|
|
|
- <button
|
|
|
- onClick={() => {
|
|
|
- switchTenant(node.tenantId);
|
|
|
- setShowTenantMenu(false);
|
|
|
- }}
|
|
|
- className={cn(
|
|
|
- "w-full text-left px-4 py-2.5 rounded-xl flex items-center justify-between group transition-colors",
|
|
|
- isSelected
|
|
|
- ? "bg-blue-50 text-blue-700"
|
|
|
- : "hover:bg-slate-50 text-slate-600"
|
|
|
- )}
|
|
|
- style={{ marginLeft: `${depth * 12}px`, width: `calc(100% - ${depth * 12}px)` }}
|
|
|
- >
|
|
|
- <div className="flex items-center gap-2 min-w-0">
|
|
|
- {depth > 0 && (
|
|
|
- <ChevronRight size={12} className="text-slate-300 shrink-0" />
|
|
|
- )}
|
|
|
- <div className="flex flex-col min-w-0">
|
|
|
- <span className="text-sm font-bold truncate">{node.tenant.name}</span>
|
|
|
- <span className="text-[9px] font-black opacity-40 uppercase tracking-tighter leading-none">{node.role?.replace('_', ' ')}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- {isSelected && (
|
|
|
- <div className="w-1.5 h-1.5 bg-blue-600 rounded-full shrink-0 ml-2" />
|
|
|
- )}
|
|
|
- </button>
|
|
|
- {node.children && node.children.length > 0 && (
|
|
|
- <div className="mt-0.5 mb-1">
|
|
|
- {node.children.map(child => renderTenantItem(child, depth + 1))}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </React.Fragment>
|
|
|
- );
|
|
|
- };
|
|
|
- return tree.map(root => renderTenantItem(root));
|
|
|
- })()}
|
|
|
+ <div className="max-h-64 overflow-y-auto p-1">
|
|
|
+ {tenantTree.map(membership => renderTenantItem(membership))}
|
|
|
</div>
|
|
|
</motion.div>
|
|
|
</>
|
|
|
@@ -265,20 +248,21 @@ const WorkspaceLayout: React.FC = () => {
|
|
|
</AnimatePresence>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <div className="flex items-center gap-2 px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-200/50">
|
|
|
- <div className="flex items-center justify-center w-8 h-8 text-sm font-bold text-white bg-indigo-600 rounded-full shrink-0 shadow-sm">
|
|
|
- {user?.username?.[0]?.toUpperCase() || 'A'}
|
|
|
+
|
|
|
+ <div className="flex items-center gap-4">
|
|
|
+ <div className="flex items-center gap-3 px-3 py-1.5 bg-slate-50/50 border border-slate-100 rounded-2xl">
|
|
|
+ <div className="flex items-center justify-center w-8 h-8 text-xs font-bold text-white bg-indigo-600 rounded-full shrink-0 shadow-sm">
|
|
|
+ {user?.displayName?.[0]?.toUpperCase() || user?.username?.[0]?.toUpperCase() || 'A'}
|
|
|
+ </div>
|
|
|
+ <div className="flex flex-col">
|
|
|
+ <p className="text-sm font-bold text-slate-900 truncate max-w-[120px]">
|
|
|
+ {user?.displayName || user?.username}
|
|
|
+ </p>
|
|
|
+ <span className="text-[9px] font-black text-blue-600 uppercase tracking-tighter">
|
|
|
+ {activeTenant?.role?.replace('_', ' ') || user?.role?.replace('_', ' ') || 'USER'}
|
|
|
+ </span>
|
|
|
</div>
|
|
|
- <p className="text-sm font-semibold text-slate-900 truncate">{user?.displayName || user?.username}</p>
|
|
|
</div>
|
|
|
- <button
|
|
|
- onClick={logout}
|
|
|
- className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
|
|
- title={t('logout')}
|
|
|
- >
|
|
|
- <LogOut size={18} />
|
|
|
- </button>
|
|
|
</div>
|
|
|
</header>
|
|
|
|