From e826d0dea6b27834db79b52315cb5c9e8cbefe92 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 3 Jun 2026 17:41:56 -0700 Subject: [PATCH] Add better loading spinner --- messages/en-US.json | 21 +++++++- src/app/ssh/SshClient.tsx | 103 +++++++++++++++++++++++++------------- 2 files changed, 87 insertions(+), 37 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index bee581c73..617dc9af2 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -3433,5 +3433,24 @@ "memberPortalNext": "Next", "httpSettings": "HTTP Settings", "tcpSettings": "TCP Settings", - "udpSettings": "UDP Settings" + "udpSettings": "UDP Settings", + "sshTitle": "SSH", + "sshConnectingDescription": "Establishing a secure connection…", + "sshConnecting": "Connecting…", + "sshInitializing": "Initializing…", + "sshSignInTitle": "Sign in to SSH", + "sshSignInDescription": "Enter your SSH credentials", + "sshPasswordTab": "Password", + "sshPrivateKeyTab": "Private Key", + "sshPrivateKeyField": "Private Key", + "sshPrivateKeyDisclaimer": "Your private key is not stored or visible to Pangolin. Alternatively, you can use short-lived certificates for seamless authentication using your existing Pangolin identity.", + "sshLearnMore": "Learn more", + "sshPrivateKeyFile": "Private Key File", + "sshAuthenticate": "Authenticate", + "sshTerminate": "Terminate", + "sshPoweredBy": "Powered by", + "sshErrorNoTarget": "No target specified", + "sshErrorWebSocket": "WebSocket connection failed", + "sshErrorAuthFailed": "Authentication failed", + "sshErrorConnectionClosed": "Connection closed before authentication completed" } diff --git a/src/app/ssh/SshClient.tsx b/src/app/ssh/SshClient.tsx index 3fb2e82e3..bf899887f 100644 --- a/src/app/ssh/SshClient.tsx +++ b/src/app/ssh/SshClient.tsx @@ -15,9 +15,11 @@ import { CardDescription } from "@app/components/ui/card"; import Link from "next/link"; -import { ExternalLink } from "lucide-react"; +import { ExternalLink, Loader2, AlertCircle } from "lucide-react"; +import { Alert, AlertDescription } from "@/components/ui/alert"; import { cn } from "@app/lib/cn"; import type { SignSshKeyResponse } from "@server/routers/ssh/types"; +import { useTranslations } from "next-intl"; type AuthTab = "password" | "privateKey"; @@ -57,6 +59,8 @@ export default function SshClient({ return { username: "", password: "", privateKey: "" }; }); + const t = useTranslations(); + const [authTab, setAuthTab] = useState("password"); function handleKeyFile(e: React.ChangeEvent) { @@ -184,7 +188,7 @@ export default function SshClient({ setConnecting(true); if (!target) { - setConnectError("No target specified"); + setConnectError(t("sshErrorNoTarget")); setConnecting(false); return; } @@ -261,7 +265,7 @@ export default function SshClient({ authErrorShown = true; setConnecting(false); setConnectError( - msg.error ?? "Authentication failed" + msg.error ?? t("sshErrorAuthFailed") ); } else { xtermRef.current?.writeln( @@ -292,7 +296,7 @@ export default function SshClient({ ws.onerror = () => { setConnecting(false); setConnected(false); - setConnectError("WebSocket connection failed"); + setConnectError(t("sshErrorWebSocket")); }; ws.onclose = (evt) => { @@ -306,9 +310,7 @@ export default function SshClient({ // If auth was never confirmed the login form is already visible; // a generic error is shown only when no specific error was received. if (!authConfirmed && !authErrorShown) { - setConnectError( - "Connection closed before authentication completed" - ); + setConnectError(t("sshErrorConnectionClosed")); } }; } @@ -325,14 +327,38 @@ export default function SshClient({ return ( <> {!connected && ( -
-

- {connectError - ? connectError - : connecting - ? "Connecting…" - : "Initializing…"} -

+
+ + + {t("sshTitle")} + + {t("sshConnectingDescription")} + + + + {!connectError && ( +
+ + + {connecting + ? t("sshConnecting") + : t("sshInitializing")} + +
+ )} + {connectError && ( + + + + {connectError} + + + )} +
+
)} {connected && ( @@ -353,7 +379,7 @@ export default function SshClient({
- Powered by{" "} + {t("sshPoweredBy")}{" "} - SSH + {t("sshTitle")}

{error}

@@ -382,7 +408,7 @@ export default function SshClient({
- Powered by{" "} + {t("sshPoweredBy")}{" "} - Sign in to SSH + {t("sshSignInTitle")} - Enter credentials to access xxxx + {t("sshSignInDescription")} @@ -417,8 +443,8 @@ export default function SshClient({ )} > {tab === "password" - ? "Password" - : "Private Key"} + ? t("sshPasswordTab") + : t("sshPrivateKeyTab")} ) )} @@ -426,7 +452,10 @@ export default function SshClient({ {authTab === "password" && (
- + - +

- Your private key is not stored or - visible to Pangolin. Alternatively, you - can use short-lived certificates for - seamless authentication using your - existing Pangolin identity.{" "} + {t("sshPrivateKeyDisclaimer")}{" "} - Learn more + {t("sshLearnMore")}

- + - +