import { useCallback, useEffect, useRef, useState } from "react"; import { ServerIcon } from "lucide-react"; import { getExecutorServerAuthorizationHeader, normalizeExecutorServerConnection, useExecutorServerConnection, useSetExecutorServerConnection, type ExecutorServerAuth, type ExecutorServerConnection, type ExecutorServerConnectionInput, } from "@executor-js/react/api/server-connection"; import { EXECUTOR_SERVER_PROFILES_STORAGE_KEY, getActiveExecutorServerProfile, normalizeExecutorServerProfilesSnapshot, parseExecutorServerProfilesSnapshot, readExecutorServerProfiles, removeExecutorServerProfile, selectExecutorServerProfile, serializeExecutorServerProfilesSnapshot, upsertExecutorServerProfile, writeExecutorServerProfiles, type ExecutorServerProfilesSnapshot, } from "@executor-js/react/api/server-profiles"; import { Button } from "@executor-js/react/components/button"; import { Input } from "@executor-js/react/components/input"; import { Label } from "@executor-js/react/components/label"; import { NativeSelect, NativeSelectOption } from "@executor-js/react/components/native-select"; import { Popover, PopoverContent, PopoverHeader, PopoverTitle, PopoverTrigger, } from "@executor-js/react/components/popover"; import { cn } from "@executor-js/react/lib/utils"; type AuthMode = "none" | "bearer" | "basic"; interface DraftProfile { readonly origin: string; readonly displayName: string; readonly authMode: AuthMode; readonly username: string; readonly secret: string; } const emptyDraft: DraftProfile = { origin: "", displayName: "", authMode: "none", username: "executor", secret: "", }; interface DesktopProfileStorageBridge { readonly getServerProfiles: () => Promise; readonly setServerProfiles: (value: string) => Promise; } const browserStorage = () => globalThis.window?.localStorage ?? null; const desktopProfileStorageBridge = (): DesktopProfileStorageBridge | null => { const bridge = globalThis.window?.executor; if ( !bridge || typeof bridge.getServerProfiles !== "function" || typeof bridge.setServerProfiles !== "function" ) { return null; } return { getServerProfiles: bridge.getServerProfiles, setServerProfiles: bridge.setServerProfiles, }; }; const hasDesktopServerConnectionBridge = (): boolean => Boolean(desktopProfileStorageBridge()) || typeof globalThis.window?.executor?.getServerConnection === "function"; const readDesktopServerConnection = (): Promise | null => { const bridge = globalThis.window?.executor; if (!bridge || typeof bridge.getServerConnection !== "function") return null; return bridge.getServerConnection(); }; const readBrowserProfiles = (): ExecutorServerProfilesSnapshot => readExecutorServerProfiles(browserStorage()); const clearBrowserProfiles = (): void => { browserStorage()?.removeItem(EXECUTOR_SERVER_PROFILES_STORAGE_KEY); }; const readStoredProfiles = (): Promise => { const bridge = desktopProfileStorageBridge(); const browserProfiles = readBrowserProfiles(); if (!bridge) return Promise.resolve(browserProfiles); return bridge.getServerProfiles().then( (raw) => { const desktopProfiles = parseExecutorServerProfilesSnapshot(raw); if (desktopProfiles.profiles.length > 0) { if (browserProfiles.profiles.length > 0) { const mergedProfiles = normalizeExecutorServerProfilesSnapshot({ activeKey: desktopProfiles.activeKey ?? browserProfiles.activeKey, profiles: [...browserProfiles.profiles, ...desktopProfiles.profiles], }); void bridge .setServerProfiles(serializeExecutorServerProfilesSnapshot(mergedProfiles)) .then(clearBrowserProfiles, () => undefined); return mergedProfiles; } clearBrowserProfiles(); return desktopProfiles; } if (browserProfiles.profiles.length > 0) { void bridge .setServerProfiles(serializeExecutorServerProfilesSnapshot(browserProfiles)) .then(clearBrowserProfiles, () => undefined); return browserProfiles; } return desktopProfiles; }, () => browserProfiles, ); }; const writeStoredProfiles = (snapshot: ExecutorServerProfilesSnapshot): void => { const bridge = desktopProfileStorageBridge(); if (!bridge) { writeExecutorServerProfiles(browserStorage(), snapshot); return; } void bridge .setServerProfiles(serializeExecutorServerProfilesSnapshot(snapshot)) .then(clearBrowserProfiles, () => undefined); }; const serverLabel = (connection: ExecutorServerConnection): string => connection.displayName || connection.origin.replace(/^https?:\/\//, ""); const serverDescription = (connection: ExecutorServerConnection): string => connection.origin.replace(/^https?:\/\//, ""); const serverKindLabel = (connection: ExecutorServerConnection): string => { if (connection.kind === "desktop-sidecar") return "Local"; const hostname = new URL(connection.origin).hostname; return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" ? "Local" : "Remote"; }; const authLabel = (connection: ExecutorServerConnection): string => { const authorization = getExecutorServerAuthorizationHeader(connection); if (!authorization) return "No auth"; if (authorization.startsWith("Bearer ")) return "Bearer"; if (authorization.startsWith("Basic ")) return "Basic"; return "Auth"; }; const isLoopbackHost = (host: string): boolean => host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]"; const isLoopbackConnection = (connection: ExecutorServerConnection): boolean => { const url = new URL(connection.origin); return isLoopbackHost(url.hostname); }; const sameLoopbackServer = (a: ExecutorServerConnection, b: ExecutorServerConnection): boolean => { const left = new URL(a.origin); const right = new URL(b.origin); return ( left.protocol === right.protocol && isLoopbackHost(left.hostname) && isLoopbackHost(right.hostname) && (left.port || (left.protocol === "https:" ? "443" : "80")) === (right.port || (right.protocol === "https:" ? "443" : "80")) ); }; const hasBearerAuth = (connection: ExecutorServerConnection): boolean => connection.auth?.kind === "bearer" && connection.auth.token.length > 0; const snapshotWithCurrent = ( snapshot: ExecutorServerProfilesSnapshot, connection: ExecutorServerConnection, makeActive: boolean, ): ExecutorServerProfilesSnapshot => upsertExecutorServerProfile(snapshot, connection, { makeActive }) ?? snapshot; const withoutLoopbackProfiles = ( snapshot: ExecutorServerProfilesSnapshot, ): ExecutorServerProfilesSnapshot => normalizeExecutorServerProfilesSnapshot({ activeKey: snapshot.activeKey, profiles: snapshot.profiles.filter((profile) => !isLoopbackConnection(profile)), }); const draftAuth = (draft: DraftProfile): ExecutorServerAuth | undefined => { const secret = draft.secret.trim(); if (draft.authMode === "bearer" && secret) { return { kind: "bearer", token: secret }; } if (draft.authMode === "basic" && secret) { return { kind: "basic", username: draft.username.trim() || undefined, password: secret, }; } return undefined; }; interface ServerConnectionMenuProps { readonly side?: "top" | "right" | "bottom" | "left"; readonly align?: "start" | "center" | "end"; readonly variant?: "default" | "header"; } export function ServerConnectionMenu(props: ServerConnectionMenuProps = {}) { const connection = useExecutorServerConnection(); const setServerConnection = useSetExecutorServerConnection(); const hydratedRef = useRef(false); const [hydrated, setHydrated] = useState(false); const [snapshot, setSnapshot] = useState(() => ({ activeKey: connection.key, profiles: [connection], })); const [draft, setDraft] = useState(emptyDraft); const [error, setError] = useState(null); const [showCustomServer, setShowCustomServer] = useState(false); const persistSnapshot = useCallback((next: ExecutorServerProfilesSnapshot) => { setSnapshot(next); writeStoredProfiles(next); }, []); useEffect(() => { if (hydratedRef.current) return; hydratedRef.current = true; let cancelled = false; void readStoredProfiles().then((stored) => { void (async () => { if (cancelled) return; const desktopBridge = hasDesktopServerConnectionBridge(); const desktopConnection = (await readDesktopServerConnection()?.then( (value) => value, () => null, )) ?? null; const storedActive = getActiveExecutorServerProfile(stored); const current = desktopConnection ? normalizeExecutorServerConnection(desktopConnection) : connection; const baseStored = desktopConnection ? withoutLoopbackProfiles(stored) : stored; const shouldKeepCurrent = desktopBridge || storedActive === null || (hasBearerAuth(current) && storedActive !== null && sameLoopbackServer(current, storedActive)); const next = snapshotWithCurrent(baseStored, current, shouldKeepCurrent); persistSnapshot(next); if (desktopConnection) { setServerConnection(desktopConnection); } else if (!shouldKeepCurrent && storedActive && storedActive.key !== connection.key) { setServerConnection(storedActive); } setHydrated(true); })(); }); return () => { cancelled = true; }; }, [connection, persistSnapshot, setServerConnection]); useEffect(() => { if (!hydrated) return; setSnapshot((previous) => { const base = hasDesktopServerConnectionBridge() ? withoutLoopbackProfiles(previous) : previous; const next = snapshotWithCurrent(base, connection, true); writeStoredProfiles(next); return next; }); }, [connection, hydrated]); const selectProfile = (key: string): void => { const next = selectExecutorServerProfile(snapshot, key); const active = getActiveExecutorServerProfile(next); persistSnapshot(next); if (active) setServerConnection(active); }; const removeProfile = (key: string): void => { if (snapshot.profiles.length <= 1) return; const next = removeExecutorServerProfile(snapshot, key); const active = getActiveExecutorServerProfile(next); persistSnapshot(next); if (key === connection.key && active) setServerConnection(active); }; const addProfile = (event: React.FormEvent): void => { event.preventDefault(); const origin = draft.origin.trim(); if (!origin) { setError("Enter a server origin."); return; } if (draft.authMode !== "none" && !draft.secret.trim()) { setError("Enter the credential value."); return; } const auth = draftAuth(draft); const input: ExecutorServerConnectionInput = { kind: "http", origin, ...(draft.displayName.trim() ? { displayName: draft.displayName.trim() } : {}), ...(auth ? { auth } : {}), }; const next = upsertExecutorServerProfile(snapshot, input); const active = next ? getActiveExecutorServerProfile(next) : null; if (!next || !active) { setError("Enter a valid http or https origin."); return; } setError(null); setDraft(emptyDraft); setShowCustomServer(false); persistSnapshot(next); setServerConnection(active); }; const trigger = props.variant === "header" ? ( ) : ( ); return ( {trigger} Server profiles
{snapshot.profiles.map((profile) => { const active = profile.key === connection.key; const profileAuthLabel = authLabel(profile); return (
{serverKindLabel(profile)} {profileAuthLabel !== "No auth" && ( {profileAuthLabel} )}
); })}
{showCustomServer && (
{draft.authMode === "basic" && ( )} {draft.authMode !== "none" && ( )} {error &&

{error}

}
)}
); }