Files
Netcatty/components/QuickConnectWizard.tsx
T
57089a3abf feat: temporary network device mode and tab-bar quick connect (#3793)
* feat(ssh): opt temporary connections into network device mode

Saved hosts and group-backed clones keep their own device type. SSH
connections with no saved host, including external launches and quick
connect, can start in network device mode when the new Terminal setting
is on. The setting stays off unless the user turns it on.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* feat(terminal): quick connect typed addresses from the tab switcher

The tab-bar quick switcher now offers the typed address when it already
parses as a connection target. Choosing that row opens the existing
quick connect wizard, including from a terminal tab where the vault
surface is hidden.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix(ssh): keep temporary Mosh and ET off network device mode

Those transports are stored as SSH plus a flag. Stamping network device
mode onto them made raw-host checks skip the copy-session working
directory probe. Plain temporary SSH still opts in.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix(ssh): read temporary connection settings at connect time

VaultView keeps a stale onConnect callback because its memo ignores that
prop. Read both temporary-connection toggles from a ref when connecting
so turning them on or off applies in the same session.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix(terminal): stop word queries from opening quick connect

Show the switcher quick-connect row only for address-like input, and
place it after matching hosts so Enter still connects to a saved host.
Translate the Hosts, Tabs, and New Workspace labels.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix(terminal): keep matching tabs ahead of quick connect

An address that matches an open session or workspace was still the
default Enter target when no saved host matched. Put that row after
those tabs so Enter switches to the existing one.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-10-20 18:29:41 +08:00

812 lines
27 KiB
TypeScript

import {
ArrowLeft,
ChevronDown,
Eye,
EyeOff,
Key,
Lock,
Plug,
Plus,
User,
} from "lucide-react";
import React, { useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { useI18n } from "../application/i18n/I18nProvider";
import type { QuickConnectTarget } from "../domain/quickConnect";
import { formatHostPort } from "../domain/host";
import {
buildQuickConnectHost,
getQuickConnectDefaultPort,
isQuickConnectIdentityCompatible,
isQuickConnectIdentityUsable,
type QuickConnectAuthMethod,
type QuickConnectProtocol,
} from "../domain/quickConnectHost";
import { cn } from "../lib/utils";
import { Host, Identity, SSHKey } from "../types";
import { Button } from "./ui/button";
import { Combobox } from "./ui/combobox";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Dropdown, DropdownContent, DropdownTrigger } from "./ui/dropdown";
import { ScrollArea } from "./ui/scroll-area";
import { PROTOCOL_VISUAL_STYLES } from "./protocolVisuals";
// Wizard steps
type WizardStep = "protocol" | "username" | "knownhost" | "auth";
interface QuickConnectWizardProps {
open: boolean;
target: QuickConnectTarget;
keys: SSHKey[];
identities: Identity[];
warnings?: string[];
onConnect: (host: Host) => void;
onSaveHost?: (host: Host) => void;
onAddKey?: () => void;
onClose: () => void;
}
const QuickConnectWizard: React.FC<QuickConnectWizardProps> = ({
open,
target,
keys,
identities,
warnings,
onConnect,
onSaveHost,
onAddKey,
onClose,
}) => {
const { t } = useI18n();
// Wizard state
const [step, setStep] = useState<WizardStep>("protocol");
const [protocol, setProtocol] = useState<QuickConnectProtocol>("ssh");
const [username, setUsername] = useState(target.username || "");
const [port, setPort] = useState<number>(target.port || 22);
// Known host verification state
const [knownHostInfo, setKnownHostInfo] = useState<{
keyType: string;
fingerprint: string;
} | null>(null);
// Auth state
const [authMethod, setAuthMethod] = useState<QuickConnectAuthMethod>("password");
const [password, setPassword] = useState("");
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(null);
const [selectedIdentityId, setSelectedIdentityId] = useState<string | null>(null);
const [saveOptionsOpen, setSaveOptionsOpen] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const selectedIdentity = useMemo(
() => identities.find((identity) => identity.id === selectedIdentityId),
[identities, selectedIdentityId],
);
// Reset state when target changes
React.useEffect(() => {
if (open) {
setStep("protocol");
setProtocol("ssh");
setUsername(target.username || "");
setPort(target.port || 22);
setPassword("");
setSelectedKeyId(null);
setSelectedIdentityId(null);
setSaveOptionsOpen(false);
setShowPassword(false);
setKnownHostInfo(null);
}
}, [open, target]);
const canUseSelectedIdentity = useMemo(
() => isQuickConnectIdentityUsable(selectedIdentity, keys, protocol),
[keys, protocol, selectedIdentity],
);
const identityOptions = useMemo(
() => identities.filter((identity) => isQuickConnectIdentityCompatible(identity, target)).map((identity) => ({
value: identity.id,
label: identity.label,
sublabel: identity.username,
icon: <User size={14} />,
})),
[identities, target],
);
const detachSelectedIdentity = () => {
setSelectedIdentityId(null);
setAuthMethod("password");
setPassword("");
setSelectedKeyId(null);
};
const clearSelectedIdentity = () => {
detachSelectedIdentity();
setUsername(target.username || "");
};
const handleIdentitySelect = (identityId: string) => {
if (!identityId) {
clearSelectedIdentity();
return;
}
const identity = identities.find((candidate) => candidate.id === identityId);
if (!identity || !isQuickConnectIdentityCompatible(identity, target)) return;
setSelectedIdentityId(identity.id);
setUsername(identity.username);
setAuthMethod(identity.authMethod);
setPassword("");
setSelectedKeyId(identity.keyId || null);
};
// Handle protocol selection
const handleProtocolSelect = (proto: QuickConnectProtocol) => {
setProtocol(proto);
if (proto === "telnet") {
if (selectedIdentityId) clearSelectedIdentity();
setAuthMethod("password");
}
if (port === getQuickConnectDefaultPort(protocol)) {
setPort(getQuickConnectDefaultPort(proto));
}
};
const handleProtocolPortChange = (
proto: QuickConnectProtocol,
rawValue: string,
fallbackPort: number,
) => {
handleProtocolSelect(proto);
setPort(parseInt(rawValue) || fallbackPort);
};
// Navigate to next step
const handleContinue = () => {
switch (step) {
case "protocol":
if (canUseSelectedIdentity) {
handleConnect();
break;
}
// Always go to username step to let user confirm/edit username
setStep("username");
break;
case "username":
setStep("auth");
break;
case "knownhost":
setStep("auth");
break;
case "auth":
handleConnect();
break;
}
};
// Navigate back
const handleBack = () => {
switch (step) {
case "username":
setStep("protocol");
break;
case "knownhost":
setStep("username");
break;
case "auth":
// Always go back to username step
setStep("username");
break;
}
};
// Create host and connect
const handleConnect = (save = false) => {
const effectiveUsername = username || target.username || "root";
const effectivePort = port || getQuickConnectDefaultPort(protocol);
const now = Date.now();
const tempHost = buildQuickConnectHost({
id: `quick-${now}-${Math.random().toString(36).slice(2, 11)}`,
createdAt: now,
target,
protocol,
port: effectivePort,
username: effectiveUsername,
authMethod,
password,
selectedKeyId,
selectedIdentityId: selectedIdentity?.id,
save,
});
if (save && onSaveHost) {
onSaveHost(tempHost);
}
onConnect(tempHost);
onClose();
};
// Check if can proceed
const canProceed = useMemo(() => {
switch (step) {
case "protocol":
return true;
case "username":
return username.trim().length > 0;
case "knownhost":
return true;
case "auth":
if (selectedIdentity && protocol !== "telnet") return canUseSelectedIdentity;
if (authMethod === "password") {
// Whitespace-only passwords are valid SSH secrets (issue #2036).
return password.length > 0;
}
return Boolean(selectedKeyId && keys.some((key) => key.id === selectedKeyId));
}
}, [
step,
username,
authMethod,
password,
selectedKeyId,
selectedIdentity,
canUseSelectedIdentity,
keys,
protocol,
]);
// Render protocol selection step
const renderProtocolStep = () => (
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-sm font-medium">{t("protocolSelect.chooseProtocol")}</h3>
<div className="space-y-3">
{/* SSH */}
<button
className={cn(
"w-full flex items-center justify-between px-4 py-3 rounded-xl border-2 transition-all text-left",
protocol === "ssh"
? "border-primary bg-primary/5"
: "border-border/60 hover:border-border hover:bg-secondary/50",
)}
onClick={() => handleProtocolSelect("ssh")}
>
<div className="flex items-center gap-3">
<div
className={cn(
"h-10 w-10 rounded-lg flex items-center justify-center",
protocol === "ssh"
? PROTOCOL_VISUAL_STYLES.ssh.selected
: PROTOCOL_VISUAL_STYLES.ssh.idle,
)}
>
{PROTOCOL_VISUAL_STYLES.ssh.icon}
</div>
<div>
<div className="font-medium">SSH</div>
<div className="text-xs text-muted-foreground font-mono">
ssh {target.hostname}
</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={protocol === "ssh" ? port : 22}
onChange={(e) => handleProtocolPortChange("ssh", e.target.value, 22)}
onClick={(e) => e.stopPropagation()}
className="w-16 h-7 text-xs text-center"
min={1}
max={65535}
/>
</div>
</button>
{/* Mosh */}
<button
className={cn(
"w-full flex items-center justify-between px-4 py-3 rounded-xl border-2 transition-all text-left",
protocol === "mosh"
? "border-primary bg-primary/5"
: "border-border/60 hover:border-border hover:bg-secondary/50",
)}
onClick={() => handleProtocolSelect("mosh")}
>
<div className="flex items-center gap-3">
<div
className={cn(
"h-10 w-10 rounded-lg flex items-center justify-center",
protocol === "mosh"
? PROTOCOL_VISUAL_STYLES.mosh.selected
: PROTOCOL_VISUAL_STYLES.mosh.idle,
)}
>
{PROTOCOL_VISUAL_STYLES.mosh.icon}
</div>
<div>
<div className="font-medium">Mosh</div>
<div className="text-xs text-muted-foreground font-mono">
mosh {target.hostname}
</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={protocol === "mosh" ? port : 22}
onChange={(e) => handleProtocolPortChange("mosh", e.target.value, 22)}
onClick={(e) => e.stopPropagation()}
className="w-16 h-7 text-xs text-center"
min={1}
max={65535}
/>
</div>
</button>
{/* Eternal Terminal */}
<button
className={cn(
"w-full flex items-center justify-between px-4 py-3 rounded-xl border-2 transition-all text-left",
protocol === "et"
? "border-primary bg-primary/5"
: "border-border/60 hover:border-border hover:bg-secondary/50",
)}
onClick={() => handleProtocolSelect("et")}
>
<div className="flex items-center gap-3">
<div
className={cn(
"h-10 w-10 rounded-lg flex items-center justify-center",
protocol === "et"
? PROTOCOL_VISUAL_STYLES.et.selected
: PROTOCOL_VISUAL_STYLES.et.idle,
)}
>
{PROTOCOL_VISUAL_STYLES.et.icon}
</div>
<div>
<div className="font-medium">Eternal Terminal</div>
<div className="text-xs text-muted-foreground font-mono">
et {target.hostname}
</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={protocol === "et" ? port : 22}
onChange={(e) => handleProtocolPortChange("et", e.target.value, 22)}
onClick={(e) => e.stopPropagation()}
className="w-16 h-7 text-xs text-center"
min={1}
max={65535}
/>
</div>
</button>
{/* Telnet */}
<button
className={cn(
"w-full flex items-center justify-between px-4 py-3 rounded-xl border-2 transition-all text-left",
protocol === "telnet"
? "border-primary bg-primary/5"
: "border-border/60 hover:border-border hover:bg-secondary/50",
)}
onClick={() => handleProtocolSelect("telnet")}
>
<div className="flex items-center gap-3">
<div
className={cn(
"h-10 w-10 rounded-lg flex items-center justify-center",
protocol === "telnet"
? PROTOCOL_VISUAL_STYLES.telnet.selected
: PROTOCOL_VISUAL_STYLES.telnet.idle,
)}
>
{PROTOCOL_VISUAL_STYLES.telnet.icon}
</div>
<div>
<div className="font-medium">Telnet</div>
<div className="text-xs text-muted-foreground font-mono">
telnet {target.hostname}
</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={protocol === "telnet" ? port : 23}
onChange={(e) => handleProtocolPortChange("telnet", e.target.value, 23)}
onClick={(e) => e.stopPropagation()}
className="w-16 h-7 text-xs text-center"
min={1}
max={65535}
/>
</div>
</button>
</div>
</div>
{protocol !== "telnet" && identityOptions.length > 0 && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3">
<Label className="text-sm font-medium">{t("quickConnect.identity.label")}</Label>
<Combobox
options={identityOptions}
value={selectedIdentityId || undefined}
onValueChange={handleIdentitySelect}
placeholder={t("quickConnect.identity.placeholder")}
emptyText={t("quickConnect.identity.empty")}
icon={<User size={14} />}
/>
</div>
<p className="text-xs text-muted-foreground">
{selectedIdentity
? t("quickConnect.identity.selectedHint", { username: selectedIdentity.username })
: t("quickConnect.identity.hint")}
</p>
</div>
)}
</div>
);
// Render username step
const renderUsernameStep = () => (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="quick-username">{t("terminal.auth.username")}</Label>
<Input
id="quick-username"
value={username}
onChange={(e) => {
if (selectedIdentityId) detachSelectedIdentity();
setUsername(e.target.value);
}}
placeholder={t("terminal.auth.username.placeholder")}
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter" && username.trim()) {
handleContinue();
}
}}
/>
</div>
</div>
);
// Render known host verification step
const renderKnownHostStep = () => (
<div className="space-y-4">
<h3 className="text-base font-semibold text-amber-600 dark:text-amber-500">
{t("quickConnect.knownHost.title")}
</h3>
<div className="space-y-2 text-sm text-muted-foreground">
<p>{t("quickConnect.knownHost.authenticity", { hostname: target.hostname })}</p>
{knownHostInfo && (
<>
<p className="font-medium text-foreground">
{t("quickConnect.knownHost.fingerprintLabel", { keyType: knownHostInfo.keyType })}
</p>
<p className="font-mono text-xs bg-muted p-2 rounded break-all">
{knownHostInfo.fingerprint}
</p>
</>
)}
<p>{t("quickConnect.knownHost.addQuestion")}</p>
</div>
</div>
);
// Render auth step
const renderAuthStep = () => (
<div className="space-y-4">
{selectedIdentity ? (
<div className="rounded-xl border border-primary/40 bg-primary/5 p-4 space-y-3">
<div className="flex items-center gap-3">
<div className="h-9 w-9 rounded-lg bg-primary/15 text-primary flex items-center justify-center">
<User size={16} />
</div>
<div className="min-w-0 flex-1">
<div className="font-medium truncate">{selectedIdentity.label}</div>
<div className="text-xs text-muted-foreground truncate">
{selectedIdentity.username} · {t(`quickConnect.identity.method.${selectedIdentity.authMethod}`)}
</div>
</div>
</div>
{!canUseSelectedIdentity && (
<p className="text-xs text-destructive">
{t("quickConnect.identity.unavailable")}
</p>
)}
<Button variant="outline" size="sm" className="w-full" onClick={detachSelectedIdentity}>
{t("quickConnect.identity.useCustom")}
</Button>
</div>
) : (
<>
{/* Auth method tabs */}
<div className="flex gap-1 p-1 bg-secondary/80 rounded-lg border border-border/60">
<button
className={cn(
"flex-1 flex items-center justify-center gap-2 py-2 text-sm font-medium rounded-md transition-all",
authMethod === "password"
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-secondary",
)}
onClick={() => {
setSelectedIdentityId(null);
setAuthMethod("password");
}}
>
<Lock size={14} />
{t("terminal.auth.password")}
</button>
<button
className={cn(
"flex-1 flex items-center justify-center gap-2 py-2 text-sm font-medium rounded-md transition-all",
authMethod !== "password"
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-secondary",
)}
onClick={() => {
setSelectedIdentityId(null);
setAuthMethod("key");
}}
>
<Key size={14} />
{t("terminal.auth.sshKey")}
</button>
</div>
{/* Password field */}
{authMethod === "password" && (
<div className="space-y-2">
<Label htmlFor="quick-password">{t("terminal.auth.passwordLabel")}</Label>
<div className="relative">
<Input
id="quick-password"
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t("terminal.auth.password.placeholder")}
className="pr-10"
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter" && password.length > 0) {
handleConnect();
}
}}
/>
<button
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
)}
{/* Key selection */}
{authMethod === "key" && (
<div className="space-y-2">
{keys.filter((k) => k.category === "key").length === 0 ? (
<div className="text-sm text-muted-foreground p-3 border border-dashed border-border/60 rounded-lg text-center">
{t("terminal.auth.noKeysHint")}
</div>
) : (
<ScrollArea className="max-h-48">
<div className="space-y-2">
{keys
.filter((k) => k.category === "key")
.map((key) => (
<button
key={key.id}
className={cn(
"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg border transition-colors text-left",
selectedKeyId === key.id
? "border-primary bg-primary/5"
: "border-border/50 hover:bg-secondary/50",
)}
onClick={() => {
setSelectedIdentityId(null);
setSelectedKeyId(key.id);
}}
>
<div
className={cn(
"h-8 w-8 rounded-lg flex items-center justify-center",
"bg-primary/20 text-primary",
)}
>
<Key size={14} />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">
{key.label}
</div>
<div className="text-xs text-muted-foreground">
Type {key.type}
</div>
</div>
</button>
))}
</div>
</ScrollArea>
)}
{onAddKey && (
<Button
variant="outline"
size="sm"
className="w-full mt-2"
onClick={onAddKey}
>
<Plus size={14} className="mr-2" />
Add key
</Button>
)}
</div>
)}
</>
)}
</div>
);
const hostEndpoint = formatHostPort(target.hostname, port || getQuickConnectDefaultPort(protocol));
const connectTitle = t("quickConnect.connectTitle", { host: hostEndpoint });
const protocolLabel = protocol === "et" ? "ET" : protocol.toUpperCase();
const effectiveUsername = username || target.username || "";
const connectSubtitle = step === "protocol"
? null
: (
effectiveUsername
? `${protocolLabel} ${effectiveUsername}@${hostEndpoint}`
: `${protocolLabel} ${hostEndpoint}`
);
const wizard = (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
onClick={onClose}
>
<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-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">{connectTitle}</h2>
{connectSubtitle ? (
<p className="text-xs text-muted-foreground font-mono truncate">{connectSubtitle}</p>
) : null}
</div>
</div>
</div>
{warnings && warnings.length > 0 && (
<div className="px-6 pb-2">
<div className="text-xs text-amber-600 bg-amber-500/10 border border-amber-500/30 rounded-lg px-3 py-2">
{t("quickConnect.warning.unparsedOptions", {
options: warnings.join(", "),
})}
</div>
</div>
)}
{/* Content */}
<div className="px-6 py-4">
{step === "protocol" && renderProtocolStep()}
{step === "username" && renderUsernameStep()}
{step === "knownhost" && renderKnownHostStep()}
{step === "auth" && renderAuthStep()}
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-border/50 flex items-center justify-between">
<Button
variant="secondary"
onClick={step === "protocol" ? onClose : handleBack}
>
{step === "protocol" ? (
t("common.close")
) : (
<>
<ArrowLeft size={14} className="mr-2" />
{t("common.back")}
</>
)}
</Button>
{step === "knownhost" ? (
<div className="flex items-center gap-2">
<Button variant="secondary" onClick={handleContinue}>
{t("common.continue")}
</Button>
<Button
onClick={() => {
// Add to known hosts then continue
handleContinue();
}}
>
{t("quickConnect.knownHost.addAndContinue")}
</Button>
</div>
) : step === "auth" ? (
<div className="flex items-center gap-2">
{onAddKey &&
authMethod === "key" &&
keys.filter((k) => k.category === "key").length === 0 && (
<Button variant="secondary" onClick={onAddKey}>
{t("quickConnect.addKey")}
</Button>
)}
<Dropdown open={saveOptionsOpen} onOpenChange={setSaveOptionsOpen}>
<div className="flex items-center rounded-md bg-primary text-primary-foreground">
<Button
disabled={!canProceed}
onClick={() => handleConnect(false)}
className="rounded-r-none bg-transparent hover:bg-white/10 shadow-none"
>
{t("common.continue")}
</Button>
<DropdownTrigger asChild>
<Button
disabled={!canProceed}
aria-label={t("terminal.auth.continueSave")}
aria-haspopup="menu"
aria-expanded={saveOptionsOpen}
className="px-2 rounded-l-none bg-transparent hover:bg-white/10 border-l border-primary-foreground/20 shadow-none"
>
<ChevronDown size={14} />
</Button>
</DropdownTrigger>
</div>
<DropdownContent className="w-44 p-1 z-50" align="end">
<button
className="w-full px-3 py-2 text-sm text-left hover:bg-secondary rounded-md"
onClick={() => handleConnect(true)}
disabled={!canProceed}
>
{t("terminal.auth.continueSave")}
</button>
</DropdownContent>
</Dropdown>
</div>
) : (
<Button onClick={handleContinue} disabled={!canProceed}>
{step === "protocol" && canUseSelectedIdentity
? t("quickConnect.identity.connect")
: t("common.continue")}
</Button>
)}
</div>
</div>
</div>
);
// The vault surface is visibility:hidden while a terminal tab is active.
// Portal so the same wizard opened from the tab-bar switcher stays visible.
if (typeof document !== "undefined" && document.body) {
return createPortal(wizard, document.body);
}
return wizard;
};
export default QuickConnectWizard;