From 0effda91b39cb56a5d63eda47d88e703f2120909 Mon Sep 17 00:00:00 2001 From: miracle Date: Thu, 25 Jun 2026 00:40:48 +0800 Subject: [PATCH] feat(zookeeper): add agent client browsing and node management --- .../public/icons/database/zookeeper.svg | 19 + apps/desktop/src/App.vue | 4 + .../connection/ConnectionDialog.vue | 61 ++ .../src/components/diff/DataCompareDialog.vue | 4 + .../src/components/etcd/EtcdKeyBrowser.vue | 315 ++-------- .../export/DatabaseExportDialog.vue | 4 + .../src/components/icons/DatabaseIcon.vue | 1 + .../src/components/kv/KvKeyBrowser.vue | 536 ++++++++++++++++++ .../src/components/layout/AppTabBar.vue | 4 +- .../src/components/layout/ContentArea.vue | 25 +- .../src/components/layout/EditorToolbar.vue | 8 + .../src/components/sidebar/ConnectionTree.vue | 2 +- .../src/components/sidebar/TreeItem.vue | 17 +- .../sql-file/SqlFileExecutionDialog.vue | 4 + .../zookeeper/ZooKeeperKeyBrowser.vue | 70 +++ apps/desktop/src/i18n/locales/en.ts | 40 ++ apps/desktop/src/i18n/locales/zh-CN.ts | 40 ++ .../connectionAttemptTimeout.spec.ts | 4 + .../__tests__/connectionUrl.zookeeper.spec.ts | 8 + .../src/lib/__tests__/kvKeyTree.spec.ts | 48 ++ .../lib/__tests__/kvRefreshSelection.spec.ts | 15 + .../lib/__tests__/kvRootPresentation.spec.ts | 9 + .../src/lib/__tests__/kvValueDisplay.spec.ts | 88 +++ .../lib/__tests__/sqlStatementRanges.spec.ts | 1 + .../__tests__/zookeeperApiContract.spec.ts | 25 + .../lib/__tests__/zookeeperConnection.spec.ts | 24 + apps/desktop/src/lib/api.ts | 9 + .../src/lib/connectionAttemptTimeout.ts | 1 + .../desktop/src/lib/connectionPresentation.ts | 3 + apps/desktop/src/lib/connectionUrl.ts | 24 + .../src/lib/connectionVisibleDatabases.ts | 2 +- apps/desktop/src/lib/etcdKeyTree.ts | 128 +---- apps/desktop/src/lib/http.ts | 21 + apps/desktop/src/lib/kvKeyTree.ts | 136 +++++ apps/desktop/src/lib/kvRefreshSelection.ts | 6 + apps/desktop/src/lib/kvRootPresentation.ts | 5 + apps/desktop/src/lib/kvValueDisplay.ts | 77 +++ .../desktop/src/lib/sidebarActiveTabTarget.ts | 12 + apps/desktop/src/lib/sidebarTreeItemLayout.ts | 2 +- apps/desktop/src/lib/sqlStatementRanges.ts | 3 +- apps/desktop/src/lib/tabPresentation.ts | 5 + apps/desktop/src/lib/tauri.ts | 39 ++ apps/desktop/src/lib/treeNodeClick.ts | 2 +- apps/desktop/src/lib/zookeeperConnection.ts | 48 ++ apps/desktop/src/stores/connectionStore.ts | 43 +- apps/desktop/src/types/database.ts | 4 +- .../assets/database-drivers.manifest.json | 30 + crates/dbx-core/src/agent_catalog.rs | 7 + crates/dbx-core/src/agent_connection.rs | 37 ++ crates/dbx-core/src/agent_kv.rs | 149 ++++- crates/dbx-core/src/connection.rs | 26 + crates/dbx-core/src/models/connection.rs | 33 ++ .../dbx-core/tests/database_capabilities.rs | 12 + crates/dbx-web/src/main.rs | 5 + crates/dbx-web/src/routes/mod.rs | 1 + crates/dbx-web/src/routes/zookeeper.rs | 98 ++++ .../connectionVisibleDatabases.test.ts | 6 +- .../app-tests/sidebarActiveTabTarget.test.ts | 24 + .../app-tests/sidebarTreeItemLayout.test.ts | 4 + packages/app-tests/tabPresentation.test.ts | 21 + packages/app-tests/treeNodeClick.test.ts | 4 + packages/node-core/src/diagnostics.ts | 1 + src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/zookeeper_cmd.rs | 48 ++ src-tauri/src/lib.rs | 4 + 65 files changed, 2037 insertions(+), 420 deletions(-) create mode 100644 apps/desktop/public/icons/database/zookeeper.svg create mode 100644 apps/desktop/src/components/kv/KvKeyBrowser.vue create mode 100644 apps/desktop/src/components/zookeeper/ZooKeeperKeyBrowser.vue create mode 100644 apps/desktop/src/lib/__tests__/connectionUrl.zookeeper.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/kvKeyTree.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/kvRefreshSelection.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/kvRootPresentation.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/kvValueDisplay.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/zookeeperApiContract.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/zookeeperConnection.spec.ts create mode 100644 apps/desktop/src/lib/kvKeyTree.ts create mode 100644 apps/desktop/src/lib/kvRefreshSelection.ts create mode 100644 apps/desktop/src/lib/kvRootPresentation.ts create mode 100644 apps/desktop/src/lib/kvValueDisplay.ts create mode 100644 apps/desktop/src/lib/zookeeperConnection.ts create mode 100644 crates/dbx-web/src/routes/zookeeper.rs create mode 100644 src-tauri/src/commands/zookeeper_cmd.rs diff --git a/apps/desktop/public/icons/database/zookeeper.svg b/apps/desktop/public/icons/database/zookeeper.svg new file mode 100644 index 0000000000..7d0da946df --- /dev/null +++ b/apps/desktop/public/icons/database/zookeeper.svg @@ -0,0 +1,19 @@ + + \ No newline at end of file diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 7f4f6e4bb1..610e1b607c 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1036,6 +1036,8 @@ async function handleQuickOpenSelect(item: any) { await connectionStore.loadRedisDatabases(item.connectionId); } else if (config?.db_type === "etcd") { await connectionStore.loadEtcdRoot(item.connectionId); + } else if (config?.db_type === "zookeeper") { + await connectionStore.loadZooKeeperRoot(item.connectionId); } else if (config?.db_type === "mongodb") { await connectionStore.loadMongoDatabases(item.connectionId); } else if (config?.db_type === "elasticsearch") { @@ -1059,6 +1061,8 @@ async function handleQuickOpenSelect(item: any) { await connectionStore.loadRedisDatabases(item.connectionId); } else if (config?.db_type === "etcd") { await connectionStore.loadEtcdRoot(item.connectionId); + } else if (config?.db_type === "zookeeper") { + await connectionStore.loadZooKeeperRoot(item.connectionId); } else if (config?.db_type === "mongodb") { await connectionStore.loadMongoDatabases(item.connectionId); } else if (config?.db_type === "elasticsearch") { diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 1700e7f137..8430c0f8c9 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -26,6 +26,7 @@ import { applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnecti import type { ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink"; import { connectionUrlPlaceholder as getUrlPlaceholder } from "@/lib/connectionPresentation"; import { h2ConnectionModeForConfig, h2FileJdbcUrl, h2FilePathFromJdbcUrl, type H2ConnectionMode } from "@/lib/h2Connection"; +import { firstZooKeeperEndpoint, normalizeZooKeeperConnectString } from "@/lib/zookeeperConnection"; import { isLocalFileTypeDb } from "@/lib/connectionFile"; import { MQ_PINNED_VERSION_OPTIONS, pinnedVersionToSelection, selectionToPinnedVersion } from "@/lib/mqPinnedVersionOptions"; import { mongodbAuthFailureHint, mongoUrlParam, setMongoUrlParam } from "@/lib/mongoConnectionOptions"; @@ -523,6 +524,7 @@ const driverProfiles: Record< xugu: { type: "xugu", port: 5138, user: "", label: "虚谷 XuguDB", icon: "xugu" }, iotdb: { type: "iotdb", port: 6667, user: "root", label: "Apache IoTDB", icon: "iotdb" }, etcd: { type: "etcd", port: 2379, user: "", label: "etcd", icon: "etcd" }, + zookeeper: { type: "zookeeper", port: 2181, user: "", label: "Apache ZooKeeper", icon: "zookeeper" }, mq: { type: "mq", port: 8080, user: "", label: "Apache Pulsar", icon: "pulsar", host: "127.0.0.1" }, nacos: { type: "nacos", port: 8848, user: "nacos", label: "Nacos", icon: "nacos", host: "127.0.0.1" }, iris: { type: "iris", port: 1972, user: "_SYSTEM", label: "IRIS", icon: "iris" }, @@ -799,6 +801,14 @@ function applyProfile(val: string, preserveConnectionFields = false) { form.value.database = undefined; form.value.connection_string = undefined; } + if (profile.type === "zookeeper") { + form.value.database = undefined; + form.value.connection_string = ""; + form.value.ssl = false; + form.value.ca_cert_path = ""; + form.value.client_cert_path = ""; + form.value.client_key_path = ""; + } if (profile.type === "nacos") { resetNacosFields(); form.value.database = undefined; @@ -1042,6 +1052,7 @@ const iconTypeMap: Record = { xugu: "xugu", iotdb: "iotdb", etcd: "etcd", + zookeeper: "zookeeper", mq: "mq", nacos: "nacos", dm: "dm", @@ -1126,6 +1137,7 @@ const dbOptions: DbOption[] = [ { value: "xugu", label: "虚谷 XuguDB" }, { value: "iotdb", label: "Apache IoTDB" }, { value: "etcd", label: "etcd" }, + { value: "zookeeper", label: "Apache ZooKeeper" }, { value: "mq", label: "Apache Pulsar" }, { value: "nacos", label: "Nacos" }, { value: "influxdb", label: "InfluxDB" }, @@ -1251,6 +1263,12 @@ const etcdEndpointsLines = computed({ form.value.etcd_endpoints = normalizeEndpointLines(value); }, }); +const zookeeperConnectString = computed({ + get: () => form.value.connection_string || "", + set: (value: string) => { + form.value.connection_string = normalizeZooKeeperConnectString(value); + }, +}); const canUseTransportLayers = computed(() => form.value.db_type !== "sqlite" && form.value.db_type !== "access" && !isH2FileMode.value); const shouldShowAgentDriverInstallHint = computed(() => showAgentDriverInstallHint(form.value.db_type, agentDrivers.value, form.value.driver_profile)); const h2DriverMissing = computed(() => form.value.db_type === "h2" && isH2FileMode.value && agentDrivers.value.find((d) => d.db_type === "h2")?.installed !== true); @@ -1284,6 +1302,7 @@ const testResultMessage = computed(() => { }); const hasRequiredConnectionTarget = computed(() => { if (form.value.db_type === "mq") return !!mqAdminUrl.value.trim(); + if (form.value.db_type === "zookeeper") return !!(form.value.host || form.value.connection_string || connectionUrlInput.value.trim()); if (form.value.db_type === "nacos") return !!nacosServerAddr.value.trim(); if (isH2FileMode.value) return !!(form.value.host.trim() || h2FilePathFromJdbcUrl(form.value.connection_string)); return !!(form.value.host || (mongoUseUrl.value && form.value.connection_string) || (form.value.db_type === "jdbc" && form.value.connection_string) || connectionUrlInput.value.trim()); @@ -1500,6 +1519,17 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { if (config.db_type === "redis") { config.redis_key_separator = config.redis_key_separator?.trim() ?? ":"; } + if (config.db_type === "zookeeper") { + const normalizedConnectString = normalizeZooKeeperConnectString(config.connection_string || ""); + config.connection_string = normalizedConnectString || undefined; + const firstEndpoint = firstZooKeeperEndpoint(normalizedConnectString || (config.host ? `${config.host}:${config.port || 2181}` : "")); + if (firstEndpoint) { + config.host = firstEndpoint.host; + config.port = firstEndpoint.port; + } + config.database = undefined; + config.ssl = false; + } if (config.db_type === "etcd") { config.etcd_endpoints = normalizeEndpointLines(config.etcd_endpoints || ""); const firstEndpoint = firstEtcdEndpoint(config.etcd_endpoints); @@ -3083,6 +3113,37 @@ function openExternalUrl(url: string) { + +