import { useState, useEffect, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { Network, Plus, Loader2, Search, MoreHorizontal, ChevronRight, Users, Trash2, Pencil, Eye, CheckCircle, Ban, Zap, Download, RefreshCw, AlertCircle, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from "@/components/ui/sheet"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { useToast } from "@/hooks/use-toast"; import { api, ApiError, AvailableZtNetwork, PortalNetwork, NetworkEnvironment, NetworkRequestMode, } from "@/lib/api"; import { useCurrentOrganizationId } from "@/hooks/useCurrentOrganization"; const ENVIRONMENTS: { value: NetworkEnvironment; label: string }[] = [ { value: "production", label: "Production" }, { value: "staging", label: "Staging" }, { value: "development", label: "Development" }, { value: "lab", label: "Lab" }, ]; const REQUEST_MODES: { value: NetworkRequestMode; label: string }[] = [ { value: "open", label: "Open — anyone can join" }, { value: "approval_required", label: "Approval Required" }, { value: "invite_only", label: "Invite Only" }, ]; function formatDate(d: string | null | undefined) { if (!d) return "—"; return new Date(d).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", }); } function EnvironmentBadge({ env }: { env: NetworkEnvironment }) { const colors: Record = { production: "bg-red-500/10 text-red-600 border-red-200", staging: "bg-yellow-500/10 text-yellow-600 border-yellow-200", development: "bg-green-500/10 text-green-600 border-green-200", lab: "bg-blue-500/10 text-blue-600 border-blue-200", }; return ( {env.charAt(0).toUpperCase() + env.slice(1)} ); } function RequestModeBadge({ mode }: { mode: NetworkRequestMode }) { if (mode === "open") return Open; if (mode === "approval_required") return Approval Required; return Invite Only; } function cn(...classes: (string | boolean | undefined | null)[]) { return classes.filter(Boolean).join(" "); } export default function NetworksPage() { const { orgId } = useCurrentOrganizationId(); const { toast } = useToast(); const navigate = useNavigate(); const [networks, setNetworks] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [search, setSearch] = useState(""); const [showCreate, setShowCreate] = useState(false); const [createName, setCreateName] = useState(""); const [createZtId, setCreateZtId] = useState(""); const [createDesc, setCreateDesc] = useState(""); const [createEnv, setCreateEnv] = useState("development"); const [createMode, setCreateMode] = useState("approval_required"); const [createDefaultLifetime, setCreateDefaultLifetime] = useState("480"); const [createMaxLifetime, setCreateMaxLifetime] = useState(""); const [isCreating, setIsCreating] = useState(false); const [createError, setCreateError] = useState(null); const [editingNetwork, setEditingNetwork] = useState(null); const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(""); const [editDesc, setEditDesc] = useState(""); const [editEnv, setEditEnv] = useState("development"); const [editMode, setEditMode] = useState("approval_required"); const [editDefaultLifetime, setEditDefaultLifetime] = useState("480"); const [editMaxLifetime, setEditMaxLifetime] = useState(""); const [editError, setEditError] = useState(null); const [deleteNetwork, setDeleteNetwork] = useState(null); const [isDeleting, setIsDeleting] = useState(false); // ZeroTier network picker const [showZtPicker, setShowZtPicker] = useState(false); const [ztNetworks, setZtNetworks] = useState([]); const [isLoadingZtNetworks, setIsLoadingZtNetworks] = useState(false); const [ztNetworksError, setZtNetworksError] = useState(null); const [ztPickerSearch, setZtPickerSearch] = useState(""); const fetchNetworks = useCallback(async () => { if (!orgId) { setIsLoading(false); return; } setIsLoading(true); setError(null); try { const res = await api.zerotier.listNetworks(orgId); setNetworks(res.networks || []); } catch (err) { setError("Failed to load networks. Please try again."); } finally { setIsLoading(false); } }, [orgId]); useEffect(() => { setNetworks([]); fetchNetworks(); }, [fetchNetworks]); const openZtPicker = async () => { if (!orgId) return; setShowZtPicker(true); setZtPickerSearch(""); setZtNetworksError(null); setIsLoadingZtNetworks(true); try { const res = await api.zerotier.listAvailableZtNetworks(orgId); setZtNetworks(res.networks || []); if (res.zt_error) { setZtNetworksError(`ZeroTier API error: ${res.zt_error}`); } } catch (err) { setZtNetworksError( err instanceof ApiError ? err.message : "Failed to load ZeroTier networks.", ); setZtNetworks([]); } finally { setIsLoadingZtNetworks(false); } }; /** Pre-fill the Create Network dialog with data from a ZT network and close the picker. */ const importZtNetwork = (ztNet: AvailableZtNetwork) => { setCreateZtId(ztNet.id); setCreateName(ztNet.name && ztNet.name !== ztNet.id ? ztNet.name : ""); setCreateDesc(ztNet.description ?? ""); setShowZtPicker(false); setShowCreate(true); }; const handleCreate = async () => { if (!orgId) return; setCreateError(null); if (!createName.trim()) { setCreateError("Network name is required."); return; } if (!createZtId.trim()) { setCreateError("ZeroTier Network ID is required."); return; } setIsCreating(true); try { await api.zerotier.createNetwork(orgId, { name: createName.trim(), zerotier_network_id: createZtId.trim(), description: createDesc.trim() || undefined, environment: createEnv, request_mode: createMode, default_activation_lifetime_minutes: parseInt(createDefaultLifetime) || 480, max_activation_lifetime_minutes: createMaxLifetime ? parseInt(createMaxLifetime) : undefined, }); toast({ title: "Network created", description: `${createName} has been added.` }); setShowCreate(false); setCreateName(""); setCreateZtId(""); setCreateDesc(""); setCreateEnv("development"); setCreateMode("approval_required"); setCreateDefaultLifetime("480"); setCreateMaxLifetime(""); fetchNetworks(); } catch (err) { setCreateError(err instanceof ApiError ? err.message : "Failed to create network."); } finally { setIsCreating(false); } }; const openEditDialog = (network: PortalNetwork) => { setEditingNetwork(network); setEditName(network.name); setEditDesc(network.description || ""); setEditEnv(network.environment); setEditMode(network.request_mode); setEditDefaultLifetime(String(network.default_activation_lifetime_minutes)); setEditMaxLifetime(network.max_activation_lifetime_minutes ? String(network.max_activation_lifetime_minutes) : ""); setEditError(null); }; const handleEdit = async () => { if (!orgId || !editingNetwork) return; setEditError(null); setIsEditing(true); try { await api.zerotier.updateNetwork(orgId, editingNetwork.id, { name: editName.trim(), description: editDesc.trim() || undefined, environment: editEnv, request_mode: editMode, default_activation_lifetime_minutes: parseInt(editDefaultLifetime) || 480, max_activation_lifetime_minutes: editMaxLifetime ? parseInt(editMaxLifetime) : undefined, }); toast({ title: "Network updated", description: `${editName} has been updated.` }); setEditingNetwork(null); fetchNetworks(); } catch (err) { setEditError(err instanceof ApiError ? err.message : "Failed to update network."); } finally { setIsEditing(false); } }; const handleDelete = async () => { if (!orgId || !deleteNetwork) return; setIsDeleting(true); try { await api.zerotier.deleteNetwork(orgId, deleteNetwork.id); toast({ title: "Network deleted", description: `${deleteNetwork.name} has been removed.` }); setDeleteNetwork(null); fetchNetworks(); } catch (err) { toast({ variant: "destructive", title: "Failed to delete network", description: err instanceof ApiError ? err.message : "Something went wrong." }); } finally { setIsDeleting(false); } }; const filteredNetworks = networks.filter((n) => { const q = search.toLowerCase(); return ( n.name.toLowerCase().includes(q) || n.zerotier_network_id.toLowerCase().includes(q) || (n.description?.toLowerCase().includes(q) ?? false) ); }); return (

ZeroTier Networks

Manage ZeroTier portal networks and monitor access

setSearch(e.target.value)} className="pl-10" />
Portal Networks {!isLoading && {networks.length}} Click a network to manage members, devices, and access requests {isLoading ? (
Loading networks…
) : error ? (
{error}
) : filteredNetworks.length === 0 ? (
{search ? "No networks match your search." : "No networks configured yet. Add one to get started."}
) : (
{filteredNetworks.map((network) => ( { e.stopPropagation(); navigate(`/org/zerotier/networks/${network.id}`); }}> View details { e.stopPropagation(); openEditDialog(network); }}> Edit { e.stopPropagation(); setDeleteNetwork(network); }} > Delete ))}
)}
{/* ZeroTier Network Picker */} { if (!open) setShowZtPicker(false); }}> Import from ZeroTier Networks found in your ZeroTier account. Click one to import it into Secuird. {/* Search + refresh */}
setZtPickerSearch(e.target.value)} className="pl-10" />
Refresh list
{isLoadingZtNetworks ? (
Loading ZeroTier networks…
) : ztNetworksError ? (

Could not load ZeroTier networks

{ztNetworksError}

Make sure your ZeroTier credentials are configured under{" "} Settings → ZeroTier Configuration.

) : ztNetworks.length === 0 ? (

No ZeroTier networks found

Your ZeroTier account has no networks yet.

) : (
{ztNetworks .filter((n) => { const q = ztPickerSearch.toLowerCase(); return !q || n.name.toLowerCase().includes(q) || n.id.toLowerCase().includes(q); }) .map((ztNet) => (
!ztNet.already_managed && importZtNetwork(ztNet)} role={ztNet.already_managed ? undefined : "button"} tabIndex={ztNet.already_managed ? undefined : 0} onKeyDown={(e) => { if (!ztNet.already_managed && (e.key === "Enter" || e.key === " ")) { importZtNetwork(ztNet); } }} >

{ztNet.name}

{ztNet.already_managed && ( {ztNet.portal_network_name ? `Managed as "${ztNet.portal_network_name}"` : "Already managed"} )}

{ztNet.id}

{(ztNet.online_member_count > 0 || ztNet.total_member_count > 0) && (

{ztNet.online_member_count} online · {ztNet.total_member_count} total members

)}
{!ztNet.already_managed && ( )}
))}
)}
{/* Create Network Dialog */} { if (!open) setShowCreate(false); }}> Add Portal Network Link a ZeroTier network to your organization.
setCreateName(e.target.value)} />
setCreateZtId(e.target.value)} />

16-character hexadecimal network ID from your ZeroTier controller.

setCreateDesc(e.target.value)} />
setCreateDefaultLifetime(e.target.value)} />
setCreateMaxLifetime(e.target.value)} />
{createError &&

{createError}

}
{/* Edit Network Dialog */} { if (!open) setEditingNetwork(null); }}> Edit Network Update network settings. {editingNetwork && (
setEditName(e.target.value)} />
setEditDesc(e.target.value)} />
setEditDefaultLifetime(e.target.value)} />
setEditMaxLifetime(e.target.value)} />
{editError &&

{editError}

}
)}
{/* Delete Confirmation */} { if (!open) setDeleteNetwork(null); }}> Delete Network Are you sure you want to remove "{deleteNetwork?.name}"? This does not affect the ZeroTier network itself.
); }