feat(connection): test SSH tunnel profiles
This commit is contained in:
1 parent
27c60f0d30
commit
c7991b316e
19 files changed
+256
-4
No files matched your search
@@ -9,7 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { Loader2, Plus, Trash2 } from "@lucide/vue";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
|
||||
import { createTunnelProfile, tunnelProfileSummary, type TunnelProfileType } from "@/lib/connection/tunnelProfiles";
|
||||
import { createTunnelProfile, createTunnelProfileTestGuard, tunnelProfileSummary, type TunnelProfileType } from "@/lib/connection/tunnelProfiles";
|
||||
import type { TunnelProfile } from "@/types/database";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
|
||||
@@ -21,6 +21,9 @@ const draft = ref<TunnelProfile[]>([]);
|
||||
const selectedId = ref<string | null>(null);
|
||||
const isSaving = ref(false);
|
||||
const hasInitializedDraft = ref(false);
|
||||
const isTesting = ref(false);
|
||||
const testResult = ref<{ ok: boolean; message: string } | null>(null);
|
||||
const testGuard = createTunnelProfileTestGuard();
|
||||
|
||||
function cloneProfiles(profiles: TunnelProfile[]): TunnelProfile[] {
|
||||
return JSON.parse(JSON.stringify(profiles)) as TunnelProfile[];
|
||||
@@ -54,6 +57,22 @@ const selectedSsh = computed(() => (selected.value?.type === "ssh" ? selected.va
|
||||
const selectedProxy = computed(() => (selected.value?.type === "proxy" ? selected.value : null));
|
||||
const selectedHttp = computed(() => (selected.value?.type === "http_tunnel" ? selected.value : null));
|
||||
|
||||
function invalidateProfileTest() {
|
||||
testGuard.invalidate();
|
||||
isTesting.value = false;
|
||||
testResult.value = null;
|
||||
}
|
||||
|
||||
// Profile tests are asynchronous, so any selection or configuration change
|
||||
// must invalidate the request before it can publish a stale result.
|
||||
watch(
|
||||
[selectedId, selectedSsh],
|
||||
() => {
|
||||
invalidateProfileTest();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
function profileTypeLabel(profile: TunnelProfile): string {
|
||||
if (profile.type === "proxy") return "Proxy";
|
||||
if (profile.type === "http_tunnel") return t("connection.httpTunnel");
|
||||
@@ -96,6 +115,7 @@ function updateProxyType(value: unknown) {
|
||||
|
||||
async function save() {
|
||||
if (isSaving.value) return;
|
||||
invalidateProfileTest();
|
||||
isSaving.value = true;
|
||||
try {
|
||||
await store.saveProfiles(cloneProfiles(draft.value));
|
||||
@@ -106,6 +126,25 @@ async function save() {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testSelected() {
|
||||
const profile = selectedSsh.value;
|
||||
if (!profile || isTesting.value) return;
|
||||
const profileSnapshot = cloneProfiles([profile])[0];
|
||||
const requestId = testGuard.start(profileSnapshot);
|
||||
isTesting.value = true;
|
||||
testResult.value = null;
|
||||
try {
|
||||
const message = await store.testProfile(profileSnapshot);
|
||||
if (!testGuard.isCurrent(requestId, selectedSsh.value)) return;
|
||||
testResult.value = { ok: true, message: message || t("settings.tunnelsTestSuccess") };
|
||||
} catch (error) {
|
||||
if (!testGuard.isCurrent(requestId, selectedSsh.value)) return;
|
||||
testResult.value = { ok: false, message: t("settings.tunnelsTestFailed", { message: translateBackendError(t, String(error)) }) };
|
||||
} finally {
|
||||
if (testGuard.isCurrent(requestId, selectedSsh.value)) isTesting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -244,7 +283,7 @@ async function save() {
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Button type="button" size="sm" :disabled="!isDirty || isSaving" @click="save">
|
||||
<Loader2 v-if="isSaving" class="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
{{ t("settings.tunnelsSave") }}
|
||||
@@ -252,7 +291,15 @@ async function save() {
|
||||
<Button type="button" variant="outline" size="sm" :disabled="!isDirty || isSaving" @click="resetDraft">
|
||||
{{ t("settings.tunnelsReset") }}
|
||||
</Button>
|
||||
<Button v-if="selectedSsh" type="button" variant="outline" size="sm" :disabled="isTesting || isSaving || !selectedSsh.host.trim()" @click="testSelected">
|
||||
<Loader2 v-if="isTesting" class="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
{{ isTesting ? t("settings.tunnelsTesting") : t("settings.tunnelsTest") }}
|
||||
</Button>
|
||||
<p v-if="isDirty" class="text-xs text-muted-foreground">{{ t("settings.tunnelsUnsavedHint") }}</p>
|
||||
</div>
|
||||
|
||||
<p v-if="testResult" class="text-xs" :class="testResult.ok ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-500'">
|
||||
{{ testResult.message }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user