mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: tailscale add host and copy ip, fix outbound fetch hang and plugin ESM warning
This commit is contained in:
1 parent
c21bca8cf0
commit
fbe736bc20
70 files changed
+1117
-1139
No files matched your search
@@ -1,125 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Puzzle, ShieldCheck } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { SettingRow } from "@/components/section-card";
|
||||
import { Button } from "@/components/button";
|
||||
import {
|
||||
getPlugins,
|
||||
setPluginEnabled,
|
||||
type PluginSummary,
|
||||
} from "@/api/plugins-api";
|
||||
import { syncPlugins } from "@/plugin-host/loader";
|
||||
import { AccordionSection, AdminToggle } from "./AdminSettingsShared";
|
||||
import { PluginPermissionsDialog } from "./PluginPermissionsDialog";
|
||||
|
||||
export function AdminPluginsSection({
|
||||
open,
|
||||
onToggle,
|
||||
}: {
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [plugins, setPlugins] = useState<PluginSummary[]>([]);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [permissionsTargetId, setPermissionsTargetId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const permissionsTarget =
|
||||
plugins.find((plugin) => plugin.id === permissionsTargetId) ?? null;
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setPlugins(await getPlugins());
|
||||
} catch {
|
||||
toast.error(t("admin.pluginsLoadFailed"));
|
||||
} finally {
|
||||
setLoaded(true);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && !loaded) void load();
|
||||
}, [open, loaded, load]);
|
||||
|
||||
const toggle = async (plugin: PluginSummary) => {
|
||||
setBusy(plugin.id);
|
||||
try {
|
||||
await setPluginEnabled(plugin.id, !plugin.enabled);
|
||||
await load();
|
||||
// Re-read state so the shell picks the change up without a reload.
|
||||
await syncPlugins();
|
||||
toast.success(
|
||||
plugin.enabled
|
||||
? t("admin.pluginDisabled", { name: plugin.name })
|
||||
: t("admin.pluginEnabled", { name: plugin.name }),
|
||||
);
|
||||
} catch {
|
||||
toast.error(t("admin.pluginToggleFailed"));
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccordionSection
|
||||
label={t("admin.plugins")}
|
||||
icon={<Puzzle className="size-3.5" />}
|
||||
open={open}
|
||||
onToggle={onToggle}
|
||||
>
|
||||
{plugins.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground py-3">
|
||||
{loaded ? t("admin.pluginsNone") : t("common.loading")}
|
||||
</p>
|
||||
) : (
|
||||
plugins.map((plugin) => (
|
||||
<SettingRow
|
||||
key={plugin.id}
|
||||
label={plugin.name}
|
||||
badge={
|
||||
plugin.tier === "bundled" ? t("admin.pluginBuiltIn") : undefined
|
||||
}
|
||||
description={
|
||||
plugin.lastError
|
||||
? plugin.lastError
|
||||
: `v${plugin.version} · ${plugin.state}`
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{(plugin.capabilities?.length ?? 0) > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-none h-7 px-2 text-[10px] font-bold uppercase tracking-widest"
|
||||
onClick={() => setPermissionsTargetId(plugin.id)}
|
||||
>
|
||||
<ShieldCheck className="size-3" />
|
||||
{t("admin.pluginPermissions")}
|
||||
</Button>
|
||||
)}
|
||||
<AdminToggle
|
||||
on={plugin.enabled}
|
||||
onToggle={() => void toggle(plugin)}
|
||||
disabled={busy === plugin.id}
|
||||
/>
|
||||
</div>
|
||||
</SettingRow>
|
||||
))
|
||||
)}
|
||||
</AccordionSection>
|
||||
|
||||
<PluginPermissionsDialog
|
||||
plugin={permissionsTarget}
|
||||
open={permissionsTargetId !== null}
|
||||
onOpenChange={(next) => {
|
||||
if (!next) setPermissionsTargetId(null);
|
||||
}}
|
||||
onChanged={() => void load()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -81,9 +81,7 @@ export function AdminSecondFactorsSection({
|
||||
</span>
|
||||
{!factor.available && (
|
||||
<span className="text-[10px] text-destructive">
|
||||
{t("admin.secondFactorUnavailable", {
|
||||
plugin: factor.pluginId,
|
||||
})}
|
||||
{t("admin.secondFactorUnavailable")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,12 @@ import {
|
||||
type UserRole,
|
||||
} from "@/main-axios";
|
||||
import { type AdminSection, type Host } from "@/types/ui-types";
|
||||
import { AdminPluginsSection } from "./AdminPluginsSection";
|
||||
import {
|
||||
FeatureSettingsSection,
|
||||
featureSectionId,
|
||||
useFeatureSettings,
|
||||
type FeatureSectionId,
|
||||
} from "@/settings/FeatureSettingsSections";
|
||||
import {
|
||||
AdminRolesSection,
|
||||
AdminSessionsSection,
|
||||
@@ -109,9 +114,10 @@ export function AdminSettingsPanel({
|
||||
onOpenHostTab?: (host: Host) => void;
|
||||
} = {}) {
|
||||
const { t } = useTranslation();
|
||||
const [openSections, setOpenSections] = useState<Set<AdminSection>>(
|
||||
() => new Set(["general"]),
|
||||
);
|
||||
const [openSections, setOpenSections] = useState<
|
||||
Set<AdminSection | FeatureSectionId>
|
||||
>(() => new Set(["general"]));
|
||||
const featureSettings = useFeatureSettings("admin");
|
||||
const [manageUser, setManageUser] = useState<AdminUser | null>(null);
|
||||
const [allowRegistration, setAllowRegistration] = useState(true);
|
||||
const [allowPasswordLogin, setAllowPasswordLogin] = useState(true);
|
||||
@@ -358,7 +364,7 @@ export function AdminSettingsPanel({
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function toggle(id: AdminSection) {
|
||||
function toggle(id: AdminSection | FeatureSectionId) {
|
||||
setOpenSections((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
@@ -970,10 +976,15 @@ export function AdminSettingsPanel({
|
||||
users={users}
|
||||
/>
|
||||
|
||||
<AdminPluginsSection
|
||||
open={openSections.has("plugins")}
|
||||
onToggle={() => toggle("plugins")}
|
||||
/>
|
||||
{featureSettings.map((plugin) => (
|
||||
<FeatureSettingsSection
|
||||
key={plugin.id}
|
||||
plugin={plugin}
|
||||
scope="admin"
|
||||
open={openSections.has(featureSectionId(plugin.id))}
|
||||
onToggle={() => toggle(featureSectionId(plugin.id))}
|
||||
/>
|
||||
))}
|
||||
|
||||
<AdminCreateUserDialog
|
||||
open={createUserOpen}
|
||||
|
||||
@@ -60,6 +60,7 @@ import type { SSHHost } from "@/types";
|
||||
import { useTabsSafe } from "@/shell/TabContext";
|
||||
import { updatePluginHostSettings } from "@/api/plugins-api";
|
||||
import {
|
||||
applyHostDraft,
|
||||
buildHostEditorPayload,
|
||||
createHostEditorForm,
|
||||
mapSnippetResponse,
|
||||
@@ -89,6 +90,7 @@ import { useSshAuthEditors } from "@/plugin-host/auth-registry";
|
||||
import { useSshAuthProviders } from "@/hooks/useSshAuthProviders";
|
||||
import { SshAuthProviderFields } from "./SshAuthProviderFields";
|
||||
import type { PluginSettingsField } from "@termix/plugin-sdk/manifest";
|
||||
import type { HostDraft } from "@termix/plugin-sdk/frontend";
|
||||
import { PluginComponent } from "@/plugin-host/component-registry";
|
||||
import {
|
||||
toCredentialOption,
|
||||
@@ -99,6 +101,7 @@ const CUSTOM_FONT_OPTION = "__custom__";
|
||||
|
||||
export function HostEditor({
|
||||
host,
|
||||
draft,
|
||||
activeTab,
|
||||
onBack,
|
||||
onSave,
|
||||
@@ -113,6 +116,8 @@ export function HostEditor({
|
||||
onEditCredential,
|
||||
}: {
|
||||
host: Host | null;
|
||||
/** Fields a plugin filled in for a new host. */
|
||||
draft?: HostDraft;
|
||||
activeTab: string;
|
||||
/** Collapses the General tab to the fields needed to reach a host. */
|
||||
simpleMode?: boolean;
|
||||
@@ -136,7 +141,10 @@ export function HostEditor({
|
||||
const { setPreviewTerminalTheme } = useTabsSafe();
|
||||
const connectionDefaults = useConnectionDefaults();
|
||||
const [form, setForm] = useState(() =>
|
||||
createHostEditorForm(host, undefined, connectionDefaults),
|
||||
applyHostDraft(
|
||||
createHostEditorForm(host, undefined, connectionDefaults),
|
||||
host ? undefined : draft,
|
||||
),
|
||||
);
|
||||
const [isCustomFont, setIsCustomFont] = useState(
|
||||
() => !TERMINAL_FONTS.some((f) => f.value === form.fontFamily),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TERMINAL_THEMES } from "@/lib/terminal-themes";
|
||||
import type { Host } from "@/types/ui-types";
|
||||
import type { SSHHostData } from "@/types";
|
||||
import type { HostDraft } from "@termix/plugin-sdk/frontend";
|
||||
import type { HostDefaults } from "@/api/settings-api";
|
||||
import type { TerminalDefaults } from "@/lib/connection-defaults";
|
||||
import {
|
||||
@@ -89,6 +90,24 @@ export function mapSnippetResponse(
|
||||
}));
|
||||
}
|
||||
|
||||
/** Overlays a plugin's draft on a new host's form. */
|
||||
export function applyHostDraft(
|
||||
form: HostEditorForm,
|
||||
draft: HostDraft | undefined,
|
||||
): HostEditorForm {
|
||||
if (!draft) return form;
|
||||
return {
|
||||
...form,
|
||||
...(draft.name !== undefined ? { name: draft.name } : {}),
|
||||
...(draft.ip !== undefined ? { ip: draft.ip } : {}),
|
||||
...(draft.port !== undefined ? { sshPort: draft.port } : {}),
|
||||
...(draft.username !== undefined ? { username: draft.username } : {}),
|
||||
...(draft.authType !== undefined
|
||||
? { authType: draft.authType as HostAuthType }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createHostEditorForm(
|
||||
host: Host | null,
|
||||
defaults?: HostDefaults,
|
||||
|
||||
@@ -11,6 +11,7 @@ import React, {
|
||||
type MutableRefObject,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { HostDraft } from "@termix/plugin-sdk/frontend";
|
||||
import { CredentialShareModal } from "./CredentialShareModal";
|
||||
|
||||
import { Button } from "@/components/button";
|
||||
@@ -82,6 +83,12 @@ export function HostManager({
|
||||
useHostEditorSections();
|
||||
const { t } = useTranslation();
|
||||
const [editingHost, setEditingHost] = useState<Host | "new" | null>(null);
|
||||
// Fields a plugin filled in for a new host, keyed so a second draft remounts
|
||||
// the editor.
|
||||
const [hostDraft, setHostDraft] = useState<{
|
||||
key: number;
|
||||
draft: HostDraft;
|
||||
} | null>(null);
|
||||
const [shareCredential, setShareCredential] = useState<Credential | null>(
|
||||
null,
|
||||
);
|
||||
@@ -189,6 +196,7 @@ export function HostManager({
|
||||
pendingAction.current = null;
|
||||
if (action === "add-host") {
|
||||
setEditingHost("new");
|
||||
setHostDraft(null);
|
||||
setEditingCredential(null);
|
||||
setEditingProtocols(hostProtocolFlags(null));
|
||||
setActiveHostTab("general");
|
||||
@@ -202,8 +210,10 @@ export function HostManager({
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
const handleAddHost = () => {
|
||||
const handleAddHost = (e: Event) => {
|
||||
const draft = (e as CustomEvent<HostDraft | undefined>).detail;
|
||||
setEditingHost("new");
|
||||
setHostDraft(draft ? { key: Date.now(), draft } : null);
|
||||
setEditingCredential(null);
|
||||
setEditingProtocols(hostProtocolFlags(null));
|
||||
setActiveHostTab("general");
|
||||
@@ -518,9 +528,12 @@ export function HostManager({
|
||||
{isHost ? (
|
||||
<HostEditor
|
||||
key={
|
||||
editingHost === "new" ? "new-host" : (editingHost as Host).id
|
||||
editingHost === "new"
|
||||
? `new-host-${hostDraft?.key ?? ""}`
|
||||
: (editingHost as Host).id
|
||||
}
|
||||
host={editingHost === "new" ? null : (editingHost as Host)}
|
||||
draft={editingHost === "new" ? hostDraft?.draft : undefined}
|
||||
activeTab={effectiveHostTab}
|
||||
simpleMode={collapseAdvanced}
|
||||
onBack={requestCloseHostEditor}
|
||||
|
||||
@@ -43,6 +43,8 @@ export interface HostEditorSectionDef {
|
||||
pluginId?: string;
|
||||
group: "top" | "ssh";
|
||||
labelKey: string;
|
||||
/** Already-translated label, used instead of labelKey when set. */
|
||||
label?: string;
|
||||
icon?: ComponentType<{ className?: string }>;
|
||||
/** Position among the group's tabs, core ones included. */
|
||||
order?: number;
|
||||
@@ -96,7 +98,7 @@ function mergeByOrder(
|
||||
const Icon = section.icon;
|
||||
return {
|
||||
id: section.id,
|
||||
label: t(section.labelKey),
|
||||
label: section.label ?? t(section.labelKey),
|
||||
icon: Icon ? <Icon className="size-3" /> : null,
|
||||
order: section.order ?? 1000,
|
||||
};
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/dialog";
|
||||
import {
|
||||
grantPluginCapability,
|
||||
revokePluginCapability,
|
||||
type PluginSummary,
|
||||
} from "@/api/plugins-api";
|
||||
import { AdminToggle } from "./AdminSettingsShared";
|
||||
import { CAPABILITY_CATALOG } from "@termix/plugin-sdk/capabilities";
|
||||
|
||||
/**
|
||||
* The catalog is the list. It is ordered worst first, so the riskiest thing a
|
||||
* plugin asked for is the first row an admin reads rather than something they
|
||||
* have to scroll for.
|
||||
*/
|
||||
const LABELED_PERMISSIONS = CAPABILITY_CATALOG.map((capability) => ({
|
||||
capability: capability.id,
|
||||
labelKey: capability.titleKey,
|
||||
descriptionKey: capability.consequenceKey,
|
||||
}));
|
||||
|
||||
export function PluginPermissionsDialog({
|
||||
plugin,
|
||||
open,
|
||||
onOpenChange,
|
||||
onChanged,
|
||||
}: {
|
||||
plugin: PluginSummary | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onChanged: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
|
||||
if (!plugin) return null;
|
||||
|
||||
const declared = plugin.capabilities ?? [];
|
||||
const rows = LABELED_PERMISSIONS.filter((row) =>
|
||||
declared.includes(row.capability),
|
||||
);
|
||||
const granted = new Set(plugin.grantedCapabilities ?? []);
|
||||
const publicRoutes = [
|
||||
...(plugin.publicRoutes?.http ?? []).map(
|
||||
(path) => `/plugin-api/${plugin.id}${path}`,
|
||||
),
|
||||
...(plugin.publicRoutes?.ws ?? []).map(
|
||||
(path) => `/plugin-ws/${plugin.id}${path}`,
|
||||
),
|
||||
];
|
||||
|
||||
const toggle = async (capability: string, isGranted: boolean) => {
|
||||
setBusy(capability);
|
||||
try {
|
||||
if (isGranted) {
|
||||
await revokePluginCapability(plugin.id, capability);
|
||||
} else {
|
||||
await grantPluginCapability(plugin.id, capability);
|
||||
}
|
||||
onChanged();
|
||||
} catch {
|
||||
toast.error(
|
||||
isGranted
|
||||
? t("admin.pluginRevokeFailed")
|
||||
: t("admin.pluginGrantFailed"),
|
||||
);
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-[calc(100vw-2rem)] sm:max-w-lg rounded-none border-border bg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xs font-bold uppercase tracking-widest flex items-center gap-2">
|
||||
<ShieldCheck className="size-4 text-accent-brand" />
|
||||
{t("admin.pluginPermissions")}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-[10px] font-bold tracking-tight text-muted-foreground">
|
||||
{plugin.name}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-3">
|
||||
{rows.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground py-3">
|
||||
{t("admin.pluginPermissionsNone")}
|
||||
</p>
|
||||
) : (
|
||||
<div className="border border-border overflow-hidden">
|
||||
{rows.map((row, i) => {
|
||||
const isGranted = granted.has(row.capability);
|
||||
return (
|
||||
<div
|
||||
key={row.capability}
|
||||
className={`flex items-center justify-between gap-3 px-3 py-3 ${
|
||||
i < rows.length - 1 ? "border-b border-border" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col gap-0.5 min-w-0 flex-1">
|
||||
<span className="text-xs font-semibold">
|
||||
{t(row.labelKey)}
|
||||
</span>
|
||||
<span className="text-[11px] text-muted-foreground leading-snug">
|
||||
{t(row.descriptionKey)}
|
||||
</span>
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mt-0.5">
|
||||
{isGranted
|
||||
? t("admin.pluginPermissionGranted")
|
||||
: t("admin.pluginPermissionNotGranted")}
|
||||
</span>
|
||||
</div>
|
||||
<AdminToggle
|
||||
on={isGranted}
|
||||
onToggle={() => void toggle(row.capability, isGranted)}
|
||||
disabled={busy === row.capability}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{publicRoutes.length > 0 && (
|
||||
<div className="pb-3">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1">
|
||||
{t("admin.pluginPublicRoutes")}
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground leading-snug mb-2">
|
||||
{t("admin.pluginPublicRoutesDescription")}
|
||||
</p>
|
||||
<div className="border border-border">
|
||||
{publicRoutes.map((route) => (
|
||||
<div
|
||||
key={route}
|
||||
className="px-3 py-1.5 text-[11px] font-mono break-all border-b border-border last:border-b-0"
|
||||
>
|
||||
{route}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -76,9 +76,21 @@ import { changeAppLanguage, normalizeLanguageCode } from "@/i18n/i18n";
|
||||
import { Select2 } from "@/components/select2";
|
||||
import { clearLocalAdaptivePreferences } from "@/lib/local-adaptive-preferences";
|
||||
import { ConnectionDefaultsSettings } from "./ConnectionDefaultsSettings";
|
||||
import {
|
||||
FeatureSettingsSection,
|
||||
featureSectionId,
|
||||
useFeatureSettings,
|
||||
type FeatureSectionId,
|
||||
} from "@/settings/FeatureSettingsSections";
|
||||
|
||||
type UserProfileSection =
|
||||
"account" | "interface" | "appearance" | "security" | "api-keys" | "data";
|
||||
| "account"
|
||||
| "interface"
|
||||
| "appearance"
|
||||
| "security"
|
||||
| "api-keys"
|
||||
| "data"
|
||||
| FeatureSectionId;
|
||||
|
||||
const THEMES: { id: ThemeId; preview: string }[] = [
|
||||
{ id: "system", preview: "auto" },
|
||||
@@ -557,6 +569,7 @@ export function UserProfilePanel({
|
||||
const [openSections, setOpenSections] = useState<Set<UserProfileSection>>(
|
||||
() => new Set(["account"]),
|
||||
);
|
||||
const featureSettings = useFeatureSettings("user");
|
||||
|
||||
// User info
|
||||
const [userId, setUserId] = useState("");
|
||||
@@ -2201,6 +2214,16 @@ export function UserProfilePanel({
|
||||
</div>
|
||||
</AccordionSection>
|
||||
|
||||
{featureSettings.map((plugin) => (
|
||||
<FeatureSettingsSection
|
||||
key={plugin.id}
|
||||
plugin={plugin}
|
||||
scope="user"
|
||||
open={openSections.has(featureSectionId(plugin.id))}
|
||||
onToggle={() => toggle(featureSectionId(plugin.id))}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Delete account dialog */}
|
||||
<Dialog
|
||||
open={showDeleteConfirm}
|
||||
|
||||
@@ -15,6 +15,16 @@ type QuickConnectInput = Pick<
|
||||
|
||||
const QUICK_CONNECT_ID_PREFIX = "quick-connect-";
|
||||
|
||||
// The auth types quickConnectHostToPayload carries over. A plugin auth type
|
||||
// keeps its fields elsewhere, so saving it here would lose them.
|
||||
const SAVABLE_AUTH_TYPES = new Set([
|
||||
"password",
|
||||
"key",
|
||||
"credential",
|
||||
"none",
|
||||
"agent",
|
||||
]);
|
||||
|
||||
export function isQuickConnectHost(host: Pick<Host, "id">): boolean {
|
||||
return host.id.startsWith(QUICK_CONNECT_ID_PREFIX);
|
||||
}
|
||||
@@ -33,6 +43,7 @@ export function createQuickConnectHost(input: QuickConnectInput): Host {
|
||||
...login,
|
||||
port: input.port,
|
||||
enableSsh: false,
|
||||
quickConnectSavable: false,
|
||||
domain: input.domain,
|
||||
pluginSettings: {
|
||||
[protocol.pluginId]: {
|
||||
@@ -62,6 +73,8 @@ export function createQuickConnectHost(input: QuickConnectInput): Host {
|
||||
quickActions: [],
|
||||
enableSsh: true,
|
||||
sshPort: input.port,
|
||||
quickConnectSavable:
|
||||
!input.authFields && SAVABLE_AUTH_TYPES.has(input.authType),
|
||||
...input.authFields,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user