feat: improve connection and table tree UX

This commit is contained in:
SuLea-IT 2026-05-04 11:49:28 +08:00
parent efdea3b13d
commit 8bd1d4c2a8
6 changed files with 137 additions and 28 deletions

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted, nextTick, defineAsyncComponent, type Ref } from "vue";
import { useI18n } from "vue-i18n";
import { DatabaseZap, FilePlus2, Play, Loader2, Square, X, Globe, Moon, Sun, Upload, Download, Plus, History, Server, Table2, Database, Search, ShieldCheck, Bot, Pin, AlignLeft, CloudDownload, ArrowLeftRight, FileCode, Settings, Sparkles, GitBranch } from "lucide-vue-next";
import { DatabaseZap, FilePlus2, Play, Loader2, Square, X, Globe, Moon, Sun, Upload, Download, Plus, History, Table2, Database, Search, ShieldCheck, Bot, Pin, AlignLeft, CloudDownload, ArrowLeftRight, FileCode, Settings, Sparkles, GitBranch } from "lucide-vue-next";
import { Splitpanes, Pane } from "splitpanes";
import "splitpanes/dist/splitpanes.css";
import { Button } from "@/components/ui/button";
@ -55,6 +55,7 @@ import { getCurrentWebview } from "@tauri-apps/api/webview";
import { getVersion } from "@tauri-apps/api/app";
import * as api from "@/lib/tauri";
import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/queryExecutionState";
import { connectionDriverLabel, connectionIconType, connectionOptionSubtitle } from "@/lib/connectionPresentation";
import { resolveExecutableSql } from "@/lib/sqlExecutionTarget";
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
import { isTauriRuntime } from "@/lib/tauriRuntime";
@ -438,14 +439,6 @@ function connectionDisplayName(connectionId: string): string {
return connectionStore.getConfig(connectionId)?.name || connectionId;
}
function connectionDriverLabel(connection?: ConnectionConfig): string {
return connection?.driver_label || connection?.db_type.toUpperCase() || "";
}
function connectionIconType(connection?: ConnectionConfig): string {
return connection?.driver_profile || connection?.db_type || "postgres";
}
function connectionColor(connectionId: string): string {
return connectionStore.getConfig(connectionId)?.color || "";
}
@ -1229,25 +1222,31 @@ async function setupFileDrop() {
<div class="flex items-center gap-2">
<div class="flex items-center gap-1">
<span v-if="activeConnection?.color" class="h-4 w-1 rounded-full shrink-0" :style="{ backgroundColor: activeConnection.color }" />
<Server class="h-3.5 w-3.5 shrink-0" />
<Select
:model-value="activeConnectionValue"
@update:model-value="changeActiveConnection"
>
<SelectTrigger class="h-6 w-auto max-w-48 border-0 bg-transparent px-1 text-xs font-medium text-foreground shadow-none focus:ring-0">
<SelectValue :placeholder="t('editor.selectConnection')">
{{ connectionDisplayName(activeConnectionValue) }}
</SelectValue>
<SelectTrigger class="h-6 w-auto max-w-56 border-0 bg-transparent px-1 text-xs font-medium text-foreground shadow-none focus:ring-0">
<div v-if="activeConnection" class="flex min-w-0 items-center gap-1.5">
<DatabaseIcon :db-type="connectionIconType(activeConnection)" class="h-3.5 w-3.5 shrink-0" />
<span class="truncate">{{ connectionDisplayName(activeConnectionValue) }}</span>
</div>
<SelectValue v-else :placeholder="t('editor.selectConnection')" />
</SelectTrigger>
<SelectContent>
<SelectContent class="min-w-64">
<SelectItem
v-for="connection in connectionStore.connections"
:key="connection.id"
:value="connection.id"
>
<div class="flex items-center gap-2">
<div class="flex min-w-0 items-center gap-2">
<span v-if="connection.color" class="h-3.5 w-1 rounded-full shrink-0" :style="{ backgroundColor: connection.color }" />
<span>{{ connection.name }}</span>
<span v-else class="h-3.5 w-1 shrink-0" />
<DatabaseIcon :db-type="connectionIconType(connection)" class="h-3.5 w-3.5 shrink-0" />
<div class="min-w-0 flex-1">
<div class="truncate">{{ connection.name }}</div>
<div class="truncate text-[11px] font-normal text-muted-foreground">{{ connectionOptionSubtitle(connection) }}</div>
</div>
</div>
</SelectItem>
</SelectContent>
@ -1497,7 +1496,7 @@ async function setupFileDrop() {
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium">{{ connection.name }}</div>
<div class="truncate text-xs text-muted-foreground">
{{ connectionDriverLabel(connection) }} · {{ connection.host || connection.database || 'local' }}{{ connection.port ? ':' + connection.port : '' }}
{{ connectionOptionSubtitle(connection) || connectionDriverLabel(connection) }}
</div>
</div>
<FilePlus2 class="h-4 w-4 text-muted-foreground" />

View File

@ -25,6 +25,7 @@ import {
type ExportedTableSql,
} from "@/lib/databaseExport";
import { qualifiedTableName as buildQualifiedTableName, quoteTableIdentifier } from "@/lib/tableSelectSql";
import { treeNodeRowAction } from "@/lib/treeNodeClick";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
@ -178,14 +179,10 @@ async function toggle() {
function onClick() {
const node = props.node;
if (node.type === "table" || node.type === "view") {
const action = treeNodeRowAction(node.type, canExpand);
if (action === "open-data") {
openData();
toggle();
} else if (node.type === "redis-db") {
toggle();
} else if (node.type === "mongo-collection") {
toggle();
} else if (canExpand) {
} else if (action === "toggle") {
toggle();
}
}
@ -629,9 +626,15 @@ async function showMore() {
@click="onClick"
>
<template v-if="canExpand">
<Loader2 v-if="node.isLoading" class="w-3.5 h-3.5 shrink-0 animate-spin text-muted-foreground" />
<ChevronDown v-else-if="node.isExpanded" class="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
<ChevronRight v-else class="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
<button
type="button"
class="-m-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground"
@click.stop="toggle"
>
<Loader2 v-if="node.isLoading" class="w-3.5 h-3.5 animate-spin" />
<ChevronDown v-else-if="node.isExpanded" class="w-3.5 h-3.5" />
<ChevronRight v-else class="w-3.5 h-3.5" />
</button>
</template>
<span v-else class="w-3.5 h-3.5 shrink-0" />
<DatabaseIcon v-if="node.type === 'connection'" :db-type="connectionIconType(node.connectionId)" class="w-3.5 h-3.5 shrink-0" />

View File

@ -0,0 +1,29 @@
import type { ConnectionConfig } from "@/types/database";
type ConnectionPresentationConfig = Pick<
ConnectionConfig,
"db_type" | "driver_profile" | "driver_label" | "host" | "port" | "database"
>;
const LOCAL_DATABASE_TYPES = new Set(["sqlite", "duckdb"]);
export function connectionIconType(connection?: Pick<ConnectionConfig, "db_type" | "driver_profile">): string {
return connection?.driver_profile || connection?.db_type || "postgres";
}
export function connectionDriverLabel(connection?: Pick<ConnectionConfig, "db_type" | "driver_label">): string {
return connection?.driver_label || connection?.db_type.toUpperCase() || "";
}
export function connectionEndpointLabel(connection?: ConnectionPresentationConfig): string {
if (!connection) return "";
if (LOCAL_DATABASE_TYPES.has(connection.db_type)) {
return connection.host || connection.database || "local";
}
if (connection.host && connection.port) return `${connection.host}:${connection.port}`;
return connection.host || connection.database || "";
}
export function connectionOptionSubtitle(connection?: ConnectionPresentationConfig): string {
return [connectionDriverLabel(connection), connectionEndpointLabel(connection)].filter(Boolean).join(" · ");
}

13
src/lib/treeNodeClick.ts Normal file
View File

@ -0,0 +1,13 @@
import type { TreeNodeType } from "@/types/database";
export type TreeNodeRowAction = "open-data" | "toggle" | "none";
const dataNodeTypes = new Set<TreeNodeType>(["table", "view"]);
const toggleLeafNodeTypes = new Set<TreeNodeType>(["redis-db", "mongo-collection"]);
export function treeNodeRowAction(type: TreeNodeType, canExpand: boolean): TreeNodeRowAction {
if (dataNodeTypes.has(type)) return "open-data";
if (toggleLeafNodeTypes.has(type)) return "toggle";
if (canExpand) return "toggle";
return "none";
}

View File

@ -0,0 +1,40 @@
import test from "node:test";
import assert from "node:assert/strict";
import type { ConnectionConfig } from "../src/types/database.ts";
import { connectionDriverLabel, connectionEndpointLabel, connectionIconType, connectionOptionSubtitle } from "../src/lib/connectionPresentation.ts";
const baseConnection: ConnectionConfig = {
id: "conn-1",
name: "localhost",
db_type: "mysql",
driver_profile: "tidb",
driver_label: "TiDB",
host: "127.0.0.1",
port: 4000,
username: "root",
password: "",
database: "test",
};
test("uses driver profile for connection option icon identity", () => {
assert.equal(connectionIconType(baseConnection), "tidb");
});
test("builds a compact subtitle for duplicate connection names", () => {
assert.equal(connectionDriverLabel(baseConnection), "TiDB");
assert.equal(connectionEndpointLabel(baseConnection), "127.0.0.1:4000");
assert.equal(connectionOptionSubtitle(baseConnection), "TiDB · 127.0.0.1:4000");
});
test("uses file path as endpoint for local database connections", () => {
const sqliteConnection: ConnectionConfig = {
...baseConnection,
db_type: "sqlite",
driver_profile: "sqlite",
driver_label: "SQLite",
host: "/tmp/local.db",
port: 0,
};
assert.equal(connectionOptionSubtitle(sqliteConnection), "SQLite · /tmp/local.db");
});

View File

@ -0,0 +1,25 @@
import test from "node:test";
import assert from "node:assert/strict";
import { treeNodeRowAction } from "../src/lib/treeNodeClick.ts";
test("table and view rows open data without toggling structure groups", () => {
assert.equal(treeNodeRowAction("table", true), "open-data");
assert.equal(treeNodeRowAction("view", true), "open-data");
});
test("expandable non-table rows still toggle from row clicks", () => {
assert.equal(treeNodeRowAction("connection", true), "toggle");
assert.equal(treeNodeRowAction("database", true), "toggle");
assert.equal(treeNodeRowAction("schema", true), "toggle");
assert.equal(treeNodeRowAction("group-columns", true), "toggle");
});
test("leaf data browser nodes keep their open behavior through toggle handler", () => {
assert.equal(treeNodeRowAction("redis-db", false), "toggle");
assert.equal(treeNodeRowAction("mongo-collection", false), "toggle");
});
test("plain metadata leaf rows do nothing on row clicks", () => {
assert.equal(treeNodeRowAction("column", false), "none");
assert.equal(treeNodeRowAction("index", false), "none");
});