feature: add relay urls for tunnel command

Hee Sung Son committed Dec 6, 2025 at 20:51 UTC 7f6690478e0969cacd3b73aaeb6277920cb1cfa1
1 file changed +79 -11
cmd/relay-server/frontend/src/components/TunnelCommandModal.tsx
+79 -11
@@ -1,5 +1,5 @@
1 -import { useState, useMemo } from "react";
2 -import { Copy, Check, Terminal } from "lucide-react";
1 +import { useState, useMemo, useRef } from "react";
2 +import { Copy, Check, Terminal, X } from "lucide-react";
3 import {
4 Dialog,
5 DialogContent,
@@ -19,24 +19,55 @@ export function TunnelCommandModal({ trigger }: TunnelCommandModalProps) {
19 const defaultPort = "3000";
20 const defaultName = "myapp";
21
22 - const [port, setPort] = useState(defaultPort);
23 - const [name, setName] = useState(defaultName);
24 - const [copied, setCopied] = useState(false);
25 -
22 // Get current host URL dynamically
27 - const hostUrl = useMemo(() => {
23 + const currentOrigin = useMemo(() => {
24 if (typeof window !== "undefined") {
25 return window.location.origin;
26 }
27 return "http://localhost:4017";
28 }, []);
29
30 + const [port, setPort] = useState(defaultPort);
31 + const [name, setName] = useState(defaultName);
32 + const [relayUrls, setRelayUrls] = useState<string[]>([currentOrigin]);
33 + const [urlInput, setUrlInput] = useState("");
34 + const [copied, setCopied] = useState(false);
35 + const urlInputRef = useRef<HTMLInputElement>(null);
36 +
37 + const addRelayUrl = (url: string) => {
38 + const trimmed = url.trim();
39 + if (!trimmed || relayUrls.includes(trimmed)) return;
40 + // Basic URL validation
41 + try {
42 + new URL(trimmed);
43 + setRelayUrls([...relayUrls, trimmed]);
44 + setUrlInput("");
45 + } catch {
46 + // Invalid URL, ignore
47 + }
48 + };
49 +
50 + const removeRelayUrl = (url: string) => {
51 + setRelayUrls(relayUrls.filter((u) => u !== url));
52 + };
53 +
54 + const handleUrlKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
55 + if (e.key === "Enter") {
56 + e.preventDefault();
57 + addRelayUrl(urlInput);
58 + } else if (e.key === "Backspace" && urlInput === "" && relayUrls.length > 0) {
59 + // Remove last URL when backspace on empty input
60 + setRelayUrls(relayUrls.slice(0, -1));
61 + }
62 + };
63 +
64 // Generate the tunnel command
65 const command = useMemo(() => {
36 - return `curl -fsSL ${hostUrl}/tunnel | PORT=${
37 - port === "" ? defaultPort : port
38 - } NAME=${name === "" ? defaultName : name} sh`;
39 - }, [hostUrl, port, name]);
66 + const portVal = port === "" ? defaultPort : port;
67 + const nameVal = name === "" ? defaultName : name;
68 + const relayUrlVal = relayUrls.length > 0 ? relayUrls.join(",") : currentOrigin;
69 + return `curl -fsSL ${currentOrigin}/tunnel | PORT=${portVal} NAME=${nameVal} RELAY_URL="${relayUrlVal}" sh`;
70 + }, [currentOrigin, port, name, relayUrls]);
71
72 const handleCopy = async () => {
73 try {
@@ -118,6 +149,43 @@ export function TunnelCommandModal({ trigger }: TunnelCommandModalProps) {
149 </p>
150 </div>
151
152 + {/* Relay URLs Input */}
153 + <div className="space-y-2">
154 + <label className="text-sm font-medium text-foreground">
155 + Relay URLs
156 + </label>
157 + <div className="flex flex-wrap items-center gap-2 rounded-md border border-input bg-transparent p-2 min-h-10">
158 + {relayUrls.map((url) => (
159 + <span
160 + key={url}
161 + className="inline-flex items-center gap-1 rounded-md bg-secondary px-2 py-1 text-xs font-medium text-secondary-foreground"
162 + >
163 + {url}
164 + <button
165 + type="button"
166 + onClick={() => removeRelayUrl(url)}
167 + className="ml-1 rounded-sm hover:bg-destructive/20 p-0.5"
168 + aria-label={`Remove ${url}`}
169 + >
170 + <X className="h-3 w-3" />
171 + </button>
172 + </span>
173 + ))}
174 + <input
175 + ref={urlInputRef}
176 + type="text"
177 + value={urlInput}
178 + onChange={(e) => setUrlInput(e.target.value)}
179 + onKeyDown={handleUrlKeyDown}
180 + placeholder="Add relay URL..."
181 + className="min-w-[140px] flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
182 + />
183 + </div>
184 + <p className="text-xs text-text-muted">
185 + Press Enter to add. Multiple relay servers for redundancy.
186 + </p>
187 + </div>
188 +
189 {/* Generated Command */}
190 <div className="space-y-2">
191 <label className="text-sm font-medium text-foreground">