feat(sidebar): show connection details in tooltip

This commit is contained in:
t8y2 2026-06-18 15:31:38 +08:00
parent ede7d65165
commit 35c2d7973b
2 changed files with 102 additions and 8 deletions

View File

@ -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<ConnectionConfig, "db_type" | "port">): 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<ConnectionConfig, "db_type" | "ssl">): 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[] {
<template>
<CustomContextMenu :items="treeItemMenuItems()" v-slot="{ onContextMenu }">
<div @contextmenu="onTreeItemContextMenu($event, onContextMenu)">
<LightTooltip :text="displayLabel(node)" :disabled="isTooltipDisabled" side="right" :side-offset="8" :delay="0">
<LightTooltip :text="displayLabel(node)" :disabled="isTooltipDisabled()" side="right" :side-offset="8" :delay="0" :close-delay="0">
<div
ref="rowRef"
class="group flex items-center gap-1.5 py-1 px-2 cursor-pointer hover:bg-accent relative outline-none"
@ -3535,10 +3623,10 @@ function treeItemMenuItems(): ContextMenuItem[] {
<Pin class="w-3 h-3" :class="{ 'fill-current': isPinned }" />
</button>
</div>
<template v-if="tableInfoTooltip" #content>
<template v-if="detailTooltip" #content>
<div class="w-max min-w-40 max-w-[min(28rem,calc(100vw-24px))] rounded-md border border-border bg-popover p-2 text-popover-foreground shadow-lg">
<div class="space-y-1">
<div v-for="row in tableInfoTooltip.rows" :key="row.label" class="grid grid-cols-[max-content_minmax(0,1fr)] gap-2 text-[11px] leading-4">
<div v-for="row in detailTooltip.rows" :key="row.label" class="grid grid-cols-[max-content_minmax(0,1fr)] gap-2 text-xs leading-5">
<span class="text-muted-foreground">{{ row.label }}</span>
<span v-if="row.multiline" class="max-h-20 overflow-hidden whitespace-pre-wrap break-words text-foreground/90">
{{ row.value }}

View File

@ -8,6 +8,7 @@ const props = withDefaults(
side?: "top" | "right" | "bottom" | "left";
sideOffset?: number;
delay?: number;
closeDelay?: number;
openOnFocus?: boolean;
}>(),
{
@ -15,6 +16,7 @@ const props = withDefaults(
side: "top",
sideOffset: 8,
delay: 300,
closeDelay: 100,
openOnFocus: true,
},
);
@ -126,11 +128,15 @@ function closeIfTriggerInactive() {
}
function scheduleClose() {
if (props.closeDelay <= 0) {
if (!isTriggerActive()) close();
return;
}
if (closeTimer) return;
closeTimer = setTimeout(() => {
closeTimer = null;
if (!isTriggerActive()) close();
}, 100);
}, props.closeDelay);
}
function onPointerDown(e: PointerEvent) {