Files
NetMesh/components/ProtocolSelectDialog.tsx
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

230 lines
8.1 KiB
TypeScript

import { Plug } from "lucide-react";
import React, { useMemo, useState } from "react";
import { useI18n } from "../application/i18n/I18nProvider";
import { formatHostPort } from "../domain/host";
import { isPluginHostProtocol } from "../domain/pluginConnection";
import { cn } from "../lib/utils";
import { Host, HostProtocol } from "../types";
import { getProtocolVisualStyle, type ProtocolVisualKey } from "./protocolVisuals";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
interface ProtocolOption {
protocol: HostProtocol;
port: number;
label: string;
description: string;
enabled: boolean;
}
interface ProtocolSelectDialogProps {
host: Host;
onSelect: (protocol: HostProtocol, port: number) => void;
onCancel: () => void;
}
const ProtocolSelectDialog: React.FC<ProtocolSelectDialogProps> = ({
host,
onSelect,
onCancel,
}) => {
const { t } = useI18n();
const protocolOptions = useMemo<ProtocolOption[]>(() => {
const options: ProtocolOption[] = [];
const pluginOption = isPluginHostProtocol(host.protocol) && host.pluginConnection
? {
protocol: host.protocol,
port: host.port || 22,
label: host.pluginConnection.providerId,
description: host.pluginConnection.providerId,
enabled: true,
} satisfies ProtocolOption
: null;
if (pluginOption) options.push(pluginOption);
const sshEnabled =
host.protocol === "ssh" ||
!host.protocol ||
host.protocols?.some((p) => p.protocol === "ssh" && p.enabled);
if (sshEnabled !== false) {
const sshConfig = host.protocols?.find((p) => p.protocol === "ssh");
options.push({
protocol: "ssh",
port: sshConfig?.port || host.port || 22,
label: "SSH",
description: `ssh ${host.hostname}`,
enabled: true,
});
}
if (host.moshEnabled || host.protocols?.some((p) => p.protocol === "mosh" && p.enabled)) {
const moshConfig = host.protocols?.find((p) => p.protocol === "mosh");
options.push({
protocol: "mosh",
port: moshConfig?.port || host.port || 22,
label: "Mosh",
description: `mosh ${host.hostname}`,
enabled: true,
});
}
if (host.etEnabled || host.protocols?.some((p) => p.protocol === "et" && p.enabled)) {
options.push({
protocol: "et",
port: host.port || 22,
label: "EternalTerminal",
description: `et ${host.hostname}`,
enabled: true,
});
}
if (
host.telnetEnabled ||
host.protocol === "telnet" ||
host.protocols?.some((p) => p.protocol === "telnet" && p.enabled)
) {
const telnetConfig = host.protocols?.find((p) => p.protocol === "telnet");
options.push({
protocol: "telnet",
port: telnetConfig?.port || host.telnetPort || 23,
label: "Telnet",
description: `telnet ${host.hostname}`,
enabled: true,
});
}
return options;
}, [host]);
const [ports, setPorts] = useState<Record<HostProtocol, number>>(() => {
const initial: Record<string, number> = {};
protocolOptions.forEach((opt) => {
initial[opt.protocol] = opt.port;
});
return initial as Record<HostProtocol, number>;
});
const [selectedProtocol, setSelectedProtocol] = useState<HostProtocol>(
protocolOptions[0]?.protocol || "ssh",
);
const handlePortChange = (protocol: HostProtocol, value: string) => {
const port = parseInt(value, 10);
if (!isNaN(port) && port > 0 && port <= 65535) {
setPorts((prev) => ({ ...prev, [protocol]: port }));
}
};
const handleContinue = () => {
onSelect(selectedProtocol, ports[selectedProtocol] || 22);
};
const hostEndpoint = isPluginHostProtocol(selectedProtocol)
? host.hostname
: formatHostPort(
host.hostname,
ports[selectedProtocol] || host.port || 22,
);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center" onClick={onCancel}>
<div
className="w-[560px] max-w-[90vw] bg-background border border-border rounded-2xl animate-in fade-in-0 zoom-in-95 duration-200"
style={{
boxShadow:
"0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 12px 24px -8px rgba(0, 0, 0, 0.15)",
}}
onClick={(e) => e.stopPropagation()}
>
<div className="px-6 py-4 border-b border-border/50">
<div className="flex items-center gap-2 min-w-0">
<Plug size={18} className="shrink-0 text-foreground" />
<div className="min-w-0">
<h2 className="text-base font-semibold truncate">
{t("quickConnect.connectTitle", {
host: host.label?.trim() || host.hostname,
})}
</h2>
<p className="text-xs text-muted-foreground font-mono truncate">{hostEndpoint}</p>
</div>
</div>
</div>
<div className="px-6 py-4">
<div className="space-y-4">
<h3 className="text-sm font-medium">{t("protocolSelect.chooseProtocol")}</h3>
<div className="space-y-3">
{protocolOptions.map((option) => {
const visual = isPluginHostProtocol(option.protocol)
? {
icon: <Plug size={18} />,
selected: "bg-primary text-primary-foreground",
idle: "bg-secondary text-muted-foreground",
}
: getProtocolVisualStyle(option.protocol as ProtocolVisualKey);
return (
<button
key={option.protocol}
className={cn(
"w-full flex items-center justify-between px-4 py-3 rounded-xl border-2 transition-all text-left",
selectedProtocol === option.protocol
? "border-primary bg-primary/5"
: "border-border/60 hover:border-border hover:bg-secondary/50",
)}
onClick={() => setSelectedProtocol(option.protocol)}
>
<div className="flex items-center gap-3">
<div
className={cn(
"h-10 w-10 rounded-lg flex items-center justify-center",
selectedProtocol === option.protocol ? visual.selected : visual.idle,
)}
>
{visual.icon}
</div>
<div>
<div className="font-medium">{option.label}</div>
<div className="text-xs text-muted-foreground font-mono">
{option.description}
</div>
</div>
</div>
{!isPluginHostProtocol(option.protocol) && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{t("protocolSelect.port")}</span>
<Input
type="number"
value={ports[option.protocol] || option.port}
onChange={(e) => handlePortChange(option.protocol, e.target.value)}
onClick={(e) => {
e.stopPropagation();
setSelectedProtocol(option.protocol);
}}
className="w-16 h-7 text-xs text-center"
min={1}
max={65535}
/>
</div>
)}
</button>
);
})}
</div>
</div>
</div>
<div className="px-6 py-4 border-t border-border/50 flex items-center justify-between">
<Button variant="secondary" onClick={onCancel}>
{t("common.close")}
</Button>
<Button onClick={handleContinue}>{t("common.continue")}</Button>
</div>
</div>
</div>
);
};
export default ProtocolSelectDialog;