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">