diff --git a/app/src/components/HostSwitcher.tsx b/app/src/components/HostSwitcher.tsx new file mode 100644 index 0000000..030eeb3 --- /dev/null +++ b/app/src/components/HostSwitcher.tsx @@ -0,0 +1,101 @@ +import { useEffect, useRef, useState } from "react" +import { Check, ChevronsUpDown, ExternalLink, Hexagon } from "lucide-react" +import { useNodes } from "@/services/api/hooks" +import type { NodeSummary } from "@/types" +import { cn } from "@/lib/utils" + +// Each machine's castle is served at its own origin (castle.), so hopping +// between hosts is a cross-origin navigation. This shows which host you're driving +// (the is_local node) and lets you jump to a peer's dashboard, preserving the view. +function dashboardUrl(n: NodeSummary): string { + const base = n.gateway_domain + ? `https://castle.${n.gateway_domain}` + : `http://${n.hostname}:${n.gateway_port}` + return `${base}${window.location.pathname}` +} + +export function HostSwitcher({ collapsed }: { collapsed: boolean }) { + const { data: nodes } = useNodes() + const [open, setOpen] = useState(false) + const ref = useRef(null) + + useEffect(() => { + if (!open) return + const onDown = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) + } + window.addEventListener("mousedown", onDown) + return () => window.removeEventListener("mousedown", onDown) + }, [open]) + + const list = nodes ?? [] + const current = list.find((n) => n.is_local) + const others = list.filter((n) => !n.is_local) + const label = current?.hostname ?? "this node" + const canSwitch = others.length > 0 + + return ( +
+ + {open && ( +
+
+ Castle hosts +
+ {list.map((n) => { + const isCur = n.is_local + const inner = ( + <> + + {n.hostname} + {isCur ? ( + + ) : ( + + )} + + ) + const cls = "flex items-center gap-2 px-2.5 py-2 text-sm" + if (isCur) + return ( +
+ {inner} +
+ ) + return ( + setOpen(false)} + className={cn(cls, "hover:bg-white/5", !n.online && "opacity-40")} + > + {inner} + + ) + })} +
+ )} +
+ ) +} diff --git a/app/src/components/Layout.tsx b/app/src/components/Layout.tsx index 51ba28b..ae99652 100644 --- a/app/src/components/Layout.tsx +++ b/app/src/components/Layout.tsx @@ -14,7 +14,6 @@ import { Server, Share2, Map as MapIcon, - Network, Wrench, X, type LucideIcon, @@ -23,6 +22,7 @@ import { cn } from "@/lib/utils" import { useEventStream } from "@/services/api/hooks" import { AssistantDock } from "@/components/AssistantDock" import { CommandPalette } from "@/components/CommandPalette" +import { HostSwitcher } from "@/components/HostSwitcher" type NavLeaf = { to: string; label: string; icon: LucideIcon; end?: boolean } type NavGroup = { label: string; icon: LucideIcon; children: NavLeaf[] } @@ -42,7 +42,6 @@ const NAV: (NavLeaf | NavGroup)[] = [ ], }, { to: "/programs", label: "Programs", icon: Package }, - { to: "/graph", label: "Graph", icon: Network }, { to: "/map", label: "System Map", icon: MapIcon }, { to: "/mesh", label: "Mesh", icon: Share2 }, ] @@ -202,6 +201,7 @@ export function Layout() { + setMobileOpen(false)} /> @@ -222,6 +222,7 @@ export function Layout() { > +