mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-09-24 15:49:09 +00:00
c3224d30c6
* feat: add deviceType field to Host model for network device support Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: pass deviceType through session metadata pipeline Thread deviceType from Host model through AITerminalSessionInfo, IPC types, and mcpServerBridge so AI agents can inspect device type per session. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: route network device SSH sessions to raw PTY execution When deviceType === 'network', handleExec now uses execViaRawPty instead of execViaPty so vendor CLIs (Huawei VRP, Cisco IOS, etc.) receive commands as-is without POSIX shell wrapping or markers. The command blocklist is also skipped for network devices, consistent with the existing serial session bypass. AI context description updated to document the raw-execution behaviour for network device sessions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add network device mode toggle to host settings UI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add network device awareness to Catty Agent system prompt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: extend network device mode to Catty Agent exec path and host context - Add network device detection and raw execution routing to aiBridge.cjs (the primary Catty Agent command path), not just the MCP bridge - Export getSessionMeta from mcpServerBridge for reuse in aiBridge - Surface deviceType in Catty Agent system prompt host list so the AI can identify which sessions are network devices - Pass deviceType through buildSystemPrompt context Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: exempt network device sessions from client-side blocklist and update ACP context - Add deviceType to ExecutorContext sessions type - Skip renderer-side command blocklist for deviceType=network sessions in shared toolExecutors.ts (not just main-process side) - Update ACP agent context hint to mention network device sessions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: only show network device mode toggle for SSH hosts Telnet and local hosts don't support the network device execution path, so hiding the toggle prevents users from enabling a broken configuration. Serial hosts already use raw mode by default. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: exclude Mosh sessions from network device raw execution path Mosh uses a shell-backed PTY and cannot connect to vendor CLIs, so network device mode should only apply to SSH and serial sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prefer session.protocol over metadata for Mosh detection Mosh tabs report protocol:"ssh" in renderer metadata but "mosh" in the main-process session object. Prioritize session.protocol (runtime truth) to correctly exclude Mosh from network device raw execution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: suppress deviceType metadata for Mosh sessions Mosh requires a shell-backed PTY and cannot connect to vendor CLIs, so omit deviceType from AI-facing metadata when session is Mosh-backed. This prevents the AI from being told to use vendor CLI syntax when the actual execution path uses normal shell wrapping. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use exit code 0 for network device sessions and hide toggle for Mosh - Network device / serial sessions return exitCode: null from vendor CLIs. Default to 0 instead of -1 so the AI doesn't misinterpret successful commands as failures. - Hide the network device mode toggle when Mosh is enabled, since the setting is suppressed at runtime for Mosh sessions anyway. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: preserve null exit codes and restrict raw mode to SSH/serial - Preserve exitCode: null for network device sessions instead of coercing to 0, so the AI knows exit status is unavailable rather than seeing a misleading success code. - Explicitly whitelist SSH/serial protocols for network device mode instead of just excluding mosh, preventing local/telnet sessions from accidentally entering raw execution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use UTF-8 encoding for SSH network device raw execution execViaRawPty hardcodes latin1 for serial port data decoding. Add an encoding option (default: latin1) and pass utf8 from SSH network device call sites so multi-byte characters aren't corrupted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use host charset for serial port decoding instead of hardcoded latin1 - Extract charsetToNodeEncoding() to module scope in terminalBridge - Serial sessions now read options.charset (from Host.charset) for both terminal display decoding and AI command output - Store serialEncoding on session object so exec paths can use it - Pass encoding through all execViaRawPty call sites - Default encoding changed from latin1 to utf8 (matches most modern network equipment and is the safer default for CJK environments) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move serialEncoding declaration before session object creation serialEncoding was referenced in the session object literal before its const declaration, causing a TDZ ReferenceError that would crash every serial connection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: tighten isNetworkDevice logic and clean up edge cases - Align toolExecutors isNetworkDevice check with bridge logic: require explicit SSH/serial protocol match instead of trusting deviceType alone - Remove empty-string protocol match from isSshOrSerial in both bridges to prevent local/unknown sessions from being treated as network devices - Widen exitCode return type to `number | null` to match actual behavior - Clear deviceType when enabling Mosh (incompatible combination) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update MCP server tool descriptions for network device sessions The get_environment and terminal_execute tool descriptions only mentioned serial/raw sessions for network devices. Updated to also reference deviceType: network SSH sessions so external AI agents (Claude, Codex) know about the new execution mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: include deviceType in get_environment and guard execViaChannel fallback - Add deviceType to executeWorkspaceGetInfo session mapping and return type so Catty Agent's get_environment tool matches MCP bridge output - Guard both aiBridge and mcpServerBridge against falling through to execViaChannel for network device sessions — network devices require an interactive PTY and exec channels would produce broken behavior Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add charset setting to serial host configuration UI Serial hosts now have a charset input in the Advanced section, defaulting to UTF-8. The value is saved to Host.charset and used by the serial decoder in terminalBridge. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add charset to serial quick-connect modal with full pipeline - Add charset input to SerialConnectModal (Advanced section) - Thread charset through onConnect callback → handleConnectSerial → createSerialSession → TerminalSession.charset - Add charset field to TerminalSession interface - Include charset in fallback host builder for quick-connect sessions so createTerminalSessionStarters can pass it to startSerialSession - Saved hosts also store charset via onSaveHost Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: constrain serial connect modal height with scrollable content Modal content could overflow the viewport when Advanced section was expanded. Add max-h-[85vh] to DialogContent with flex layout so the content area scrolls while header and footer buttons stay visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: propagate charset through all serial session creation paths - Add charset to startSerialSession type in global.d.ts - Copy host.charset to TerminalSession in connectToHost serial path - Copy host.charset in createWorkspaceWithHosts serial path - Propagate session.charset in splitSession (both workspace and standalone) - Propagate session.charset in copySession Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: propagate charset in remaining session creation paths Add host.charset to connectToHost (non-serial), createWorkspaceWithHosts (non-serial), and runSnippet session creation for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
445 lines
16 KiB
TypeScript
445 lines
16 KiB
TypeScript
/**
|
|
* Serial Port Connect Modal
|
|
* Allows users to configure and connect to a serial port
|
|
*/
|
|
import { ChevronDown, ChevronUp, Cpu, RefreshCw, Save, Usb } from 'lucide-react';
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { useI18n } from '../application/i18n/I18nProvider';
|
|
import { useTerminalBackend } from '../application/state/useTerminalBackend';
|
|
import type { Host, SerialConfig, SerialFlowControl, SerialParity } from '../domain/models';
|
|
import { cn } from '../lib/utils';
|
|
import { Button } from './ui/button';
|
|
import { Combobox, type ComboboxOption } from './ui/combobox';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from './ui/dialog';
|
|
import { Input } from './ui/input';
|
|
import { Label } from './ui/label';
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
|
|
|
|
interface SerialPort {
|
|
path: string;
|
|
manufacturer: string;
|
|
serialNumber: string;
|
|
vendorId: string;
|
|
productId: string;
|
|
pnpId: string;
|
|
type?: 'hardware' | 'pseudo' | 'custom';
|
|
}
|
|
|
|
interface SerialConnectModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onConnect: (config: SerialConfig, options?: { charset?: string }) => void;
|
|
onSaveHost?: (host: Host) => void;
|
|
}
|
|
|
|
const BAUD_RATES = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600];
|
|
const DATA_BITS: Array<5 | 6 | 7 | 8> = [5, 6, 7, 8];
|
|
const STOP_BITS: Array<1 | 1.5 | 2> = [1, 1.5, 2];
|
|
const PARITY_OPTIONS: SerialParity[] = ['none', 'even', 'odd', 'mark', 'space'];
|
|
const FLOW_CONTROL_OPTIONS: SerialFlowControl[] = ['none', 'xon/xoff', 'rts/cts'];
|
|
|
|
export const SerialConnectModal: React.FC<SerialConnectModalProps> = ({
|
|
open,
|
|
onClose,
|
|
onConnect,
|
|
onSaveHost,
|
|
}) => {
|
|
const { t } = useI18n();
|
|
const [ports, setPorts] = useState<SerialPort[]>([]);
|
|
const [isLoadingPorts, setIsLoadingPorts] = useState(false);
|
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
|
|
// Form state
|
|
const [selectedPort, setSelectedPort] = useState('');
|
|
const [baudRate, setBaudRate] = useState(115200);
|
|
const [dataBits, setDataBits] = useState<5 | 6 | 7 | 8>(8);
|
|
const [stopBits, setStopBits] = useState<1 | 1.5 | 2>(1);
|
|
const [parity, setParity] = useState<SerialParity>('none');
|
|
const [flowControl, setFlowControl] = useState<SerialFlowControl>('none');
|
|
const [localEcho, setLocalEcho] = useState(false);
|
|
const [lineMode, setLineMode] = useState(false);
|
|
const [charset, setCharset] = useState('UTF-8');
|
|
|
|
// Save configuration state
|
|
const [saveConfig, setSaveConfig] = useState(false);
|
|
const [configLabel, setConfigLabel] = useState('');
|
|
|
|
const terminalBackend = useTerminalBackend();
|
|
|
|
const loadPorts = useCallback(async () => {
|
|
setIsLoadingPorts(true);
|
|
try {
|
|
const result = await terminalBackend.listSerialPorts();
|
|
setPorts(result);
|
|
// Auto-select first port if available and no port is selected
|
|
if (result.length > 0) {
|
|
setSelectedPort((prev) => prev || result[0].path);
|
|
}
|
|
} catch (err) {
|
|
console.error('[Serial] Failed to list ports:', err);
|
|
} finally {
|
|
setIsLoadingPorts(false);
|
|
}
|
|
}, [terminalBackend]);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
loadPorts();
|
|
}
|
|
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
// Generate a default label when port is selected
|
|
useEffect(() => {
|
|
if (selectedPort && !configLabel) {
|
|
const portName = selectedPort.split('/').pop() || selectedPort;
|
|
setConfigLabel(`Serial: ${portName}`);
|
|
}
|
|
}, [selectedPort, configLabel]);
|
|
|
|
const handleConnect = () => {
|
|
if (!selectedPort) return;
|
|
|
|
const config: SerialConfig = {
|
|
path: selectedPort,
|
|
baudRate,
|
|
dataBits,
|
|
stopBits,
|
|
parity,
|
|
flowControl,
|
|
localEcho,
|
|
lineMode,
|
|
};
|
|
|
|
// Save as host if checkbox is checked and onSaveHost is provided
|
|
if (saveConfig && onSaveHost) {
|
|
const portName = selectedPort.split('/').pop() || selectedPort;
|
|
const host: Host = {
|
|
id: `serial-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
label: configLabel.trim() || `Serial: ${portName}`,
|
|
hostname: selectedPort,
|
|
// For serial hosts, port field stores baud rate as a numeric identifier.
|
|
// The full configuration is stored in serialConfig for actual connection.
|
|
port: baudRate,
|
|
username: '',
|
|
os: 'linux',
|
|
tags: ['serial'],
|
|
protocol: 'serial',
|
|
createdAt: Date.now(),
|
|
charset,
|
|
serialConfig: config, // Store full serial configuration for connection
|
|
};
|
|
onSaveHost(host);
|
|
}
|
|
|
|
onConnect(config, { charset });
|
|
onClose();
|
|
};
|
|
|
|
// Convert ports to Combobox options
|
|
const portOptions: ComboboxOption[] = useMemo(() => {
|
|
return ports.map((port) => ({
|
|
value: port.path,
|
|
label: port.path,
|
|
sublabel: port.manufacturer || undefined,
|
|
}));
|
|
}, [ports]);
|
|
|
|
// Validate: port path must start with /dev/ (Unix/macOS) or COM/\\.\COM (Windows)
|
|
const trimmedPort = selectedPort.trim();
|
|
const isPortValid =
|
|
trimmedPort.startsWith('/dev/') ||
|
|
/^COM\d+$/i.test(trimmedPort) ||
|
|
/^\\\\\.\\COM\d+$/i.test(trimmedPort);
|
|
// Allow custom baud rates as long as they are positive integers
|
|
const isBaudRateValid = Number.isInteger(baudRate) && baudRate > 0;
|
|
|
|
// Check if using 1.5 stop bits (limited Windows support)
|
|
const isStopBits15 = stopBits === 1.5;
|
|
const isValid = isPortValid && isBaudRateValid;
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
|
<DialogContent className="max-w-md max-h-[85vh] flex flex-col">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Usb size={18} />
|
|
{t('serial.modal.title')}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{t('serial.modal.desc')}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-2 overflow-y-auto flex-1 min-h-0">
|
|
{/* Serial Port Selection */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="serial-port">{t('serial.field.port')}</Label>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={loadPorts}
|
|
disabled={isLoadingPorts}
|
|
className="h-6 px-2 text-xs"
|
|
>
|
|
<RefreshCw size={12} className={cn("mr-1", isLoadingPorts && "animate-spin")} />
|
|
{t('common.refresh')}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Combobox for port selection with manual input support */}
|
|
<Combobox
|
|
options={portOptions}
|
|
value={selectedPort}
|
|
onValueChange={setSelectedPort}
|
|
placeholder={t('serial.field.selectPort')}
|
|
emptyText={t('serial.noPorts')}
|
|
allowCreate
|
|
createText={t('common.use')}
|
|
icon={<Usb size={14} className="text-muted-foreground" />}
|
|
/>
|
|
|
|
{!isPortValid && selectedPort && (
|
|
<p className="text-xs text-destructive">
|
|
{t('serial.field.customPortPlaceholder')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Baud Rate */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="baud-rate">{t('serial.field.baudRate')}</Label>
|
|
<Combobox
|
|
options={BAUD_RATES.map((rate) => ({
|
|
value: String(rate),
|
|
label: String(rate),
|
|
}))}
|
|
value={String(baudRate)}
|
|
onValueChange={(val) => {
|
|
const parsed = parseInt(val, 10);
|
|
if (!isNaN(parsed) && parsed > 0) {
|
|
setBaudRate(parsed);
|
|
}
|
|
}}
|
|
placeholder={t('serial.field.baudRatePlaceholder')}
|
|
emptyText={t('serial.field.baudRateEmpty')}
|
|
allowCreate
|
|
createText={t('common.use')}
|
|
/>
|
|
{baudRate > 0 && !BAUD_RATES.includes(baudRate) && (
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('serial.field.customBaudRate')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Advanced Options */}
|
|
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
|
|
<CollapsibleTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className="w-full justify-between h-9 px-0 hover:bg-transparent"
|
|
>
|
|
<span className="text-sm font-medium text-muted-foreground">
|
|
{t('common.advanced')}
|
|
</span>
|
|
{showAdvanced ? (
|
|
<ChevronUp size={14} className="text-muted-foreground" />
|
|
) : (
|
|
<ChevronDown size={14} className="text-muted-foreground" />
|
|
)}
|
|
</Button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent className="space-y-4 pt-2">
|
|
{/* Data Bits */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="data-bits">{t('serial.field.dataBits')}</Label>
|
|
<select
|
|
id="data-bits"
|
|
value={dataBits}
|
|
onChange={(e) => setDataBits(parseInt(e.target.value, 10) as 5 | 6 | 7 | 8)}
|
|
className="w-full h-10 px-3 rounded-md border border-input bg-background text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
>
|
|
{DATA_BITS.map((bits) => (
|
|
<option key={bits} value={bits}>
|
|
{bits}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Stop Bits */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="stop-bits">{t('serial.field.stopBits')}</Label>
|
|
<select
|
|
id="stop-bits"
|
|
value={stopBits}
|
|
onChange={(e) => setStopBits(parseFloat(e.target.value) as 1 | 1.5 | 2)}
|
|
className="w-full h-10 px-3 rounded-md border border-input bg-background text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
>
|
|
{STOP_BITS.map((bits) => (
|
|
<option key={bits} value={bits}>
|
|
{bits}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{isStopBits15 && (
|
|
<p className="text-xs text-yellow-500">
|
|
{t('serial.field.stopBits15Warning')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Parity */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="parity">{t('serial.field.parity')}</Label>
|
|
<select
|
|
id="parity"
|
|
value={parity}
|
|
onChange={(e) => setParity(e.target.value as SerialParity)}
|
|
className="w-full h-10 px-3 rounded-md border border-input bg-background text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
>
|
|
{PARITY_OPTIONS.map((option) => (
|
|
<option key={option} value={option}>
|
|
{t(`serial.parity.${option}`)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Flow Control */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="flow-control">{t('serial.field.flowControl')}</Label>
|
|
<select
|
|
id="flow-control"
|
|
value={flowControl}
|
|
onChange={(e) => setFlowControl(e.target.value as SerialFlowControl)}
|
|
className="w-full h-10 px-3 rounded-md border border-input bg-background text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
>
|
|
{FLOW_CONTROL_OPTIONS.map((option) => (
|
|
<option key={option} value={option}>
|
|
{t(`serial.flowControl.${option}`)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Terminal Options */}
|
|
<div className="space-y-3 pt-2 border-t border-border/60">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="local-echo" className="text-sm font-medium cursor-pointer">
|
|
{t('serial.field.localEcho')}
|
|
</Label>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('serial.field.localEchoDesc')}
|
|
</p>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
id="local-echo"
|
|
checked={localEcho}
|
|
onChange={(e) => setLocalEcho(e.target.checked)}
|
|
className="h-4 w-4 rounded border-input"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="line-mode" className="text-sm font-medium cursor-pointer">
|
|
{t('serial.field.lineMode')}
|
|
</Label>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('serial.field.lineModeDesc')}
|
|
</p>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
id="line-mode"
|
|
checked={lineMode}
|
|
onChange={(e) => setLineMode(e.target.checked)}
|
|
className="h-4 w-4 rounded border-input"
|
|
/>
|
|
</div>
|
|
|
|
{/* Charset */}
|
|
<div className="space-y-1">
|
|
<Label htmlFor="serial-charset" className="text-sm font-medium">
|
|
{t('serial.field.charset')}
|
|
</Label>
|
|
<Input
|
|
id="serial-charset"
|
|
placeholder={t("hostDetails.charset.placeholder")}
|
|
value={charset}
|
|
onChange={(e) => setCharset(e.target.value)}
|
|
className="h-9"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* Save Configuration */}
|
|
{onSaveHost && (
|
|
<div className="space-y-3 pt-2 border-t border-border/60">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="save-config" className="text-sm font-medium cursor-pointer">
|
|
{t('serial.field.saveConfig')}
|
|
</Label>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('serial.field.saveConfigDesc')}
|
|
</p>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
id="save-config"
|
|
checked={saveConfig}
|
|
onChange={(e) => setSaveConfig(e.target.checked)}
|
|
className="h-4 w-4 rounded border-input"
|
|
/>
|
|
</div>
|
|
{saveConfig && (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="config-label">{t('serial.field.configLabel')}</Label>
|
|
<Input
|
|
id="config-label"
|
|
value={configLabel}
|
|
onChange={(e) => setConfigLabel(e.target.value)}
|
|
placeholder={t('serial.field.configLabelPlaceholder')}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="ghost" onClick={onClose}>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
<Button onClick={handleConnect} disabled={!isValid}>
|
|
{saveConfig ? (
|
|
<Save size={14} className="mr-2" />
|
|
) : (
|
|
<Cpu size={14} className="mr-2" />
|
|
)}
|
|
{saveConfig ? t('serial.connectAndSave') : t('common.connect')}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default SerialConnectModal;
|