diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 7eccd7119..14d1fcdbc 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -60,7 +60,7 @@ import { useSettingsStore } from "@/stores/settingsStore"; import { useSavedSqlStore } from "@/stores/savedSqlStore"; 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/api"; import { uuid } from "@/lib/utils"; import { resolveDefaultDatabase } from "@/lib/defaultDatabase"; @@ -305,22 +305,110 @@ function visibleLabel(node: TreeNode): string { return displayLabel(node); } -function cleanTooltipValue(value: string | null | undefined): string { +type DetailTooltipRow = { + label: string; + value: string; + multiline?: boolean; +}; + +function cleanTooltipValue(value: string | number | null | undefined): string { return String(value ?? "").trim(); } +function isLocalFileConnection(config: Pick): boolean { + return config.db_type === "sqlite" || config.db_type === "duckdb" || config.db_type === "access" || (config.db_type === "h2" && config.port === 0); +} + +function redactedConnectionString(value: string): string { + return value.replace(/(:\/\/[^/\s:@?#;]+):([^@\s/?#;]+)@/g, "$1:***@").replace(/([?&;](?:password|pwd|pass|token|secret|key)=)[^&;]*/gi, "$1***"); +} + +function connectionTooltipScheme(config: Pick): string { + switch (config.db_type) { + case "postgres": + case "gaussdb": + case "kwdb": + case "yashandb": + case "redshift": + case "questdb": + return "postgresql"; + case "sqlserver": + return "mssql"; + case "elasticsearch": + case "rqlite": + case "turso": + case "mq": + return config.ssl ? "https" : "http"; + case "dameng": + return "dm"; + default: + return config.db_type; + } +} + +function hostForDisplay(host: string): string { + if (!host.includes(":") || host.startsWith("[") || host.includes("://")) return host; + return `[${host}]`; +} + +function connectionTooltipUrl(config: ConnectionConfig): string { + const explicit = cleanTooltipValue(config.connection_string); + if (explicit) return redactedConnectionString(explicit); + + const host = cleanTooltipValue(config.host); + if (!host) return ""; + if (host.includes("://")) return redactedConnectionString(host); + + if (isLocalFileConnection(config)) { + if (config.db_type === "access") return `jdbc:ucanaccess://${host}`; + return `${config.db_type}://${host}`; + } + + const scheme = connectionTooltipScheme(config); + const port = Number(config.port) > 0 ? `:${config.port}` : ""; + const user = cleanTooltipValue(config.username); + const userInfo = user ? `${encodeURIComponent(user)}@` : ""; + const database = cleanTooltipValue(config.database); + const path = database ? `/${encodeURIComponent(database)}` : ""; + const params = cleanTooltipValue(config.url_params); + const query = params ? (params.startsWith("?") ? params : `?${params}`) : ""; + return redactedConnectionString(`${scheme}://${userInfo}${hostForDisplay(host)}${port}${path}${query}`); +} + +const connectionInfoTooltip = computed(() => { + const node = props.node; + if (node.type !== "connection" || !node.connectionId) return null; + const config = connectionStore.getConfig(node.connectionId); + if (!config) return null; + + const hostLabel = isLocalFileConnection(config) ? t("connection.filePath") : t("connection.host"); + const rows: DetailTooltipRow[] = [ + { label: t("connection.name"), value: cleanTooltipValue(config.name) }, + { label: "URL", value: connectionTooltipUrl(config), multiline: true }, + { label: hostLabel, value: cleanTooltipValue(config.host), multiline: isLocalFileConnection(config) }, + { label: "Port", value: Number(config.port) > 0 ? String(config.port) : "" }, + { label: t("connection.database"), value: cleanTooltipValue(config.database) }, + { label: t("connection.user"), value: cleanTooltipValue(config.username) }, + { label: t("connection.type"), value: config.driver_label || config.driver_profile || config.db_type }, + ].filter((row) => row.value); + + return { rows }; +}); + const tableInfoTooltip = computed(() => { const node = props.node; if (node.type !== "table" && node.type !== "view") return null; - const rows = [ + const rows: DetailTooltipRow[] = [ { label: t("structureEditor.tableName"), value: visibleLabel(node) }, { label: t("structureEditor.comment"), value: cleanTooltipValue(node.comment), multiline: true }, ].filter((row) => row.value); return { rows }; }); +const detailTooltip = computed(() => connectionInfoTooltip.value ?? tableInfoTooltip.value); + function isTooltipDisabled(): boolean { - if (tableInfoTooltip.value?.rows.length && !settingsStore.editorSettings.sidebarHideTableComments) return true; + if (detailTooltip.value?.rows.length) return isRenamingGroup.value; return isRenamingGroup.value || !isLabelTruncated(); } @@ -3459,7 +3547,7 @@ function treeItemMenuItems(): ContextMenuItem[] {