'use client'; import React, { useState, useEffect, useMemo } from 'react'; import { getLoginUrl } from '@/lib/config/storage'; import { Building2, Plus, Search, MoreHorizontal, Trash2, Pencil, ChevronRight, ChevronDown, UserPlus, UserMinus, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Spinner } from '@/components/ui/spinner'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Label } from '@/components/ui/label'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { toast } from 'sonner'; import { logger } from '@/lib/utils'; interface WorkspaceMember { userId: string; email: string; displayName: string | null; role: string; joinedAt: string; } interface WorkspaceInfo { id: string; name: string; ownerId: string; ownerEmail: string | null; maxProjects: number; maxDeployments: number; maxStorageMb: number; memberCount: number; projectCount: number; deploymentCount: number; createdAt: string; updatedAt: string; } interface WorkspaceDetail extends WorkspaceInfo { members: WorkspaceMember[]; } export function WorkspacesView() { const [workspaces, setWorkspaces] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [saving, setSaving] = useState(false); // Edit dialog const [editWorkspace, setEditWorkspace] = useState(null); const [showEditDialog, setShowEditDialog] = useState(false); const [editForm, setEditForm] = useState({ name: '', maxProjects: 3, maxDeployments: 1, maxStorageMb: 100, }); // Create dialog const [showCreateDialog, setShowCreateDialog] = useState(false); const [createForm, setCreateForm] = useState({ name: '', ownerEmail: '', }); // Expand / member detail const [expandedId, setExpandedId] = useState(null); const [workspaceDetail, setWorkspaceDetail] = useState(null); const [loadingDetail, setLoadingDetail] = useState(false); // Add member dialog const [showAddMemberDialog, setShowAddMemberDialog] = useState(false); const [addMemberForm, setAddMemberForm] = useState({ email: '', role: 'editor' }); const [addingMember, setAddingMember] = useState(false); const isServerMode = process.env.NEXT_PUBLIC_SERVER_MODE === 'true'; useEffect(() => { loadWorkspaces(); }, []); const loadWorkspaces = async () => { setLoading(true); try { const res = await fetch('/api/admin/workspaces'); if (res.status === 401) { window.location.href = getLoginUrl(); return; } if (!res.ok) throw new Error('Failed to load workspaces'); const data = await res.json(); setWorkspaces(data.workspaces); } catch (err) { logger.error('[WorkspacesView] Failed to load workspaces:', err); toast.error('Failed to load workspaces'); } finally { setLoading(false); } }; const handleOpenEdit = (ws: WorkspaceInfo) => { setEditWorkspace(ws); setEditForm({ name: ws.name, maxProjects: ws.maxProjects, maxDeployments: ws.maxDeployments, maxStorageMb: ws.maxStorageMb, }); setShowEditDialog(true); }; const handleSaveEdit = async () => { if (!editWorkspace) return; setSaving(true); try { const res = await fetch(`/api/admin/workspaces/${editWorkspace.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: editForm.name || undefined, maxProjects: editForm.maxProjects, maxDeployments: editForm.maxDeployments, maxStorageMb: editForm.maxStorageMb, }), }); if (!res.ok) { const error = await res.json(); throw new Error(error.error || 'Failed to update workspace'); } toast.success('Workspace updated'); setShowEditDialog(false); setEditWorkspace(null); await loadWorkspaces(); // Refresh detail if expanded if (expandedId === editWorkspace.id) { await loadDetail(editWorkspace.id); } } catch (err) { logger.error('[WorkspacesView] Failed to update workspace:', err); toast.error(err instanceof Error ? err.message : 'Failed to update workspace'); } finally { setSaving(false); } }; const handleCreate = async () => { if (!createForm.name || !createForm.ownerEmail) { toast.error('Workspace name and owner email are required'); return; } setSaving(true); try { const res = await fetch('/api/admin/workspaces', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: createForm.name, ownerEmail: createForm.ownerEmail, }), }); if (!res.ok) { const error = await res.json(); throw new Error(error.error || 'Failed to create workspace'); } toast.success('Workspace created'); setShowCreateDialog(false); setCreateForm({ name: '', ownerEmail: '' }); await loadWorkspaces(); } catch (err) { logger.error('[WorkspacesView] Failed to create workspace:', err); toast.error(err instanceof Error ? err.message : 'Failed to create workspace'); } finally { setSaving(false); } }; const handleDelete = async (ws: WorkspaceInfo) => { if (!confirm(`Delete workspace "${ws.name}"? This cannot be undone.`)) return; try { const res = await fetch(`/api/admin/workspaces/${ws.id}`, { method: 'DELETE' }); if (!res.ok) { const error = await res.json(); throw new Error(error.error || 'Failed to delete workspace'); } toast.success('Workspace deleted'); if (expandedId === ws.id) { setExpandedId(null); setWorkspaceDetail(null); } await loadWorkspaces(); } catch (err) { logger.error('[WorkspacesView] Failed to delete workspace:', err); toast.error(err instanceof Error ? err.message : 'Failed to delete workspace'); } }; const loadDetail = async (wsId: string) => { setLoadingDetail(true); try { const res = await fetch(`/api/admin/workspaces/${wsId}`); if (!res.ok) throw new Error('Failed to load workspace details'); const data = await res.json(); setWorkspaceDetail(data); } catch (err) { logger.error('[WorkspacesView] Failed to load workspace details:', err); toast.error('Failed to load workspace details'); setExpandedId(null); } finally { setLoadingDetail(false); } }; const handleToggleExpand = async (wsId: string) => { if (expandedId === wsId) { setExpandedId(null); setWorkspaceDetail(null); return; } setExpandedId(wsId); await loadDetail(wsId); }; const handleAddMember = async () => { if (!expandedId || !addMemberForm.email) { toast.error('Email is required'); return; } setAddingMember(true); try { const res = await fetch(`/api/admin/workspaces/${expandedId}/access`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: addMemberForm.email, role: addMemberForm.role }), }); if (!res.ok) { const error = await res.json(); throw new Error(error.error || 'Failed to add member'); } toast.success('Member added'); setShowAddMemberDialog(false); setAddMemberForm({ email: '', role: 'editor' }); await loadDetail(expandedId); await loadWorkspaces(); } catch (err) { logger.error('[WorkspacesView] Failed to add member:', err); toast.error(err instanceof Error ? err.message : 'Failed to add member'); } finally { setAddingMember(false); } }; const handleRemoveMember = async (wsId: string, userId: string, memberEmail: string) => { if (!confirm(`Remove ${memberEmail} from this workspace?`)) return; try { const res = await fetch(`/api/admin/workspaces/${wsId}/access`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId }), }); if (!res.ok) { const error = await res.json(); throw new Error(error.error || 'Failed to remove member'); } toast.success('Member removed'); await loadDetail(wsId); await loadWorkspaces(); } catch (err) { logger.error('[WorkspacesView] Failed to remove member:', err); toast.error(err instanceof Error ? err.message : 'Failed to remove member'); } }; const filteredWorkspaces = useMemo(() => { if (!searchQuery) return workspaces; const query = searchQuery.toLowerCase(); return workspaces.filter( ws => ws.name.toLowerCase().includes(query) || ws.ownerEmail?.toLowerCase().includes(query) ); }, [workspaces, searchQuery]); const formatDate = (dateStr: string) => { try { return new Date(dateStr).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', }); } catch { return dateStr; } }; const roleBadgeVariant = (role: string): 'default' | 'secondary' | 'outline' => { switch (role) { case 'owner': return 'default'; case 'editor': return 'secondary'; default: return 'outline'; } }; if (!isServerMode) { return (

Workspace management is only available in Server Mode

); } if (loading) { return (

Loading workspaces...

); } return ( <>
{/* Toolbar */}
setSearchQuery(e.target.value)} className="pl-9" />
{/* Workspace List */}
{filteredWorkspaces.length === 0 ? (
{workspaces.length === 0 ? ( <>

No Workspaces Yet

Create your first workspace by clicking the "New Workspace" button above.

) : ( <>

No workspaces found

Try adjusting your search criteria

)}
) : (
{filteredWorkspaces.map((ws) => { const isExpanded = expandedId === ws.id; return (
handleToggleExpand(ws.id)} > {/* Expand chevron */}
{isExpanded ? : }
{/* Workspace Info */}
{ws.name}
{ws.ownerEmail && ( Owner: {ws.ownerEmail} )} {ws.memberCount} {ws.memberCount === 1 ? 'member' : 'members'} {ws.projectCount} {ws.projectCount === 1 ? 'project' : 'projects'} Created {formatDate(ws.createdAt)}
{/* Actions */} handleOpenEdit(ws)}> Edit handleDelete(ws)} className="text-destructive focus:text-destructive" > Delete
{/* Expanded members section */} {isExpanded && (
{loadingDetail ? (
Loading members...
) : workspaceDetail ? (
{/* Stats row */}
Max projects: {workspaceDetail.maxProjects} Max deployments: {workspaceDetail.maxDeployments} Max storage: {workspaceDetail.maxStorageMb} MB Deployments: {workspaceDetail.deploymentCount}
{/* Members */}
Members ({workspaceDetail.members.length})
{workspaceDetail.members.length > 0 ? (
{workspaceDetail.members.map((member) => (
e.stopPropagation()} >
{member.email} {member.role}
{member.displayName && (
{member.displayName}
)}
{formatDate(member.joinedAt)}
{member.userId !== workspaceDetail.ownerId && ( )}
))}
) : (
No members
)}
) : null}
)}
); })}
)}
{/* Edit Workspace Dialog */} { setShowEditDialog(open); if (!open) setEditWorkspace(null); }}> Edit Workspace
setEditForm(f => ({ ...f, name: e.target.value }))} placeholder="Workspace name" />
setEditForm(f => ({ ...f, maxProjects: Number(e.target.value) }))} />
setEditForm(f => ({ ...f, maxDeployments: Number(e.target.value) }))} />
setEditForm(f => ({ ...f, maxStorageMb: Number(e.target.value) }))} />
{/* Create Workspace Dialog */} Create Workspace
setCreateForm(f => ({ ...f, name: e.target.value }))} placeholder="Workspace name" />
setCreateForm(f => ({ ...f, ownerEmail: e.target.value }))} placeholder="owner@example.com" />
{/* Add Member Dialog */} Add Member
setAddMemberForm(f => ({ ...f, email: e.target.value }))} placeholder="user@example.com" />
); }