From fee3ed1d5ab920ff9a50c7a038530aefbeae8bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Wed, 12 Aug 2026 02:20:05 +0800 Subject: [PATCH] feat(desktop): add connection-level database browser --- .../src/components/layout/AppTabBar.vue | 7 +- .../src/components/layout/ContentArea.vue | 10 + .../layout/__tests__/AppTabBar.spec.ts | 8 +- .../components/objects/DatabaseBrowser.vue | 334 ++++++++++++++++++ .../__tests__/DatabaseBrowserFallback.spec.ts | 11 + .../sidebar/SidebarTreeRuntimeHost.vue | 27 +- apps/desktop/src/i18n/locales/en.ts | 9 + apps/desktop/src/i18n/locales/es.ts | 9 + apps/desktop/src/i18n/locales/it.ts | 9 + apps/desktop/src/i18n/locales/ja.ts | 9 + apps/desktop/src/i18n/locales/ko.ts | 9 + apps/desktop/src/i18n/locales/pt-BR.ts | 9 + apps/desktop/src/i18n/locales/zh-CN.ts | 9 + apps/desktop/src/i18n/locales/zh-TW.ts | 9 + .../database/databaseFeatureSupport.spec.ts | 8 + .../sidebar/sidebarActiveTabTarget.spec.ts | 13 + .../__tests__/sidebar/treeNodeClick.spec.ts | 5 + apps/desktop/src/lib/backend/api.ts | 1 + apps/desktop/src/lib/backend/http.ts | 4 + apps/desktop/src/lib/backend/tauri.ts | 4 + .../lib/database/databaseFeatureSupport.ts | 4 + .../src/lib/sidebar/sidebarActiveTabTarget.ts | 12 + apps/desktop/src/lib/sidebar/treeNodeClick.ts | 5 +- apps/desktop/src/lib/tabs/tabPresentation.ts | 5 + .../queryStore.database-open.spec.ts | 14 + apps/desktop/src/stores/queryStore.ts | 24 ++ apps/desktop/src/types/database.ts | 7 + crates/dbx-core/src/db/clickhouse_driver.rs | 6 +- crates/dbx-core/src/db/cloudflare_d1/mod.rs | 2 +- crates/dbx-core/src/db/dolt.rs | 2 +- crates/dbx-core/src/db/hbase_driver.rs | 2 +- crates/dbx-core/src/db/influxdb_driver.rs | 4 +- crates/dbx-core/src/db/mysql.rs | 46 ++- crates/dbx-core/src/db/ob_oracle.rs | 2 +- crates/dbx-core/src/db/postgres.rs | 43 ++- crates/dbx-core/src/db/rqlite_driver.rs | 2 +- crates/dbx-core/src/db/sqlite.rs | 2 +- crates/dbx-core/src/db/sqlserver.rs | 44 ++- crates/dbx-core/src/db/turso_driver.rs | 2 +- .../dbx-core/src/db/victoriametrics_driver.rs | 2 +- crates/dbx-core/src/schema.rs | 43 ++- crates/dbx-core/src/types.rs | 14 +- crates/dbx-web/src/main.rs | 1 + crates/dbx-web/src/routes/schema.rs | 9 + src-tauri/src/commands/schema.rs | 8 + src-tauri/src/lib.rs | 1 + 46 files changed, 781 insertions(+), 29 deletions(-) create mode 100644 apps/desktop/src/components/objects/DatabaseBrowser.vue create mode 100644 apps/desktop/src/components/objects/__tests__/DatabaseBrowserFallback.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/sidebar/sidebarActiveTabTarget.spec.ts diff --git a/apps/desktop/src/components/layout/AppTabBar.vue b/apps/desktop/src/components/layout/AppTabBar.vue index e67a8b57b1..d3e4921ce2 100644 --- a/apps/desktop/src/components/layout/AppTabBar.vue +++ b/apps/desktop/src/components/layout/AppTabBar.vue @@ -2,7 +2,7 @@ import { computed, ref, watch, nextTick, onUnmounted } from "vue"; import type { CSSProperties } from "vue"; import { useI18n } from "vue-i18n"; -import { X, Pin, ChevronDown, Table2, Code2, TableProperties, PencilRuler, KeyRound, Pencil, Package, Lock, Copy, AlertTriangle, Network, Minimize2, Maximize2, Settings, CalendarClock, Activity, Gauge, ShieldCheck } from "@lucide/vue"; +import { X, Pin, ChevronDown, Table2, Code2, TableProperties, PencilRuler, KeyRound, Pencil, Package, Lock, Copy, AlertTriangle, Network, Minimize2, Maximize2, Settings, CalendarClock, Activity, Gauge, ShieldCheck, Database } from "@lucide/vue"; import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -494,7 +494,7 @@ function tabColorStyle(tab: QueryTab) { function tabIconClass(tab: QueryTab) { if (tab.externalSqlFileMissing) return "text-amber-600 dark:text-amber-400"; if (tab.mode === "mq") return ""; - if (tab.mode === "objects") return "text-amber-500 dark:text-amber-400"; + if (tab.mode === "databases" || tab.mode === "objects") return "text-amber-500 dark:text-amber-400"; if (tab.mode === "data" || tab.mode === "mongo" || tab.mode === "vector" || tab.mode === "redis" || tab.mode === "hbase" || tab.mode === "structure") return "text-emerald-600 dark:text-emerald-400"; return "text-blue-600 dark:text-blue-400"; } @@ -530,6 +530,7 @@ function tabMenuIcon(tab: QueryTab) { if (tab.mode === "etcd-dashboard") return Gauge; if (tab.mode === "etcd-access-control") return ShieldCheck; if (tab.mode === "nacos") return Network; + if (tab.mode === "databases") return Database; if (tab.mode === "objects") return TableProperties; if (tab.mode === "structure") return PencilRuler; if (tab.mode === "dameng-jobs") return CalendarClock; @@ -677,6 +678,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul + @@ -874,6 +876,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul + diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 2001c12d3e..03830b3cc4 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -66,6 +66,7 @@ const MqAdminConsole = defineAsyncComponent(() => import("@/components/mq/MqAdmi const MqttAdminConsole = defineAsyncComponent(() => import("@/components/mqtt/MqttAdminConsole.vue")); const NacosAdminConsole = defineAsyncComponent(() => import("@/components/nacos/NacosAdminConsole.vue")); const NacosDashboard = defineAsyncComponent(() => import("@/components/nacos/NacosDashboard.vue")); +const DatabaseBrowser = defineAsyncComponent(() => import("@/components/objects/DatabaseBrowser.vue")); const ObjectBrowser = defineAsyncComponent(() => import("@/components/objects/ObjectBrowser.vue")); const TableStructureEditor = defineAsyncComponent(() => import("@/components/structure/TableStructureEditor.vue")); const DatabaseUserAdmin = defineAsyncComponent(() => import("@/components/admin/DatabaseUserAdmin.vue")); @@ -247,6 +248,7 @@ const etcdDashboardRef = ref<{ refresh?: () => boolean }>(); const zookeeperKeyBrowserRef = ref(); const consulOverviewRef = ref<{ refresh?: () => boolean }>(); const consulWorkspaceRef = ref(); +const databaseBrowserRef = ref(); const objectBrowserRef = ref(); const activeTableMeta = computed(() => props.activeTab.tableMeta); const activeDataTabTableMeta = computed(() => tableMetaForDataTab(props.activeTab)); @@ -785,6 +787,7 @@ function focusSearch(): boolean { if (props.activeTab.mode === "etcd") return etcdKeyBrowserRef.value?.focusSearch() ?? false; if (props.activeTab.mode === "zookeeper") return zookeeperKeyBrowserRef.value?.focusSearch() ?? false; if (props.activeTab.mode === "consul") return consulWorkspaceRef.value?.focusSearch() ?? false; + if (props.activeTab.mode === "databases") return databaseBrowserRef.value?.focusSearch() ?? false; if (props.activeTab.mode === "objects") return objectBrowserRef.value?.focusSearch() ?? false; if (props.activeTab.mode === "query") return queryEditorRef.value?.openSearch() ?? false; return dataGridRef.value?.focusSearch() ?? false; @@ -804,6 +807,7 @@ function refreshData(): boolean { if (props.activeTab.mode === "zookeeper") return zookeeperKeyBrowserRef.value?.refresh?.() ?? false; if (props.activeTab.mode === "consul-overview") return consulOverviewRef.value?.refresh?.() ?? false; if (props.activeTab.mode === "consul") return consulWorkspaceRef.value?.refresh?.() ?? false; + if (props.activeTab.mode === "databases") return databaseBrowserRef.value?.refresh?.() ?? false; // Restored data tabs intentionally omit row data, so refresh must work before DataGrid mounts. if (canReloadUnavailableDataTab(props.activeTab)) { emit("reload"); @@ -2041,6 +2045,12 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand + +