Replace tab component

This commit is contained in:
Owen
2026-06-04 16:21:30 -07:00
parent 769d36e289
commit 6420a90d08
2 changed files with 97 additions and 68 deletions

View File

@@ -17,7 +17,7 @@ import {
import Link from "next/link";
import { ExternalLink, Loader2, AlertCircle } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { cn } from "@app/lib/cn";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import type { SignSshKeyResponse } from "@server/routers/ssh/types";
import { useTranslations } from "next-intl";
@@ -61,8 +61,6 @@ export default function SshClient({
const t = useTranslations();
const [authTab, setAuthTab] = useState<AuthTab>("password");
function handleKeyFile(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
@@ -182,7 +180,10 @@ export default function SshClient({
}
}, []);
function connect(override?: ConnectCredentials) {
function connect(
override?: ConnectCredentials,
authMethod: AuthTab = "password"
) {
setConnectError(null);
setConnecting(true);
@@ -194,10 +195,11 @@ export default function SshClient({
const username = override?.username ?? form.username;
const password =
override?.password ?? (authTab === "password" ? form.password : "");
override?.password ??
(authMethod === "password" ? form.password : "");
const privateKey =
override?.privateKey ??
(authTab === "privateKey" ? form.privateKey : "");
(authMethod === "privateKey" ? form.privateKey : "");
const certificate = override?.certificate;
const proxyAddress = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/gateway/ssh`;
@@ -224,7 +226,7 @@ export default function SshClient({
ws.onopen = () => {
// Send credentials as the first frame so the proxy can complete
// SSH authentication before piping pty data. Stay in "connecting"
// state until the server responds this prevents the flash to the
// state until the server responds - this prevents the flash to the
// terminal page that would occur if we set connected=true here.
ws.send(
JSON.stringify({
@@ -260,7 +262,7 @@ export default function SshClient({
xtermRef.current?.write(msg.data);
} else if (msg.type === "error") {
if (!authConfirmed) {
// Auth-phase error show in the login form.
// Auth-phase error - show in the login form.
authErrorShown = true;
setConnecting(false);
setConnectError(
@@ -281,13 +283,13 @@ export default function SshClient({
xtermRef.current?.write(evt.data);
}
} else if (evt.data instanceof Blob) {
evt.data.text().then((t) => {
evt.data.text().then((text) => {
if (!authConfirmed) {
authConfirmed = true;
setConnecting(false);
setConnected(true);
}
xtermRef.current?.write(t);
xtermRef.current?.write(text);
});
}
};
@@ -426,31 +428,15 @@ export default function SshClient({
</CardDescription>
</CardHeader>
<CardContent>
{/* Tab row */}
<div className="flex space-x-4 border-b mb-4">
{(["password", "privateKey"] as const).map(
(tab) => (
<button
key={tab}
type="button"
onClick={() => setAuthTab(tab)}
className={cn(
"px-4 py-2 text-sm font-medium transition-colors whitespace-nowrap relative",
authTab === tab
? "text-primary after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-primary after:rounded-full"
: "text-muted-foreground hover:text-foreground"
)}
>
{tab === "password"
? t("sshPasswordTab")
: t("sshPrivateKeyTab")}
</button>
)
)}
</div>
{authTab === "password" && (
<div className="space-y-4">
<HorizontalTabs
clientSide
defaultTab={0}
items={[
{ title: t("sshPasswordTab"), href: "#" },
{ title: t("sshPrivateKeyTab"), href: "#" }
]}
>
<div className="space-y-4 mt-4 p-1">
<Field
label={t("username")}
id="username-pw"
@@ -464,7 +450,6 @@ export default function SshClient({
username: e.target.value
})
}
placeholder="root"
/>
</Field>
<Field label={t("password")} id="password">
@@ -480,11 +465,31 @@ export default function SshClient({
}
/>
</Field>
</div>
)}
<div className="mt-4 space-y-3">
{connectError && (
<p className="text-destructive text-sm">
{connectError}
</p>
)}
{authTab === "privateKey" && (
<div className="space-y-4">
<Button
onClick={() =>
connect(undefined, "password")
}
loading={connecting}
disabled={
!form.username || !form.password
}
className="w-full"
>
{connecting
? t("sshConnecting")
: t("sshAuthenticate")}
</Button>
</div>
</div>
<div className="space-y-4 mt-4 p-1">
<p className="text-sm text-muted-foreground">
{t("sshPrivateKeyDisclaimer")}{" "}
<Link
@@ -510,7 +515,6 @@ export default function SshClient({
username: e.target.value
})
}
placeholder="root"
/>
</Field>
<Field
@@ -542,32 +546,31 @@ export default function SshClient({
onChange={handleKeyFile}
/>
</Field>
<div className="mt-4 space-y-3">
{connectError && (
<p className="text-destructive text-sm">
{connectError}
</p>
)}
<Button
onClick={() =>
connect(undefined, "privateKey")
}
loading={connecting}
disabled={
!form.username ||
!form.privateKey
}
className="w-full"
>
{connecting
? t("sshConnecting")
: t("sshAuthenticate")}
</Button>
</div>
</div>
)}
<div className="mt-4 space-y-3">
{connectError && (
<p className="text-destructive text-sm">
{connectError}
</p>
)}
<Button
onClick={() => connect()}
loading={connecting}
disabled={
!form.username ||
(authTab === "password"
? !form.password
: !form.privateKey)
}
className="w-full"
>
{connecting
? t("sshConnecting")
: t("sshAuthenticate")}
</Button>
</div>
</HorizontalTabs>
</CardContent>
</Card>
</div>

View File

@@ -43,11 +43,14 @@ export function NewtSiteInstallCommands({
const t = useTranslations();
const [acceptClients, setAcceptClients] = useState(true);
const [allowPangolinSsh, setAllowPangolinSsh] = useState(true);
const [platform, setPlatform] = useState<Platform>("linux");
const [architecture, setArchitecture] = useState(
() => getArchitectures(platform)[0]
);
const supportsSshOption = platform === "linux" || platform === "nixos";
const acceptClientsFlag = !acceptClients ? " --disable-clients" : "";
const acceptClientsEnv = !acceptClients
? "\n - DISABLE_CLIENTS=true"
@@ -57,6 +60,11 @@ export function NewtSiteInstallCommands({
--set newtInstances[0].acceptClients=true`
: "";
const disableSshFlag =
supportsSshOption && !allowPangolinSsh ? " --disable-ssh" : "";
const runAsRootPrefix =
supportsSshOption && allowPangolinSsh ? "sudo " : "";
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
linux: {
Run: [
@@ -66,7 +74,7 @@ export function NewtSiteInstallCommands({
},
{
title: t("run"),
command: `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
command: `${runAsRootPrefix}newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}${disableSshFlag}`
}
],
"Systemd Service": [
@@ -86,6 +94,11 @@ PANGOLIN_ENDPOINT=${endpoint}${
? `
DISABLE_CLIENTS=true`
: ""
}${
!allowPangolinSsh
? `
DISABLE_SSH=true`
: ""
}
EOF
sudo chmod 600 /etc/newt/newt.env`
@@ -205,7 +218,7 @@ WantedBy=default.target`
},
nixos: {
Flake: [
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
`${runAsRootPrefix}nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}${disableSshFlag}`
]
}
};
@@ -273,6 +286,19 @@ WantedBy=default.target`
label={t("siteAcceptClientConnections")}
/>
</div>
{supportsSshOption && (
<div className="flex items-center space-x-2 mb-2">
<CheckboxWithLabel
id="allowPangolinSsh"
checked={allowPangolinSsh}
onCheckedChange={(checked) => {
const value = checked as boolean;
setAllowPangolinSsh(value);
}}
label="Allow Pangolin SSH"
/>
</div>
)}
<p
id="acceptClients-desc"
className="text-sm text-muted-foreground"