fix(tunnels): 恢复 SSH config 别名选择 (#3540)

This commit is contained in:
zipg authored and GitHub committed 2026-07-15 18:05:30 +08:00
1 parent 7ca89b5740
commit d65917d9f0
4 files changed
+123 -19

No files matched your search

@@ -20,6 +20,7 @@ import type { NacosAdminConfig, NacosAuthConfig } from "@/types/nacos";
import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stores/connectionStore";
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSummary } from "@/lib/connection/tunnelProfiles";
import { applySshConfigHostAliasPrefill as prefillSshConfigHostAlias } from "@/lib/connection/sshConfigHosts";
import { REDIS_SCAN_PAGE_SIZE_DEFAULT, REDIS_SCAN_PAGE_SIZE_MIN, REDIS_SCAN_PAGE_SIZE_MAX, REDIS_SCAN_PAGE_SIZE_OPTIONS } from "@/lib/redis/redisKeyPattern";
import { useSettingsStore } from "@/stores/settingsStore";
import { useToast } from "@/composables/useToast";
@@ -3480,24 +3481,8 @@ watch([() => editingId.value, () => open.value], () => {
const sshConfigHostAliases = computed(() => sshConfigHosts.value.map((entry) => entry.alias));
/**
* Prefills user/port/key_path from a matching ~/.ssh/config alias, without
* overwriting values the user already changed away from the form defaults.
* This is a UX preview only — the authoritative resolution happens in the
* Rust backend at connect time (see resolve_ssh_tunnel_config), so imported
* configs that never touched this UI still resolve correctly.
*/
function applySshConfigHostAliasPrefill(target: SshTunnelConfig) {
const entry = sshConfigHosts.value.find((candidate) => candidate.alias === target.host);
if (!entry) return;
if (target.user === DEFAULT_SSH_USER && entry.user) target.user = entry.user;
if (target.port === 22 && entry.port) target.port = entry.port;
if (!target.key_path && entry.identity_file) {
target.key_path = entry.identity_file;
if ((!target.auth_method || target.auth_method === "password") && !target.password?.trim()) {
target.auth_method = "key";
}
}
prefillSshConfigHostAlias(target, sshConfigHosts.value);
}
async function browseSshKeyPath(target?: SshTunnelConfig | null) {
@@ -10,7 +10,9 @@ import { Loader2, Plus, Trash2 } from "@lucide/vue";
import { useToast } from "@/composables/useToast";
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
import { createTunnelProfile, createTunnelProfileTestGuard, tunnelProfileSummary, type TunnelProfileType } from "@/lib/connection/tunnelProfiles";
import type { TunnelProfile } from "@/types/database";
import { applySshConfigHostAliasPrefill as prefillSshConfigHostAlias } from "@/lib/connection/sshConfigHosts";
import * as api from "@/lib/backend/api";
import type { SshConfigHostEntry, TunnelProfile } from "@/types/database";
import { translateBackendError } from "@/i18n/backend-errors";
const { t } = useI18n();
@@ -24,6 +26,7 @@ const hasInitializedDraft = ref(false);
const isTesting = ref(false);
const testResult = ref<{ ok: boolean; message: string } | null>(null);
const testGuard = createTunnelProfileTestGuard();
const sshConfigHosts = ref<SshConfigHostEntry[]>([]);
function cloneProfiles(profiles: TunnelProfile[]): TunnelProfile[] {
return JSON.parse(JSON.stringify(profiles)) as TunnelProfile[];
@@ -56,6 +59,23 @@ const selected = computed(() => draft.value.find((profile) => profile.id === sel
const selectedSsh = computed(() => (selected.value?.type === "ssh" ? selected.value : null));
const selectedProxy = computed(() => (selected.value?.type === "proxy" ? selected.value : null));
const selectedHttp = computed(() => (selected.value?.type === "http_tunnel" ? selected.value : null));
const sshConfigHostAliases = computed(() => sshConfigHosts.value.map((entry) => entry.alias));
async function loadSshConfigHosts() {
try {
sshConfigHosts.value = await api.listSshConfigHosts();
} catch {
sshConfigHosts.value = [];
}
}
function updateSelectedSshHost(value: string | number) {
if (!selectedSsh.value) return;
selectedSsh.value.host = String(value);
prefillSshConfigHostAlias(selectedSsh.value, sshConfigHosts.value);
}
void loadSshConfigHosts();
function invalidateProfileTest() {
testGuard.invalidate();
@@ -190,7 +210,10 @@ async function testSelected() {
<template v-if="selectedSsh">
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-xs">{{ t("connection.sshHost") }}</Label>
<Input v-model="selectedSsh.host" class="col-span-2" :placeholder="t('connection.sshHostPlaceholder')" />
<Input :model-value="selectedSsh.host" class="col-span-2" list="tunnel-profile-ssh-config-host-aliases" :placeholder="t('connection.sshHostPlaceholder')" @update:model-value="updateSelectedSshHost" />
<datalist id="tunnel-profile-ssh-config-host-aliases">
<option v-for="alias in sshConfigHostAliases" :key="alias" :value="alias" />
</datalist>
<Input v-model.number="selectedSsh.port" type="number" min="1" max="65535" class="col-span-1" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
@@ -0,0 +1,23 @@
import type { SshConfigHostEntry, SshTunnelConfig } from "@/types/database";
const DEFAULT_SSH_USER = "root";
const DEFAULT_SSH_PORT = 22;
/**
* Prefills an SSH form from a matching ~/.ssh/config alias without
* overwriting values the user already changed away from the form defaults.
* The Rust backend remains responsible for authoritative alias resolution at
* connect time; this helper only keeps the form preview consistent.
*/
export function applySshConfigHostAliasPrefill(target: SshTunnelConfig, hosts: readonly SshConfigHostEntry[]): void {
const entry = hosts.find((candidate) => candidate.alias === target.host);
if (!entry) return;
if (target.user === DEFAULT_SSH_USER && entry.user) target.user = entry.user;
if (target.port === DEFAULT_SSH_PORT && entry.port) target.port = entry.port;
if (!target.key_path && entry.identity_file) {
target.key_path = entry.identity_file;
if ((!target.auth_method || target.auth_method === "password") && !target.password?.trim()) {
target.auth_method = "key";
}
}
}