feat: improve connection and table tree UX
This commit is contained in:
parent
efdea3b13d
commit
8bd1d4c2a8
35
src/App.vue
35
src/App.vue
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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(" · ");
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
Loading…
Reference in New Issue