mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-11 00:30:57 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f26fd41414 | ||
|
|
417bcc86b0 | ||
|
|
e7e46e3766 |
No files matched your search
@@ -49,11 +49,13 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
showAlgorithmOverrides,
|
||||
setShowAlgorithmOverrides,
|
||||
chainedHosts,
|
||||
hostChainInherited = false,
|
||||
setActiveSubPanel,
|
||||
clearHostChain,
|
||||
proxySummaryType,
|
||||
proxySummaryLabel,
|
||||
proxySummaryTooltip,
|
||||
proxyInherited = false,
|
||||
clearProxyConfig,
|
||||
}) => {
|
||||
const inheritedDeviceType = effectiveGroupDefaults?.deviceType;
|
||||
@@ -641,9 +643,16 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
title={t("hostDetails.jumpHosts")}
|
||||
action={
|
||||
chainedHosts.length > 0 ? (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{t("hostDetails.jumpHosts.hops", { count: chainedHosts.length })}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-1">
|
||||
{hostChainInherited && (
|
||||
<Badge variant="outline" className="text-xs text-muted-foreground">
|
||||
{t("vault.groups.details.inherited")}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{t("hostDetails.jumpHosts.hops", { count: chainedHosts.length })}
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-xs text-muted-foreground">
|
||||
{t("hostDetails.jumpHosts.direct")}
|
||||
@@ -664,8 +673,10 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("hostDetails.jumpHosts.hops", { count: chainedHosts.length })}
|
||||
{hostChainInherited ? ` · ${t("vault.groups.details.inherited")}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
{!hostChainInherited && (
|
||||
<X
|
||||
size={14}
|
||||
className="text-muted-foreground hover:text-destructive flex-shrink-0"
|
||||
@@ -674,6 +685,7 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
clearHostChain();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full space-y-1 pl-5">
|
||||
{chainedHosts.slice(0, 5).map((h, idx) => (
|
||||
@@ -735,7 +747,7 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
) : form.proxyConfig?.host || form.proxyConfig?.command || form.proxyProfileId ? (
|
||||
) : form.proxyConfig?.host || form.proxyConfig?.command || form.proxyProfileId || proxyInherited ? (
|
||||
<div className="w-full min-w-0 grid grid-cols-[minmax(0,1fr)_auto] items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
@@ -750,6 +762,7 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
<TooltipTrigger asChild>
|
||||
<span className="block min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
||||
{proxySummaryLabel}
|
||||
{proxyInherited ? ` · ${t("vault.groups.details.inherited")}` : ""}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="start" className="max-w-xs break-all">
|
||||
@@ -758,6 +771,7 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</button>
|
||||
{!proxyInherited && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -768,6 +782,7 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { GroupConfig } from "../domain/models";
|
||||
import { applyGroupDefaults } from "../domain/groupConfig";
|
||||
import {
|
||||
findProxyProfile,
|
||||
hasIncompleteProxyIdentity,
|
||||
@@ -157,3 +158,69 @@ export const prepareProxyConfigForSave = (params: {
|
||||
normalizedProxyConfig: normalizeManualProxyConfig(params.proxyConfig),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export const EMPTY_CUSTOM_PROXY_DRAFT: ProxyConfig = { type: "http", host: "", port: 8080 };
|
||||
|
||||
export const resolveDisplayProxyProfileId = (
|
||||
form: Pick<Host, "directProxy" | "proxyProfileId" | "proxyConfig">,
|
||||
inheritedProfileId?: string,
|
||||
): string | undefined => {
|
||||
if (form.directProxy || form.proxyConfig !== undefined) return form.proxyProfileId;
|
||||
return form.proxyProfileId ?? inheritedProfileId;
|
||||
};
|
||||
|
||||
export const nextFormAfterSelectProxyProfile = (
|
||||
prev: Host,
|
||||
profileId: string | undefined,
|
||||
groupDefaults?: Partial<GroupConfig>,
|
||||
): Host => {
|
||||
const { proxyConfig: _proxyConfig, proxyProfileId: _proxyProfileId, directProxy: _directProxy, ...rest } = prev;
|
||||
if (!profileId) {
|
||||
const inheritedConfig = !prev.directProxy
|
||||
&& prev.proxyProfileId === undefined
|
||||
&& prev.proxyConfig === undefined
|
||||
&& groupDefaults
|
||||
? applyGroupDefaults(prev, groupDefaults).proxyConfig
|
||||
: undefined;
|
||||
return {
|
||||
...rest,
|
||||
proxyConfig: prev.proxyConfig ?? inheritedConfig ?? EMPTY_CUSTOM_PROXY_DRAFT,
|
||||
} as Host;
|
||||
}
|
||||
return { ...rest, proxyProfileId: profileId } as Host;
|
||||
};
|
||||
|
||||
export const resolveFormHostChainIds = (
|
||||
host: Host,
|
||||
groupDefaults?: Partial<GroupConfig>,
|
||||
): string[] => {
|
||||
if (host.hostChain !== undefined) return host.hostChain.hostIds || [];
|
||||
if (!groupDefaults) return [];
|
||||
return applyGroupDefaults(host, groupDefaults).hostChain?.hostIds || [];
|
||||
};
|
||||
|
||||
export const nextFormAfterClearHostChain = (
|
||||
prev: Host,
|
||||
groupDefaults?: Partial<GroupConfig>,
|
||||
): Host => {
|
||||
const groupWouldRestore = Boolean(
|
||||
groupDefaults
|
||||
&& applyGroupDefaults({ ...prev, hostChain: undefined }, groupDefaults).hostChain?.hostIds?.length,
|
||||
);
|
||||
if (prev.hostChain === undefined || groupWouldRestore) {
|
||||
return { ...prev, hostChain: { hostIds: [] } };
|
||||
}
|
||||
const { hostChain: _hostChain, ...rest } = prev;
|
||||
return rest as Host;
|
||||
};
|
||||
|
||||
export const nextFormAfterRemoveHostFromChain = (
|
||||
prev: Host,
|
||||
index: number,
|
||||
groupDefaults?: Partial<GroupConfig>,
|
||||
): Host => {
|
||||
const ids = resolveFormHostChainIds(prev, groupDefaults).filter((_, i) => i !== index);
|
||||
if (ids.length > 0) return { ...prev, hostChain: { hostIds: ids } };
|
||||
return nextFormAfterClearHostChain(prev, groupDefaults);
|
||||
};
|
||||
@@ -6,12 +6,17 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { I18nProvider } from "../application/i18n/I18nProvider.tsx";
|
||||
import type { Host, Identity } from "../types.ts";
|
||||
import HostDetailsPanel, { parseOptionalPortInput } from "./HostDetailsPanel.tsx";
|
||||
import { applyGroupDefaults } from "../domain/groupConfig.ts";
|
||||
import {
|
||||
prepareProxyConfigForSave,
|
||||
prepareTelnetCredentialsForSave,
|
||||
resolvePrimaryProtocolSavePort,
|
||||
resolvePrimaryProtocolSwitchPort,
|
||||
validateProxyConfigForSave,
|
||||
nextFormAfterSelectProxyProfile,
|
||||
nextFormAfterClearHostChain,
|
||||
nextFormAfterRemoveHostFromChain,
|
||||
resolveDisplayProxyProfileId,
|
||||
} from "./HostDetailsPanel.helpers.ts";
|
||||
import { TooltipProvider } from "./ui/tooltip.tsx";
|
||||
|
||||
@@ -549,3 +554,198 @@ test("HostDetailsPanel expands notes when the host already has notes", () => {
|
||||
"expected notes to render after connection settings",
|
||||
);
|
||||
});
|
||||
|
||||
const networkSshHost: Host = {
|
||||
...hostWithMissingProxyProfile,
|
||||
proxyProfileId: undefined,
|
||||
group: "network",
|
||||
};
|
||||
|
||||
const jumpHost: Host = {
|
||||
id: "jump-1",
|
||||
label: "Bastion",
|
||||
hostname: "bastion.example.com",
|
||||
username: "root",
|
||||
tags: [],
|
||||
os: "linux",
|
||||
port: 22,
|
||||
protocol: "ssh",
|
||||
authMethod: "password",
|
||||
createdAt: 1,
|
||||
};
|
||||
|
||||
const hostOwnedJump: Host = {
|
||||
...jumpHost,
|
||||
id: "jump-host",
|
||||
label: "Edge",
|
||||
hostname: "edge.example.com",
|
||||
};
|
||||
|
||||
const renderHostDetailsWithGroup = (
|
||||
initialData: Host,
|
||||
groupConfig: Record<string, unknown>,
|
||||
extraHosts: Host[] = [],
|
||||
) =>
|
||||
renderToStaticMarkup(
|
||||
React.createElement(
|
||||
I18nProvider,
|
||||
{ locale: "en" },
|
||||
React.createElement(
|
||||
TooltipProvider,
|
||||
null,
|
||||
React.createElement(HostDetailsPanel, {
|
||||
initialData,
|
||||
availableKeys: [],
|
||||
identities: [],
|
||||
proxyProfiles: [],
|
||||
groups: ["network"],
|
||||
managedSources: [],
|
||||
allTags: [],
|
||||
allHosts: extraHosts,
|
||||
terminalThemeId: "default",
|
||||
terminalFontSize: 14,
|
||||
groupConfigs: [{ path: "network", ...groupConfig }],
|
||||
onSave: () => {},
|
||||
onCancel: () => {},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
test("HostDetailsPanel displays inherited group HTTP proxy", () => {
|
||||
const markup = renderHostDetailsWithGroup(networkSshHost, {
|
||||
proxyConfig: { type: "http", host: "group-proxy.example.com", port: 8080 },
|
||||
});
|
||||
|
||||
assert.match(markup, /group-proxy\.example\.com/);
|
||||
assert.match(markup, /Inherited from group/);
|
||||
});
|
||||
|
||||
test("HostDetailsPanel host proxy override wins over the group proxy", () => {
|
||||
const markup = renderHostDetailsWithGroup({
|
||||
...networkSshHost,
|
||||
proxyConfig: { type: "http", host: "host-proxy.example.com", port: 9090 },
|
||||
}, {
|
||||
proxyConfig: { type: "http", host: "group-proxy.example.com", port: 8080 },
|
||||
});
|
||||
|
||||
assert.match(markup, /host-proxy\.example\.com/);
|
||||
assert.doesNotMatch(markup, /group-proxy\.example\.com/);
|
||||
assert.doesNotMatch(markup, /Inherited from group/);
|
||||
});
|
||||
|
||||
test("HostDetailsPanel direct proxy hides inherited group proxy", () => {
|
||||
const markup = renderHostDetailsWithGroup({
|
||||
...networkSshHost,
|
||||
directProxy: true,
|
||||
}, {
|
||||
proxyConfig: { type: "http", host: "group-proxy.example.com", port: 8080 },
|
||||
});
|
||||
|
||||
assert.match(markup, /Direct \(no proxy\)/);
|
||||
assert.doesNotMatch(markup, /group-proxy\.example\.com/);
|
||||
});
|
||||
|
||||
test("HostDetailsPanel displays inherited group jump hosts", () => {
|
||||
const markup = renderHostDetailsWithGroup(networkSshHost, {
|
||||
hostChain: { hostIds: ["jump-1"] },
|
||||
}, [jumpHost]);
|
||||
|
||||
assert.match(markup, /bastion\.example\.com/);
|
||||
assert.match(markup, /Inherited from group/);
|
||||
});
|
||||
|
||||
test("HostDetailsPanel host jump-host override wins over the group chain", () => {
|
||||
const markup = renderHostDetailsWithGroup({
|
||||
...networkSshHost,
|
||||
hostChain: { hostIds: ["jump-host"] },
|
||||
}, {
|
||||
hostChain: { hostIds: ["jump-1"] },
|
||||
}, [jumpHost, hostOwnedJump]);
|
||||
|
||||
assert.match(markup, /edge\.example\.com/);
|
||||
assert.doesNotMatch(markup, /bastion\.example\.com/);
|
||||
assert.doesNotMatch(markup, /Inherited from group/);
|
||||
});
|
||||
|
||||
|
||||
test("selecting Custom while inheriting a saved proxy keeps a custom draft instead of the profile", () => {
|
||||
const group = { proxyProfileId: "office-proxy" };
|
||||
const next = nextFormAfterSelectProxyProfile(networkSshHost, undefined, group);
|
||||
|
||||
assert.equal(next.proxyProfileId, undefined);
|
||||
assert.equal(next.directProxy, undefined);
|
||||
assert.deepEqual(next.proxyConfig, { type: "http", host: "", port: 8080 });
|
||||
assert.equal(resolveDisplayProxyProfileId(next, "office-proxy"), undefined);
|
||||
assert.equal(applyGroupDefaults(next, group).proxyProfileId, undefined);
|
||||
});
|
||||
|
||||
test("removing the last hop of a multi-hop inherited chain keeps an empty override", () => {
|
||||
const group = { hostChain: { hostIds: ["jump-1", "jump-host"] } };
|
||||
const afterFirst = nextFormAfterRemoveHostFromChain(networkSshHost, 0, group);
|
||||
assert.deepEqual(afterFirst.hostChain, { hostIds: ["jump-host"] });
|
||||
|
||||
const afterLast = nextFormAfterRemoveHostFromChain(afterFirst, 0, group);
|
||||
assert.deepEqual(afterLast.hostChain, { hostIds: [] });
|
||||
assert.deepEqual(applyGroupDefaults(afterLast, group).hostChain, { hostIds: [] });
|
||||
});
|
||||
|
||||
test("clearing an inherited-only chain keeps an empty override instead of restoring group hops", () => {
|
||||
const group = { hostChain: { hostIds: ["jump-1", "jump-host"] } };
|
||||
const cleared = nextFormAfterClearHostChain(networkSshHost, group);
|
||||
assert.deepEqual(cleared.hostChain, { hostIds: [] });
|
||||
assert.deepEqual(applyGroupDefaults(cleared, group).hostChain, { hostIds: [] });
|
||||
});
|
||||
|
||||
test("HostDetailsPanel custom proxy draft hides the inherited saved proxy", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
React.createElement(
|
||||
I18nProvider,
|
||||
{ locale: "en" },
|
||||
React.createElement(
|
||||
TooltipProvider,
|
||||
null,
|
||||
React.createElement(HostDetailsPanel, {
|
||||
initialData: {
|
||||
...networkSshHost,
|
||||
proxyConfig: { type: "http", host: "", port: 8080 },
|
||||
},
|
||||
availableKeys: [],
|
||||
identities: [],
|
||||
proxyProfiles: [{
|
||||
id: "office-proxy",
|
||||
label: "Office Proxy",
|
||||
config: { type: "http", host: "office-proxy.example.com", port: 8080 },
|
||||
createdAt: 1,
|
||||
}],
|
||||
groups: ["network"],
|
||||
managedSources: [],
|
||||
allTags: [],
|
||||
allHosts: [],
|
||||
terminalThemeId: "default",
|
||||
terminalFontSize: 14,
|
||||
groupConfigs: [{ path: "network", proxyProfileId: "office-proxy" }],
|
||||
onSave: () => {},
|
||||
onCancel: () => {},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
assert.doesNotMatch(markup, /Office Proxy/);
|
||||
assert.doesNotMatch(markup, /office-proxy\.example\.com/);
|
||||
assert.doesNotMatch(markup, /Inherited from group/);
|
||||
});
|
||||
|
||||
test("HostDetailsPanel empty jump-host override hides the inherited group chain", () => {
|
||||
const markup = renderHostDetailsWithGroup({
|
||||
...networkSshHost,
|
||||
hostChain: { hostIds: [] },
|
||||
}, {
|
||||
hostChain: { hostIds: ["jump-1", "jump-host"] },
|
||||
}, [jumpHost, hostOwnedJump]);
|
||||
|
||||
assert.doesNotMatch(markup, /bastion\.example\.com/);
|
||||
assert.doesNotMatch(markup, /edge\.example\.com/);
|
||||
assert.doesNotMatch(markup, /Inherited from group/);
|
||||
});
|
||||
@@ -55,6 +55,11 @@ import {
|
||||
resolvePrimaryProtocolSwitchPort,
|
||||
prepareTelnetCredentialsForSave,
|
||||
prepareProxyConfigForSave,
|
||||
nextFormAfterSelectProxyProfile,
|
||||
nextFormAfterClearHostChain,
|
||||
nextFormAfterRemoveHostFromChain,
|
||||
resolveFormHostChainIds,
|
||||
resolveDisplayProxyProfileId,
|
||||
} from "./HostDetailsPanel.helpers";
|
||||
export { parseOptionalPortInput } from "./HostDetailsPanel.helpers";
|
||||
import { TerminalEncodingSelect } from "./TerminalEncodingSelect";
|
||||
@@ -424,24 +429,34 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
|
||||
);
|
||||
|
||||
const effectiveFormDistro = getEffectiveHostDistro(form);
|
||||
const hasHostProxyOverride = form.directProxy === true
|
||||
|| form.proxyProfileId !== undefined
|
||||
|| form.proxyConfig !== undefined;
|
||||
const displayProxyProfileId = resolveDisplayProxyProfileId(form, effectiveAuthHost.proxyProfileId);
|
||||
const displayProxyConfig = form.directProxy
|
||||
? undefined
|
||||
: (form.proxyConfig ?? effectiveAuthHost.proxyConfig);
|
||||
const selectedProxyProfile = useMemo(
|
||||
() => proxyProfiles.find((profile) => profile.id === form.proxyProfileId),
|
||||
[form.proxyProfileId, proxyProfiles],
|
||||
() => proxyProfiles.find((profile) => profile.id === displayProxyProfileId),
|
||||
[displayProxyProfileId, proxyProfiles],
|
||||
);
|
||||
const hasMissingProxyProfile = Boolean(form.proxyProfileId && !selectedProxyProfile);
|
||||
const hasMissingProxyProfile = Boolean(displayProxyProfileId && !selectedProxyProfile);
|
||||
const proxyInherited = !hasHostProxyOverride && Boolean(
|
||||
displayProxyProfileId || displayProxyConfig?.host || displayProxyConfig?.command,
|
||||
) && Boolean(effectiveGroupDefaults?.proxyProfileId || effectiveGroupDefaults?.proxyConfig);
|
||||
const proxySummaryType = hasMissingProxyProfile
|
||||
? t("hostDetails.proxyPanel.missing")
|
||||
: formatProxyConfigType(selectedProxyProfile?.config || form.proxyConfig) || "HTTP";
|
||||
: formatProxyConfigType(selectedProxyProfile?.config || displayProxyConfig) || "HTTP";
|
||||
const proxySummaryLabel = hasMissingProxyProfile
|
||||
? t("hostDetails.proxyPanel.missingSaved")
|
||||
: selectedProxyProfile
|
||||
? selectedProxyProfile.label
|
||||
: formatProxyConfigEndpoint(form.proxyConfig);
|
||||
: formatProxyConfigEndpoint(displayProxyConfig);
|
||||
const proxySummaryTooltip = hasMissingProxyProfile
|
||||
? t("hostDetails.proxyPanel.missingSaved")
|
||||
: selectedProxyProfile
|
||||
? `${selectedProxyProfile.label} - ${formatProxyConfigEndpoint(selectedProxyProfile.config)}`
|
||||
: `${formatProxyConfigType(form.proxyConfig)} ${formatProxyConfigEndpoint(form.proxyConfig)}`;
|
||||
: `${formatProxyConfigType(displayProxyConfig)} ${formatProxyConfigEndpoint(displayProxyConfig)}`;
|
||||
|
||||
const handleDistroModeChange = useCallback((mode: "auto" | "manual") => {
|
||||
setForm((prev) => ({
|
||||
@@ -458,13 +473,19 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
|
||||
(field: keyof ProxyConfig, value: ProxyConfig[keyof ProxyConfig]) => {
|
||||
setForm((prev) => {
|
||||
const { proxyProfileId: _proxyProfileId, directProxy: _directProxy, ...rest } = prev;
|
||||
const inheritedConfig = !prev.directProxy
|
||||
&& prev.proxyProfileId === undefined
|
||||
&& prev.proxyConfig === undefined
|
||||
&& effectiveGroupDefaults
|
||||
? applyGroupDefaults(prev, effectiveGroupDefaults).proxyConfig
|
||||
: undefined;
|
||||
return {
|
||||
...rest,
|
||||
proxyConfig: updateProxyConfigField(prev.proxyConfig, field, value),
|
||||
proxyConfig: updateProxyConfigField(prev.proxyConfig ?? inheritedConfig, field, value),
|
||||
} as Host;
|
||||
});
|
||||
},
|
||||
[],
|
||||
[effectiveGroupDefaults],
|
||||
);
|
||||
|
||||
const clearProxyConfig = useCallback(() => {
|
||||
@@ -475,12 +496,8 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
|
||||
}, []);
|
||||
|
||||
const selectProxyProfile = useCallback((profileId: string | undefined) => {
|
||||
setForm((prev) => {
|
||||
const { proxyConfig: _proxyConfig, proxyProfileId: _proxyProfileId, directProxy: _directProxy, ...rest } = prev;
|
||||
if (!profileId) return rest as Host;
|
||||
return { ...rest, proxyProfileId: profileId } as Host;
|
||||
});
|
||||
}, []);
|
||||
setForm((prev) => nextFormAfterSelectProxyProfile(prev, profileId, effectiveGroupDefaults));
|
||||
}, [effectiveGroupDefaults]);
|
||||
|
||||
const selectDirectProxy = useCallback(() => {
|
||||
setForm((prev) => {
|
||||
@@ -493,24 +510,18 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
hostChain: {
|
||||
hostIds: [...(prev.hostChain?.hostIds || []), hostId],
|
||||
hostIds: [...resolveFormHostChainIds(prev, effectiveGroupDefaults), hostId],
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const removeHostFromChain = (index: number) => {
|
||||
setForm((prev) => {
|
||||
const ids = (prev.hostChain?.hostIds || []).filter((_, i) => i !== index);
|
||||
return { ...prev, hostChain: ids.length > 0 ? { hostIds: ids } : undefined };
|
||||
});
|
||||
setForm((prev) => nextFormAfterRemoveHostFromChain(prev, index, effectiveGroupDefaults));
|
||||
};
|
||||
|
||||
const clearHostChain = useCallback(() => {
|
||||
setForm((prev) => {
|
||||
const { hostChain: _hostChain, ...rest } = prev;
|
||||
return rest as Host;
|
||||
});
|
||||
}, []);
|
||||
setForm((prev) => nextFormAfterClearHostChain(prev, effectiveGroupDefaults));
|
||||
}, [effectiveGroupDefaults]);
|
||||
|
||||
const [newEnvName, setNewEnvName] = useState("");
|
||||
const [newEnvValue, setNewEnvValue] = useState("");
|
||||
@@ -773,8 +784,16 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
|
||||
setActiveSubPanel("none");
|
||||
};
|
||||
|
||||
const displayHostChainIds = useMemo(
|
||||
() => form.hostChain !== undefined
|
||||
? (form.hostChain.hostIds || [])
|
||||
: (effectiveAuthHost.hostChain?.hostIds || []),
|
||||
[effectiveAuthHost.hostChain?.hostIds, form.hostChain],
|
||||
);
|
||||
const hostChainInherited = form.hostChain === undefined && displayHostChainIds.length > 0;
|
||||
|
||||
const availableHostsForChain = useMemo(() => {
|
||||
const chainedIds = new Set(form.hostChain?.hostIds || []);
|
||||
const chainedIds = new Set(displayHostChainIds);
|
||||
// Chain hops are SSH ProxyJump hops; never offer local/serial/telnet/plugin
|
||||
// entries (e.g. saved local shells) as jump hosts.
|
||||
return allHosts.filter((h) =>
|
||||
@@ -782,14 +801,13 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
|
||||
&& !chainedIds.has(h.id)
|
||||
&& canServeAsSshJumpHost(h),
|
||||
);
|
||||
}, [allHosts, form.id, form.hostChain?.hostIds]);
|
||||
}, [allHosts, displayHostChainIds, form.id]);
|
||||
|
||||
const chainedHosts = useMemo(() => {
|
||||
const ids = form.hostChain?.hostIds || [];
|
||||
return ids
|
||||
return displayHostChainIds
|
||||
.map((id) => allHosts.find((h) => h.id === id))
|
||||
.filter(Boolean) as Host[];
|
||||
}, [allHosts, form.hostChain?.hostIds]);
|
||||
}, [allHosts, displayHostChainIds]);
|
||||
|
||||
const groupOptions: ComboboxOption[] = useMemo(() => {
|
||||
return groups.map((g) => ({
|
||||
@@ -905,11 +923,12 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
|
||||
if (activeSubPanel === "proxy") {
|
||||
return (
|
||||
<ProxyPanel
|
||||
proxyConfig={form.proxyConfig}
|
||||
proxyConfig={displayProxyConfig}
|
||||
proxyProfiles={proxyProfiles}
|
||||
identities={identities}
|
||||
selectedProxyProfileId={form.proxyProfileId}
|
||||
selectedProxyProfileId={displayProxyProfileId}
|
||||
directProxy={form.directProxy}
|
||||
allowClear={hasHostProxyOverride}
|
||||
onUpdateProxy={updateProxyConfig}
|
||||
onSelectProxyProfile={selectProxyProfile}
|
||||
onSelectDirectProxy={selectDirectProxy}
|
||||
@@ -1237,11 +1256,13 @@ const HostDetailsPanel: React.FC<HostDetailsPanelPropsWithResize> = ({
|
||||
showAlgorithmOverrides={showAlgorithmOverrides}
|
||||
setShowAlgorithmOverrides={setShowAlgorithmOverrides}
|
||||
chainedHosts={chainedHosts}
|
||||
hostChainInherited={hostChainInherited}
|
||||
setActiveSubPanel={setActiveSubPanel}
|
||||
clearHostChain={clearHostChain}
|
||||
proxySummaryType={proxySummaryType}
|
||||
proxySummaryLabel={proxySummaryLabel}
|
||||
proxySummaryTooltip={proxySummaryTooltip}
|
||||
proxyInherited={proxyInherited}
|
||||
clearProxyConfig={clearProxyConfig}
|
||||
/>
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface ProxyPanelProps {
|
||||
onClearProxy: () => void;
|
||||
onBack: () => void;
|
||||
onCancel: () => void;
|
||||
allowClear?: boolean;
|
||||
layout?: AsidePanelLayout;
|
||||
}
|
||||
|
||||
@@ -51,6 +52,7 @@ export const ProxyPanel: React.FC<ProxyPanelPropsWithResize> = ({
|
||||
onClearProxy,
|
||||
onBack,
|
||||
onCancel,
|
||||
allowClear = true,
|
||||
layout = 'overlay',
|
||||
resizable,
|
||||
persistWidthStorageKey,
|
||||
@@ -346,7 +348,7 @@ export const ProxyPanel: React.FC<ProxyPanelPropsWithResize> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{(proxyConfig?.host || proxyConfig?.command || selectedProxyProfileId || directProxy) && (
|
||||
{allowClear && (proxyConfig?.host || proxyConfig?.command || selectedProxyProfileId || directProxy) && (
|
||||
<Button variant="ghost" className="w-full h-10 text-destructive" onClick={onClearProxy}>
|
||||
<Trash2 size={14} className="mr-2" /> {t('hostDetails.proxyPanel.remove')}
|
||||
</Button>
|
||||
|
||||
Reference in new issue
Block a user