mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-06 07:38:46 +00:00
Add better loading spinner
This commit is contained in:
@@ -3433,5 +3433,24 @@
|
|||||||
"memberPortalNext": "Next",
|
"memberPortalNext": "Next",
|
||||||
"httpSettings": "HTTP Settings",
|
"httpSettings": "HTTP Settings",
|
||||||
"tcpSettings": "TCP 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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ import {
|
|||||||
CardDescription
|
CardDescription
|
||||||
} from "@app/components/ui/card";
|
} from "@app/components/ui/card";
|
||||||
import Link from "next/link";
|
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 { cn } from "@app/lib/cn";
|
||||||
import type { SignSshKeyResponse } from "@server/routers/ssh/types";
|
import type { SignSshKeyResponse } from "@server/routers/ssh/types";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
type AuthTab = "password" | "privateKey";
|
type AuthTab = "password" | "privateKey";
|
||||||
|
|
||||||
@@ -57,6 +59,8 @@ export default function SshClient({
|
|||||||
return { username: "", password: "", privateKey: "" };
|
return { username: "", password: "", privateKey: "" };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
const [authTab, setAuthTab] = useState<AuthTab>("password");
|
const [authTab, setAuthTab] = useState<AuthTab>("password");
|
||||||
|
|
||||||
function handleKeyFile(e: React.ChangeEvent<HTMLInputElement>) {
|
function handleKeyFile(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
@@ -184,7 +188,7 @@ export default function SshClient({
|
|||||||
setConnecting(true);
|
setConnecting(true);
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
setConnectError("No target specified");
|
setConnectError(t("sshErrorNoTarget"));
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -261,7 +265,7 @@ export default function SshClient({
|
|||||||
authErrorShown = true;
|
authErrorShown = true;
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
setConnectError(
|
setConnectError(
|
||||||
msg.error ?? "Authentication failed"
|
msg.error ?? t("sshErrorAuthFailed")
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
xtermRef.current?.writeln(
|
xtermRef.current?.writeln(
|
||||||
@@ -292,7 +296,7 @@ export default function SshClient({
|
|||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
setConnectError("WebSocket connection failed");
|
setConnectError(t("sshErrorWebSocket"));
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = (evt) => {
|
ws.onclose = (evt) => {
|
||||||
@@ -306,9 +310,7 @@ export default function SshClient({
|
|||||||
// If auth was never confirmed the login form is already visible;
|
// If auth was never confirmed the login form is already visible;
|
||||||
// a generic error is shown only when no specific error was received.
|
// a generic error is shown only when no specific error was received.
|
||||||
if (!authConfirmed && !authErrorShown) {
|
if (!authConfirmed && !authErrorShown) {
|
||||||
setConnectError(
|
setConnectError(t("sshErrorConnectionClosed"));
|
||||||
"Connection closed before authentication completed"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -325,14 +327,38 @@ export default function SshClient({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!connected && (
|
{!connected && (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center">
|
||||||
<p className="text-muted-foreground">
|
<Card className="w-full max-w-md">
|
||||||
{connectError
|
<CardHeader>
|
||||||
? connectError
|
<CardTitle>{t("sshTitle")}</CardTitle>
|
||||||
: connecting
|
<CardDescription>
|
||||||
? "Connecting…"
|
{t("sshConnectingDescription")}
|
||||||
: "Initializing…"}
|
</CardDescription>
|
||||||
</p>
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col items-center space-y-4">
|
||||||
|
{!connectError && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin" />
|
||||||
|
<span>
|
||||||
|
{connecting
|
||||||
|
? t("sshConnecting")
|
||||||
|
: t("sshInitializing")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{connectError && (
|
||||||
|
<Alert
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<AlertCircle className="h-5 w-5" />
|
||||||
|
<AlertDescription>
|
||||||
|
{connectError}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{connected && (
|
{connected && (
|
||||||
@@ -353,7 +379,7 @@ export default function SshClient({
|
|||||||
<div>
|
<div>
|
||||||
<div className="text-center mb-2">
|
<div className="text-center mb-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Powered by{" "}
|
{t("sshPoweredBy")}{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://pangolin.net/"
|
href="https://pangolin.net/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -366,7 +392,7 @@ export default function SshClient({
|
|||||||
</div>
|
</div>
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>SSH</CardTitle>
|
<CardTitle>{t("sshTitle")}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-destructive text-sm">{error}</p>
|
<p className="text-destructive text-sm">{error}</p>
|
||||||
@@ -382,7 +408,7 @@ export default function SshClient({
|
|||||||
<div>
|
<div>
|
||||||
<div className="text-center mb-2">
|
<div className="text-center mb-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Powered by{" "}
|
{t("sshPoweredBy")}{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://pangolin.net/"
|
href="https://pangolin.net/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -395,9 +421,9 @@ export default function SshClient({
|
|||||||
</div>
|
</div>
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Sign in to SSH</CardTitle>
|
<CardTitle>{t("sshSignInTitle")}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Enter credentials to access xxxx
|
{t("sshSignInDescription")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -417,8 +443,8 @@ export default function SshClient({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{tab === "password"
|
{tab === "password"
|
||||||
? "Password"
|
? t("sshPasswordTab")
|
||||||
: "Private Key"}
|
: t("sshPrivateKeyTab")}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -426,7 +452,10 @@ export default function SshClient({
|
|||||||
|
|
||||||
{authTab === "password" && (
|
{authTab === "password" && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Field label="Username" id="username-pw">
|
<Field
|
||||||
|
label={t("username")}
|
||||||
|
id="username-pw"
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
id="username-pw"
|
id="username-pw"
|
||||||
value={form.username}
|
value={form.username}
|
||||||
@@ -439,7 +468,7 @@ export default function SshClient({
|
|||||||
placeholder="root"
|
placeholder="root"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Password" id="password">
|
<Field label={t("password")} id="password">
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
@@ -458,22 +487,21 @@ export default function SshClient({
|
|||||||
{authTab === "privateKey" && (
|
{authTab === "privateKey" && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Your private key is not stored or
|
{t("sshPrivateKeyDisclaimer")}{" "}
|
||||||
visible to Pangolin. Alternatively, you
|
|
||||||
can use short-lived certificates for
|
|
||||||
seamless authentication using your
|
|
||||||
existing Pangolin identity.{" "}
|
|
||||||
<Link
|
<Link
|
||||||
href="https://docs.pangolin.net/"
|
href="https://docs.pangolin.net/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline inline-flex items-center gap-1"
|
className="underline inline-flex items-center gap-1"
|
||||||
>
|
>
|
||||||
Learn more
|
{t("sshLearnMore")}
|
||||||
<ExternalLink className="h-3 w-3" />
|
<ExternalLink className="h-3 w-3" />
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
<Field label="Username" id="username-key">
|
<Field
|
||||||
|
label={t("username")}
|
||||||
|
id="username-key"
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
id="username-key"
|
id="username-key"
|
||||||
value={form.username}
|
value={form.username}
|
||||||
@@ -486,7 +514,10 @@ export default function SshClient({
|
|||||||
placeholder="root"
|
placeholder="root"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Private Key" id="privateKey">
|
<Field
|
||||||
|
label={t("sshPrivateKeyField")}
|
||||||
|
id="privateKey"
|
||||||
|
>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="privateKey"
|
id="privateKey"
|
||||||
value={form.privateKey}
|
value={form.privateKey}
|
||||||
@@ -502,7 +533,7 @@ export default function SshClient({
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
label="Private Key File"
|
label={t("sshPrivateKeyFile")}
|
||||||
id="privateKeyFile"
|
id="privateKeyFile"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
@@ -534,8 +565,8 @@ export default function SshClient({
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{connecting
|
{connecting
|
||||||
? "Connecting..."
|
? t("sshConnecting")
|
||||||
: "Authenticate"}
|
: t("sshAuthenticate")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -551,7 +582,7 @@ export default function SshClient({
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={disconnect}
|
onClick={disconnect}
|
||||||
>
|
>
|
||||||
Terminate
|
{t("sshTerminate")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user