feat(connection): add reusable tunnel profiles

This commit is contained in:
rainhan authored and GitHub committed 2026-07-12 22:03:46 +08:00
1 parent 4dcee1aca0
commit 977e7f78ca
34 files changed
+1335 -22

No files matched your search

+28 -2
View File
@@ -1,7 +1,7 @@
import { defineStore } from "pinia";
import { uuid } from "@/lib/common/utils";
import { ref, computed, watch } from "vue";
import type { ColumnInfo, CompletionAssistantCandidate, CompletionAssistantObjectKind, CompletionAssistantRequest, ConnectionConfig, CatalogInfo, ForeignKeyInfo, ObjectInfo, SchemaInfo, SidebarLayout, TableInfo, TreeNode, VectorCollectionMeta } from "@/types/database";
import type { ColumnInfo, CompletionAssistantCandidate, CompletionAssistantObjectKind, CompletionAssistantRequest, ConnectionConfig, CatalogInfo, ForeignKeyInfo, ObjectInfo, SchemaInfo, SidebarLayout, TableInfo, TreeNode, TunnelProfile, VectorCollectionMeta } from "@/types/database";
import { applyPinnedTreeNodeState, updatePinnedTreeNodeInPlace } from "@/lib/app/pinnedItems";
import {
reconcileLayout,
@@ -23,6 +23,7 @@ import {
import type { SqlCompletionColumn, SqlCompletionForeignKey, SqlCompletionObject, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
import * as api from "@/lib/backend/api";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
import { connectionIsDorisFamilyCatalogCapable, isInternalDorisCatalog, isSchemaAware, normalizeSidebarObjectKind, sidebarObjectKindsForDatabase, usesTreeSchemaMode } from "@/lib/database/databaseCapabilities";
import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { buildDatabaseTreeNodes, buildDuckDbConnectionTreeNodes, sortSidebarDatabases, sortSidebarNames, shouldIncludeDefaultDatabaseNode } from "@/lib/database/databaseTree";
@@ -4672,7 +4673,9 @@ export const useConnectionStore = defineStore("connection", () => {
async function exportConnectionsToFile(passphrase: string) {
const { encryptConfig } = await import("@/lib/backend/configCrypto");
const exportData = { connections: connections.value, layout: sidebarLayout.value };
const tunnelProfileStore = useTunnelProfileStore();
await tunnelProfileStore.init();
const exportData = { connections: connections.value, layout: sidebarLayout.value, tunnelProfiles: tunnelProfileStore.profiles };
const json = JSON.stringify(exportData);
const payload = await encryptConfig(json, passphrase);
const content = JSON.stringify(payload, null, 2);
@@ -4860,6 +4863,7 @@ export const useConnectionStore = defineStore("connection", () => {
async function importConnectionsFromFile(content: string, passphrase: string | null): Promise<{ count: number; layout?: SidebarLayout }> {
let imported: ConnectionConfig[] = [];
let importedLayout: SidebarLayout | undefined;
let importedTunnelProfiles: TunnelProfile[] = [];
if (!passphrase && content.trimStart().startsWith("<")) {
const { parseNavicatConnections } = await import("@/lib/imports/navicatImport");
@@ -4889,6 +4893,9 @@ export const useConnectionStore = defineStore("connection", () => {
if (parsed.layout?.groups && parsed.layout?.order) {
importedLayout = parsed.layout;
}
if (Array.isArray(parsed.tunnelProfiles)) {
importedTunnelProfiles = parsed.tunnelProfiles;
}
} else {
imported = [];
}
@@ -4907,12 +4914,31 @@ export const useConnectionStore = defineStore("connection", () => {
if (decrypted.layout?.groups && decrypted.layout?.order) {
importedLayout = decrypted.layout;
}
if (Array.isArray(decrypted.tunnelProfiles)) {
importedTunnelProfiles = decrypted.tunnelProfiles;
}
} else {
imported = [];
}
}
}
// Profiles keep their original ids: imported connections reference them
// via transport_layers[].profile_id, so regenerating ids would break the
// links. Same-id profiles are overwritten with the imported copy.
if (importedTunnelProfiles.length) {
const tunnelProfileStore = useTunnelProfileStore();
await tunnelProfileStore.init();
const merged = [...tunnelProfileStore.profiles];
for (const profile of importedTunnelProfiles) {
if (!profile || typeof profile.id !== "string" || !profile.id) continue;
const index = merged.findIndex((existing) => existing.id === profile.id);
if (index >= 0) merged[index] = profile;
else merged.push(profile);
}
await tunnelProfileStore.saveProfiles(merged);
}
let count = 0;
const importedConnectionIdMap = new Map<string, string>();
for (const config of imported) {
@@ -0,0 +1,48 @@
import { ref } from "vue";
import { defineStore } from "pinia";
import * as api from "@/lib/backend/api";
import type { TunnelProfile } from "@/types/database";
/**
* Shared tunnel profiles (Settings > Tunnels). Connections reference a
* profile by id via `transport_layers[].profile_id`; the backend resolves
* the reference at connect time, so profile edits reach every referencing
* connection without touching the stored connections.
*/
export const useTunnelProfileStore = defineStore("tunnelProfiles", () => {
const profiles = ref<TunnelProfile[]>([]);
const isLoaded = ref(false);
async function init() {
if (isLoaded.value) return;
await refresh();
}
async function refresh() {
try {
profiles.value = (await api.loadTunnelProfiles()) || [];
isLoaded.value = true;
} catch {
// Backend unavailable (e.g. stale web session): keep previous state and
// retry on the next init/refresh call.
}
}
function profileById(id: string | undefined): TunnelProfile | undefined {
if (!id) return undefined;
return profiles.value.find((profile) => profile.id === id);
}
async function saveProfiles(next: TunnelProfile[]) {
const previous = profiles.value;
profiles.value = next;
try {
await api.saveTunnelProfiles(next);
} catch (error) {
profiles.value = previous;
throw error;
}
}
return { profiles, isLoaded, init, refresh, profileById, saveProfiles };
});