feat(sidebar): copy connection info as url jdbc or dsn

This commit is contained in:
邹超 authored and GitHub committed 2026-09-11 16:40:11 +08:00
1 parent 16866293a3
commit f9f9a28edf
13 files changed
+887 -1

No files matched your search

@@ -61,6 +61,7 @@ import {
Settings2,
GitBranch,
Sparkles,
Link2,
} from "@lucide/vue";
import type { ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stores/connectionStore";
@@ -70,7 +71,7 @@ import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { savedSqlErrorMessage } from "@/lib/savedSql/savedSqlErrors";
import { useToast } from "@/composables/useToast";
import { useDatabaseOptions } from "@/composables/useDatabaseOptions";
import type { ColumnInfo, DatabaseType, TreeNode, TreeNodeType } from "@/types/database";
import type { ColumnInfo, ConnectionConfig, DatabaseType, TreeNode, TreeNodeType } from "@/types/database";
import * as api from "@/lib/backend/api";
import type { ElasticsearchIndexMetadataKind } from "@/lib/backend/tauri";
import { queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
@@ -192,6 +193,7 @@ import { formatSidebarTableCopyText, type FormatSidebarTableNamesOptions } from
import { supportsScheduledDatabaseBackup } from "@/lib/backup/scheduledDatabaseBackup";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { copyToClipboard } from "@/lib/common/clipboard";
import { buildConnectionUrlCopy, CONNECTION_URL_COPY_WITH_PASSWORD_FORMATS, connectionUrlCopyFormats, type ConnectionUrlCopyFormat } from "@/lib/connection/connectionUrlBuilder";
import { rankSavedSqlHistory, type SavedSqlHistoryScope } from "@/lib/savedSql/savedSqlHistory";
import { savedSqlClipboardFileIds, savedSqlPasteTargetForNode } from "@/lib/savedSql/savedSqlClipboard";
import { exportSavedSqlFileContent } from "@/lib/savedSql/savedSqlExport";
@@ -2214,6 +2216,64 @@ function copyNameMenuItem(): ContextMenuItem {
return { label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value };
}
const CONNECTION_URL_COPY_LABEL_KEYS: Record<ConnectionUrlCopyFormat, string> = {
url: "contextMenu.copyConnectionUrl",
urlWithPassword: "contextMenu.copyConnectionUrlWithPassword",
jdbcUrl: "contextMenu.copyJdbcUrl",
jdbcUrlWithCredentials: "contextMenu.copyJdbcUrlWithCredentials",
hostPort: "contextMenu.copyHostPort",
dsn: "contextMenu.copyDsn",
dsnWithPassword: "contextMenu.copyDsnWithPassword",
psqlCommand: "contextMenu.copyPsqlCommand",
};
/**
* "Copy Connection Info" submenu for connection/database nodes: standard URL,
* JDBC URL, host:port, libpq DSN and psql command, trimmed to what the
* connection's dialect actually supports. The primary entries never embed the
* stored password; password-inclusive entries say so in their label and are
* gated behind a confirmation dialog. `databaseOverride` lets a database node
* copy the URL for that specific database instead of the default one.
*/
function connectionUrlCopyMenuItem(databaseOverride?: string): ContextMenuItem | null {
const node = activeNode.value;
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
if (!config) return null;
const formats = connectionUrlCopyFormats(config);
if (formats.length === 0) return null;
return {
label: t("contextMenu.copyConnectionInfo"),
icon: Link2,
children: formats.map((format) => ({
label: t(CONNECTION_URL_COPY_LABEL_KEYS[format]),
action: () => copyConnectionUrlFormat(config, format, databaseOverride),
})),
};
}
const showCopyConnectionSecretConfirm = shallowRef(false);
let pendingConnectionSecretCopy: { config: ConnectionConfig; format: ConnectionUrlCopyFormat; databaseOverride?: string } | null = null;
function copyConnectionUrlFormat(config: ConnectionConfig, format: ConnectionUrlCopyFormat, databaseOverride?: string) {
if (CONNECTION_URL_COPY_WITH_PASSWORD_FORMATS.has(format)) {
pendingConnectionSecretCopy = { config, format, databaseOverride };
showCopyConnectionSecretConfirm.value = true;
return;
}
return performConnectionUrlCopy(config, format, databaseOverride);
}
async function performConnectionUrlCopy(config: ConnectionConfig, format: ConnectionUrlCopyFormat, databaseOverride?: string) {
const text = buildConnectionUrlCopy(config, format, { database: databaseOverride });
if (!text) return;
try {
await copyToClipboard(text);
toast(t("connection.copied"), 2000);
} catch (e: any) {
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
}
}
async function copyCustomTypeDdl() {
const node = createSidebarActionTarget(activeNode.value);
if (!node.connectionId || !node.database) return;
@@ -4653,6 +4713,19 @@ routeDangerDialog(showDeleteSavedSqlConfirm, () =>
}),
);
routeDangerDialog(showCopyConnectionSecretConfirm, () =>
dangerRequest({
title: t("contextMenu.copyPasswordConfirmTitle"),
message: t("contextMenu.copyPasswordConfirmMessage"),
confirmLabel: t("contextMenu.copyPasswordConfirmAction"),
confirm: () => {
const pending = pendingConnectionSecretCopy;
pendingConnectionSecretCopy = null;
if (pending) return performConnectionUrlCopy(pending.config, pending.format, pending.databaseOverride);
},
}),
);
routeDangerDialog(showEmptyTableConfirm, () =>
dangerRequest({
title: t("contextMenu.confirmEmptyTableTitle"),
@@ -5347,6 +5420,8 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean
}
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
const connectionUrlCopyMenu = connectionUrlCopyMenuItem();
if (connectionUrlCopyMenu) items.push(connectionUrlCopyMenu);
items.push({ label: "", separator: true });
const supportsQueryActions = supportsConnectionQueryActions(currentDatabaseType());
const supportsAiContext = supportsAiAssistantContext(currentDatabaseType());
@@ -5565,6 +5640,8 @@ function buildDatabaseSidebarMenu(context: SidebarMenuFactoryContext): boolean {
items.unshift({ label: t("contextMenu.closeDatabaseConnection"), action: closeDatabaseConnection, icon: Unplug });
}
items.push(copyNameMenuItem());
const databaseUrlCopyMenu = connectionUrlCopyMenuItem(node.database || undefined);
if (databaseUrlCopyMenu) items.push(databaseUrlCopyMenu);
items.push({ label: "", separator: true });
if (canOpenObjectBrowser.value) {
items.push({ label: t("contextMenu.openObjectBrowser"), action: openObjectBrowser, icon: TableProperties });