Files
Netcatty/components/ProtocolSelectDialog.tsx
lateautumn233 7927da2085 Build ET UI: host settings panel, protocol picker, and session starters
Add ET configuration section in HostDetailsAdvancedSections (etPort,
etTerminalPath). Add ET option to ProtocolSelectDialog. Wire ET session
creation in createTerminalSessionStarters with proxy and multi-jump
validation. Add ET badges and labels in Terminal, TerminalToolbar,
TerminalConnectionDialog, and TerminalLayerSupport. Propagate ET
settings through GroupDetailsPanel, GroupSshSettingsSection, and
VaultView.
2026-06-03 01:45:29 +08:00

224 lines
9.4 KiB
TypeScript

import {
Globe,
Terminal as TerminalIcon,
Wifi
} from 'lucide-react';
import React,{ useMemo,useState } from 'react';
import { useI18n } from '../application/i18n/I18nProvider';
import { cn } from '../lib/utils';
import { Host,HostProtocol } from '../types';
import { DistroAvatar } from './DistroAvatar';
import { Button } from './ui/button';
import { Input } from './ui/input';
interface ProtocolOption {
protocol: HostProtocol;
port: number;
label: string;
icon: React.ReactNode;
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();
// Build protocol options from host configuration
const protocolOptions = useMemo<ProtocolOption[]>(() => {
const options: ProtocolOption[] = [];
// SSH (always available if not explicitly disabled)
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',
icon: <TerminalIcon size={18} />,
description: `ssh ${host.hostname}`,
enabled: true,
});
}
// Mosh (if enabled)
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',
icon: <Wifi size={18} />,
description: `mosh ${host.hostname}`,
enabled: true,
});
}
// EternalTerminal (if enabled)
if (host.etEnabled || host.protocols?.some(p => p.protocol === 'et' && p.enabled)) {
options.push({
protocol: 'et',
port: host.port || 22,
label: 'EternalTerminal',
icon: <Wifi size={18} />,
description: `et ${host.hostname}`,
enabled: true,
});
}
// Telnet (if enabled)
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',
icon: <Globe size={18} />,
description: `telnet ${host.hostname}`,
enabled: true,
});
}
return options;
}, [host]);
// State for custom ports (allow editing inline)
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);
};
// If only one protocol, auto-select and proceed
React.useEffect(() => {
if (protocolOptions.length === 1) {
// Don't auto-proceed, let user confirm
}
}, [protocolOptions]);
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()}>
{/* Header */}
<div className="px-6 py-5 border-b border-border/50">
<div className="flex items-center gap-3">
<DistroAvatar
host={host}
fallback={host.label.slice(0, 2).toUpperCase()}
className="h-12 w-12"
/>
<div>
<h2 className="text-base font-semibold">{host.label}</h2>
<p className="text-xs text-muted-foreground font-mono">
{host.hostname}
</p>
</div>
</div>
</div>
{/* Progress indicator */}
<div className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-full bg-primary/20 text-primary flex items-center justify-center">
<TerminalIcon size={14} />
</div>
<div className="flex-1 h-0.5 bg-muted" />
<div className="h-8 w-8 rounded-full bg-muted text-muted-foreground flex items-center justify-center text-xs font-mono">
{'>_'}
</div>
</div>
</div>
{/* Protocol selection */}
<div className="px-6 py-4 space-y-4">
<h3 className="text-base font-semibold">{t("protocolSelect.chooseProtocol")}</h3>
<div className="space-y-3">
{protocolOptions.map((option) => (
<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
? "bg-primary/20 text-primary"
: "bg-muted text-muted-foreground"
)}>
{option.icon}
</div>
<div>
<div className="font-medium">{option.label}</div>
<div className="text-xs text-muted-foreground font-mono">
{option.description}
</div>
</div>
</div>
<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>
{/* Footer */}
<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;