From 37f13018d3b320d3e40e2dd7e597a90b1a091620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Mon, 10 Aug 2026 16:25:44 +0800 Subject: [PATCH] feat(consul): add Consul full-stack workbench --- .github/scripts/database-issue-catalog.mjs | 1 + .github/scripts/i18n-autofill.mjs | 99 +- .github/scripts/label-issue-database.test.mjs | 1 + README.md | 4 +- README.zh-CN.md | 4 +- apps/desktop/public/icons/database/consul.svg | 7 + apps/desktop/src/App.vue | 6 +- .../connection/ConnectionDialog.vue | 200 ++- .../src/components/consul/ConsulAcl.vue | 296 ++++ .../src/components/consul/ConsulHealth.vue | 666 +++++++ .../components/consul/ConsulKeyBrowser.vue | 1024 +++++++++++ .../consul/ConsulListPagination.vue | 32 + .../src/components/consul/ConsulMesh.vue | 403 +++++ .../src/components/consul/ConsulOperator.vue | 165 ++ .../src/components/consul/ConsulOverview.vue | 157 ++ .../src/components/consul/ConsulScope.vue | 221 +++ .../src/components/consul/ConsulServices.vue | 941 ++++++++++ .../src/components/consul/ConsulSessions.vue | 648 +++++++ .../src/components/consul/ConsulTools.vue | 370 ++++ .../src/components/consul/ConsulWorkspace.vue | 236 +++ .../__tests__/ConsulAclInteraction.spec.ts | 127 ++ .../__tests__/ConsulScopeInteraction.spec.ts | 96 + .../ConsulServicesInteraction.spec.ts | 290 ++++ .../consul/__tests__/ConsulWatchViews.spec.ts | 267 +++ .../ConsulWorkspaceInteraction.spec.ts | 136 ++ .../src/components/diff/DataCompareDialog.vue | 2 +- .../components/diff/SchemaDiffConfigStep.vue | 2 +- .../src/components/etcd/EtcdKeyBrowser.vue | 7 + .../export/DatabaseExportDialog.vue | 2 +- .../src/components/icons/DatabaseIcon.vue | 1 + .../src/components/kv/KvKeyBrowser.vue | 618 +++++-- .../kv/__tests__/KvKeyBrowserExport.spec.ts | 13 + .../kv/__tests__/KvKeyBrowserLayout.spec.ts | 8 + .../KvKeyBrowserSearchAndSplit.spec.ts | 42 + .../src/components/layout/AppTabBar.vue | 9 +- .../src/components/layout/ContentArea.vue | 20 + .../src/components/layout/EditorToolbar.vue | 2 + .../src/components/sidebar/ConnectionTree.vue | 6 +- .../sidebar/SidebarTreeRuntimeHost.vue | 33 +- .../src/components/sidebar/TreeItem.vue | 6 +- .../sql-file/SqlFileExecutionDialog.vue | 2 +- apps/desktop/src/composables/useQuickOpen.ts | 2 +- .../src/i18n/__tests__/consulUiParity.spec.ts | 113 ++ apps/desktop/src/i18n/locales/consulUi.ts | 455 +++++ apps/desktop/src/i18n/locales/consulZhCN.ts | 517 ++++++ apps/desktop/src/i18n/locales/en.ts | 106 ++ apps/desktop/src/i18n/locales/es.ts | 106 ++ apps/desktop/src/i18n/locales/it.ts | 106 ++ apps/desktop/src/i18n/locales/ja.ts | 106 ++ apps/desktop/src/i18n/locales/ko.ts | 106 ++ apps/desktop/src/i18n/locales/pt-BR.ts | 106 ++ apps/desktop/src/i18n/locales/zh-CN.ts | 33 +- apps/desktop/src/i18n/locales/zh-TW.ts | 106 ++ .../connection/consulConnectionDialog.spec.ts | 25 + .../kv/slashPrefixLazyKeyTree.spec.ts | 23 + .../sidebar/sidebarTreeItemLayout.spec.ts | 1 + .../__tests__/sidebar/treeNodeClick.spec.ts | 13 +- .../zookeeper/zookeeperLazyKeyTree.spec.ts | 39 +- .../consulCoreEndpointContract.spec.ts | 192 ++ .../__tests__/consulTransportContract.spec.ts | 166 ++ .../lib/backend/__tests__/debugLog.spec.ts | 15 + apps/desktop/src/lib/backend/api.ts | 105 ++ apps/desktop/src/lib/backend/debugLog.ts | 2 +- apps/desktop/src/lib/backend/http.ts | 349 ++++ apps/desktop/src/lib/backend/tauri.ts | 350 ++++ .../lib/connection/connectionOpenTarget.ts | 5 +- .../lib/connection/connectionPresentation.ts | 4 + .../connection/connectionVisibleDatabases.ts | 2 +- .../lib/consul/__tests__/agentTarget.spec.ts | 16 + .../lib/consul/__tests__/consulPolicy.spec.ts | 25 + .../consul/__tests__/meshVisibility.spec.ts | 14 + .../lib/consul/__tests__/pagination.spec.ts | 17 + .../lib/consul/__tests__/sessionModel.spec.ts | 32 + .../lib/consul/__tests__/sessionRenew.spec.ts | 17 + .../lib/consul/__tests__/watchState.spec.ts | 26 + apps/desktop/src/lib/consul/agentTarget.ts | 53 + apps/desktop/src/lib/consul/consulPolicy.ts | 22 + apps/desktop/src/lib/consul/meshVisibility.ts | 8 + apps/desktop/src/lib/consul/pagination.ts | 19 + apps/desktop/src/lib/consul/sessionModel.ts | 22 + apps/desktop/src/lib/consul/sessionRenew.ts | 21 + apps/desktop/src/lib/consul/watchState.ts | 34 + .../lib/dataGrid/dataGridColumnNameCopy.ts | 2 +- .../lib/database/databaseFeatureSupport.ts | 2 +- .../lib/database/databaseNamespaceCreation.ts | 1 + .../lib/database/databasePropertyEditing.ts | 1 + apps/desktop/src/lib/kv/kvMutationError.ts | 4 +- apps/desktop/src/lib/kv/kvRootPresentation.ts | 2 +- .../src/lib/kv/slashPrefixLazyKeyTree.ts | 242 +++ .../src/lib/sidebar/sidebarActiveTabTarget.ts | 20 + .../src/lib/sidebar/sidebarTreeItemLayout.ts | 2 + apps/desktop/src/lib/sidebar/treeNodeClick.ts | 32 +- apps/desktop/src/lib/sidebar/treeNodeIcon.ts | 1 + .../desktop/src/lib/sql/sqlStatementRanges.ts | 2 +- apps/desktop/src/lib/tabs/tabPresentation.ts | 10 + .../src/lib/zookeeper/zookeeperLazyKeyTree.ts | 194 +-- apps/desktop/src/stores/connectionStore.ts | 54 +- apps/desktop/src/stores/consulStore.ts | 67 + apps/desktop/src/types/consul.ts | 944 ++++++++++ apps/desktop/src/types/database.ts | 7 +- crates/dbx-core/src/agent_kv.rs | 10 + crates/dbx-core/src/connection.rs | 37 +- crates/dbx-core/src/consul/acl.rs | 587 +++++++ crates/dbx-core/src/consul/agent.rs | 1118 ++++++++++++ crates/dbx-core/src/consul/archive.rs | 617 +++++++ crates/dbx-core/src/consul/blocking.rs | 540 ++++++ crates/dbx-core/src/consul/bulk.rs | 187 ++ crates/dbx-core/src/consul/capabilities.rs | 247 +++ crates/dbx-core/src/consul/catalog.rs | 294 ++++ crates/dbx-core/src/consul/client.rs | 314 ++++ crates/dbx-core/src/consul/config.rs | 204 +++ crates/dbx-core/src/consul/enterprise.rs | 796 +++++++++ crates/dbx-core/src/consul/health.rs | 216 +++ crates/dbx-core/src/consul/kv.rs | 573 ++++++ .../src/consul/mesh/config_entries.rs | 143 ++ crates/dbx-core/src/consul/mesh/discovery.rs | 40 + .../src/consul/mesh/exported_services.rs | 42 + crates/dbx-core/src/consul/mesh/intentions.rs | 219 +++ crates/dbx-core/src/consul/mesh/mod.rs | 11 + crates/dbx-core/src/consul/mesh/peering.rs | 111 ++ crates/dbx-core/src/consul/mod.rs | 107 ++ crates/dbx-core/src/consul/operator.rs | 557 ++++++ crates/dbx-core/src/consul/response.rs | 275 +++ crates/dbx-core/src/consul/search.rs | 201 +++ crates/dbx-core/src/consul/session.rs | 479 +++++ crates/dbx-core/src/consul/status.rs | 38 + crates/dbx-core/src/consul/test_support.rs | 72 + crates/dbx-core/src/consul/tools.rs | 560 ++++++ crates/dbx-core/src/consul/txn.rs | 527 ++++++ crates/dbx-core/src/consul/types.rs | 13 + crates/dbx-core/src/lib.rs | 1 + crates/dbx-core/src/models/connection.rs | 74 +- crates/dbx-core/src/query.rs | 4 +- .../dbx-core/src/sql_dialect/ddl_profile.rs | 2 +- crates/dbx-core/src/table_export.rs | 4 +- crates/dbx-web/src/main.rs | 103 ++ crates/dbx-web/src/routes/connection.rs | 108 +- crates/dbx-web/src/routes/consul.rs | 1537 +++++++++++++++++ crates/dbx-web/src/routes/mod.rs | 1 + docs/content/docs/databases.cn.mdx | 2 +- docs/content/docs/databases.mdx | 2 +- .../docs/specialized-workspaces.cn.mdx | 22 +- docs/content/docs/specialized-workspaces.mdx | 22 +- packages/app-tests/i18nAutofillParser.test.ts | 4 +- src-tauri/src/commands/connection.rs | 32 + src-tauri/src/commands/consul_cmd.rs | 1017 +++++++++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/lib.rs | 103 ++ 148 files changed, 24032 insertions(+), 378 deletions(-) create mode 100644 apps/desktop/public/icons/database/consul.svg create mode 100644 apps/desktop/src/components/consul/ConsulAcl.vue create mode 100644 apps/desktop/src/components/consul/ConsulHealth.vue create mode 100644 apps/desktop/src/components/consul/ConsulKeyBrowser.vue create mode 100644 apps/desktop/src/components/consul/ConsulListPagination.vue create mode 100644 apps/desktop/src/components/consul/ConsulMesh.vue create mode 100644 apps/desktop/src/components/consul/ConsulOperator.vue create mode 100644 apps/desktop/src/components/consul/ConsulOverview.vue create mode 100644 apps/desktop/src/components/consul/ConsulScope.vue create mode 100644 apps/desktop/src/components/consul/ConsulServices.vue create mode 100644 apps/desktop/src/components/consul/ConsulSessions.vue create mode 100644 apps/desktop/src/components/consul/ConsulTools.vue create mode 100644 apps/desktop/src/components/consul/ConsulWorkspace.vue create mode 100644 apps/desktop/src/components/consul/__tests__/ConsulAclInteraction.spec.ts create mode 100644 apps/desktop/src/components/consul/__tests__/ConsulScopeInteraction.spec.ts create mode 100644 apps/desktop/src/components/consul/__tests__/ConsulServicesInteraction.spec.ts create mode 100644 apps/desktop/src/components/consul/__tests__/ConsulWatchViews.spec.ts create mode 100644 apps/desktop/src/components/consul/__tests__/ConsulWorkspaceInteraction.spec.ts create mode 100644 apps/desktop/src/i18n/__tests__/consulUiParity.spec.ts create mode 100644 apps/desktop/src/i18n/locales/consulUi.ts create mode 100644 apps/desktop/src/i18n/locales/consulZhCN.ts create mode 100644 apps/desktop/src/lib/__tests__/connection/consulConnectionDialog.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/kv/slashPrefixLazyKeyTree.spec.ts create mode 100644 apps/desktop/src/lib/backend/__tests__/consulCoreEndpointContract.spec.ts create mode 100644 apps/desktop/src/lib/backend/__tests__/consulTransportContract.spec.ts create mode 100644 apps/desktop/src/lib/consul/__tests__/agentTarget.spec.ts create mode 100644 apps/desktop/src/lib/consul/__tests__/consulPolicy.spec.ts create mode 100644 apps/desktop/src/lib/consul/__tests__/meshVisibility.spec.ts create mode 100644 apps/desktop/src/lib/consul/__tests__/pagination.spec.ts create mode 100644 apps/desktop/src/lib/consul/__tests__/sessionModel.spec.ts create mode 100644 apps/desktop/src/lib/consul/__tests__/sessionRenew.spec.ts create mode 100644 apps/desktop/src/lib/consul/__tests__/watchState.spec.ts create mode 100644 apps/desktop/src/lib/consul/agentTarget.ts create mode 100644 apps/desktop/src/lib/consul/consulPolicy.ts create mode 100644 apps/desktop/src/lib/consul/meshVisibility.ts create mode 100644 apps/desktop/src/lib/consul/pagination.ts create mode 100644 apps/desktop/src/lib/consul/sessionModel.ts create mode 100644 apps/desktop/src/lib/consul/sessionRenew.ts create mode 100644 apps/desktop/src/lib/consul/watchState.ts create mode 100644 apps/desktop/src/lib/kv/slashPrefixLazyKeyTree.ts create mode 100644 apps/desktop/src/stores/consulStore.ts create mode 100644 apps/desktop/src/types/consul.ts create mode 100644 crates/dbx-core/src/consul/acl.rs create mode 100644 crates/dbx-core/src/consul/agent.rs create mode 100644 crates/dbx-core/src/consul/archive.rs create mode 100644 crates/dbx-core/src/consul/blocking.rs create mode 100644 crates/dbx-core/src/consul/bulk.rs create mode 100644 crates/dbx-core/src/consul/capabilities.rs create mode 100644 crates/dbx-core/src/consul/catalog.rs create mode 100644 crates/dbx-core/src/consul/client.rs create mode 100644 crates/dbx-core/src/consul/config.rs create mode 100644 crates/dbx-core/src/consul/enterprise.rs create mode 100644 crates/dbx-core/src/consul/health.rs create mode 100644 crates/dbx-core/src/consul/kv.rs create mode 100644 crates/dbx-core/src/consul/mesh/config_entries.rs create mode 100644 crates/dbx-core/src/consul/mesh/discovery.rs create mode 100644 crates/dbx-core/src/consul/mesh/exported_services.rs create mode 100644 crates/dbx-core/src/consul/mesh/intentions.rs create mode 100644 crates/dbx-core/src/consul/mesh/mod.rs create mode 100644 crates/dbx-core/src/consul/mesh/peering.rs create mode 100644 crates/dbx-core/src/consul/mod.rs create mode 100644 crates/dbx-core/src/consul/operator.rs create mode 100644 crates/dbx-core/src/consul/response.rs create mode 100644 crates/dbx-core/src/consul/search.rs create mode 100644 crates/dbx-core/src/consul/session.rs create mode 100644 crates/dbx-core/src/consul/status.rs create mode 100644 crates/dbx-core/src/consul/test_support.rs create mode 100644 crates/dbx-core/src/consul/tools.rs create mode 100644 crates/dbx-core/src/consul/txn.rs create mode 100644 crates/dbx-core/src/consul/types.rs create mode 100644 crates/dbx-web/src/routes/consul.rs create mode 100644 src-tauri/src/commands/consul_cmd.rs diff --git a/.github/scripts/database-issue-catalog.mjs b/.github/scripts/database-issue-catalog.mjs index 350d17d7ce..9f6ee6022e 100644 --- a/.github/scripts/database-issue-catalog.mjs +++ b/.github/scripts/database-issue-catalog.mjs @@ -69,6 +69,7 @@ const extraAliases = { const supplementalDrivers = [ { dbType: "turso", label: "Turso", aliases: ["libsql", "lib sql"] }, { dbType: "nacos", label: "Nacos", aliases: ["r-nacos", "rnacos"] }, + { dbType: "consul", label: "Consul", aliases: ["hashicorp consul"] }, { dbType: "cloudberry", label: "Apache Cloudberry" }, { dbType: "mariadb", label: "MariaDB" }, { dbType: "tidb", label: "TiDB" }, diff --git a/.github/scripts/i18n-autofill.mjs b/.github/scripts/i18n-autofill.mjs index ca6116f4f5..50c4d202a1 100644 --- a/.github/scripts/i18n-autofill.mjs +++ b/.github/scripts/i18n-autofill.mjs @@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { dirname, join, resolve } from "node:path"; const LOCALES_DIR = "apps/desktop/src/i18n/locales"; const SOURCE_LOCALE = "zh-CN"; @@ -351,27 +351,98 @@ function applyEdits(text, edits) { function flattenLeaves(root) { const result = new Map(); - flattenNode(root, [], result); + flattenNode(root, [], result, root.externalObjects || new Map(), new Set()); return result; } -function flattenNode(node, path, result) { +function flattenNode(node, path, result, externalObjects, activeExternalObjects) { for (const property of node.properties) { const nextPath = [...path, property.key]; if (property.value.type === "object") { - flattenNode(property.value, nextPath, result); + flattenNode(property.value, nextPath, result, externalObjects, activeExternalObjects); } else if (property.value.type === "string") { result.set(nextPath.join("."), property.value.value); + } else if (property.value.type === "spread") { + const [binding, ...members] = property.value.expression.split("."); + let spreadObject = externalObjects.get(binding); + for (const member of members) { + spreadObject = spreadObject?.properties.find((candidate) => candidate.key === member && candidate.value.type === "object")?.value; + } + flattenExternalObject(property.value.expression, spreadObject, path, result, externalObjects, activeExternalObjects); + } else if (property.value.type === "external") { + flattenExternalObject(property.key, externalObjects.get(property.key), nextPath, result, externalObjects, activeExternalObjects); } } } +function flattenExternalObject(name, object, path, result, externalObjects, activeExternalObjects) { + if (!object || activeExternalObjects.has(name)) return; + activeExternalObjects.add(name); + flattenNode(object, path, result, externalObjects, activeExternalObjects); + activeExternalObjects.delete(name); +} + function parseLocaleFile(text, file) { const parser = new Parser(text, file); const root = parser.parseLocaleRoot(); + root.externalObjects = parseImportedObjects(text, file); + for (const [name, object] of parseDeclaredObjects(text, file)) { + root.externalObjects.set(name, object); + } return { root }; } +function parseDeclaredObjects(text, file) { + const result = new Map(); + const declarations = /^\s*const\s+([A-Za-z_$][\w$]*)\s*=/gm; + const exportIndex = text.indexOf("export default"); + + for (const match of text.slice(0, exportIndex).matchAll(declarations)) { + const parser = new Parser(text, file); + parser.index = match.index + match[0].length; + parser.skipSpace(); + if (parser.peek() === "{") result.set(match[1], parser.parseObject()); + } + + return result; +} + +function parseImportedObjects(text, file, visited = new Set()) { + const result = new Map(); + const actualFile = existsSync(file) ? file : file.slice(file.indexOf(":") + 1); + const resolvedFile = resolve(actualFile); + if (visited.has(resolvedFile)) return result; + visited.add(resolvedFile); + const importPattern = /import\s*\{([^}]+)\}\s*from\s*["'](\.\.?\/[^"']+)["'];?/g; + + for (const match of text.matchAll(importPattern)) { + const modulePath = resolve(dirname(actualFile), `${match[2]}.ts`); + if (!existsSync(modulePath)) continue; + const moduleText = readFileSync(modulePath, "utf8"); + for (const [name, object] of parseImportedObjects(moduleText, modulePath, visited)) { + if (!result.has(name)) result.set(name, object); + } + for (const specifier of match[1].split(",")) { + const [exportedName, localName = exportedName] = specifier.trim().split(/\s+as\s+/); + if (!exportedName) continue; + const object = parseExportedObject(moduleText, modulePath, exportedName); + if (object) result.set(localName, object); + } + } + + return result; +} + +function parseExportedObject(text, file, name) { + const match = new RegExp(`export\\s+const\\s+${name.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}\\s*=`).exec(text); + if (!match) return null; + const parser = new Parser(text, file); + parser.index = match.index + match[0].length; + parser.skipSpace(); + if (parser.peek() !== "{") return null; + return parser.parseObject(); +} + class Parser { constructor(text, file) { this.text = text; @@ -412,6 +483,26 @@ class Parser { } const start = this.index; + if (this.text.startsWith("...", this.index)) { + this.index += 3; + this.skipSpace(); + const root = this.parseIdentifier(); + if (!root) this.fail("Expected spread identifier"); + let expression = root; + while (this.peek() === ".") { + this.index += 1; + const member = this.parseIdentifier(); + if (!member) this.fail("Expected spread member"); + expression += `.${member}`; + } + this.skipSpace(); + const hasComma = this.peek() === ","; + if (hasComma) this.index += 1; + else if (this.peek() !== "}") this.fail("Expected comma after spread property"); + properties.push({ key: `...${expression}`, start, end: this.index, hasComma, value: { type: "spread", expression } }); + continue; + } + const key = this.parseKey(); this.skipSpace(); if (this.peek() === "," || this.peek() === "}") { diff --git a/.github/scripts/label-issue-database.test.mjs b/.github/scripts/label-issue-database.test.mjs index 782eccf7c1..4ff5a204b9 100644 --- a/.github/scripts/label-issue-database.test.mjs +++ b/.github/scripts/label-issue-database.test.mjs @@ -46,6 +46,7 @@ test("labels native and compatibility database products with their families", () ["Turso", ["db/turso"]], ["Nacos 2.0.1", ["db/nacos"]], ["r-nacos", ["db/nacos"]], + ["HashiCorp Consul", ["db/consul"]], ["Apache Cloudberry", ["db/cloudberry", "db/postgres"]], ["TiDB v8.5", ["db/mysql", "db/tidb"]], ["OceanBase", ["db/oceanbase"]], diff --git a/README.md b/README.md index 394161b18d..239d646219 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ ### 70+ Databases, One Tool -MySQL, PostgreSQL, SQLite, Cloudflare D1, Redis, MongoDB, DuckDB, ClickHouse, SQL Server, Oracle, Elasticsearch, Easysearch, Qdrant, Milvus, Weaviate, MariaDB, TiDB, OceanBase, openGauss, GaussDB, KWDB, KingBase, Vastbase, GoldenDB, Doris, SelectDB, StarRocks, Manticore Search, Redshift, DM, TDengine, XuguDB, CockroachDB, Access, HighGo, UXDB, Dolt, and more. Agent-based profiles extend DBX to H2, Snowflake, Trino, PrestoSQL, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, JDBCX, and custom JDBC connections. New native and agent-driven drivers also cover Databricks, SAP HANA, Teradata, Vertica, Firebird, Exasol, YashanDB, GBase 8a/8s, Databend, RQLite, Turso, InfluxDB, QuestDB, IoTDB, etcd, ZooKeeper, Nacos, IRIS, and more. Message queue admin is also available for Pulsar, Kafka, and RocketMQ. All in a single ~20 MB app. No bundled Chromium. +MySQL, PostgreSQL, SQLite, Cloudflare D1, Redis, MongoDB, DuckDB, ClickHouse, SQL Server, Oracle, Elasticsearch, Easysearch, Qdrant, Milvus, Weaviate, MariaDB, TiDB, OceanBase, openGauss, GaussDB, KWDB, KingBase, Vastbase, GoldenDB, Doris, SelectDB, StarRocks, Manticore Search, Redshift, DM, TDengine, XuguDB, CockroachDB, Access, HighGo, UXDB, Dolt, and more. Agent-based profiles extend DBX to H2, Snowflake, Trino, PrestoSQL, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, JDBCX, and custom JDBC connections. New native and agent-driven drivers also cover Databricks, SAP HANA, Teradata, Vertica, Firebird, Exasol, YashanDB, GBase 8a/8s, Databend, RQLite, Turso, InfluxDB, QuestDB, IoTDB, etcd, ZooKeeper, Nacos, Consul KV, IRIS, and more. Message queue admin is also available for Pulsar, Kafka, and RocketMQ. All in a single ~20 MB app. No bundled Chromium. ### Query Editor @@ -483,7 +483,7 @@ DBX is 20 MB with no runtime dependencies for its native database features (no s
What databases are supported? -MySQL, PostgreSQL, SQLite, Cloudflare D1, Redis, MongoDB, DuckDB, ClickHouse, SQL Server, Oracle, Elasticsearch, Easysearch, Qdrant, Milvus, Weaviate, MariaDB, TiDB, OceanBase, openGauss, GaussDB, KWDB, KingBase, Vastbase, GoldenDB, Doris, SelectDB, StarRocks, Manticore Search, Redshift, DM, TDengine, XuguDB, CockroachDB, Access, HighGo, UXDB, and more. Agent-based profiles extend support to H2, Snowflake, Trino, PrestoSQL, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, JDBCX, Databricks, SAP HANA, Teradata, Vertica, Firebird, Exasol, YashanDB, GBase 8a/8s, Databend, RQLite, Turso, InfluxDB, QuestDB, IoTDB, etcd, ZooKeeper, Nacos, IRIS, and custom JDBC connections. Message queue admin (Pulsar, Kafka, RocketMQ) is also supported. +MySQL, PostgreSQL, SQLite, Cloudflare D1, Redis, MongoDB, DuckDB, ClickHouse, SQL Server, Oracle, Elasticsearch, Easysearch, Qdrant, Milvus, Weaviate, MariaDB, TiDB, OceanBase, openGauss, GaussDB, KWDB, KingBase, Vastbase, GoldenDB, Doris, SelectDB, StarRocks, Manticore Search, Redshift, DM, TDengine, XuguDB, CockroachDB, Access, HighGo, UXDB, and more. Agent-based profiles extend support to H2, Snowflake, Trino, PrestoSQL, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, JDBCX, Databricks, SAP HANA, Teradata, Vertica, Firebird, Exasol, YashanDB, GBase 8a/8s, Databend, RQLite, Turso, InfluxDB, QuestDB, IoTDB, etcd, ZooKeeper, Nacos, Consul KV, IRIS, and custom JDBC connections. Message queue admin (Pulsar, Kafka, RocketMQ) is also supported.
diff --git a/README.zh-CN.md b/README.zh-CN.md index 3cd61555d1..0e73070d74 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -90,7 +90,7 @@ ### 70+ 种数据库,一个工具搞定 -MySQL、PostgreSQL、SQLite、Cloudflare D1、Redis、MongoDB、DuckDB、ClickHouse、SQL Server、Oracle、Elasticsearch、Easysearch、MariaDB、TiDB、OceanBase、openGauss、GaussDB、KWDB、KingBase、Vastbase、GoldenDB、Doris、SelectDB、StarRocks、Manticore Search、Redshift、DM、TDengine、虚谷 XuguDB、CockroachDB、Access、HighGo、UXDB、Dolt 等数据库都能直接连接。Agent 配置还可扩展到 H2、Snowflake、Trino、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、JDBCX 和自定义 JDBC。新增的原生与 Agent 驱动还覆盖了 Databricks、SAP HANA、Teradata、Vertica、Firebird、Exasol、崖山 YashanDB、GBase、Databend、RQLite、Turso、InfluxDB、QuestDB、IoTDB、etcd、IRIS 等。全部装进约 20 MB 的应用里,不内嵌 Chromium。 +MySQL、PostgreSQL、SQLite、Cloudflare D1、Redis、MongoDB、DuckDB、ClickHouse、SQL Server、Oracle、Elasticsearch、Easysearch、MariaDB、TiDB、OceanBase、openGauss、GaussDB、KWDB、KingBase、Vastbase、GoldenDB、Doris、SelectDB、StarRocks、Manticore Search、Redshift、DM、TDengine、虚谷 XuguDB、CockroachDB、Access、HighGo、UXDB、Dolt 等数据库都能直接连接。Agent 配置还可扩展到 H2、Snowflake、Trino、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、JDBCX 和自定义 JDBC。新增的原生与 Agent 驱动还覆盖了 Databricks、SAP HANA、Teradata、Vertica、Firebird、Exasol、崖山 YashanDB、GBase、Databend、RQLite、Turso、InfluxDB、QuestDB、IoTDB、etcd、ZooKeeper、Nacos、Consul KV、IRIS 等。全部装进约 20 MB 的应用里,不内嵌 Chromium。 ### 查询编辑器 @@ -486,7 +486,7 @@ DBX 仅 20 MB,无需运行时依赖(无需 Java、无需 Python)。AI 和
支持哪些数据库? -MySQL、PostgreSQL、SQLite、Cloudflare D1、Redis、MongoDB、DuckDB、ClickHouse、SQL Server、Oracle、Elasticsearch、Easysearch、Qdrant、Milvus、Weaviate、MariaDB、TiDB、OceanBase、openGauss、GaussDB、KWDB、KingBase、Vastbase、GoldenDB、Doris、SelectDB、StarRocks、Manticore Search、Redshift、DM、TDengine、虚谷 XuguDB、CockroachDB、Access、HighGo、UXDB 等。Agent 配置可扩展到 H2、Snowflake、Trino、PrestoSQL、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、JDBCX、Databricks、SAP HANA、Teradata、Vertica、Firebird、Exasol、崖山 YashanDB、GBase 8a/8s、Databend、RQLite、Turso、InfluxDB、QuestDB、IoTDB、etcd、ZooKeeper、Nacos、IRIS 及自定义 JDBC 连接,并支持消息队列管理(Pulsar、Kafka、RocketMQ)。 +MySQL、PostgreSQL、SQLite、Cloudflare D1、Redis、MongoDB、DuckDB、ClickHouse、SQL Server、Oracle、Elasticsearch、Easysearch、Qdrant、Milvus、Weaviate、MariaDB、TiDB、OceanBase、openGauss、GaussDB、KWDB、KingBase、Vastbase、GoldenDB、Doris、SelectDB、StarRocks、Manticore Search、Redshift、DM、TDengine、虚谷 XuguDB、CockroachDB、Access、HighGo、UXDB 等。Agent 配置可扩展到 H2、Snowflake、Trino、PrestoSQL、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、JDBCX、Databricks、SAP HANA、Teradata、Vertica、Firebird、Exasol、崖山 YashanDB、GBase 8a/8s、Databend、RQLite、Turso、InfluxDB、QuestDB、IoTDB、etcd、ZooKeeper、Nacos、Consul KV、IRIS 及自定义 JDBC 连接,并支持消息队列管理(Pulsar、Kafka、RocketMQ)。
diff --git a/apps/desktop/public/icons/database/consul.svg b/apps/desktop/public/icons/database/consul.svg new file mode 100644 index 0000000000..5220a1e9dd --- /dev/null +++ b/apps/desktop/public/icons/database/consul.svg @@ -0,0 +1,7 @@ + + + favicon + + + + diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index c5f2bff85f..16b229c6fe 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1546,7 +1546,7 @@ async function openConnectionQuery(connectionId: string) { } return; } - if (initialTarget.kind === "etcd" || initialTarget.kind === "zookeeper") { + if (initialTarget.kind === "etcd" || initialTarget.kind === "zookeeper" || initialTarget.kind === "consul") { try { await connectionStore.ensureConnected(connectionId); queryStore.createTab(connectionId, "", `${connection.name}:keys`, initialTarget.kind); @@ -1968,6 +1968,8 @@ async function handleQuickOpenSelect(item: any) { await connectionStore.loadEtcdRoot(item.connectionId); } else if (config?.db_type === "zookeeper") { await connectionStore.loadZooKeeperRoot(item.connectionId); + } else if (config?.db_type === "consul") { + await connectionStore.loadConsulRoot(item.connectionId); } else if (config?.db_type === "mongodb") { await connectionStore.loadMongoDatabases(item.connectionId); } else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch") { @@ -1993,6 +1995,8 @@ async function handleQuickOpenSelect(item: any) { await connectionStore.loadEtcdRoot(item.connectionId); } else if (config?.db_type === "zookeeper") { await connectionStore.loadZooKeeperRoot(item.connectionId); + } else if (config?.db_type === "consul") { + await connectionStore.loadConsulRoot(item.connectionId); } else if (config?.db_type === "mongodb") { await connectionStore.loadMongoDatabases(item.connectionId); } else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch") { diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 280196e4c6..a0543f45f2 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -53,6 +53,7 @@ import { prestoSqlBuiltinDriverPaths } from "@/lib/database/prestoSqlBuiltinDriv import { JDBCX_DEFAULT_URL, JDBCX_DRIVER_PROFILE, JDBCX_JDBC_DRIVER_CLASS, ensureJdbcxRuntimeDrivers, isJdbcxRuntimeBundle, isJdbcxRuntimePath, jdbcxHighPrivilegeExtensionsEnabled, setJdbcxHighPrivilegeExtensionsEnabled } from "@/lib/database/jdbcxBuiltinDriver"; import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDetection"; import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connection/connectionAttemptTimeout"; +import { consulAgentAddressesMatch } from "@/lib/consul/agentTarget"; import { appendConnectionErrorHints, isJdbcMissingRuntimeDependencyError } from "@/lib/connection/connectionErrorHints"; import { preventDialogDocumentSelectAll } from "@/lib/connection/dialogTextSelection"; import { postgresTlsModeForForm } from "@/lib/connection/postgresTlsMode"; @@ -809,6 +810,22 @@ const nacosTlsSkipVerify = ref(false); const nacosMetricsMode = ref("auto"); const nacosMetricsUrl = ref(""); const nacosPageSize = ref(20); +type ConsulConsistency = "default" | "stale" | "consistent"; +const consulServerAddr = ref("http://127.0.0.1:8500"); +const consulDatacenter = ref(""); +const consulNamespace = ref(""); +const consulPartition = ref(""); +const consulConsistency = ref("default"); +const consulTlsSkipVerify = ref(false); +const consulAgentTargetNode = ref(""); +const consulAgentTargetAddress = ref(""); +const consulMeshVisible = ref(false); +const consulOperatorVisible = ref(false); +const consulOperatorSnapshotRestoreEnabled = ref(false); +const consulOperatorAutopilotWriteEnabled = ref(false); +const consulOperatorRaftWriteEnabled = ref(false); +const consulOperatorKeyringWriteEnabled = ref(false); +const consulOperatorLicenseWriteEnabled = ref(false); // --- MQTT-specific form fields --- const mqttHost = ref("127.0.0.1"); @@ -1131,6 +1148,7 @@ const driverProfiles: Record< rocketmq: { type: "mq", port: 9876, user: "", label: "Apache RocketMQ", icon: "rocketmq", host: "127.0.0.1" }, rabbitmq: { type: "mq", port: 5672, user: "", label: "RabbitMQ", icon: "rabbitmq", host: "127.0.0.1" }, nacos: { type: "nacos", port: 8848, user: "nacos", label: "Nacos", icon: "nacos", host: "127.0.0.1" }, + consul: { type: "consul", port: 8500, user: "", label: "Consul", icon: "consul", host: "127.0.0.1" }, mqtt: { type: "mqtt", port: 1883, user: "", label: "MQTT", icon: "mqtt", host: "127.0.0.1" }, iris: { type: "iris", port: 1972, user: "_SYSTEM", label: "IRIS", icon: "iris" }, influxdb: { type: "influxdb", port: 8086, user: "", label: "InfluxDB", icon: "InfluxDB" }, @@ -1351,6 +1369,31 @@ function hydrateNacosFields(value: unknown) { resetNacosFields(value as Partial); } +function resetConsulFields(value?: Record) { + consulServerAddr.value = String(value?.serverAddr || value?.server_addr || "http://127.0.0.1:8500"); + consulDatacenter.value = String(value?.datacenter || value?.consulDatacenter || value?.consul_datacenter || ""); + consulNamespace.value = String(value?.namespace || value?.consulNamespace || value?.consul_namespace || ""); + consulPartition.value = String(value?.partition || value?.consulPartition || value?.consul_partition || ""); + const consistency = String(value?.consistency || value?.consulConsistency || value?.consul_consistency || "default"); + consulConsistency.value = (["default", "stale", "consistent"].includes(consistency) ? consistency : "default") as ConsulConsistency; + consulTlsSkipVerify.value = Boolean(value?.tlsSkipVerify || value?.tls_skip_verify || value?.consulTlsSkipVerify || value?.consul_tls_skip_verify); + const agentTarget = value?.agentTarget || value?.agent_target; + const target = agentTarget && typeof agentTarget === "object" ? (agentTarget as Record) : undefined; + consulAgentTargetNode.value = String(target?.node || ""); + consulAgentTargetAddress.value = String(target?.address || ""); + consulMeshVisible.value = Boolean(value?.consulMeshVisible); + consulOperatorVisible.value = Boolean(value?.consulOperatorVisible); + consulOperatorSnapshotRestoreEnabled.value = Boolean(value?.consulOperatorSnapshotRestoreEnabled); + consulOperatorAutopilotWriteEnabled.value = Boolean(value?.consulOperatorAutopilotWriteEnabled); + consulOperatorRaftWriteEnabled.value = Boolean(value?.consulOperatorRaftWriteEnabled); + consulOperatorKeyringWriteEnabled.value = Boolean(value?.consulOperatorKeyringWriteEnabled); + consulOperatorLicenseWriteEnabled.value = Boolean(value?.consulOperatorLicenseWriteEnabled); +} + +function hydrateConsulFields(value: unknown) { + resetConsulFields(value && typeof value === "object" ? (value as Record) : undefined); +} + function resetMqttFields(config?: Partial) { mqttHost.value = config?.host?.trim() || "127.0.0.1"; mqttPort.value = config?.port || 1883; @@ -1685,6 +1728,46 @@ function buildNacosAdminConfig(): NacosAdminConfig { }; } +function buildConsulExternalConfig(): Record { + let parsed: URL; + try { + parsed = new URL(consulServerAddr.value.trim()); + } catch { + throw new Error(t("connection.consulAddressInvalid")); + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(t("connection.consulAddressInvalid")); + if (parsed.username || parsed.password || parsed.search || parsed.hash) throw new Error(t("connection.consulAddressInvalid")); + const targetNode = consulAgentTargetNode.value.trim(); + const targetAddress = consulAgentTargetAddress.value.trim(); + if (!!targetNode !== !!targetAddress) throw new Error(t("connection.consulAgentTargetIncomplete")); + if (targetAddress && !consulAgentAddressesMatch(targetAddress, parsed.hostname)) { + throw new Error(t("connection.consulAgentTargetAddressMismatch")); + } + return { + serverAddr: parsed.toString().replace(/\/$/, ""), + datacenter: consulDatacenter.value.trim() || undefined, + namespace: consulNamespace.value.trim() || undefined, + partition: consulPartition.value.trim() || undefined, + consistency: consulConsistency.value, + tlsSkipVerify: consulTlsSkipVerify.value || undefined, + agentTarget: targetNode ? { node: targetNode, address: targetAddress } : undefined, + consulMeshVisible: consulMeshVisible.value || undefined, + consulOperatorVisible: consulOperatorVisible.value || undefined, + consulOperatorSnapshotRestoreEnabled: consulOperatorSnapshotRestoreEnabled.value || undefined, + consulOperatorAutopilotWriteEnabled: consulOperatorAutopilotWriteEnabled.value || undefined, + consulOperatorRaftWriteEnabled: consulOperatorRaftWriteEnabled.value || undefined, + consulOperatorKeyringWriteEnabled: consulOperatorKeyringWriteEnabled.value || undefined, + consulOperatorLicenseWriteEnabled: consulOperatorLicenseWriteEnabled.value || undefined, + }; +} + +function applyConsulServerAddr(config: LegacyConnectionConfig, serverAddr: string) { + const parsed = new URL(serverAddr); + config.host = parsed.hostname; + config.port = Number(parsed.port) || (parsed.protocol === "https:" ? 443 : 8500); + config.ssl = parsed.protocol === "https:"; +} + function errorMessage(error: unknown): string { if (error instanceof Error) return error.message; return String(error); @@ -2262,6 +2345,14 @@ function applyProfile(val: string, preserveConnectionFields = false) { form.value.connection_string = undefined; form.value.url_params = ""; } + if (profile.type === "consul") { + resetConsulFields(); + form.value.database = undefined; + form.value.connection_string = undefined; + form.value.username = ""; + form.value.password = ""; + form.value.url_params = ""; + } if (profile.type === "mqtt") { resetMqttFields(); form.value.database = undefined; @@ -2384,6 +2475,11 @@ watch( } else { resetNacosFields(); } + if (config.db_type === "consul") { + hydrateConsulFields(config.external_config); + } else { + resetConsulFields(); + } if (config.db_type === "mqtt") { hydrateMqttFields(config.external_config); } else { @@ -2640,6 +2736,7 @@ const iconTypeMap: Record = { rocketmq: "rocketmq", rabbitmq: "rabbitmq", nacos: "nacos", + consul: "consul", mqtt: "mqtt", dm: "dm", h2: "h2", @@ -2745,6 +2842,7 @@ const dbOptions: DbOption[] = [ { value: "rabbitmq", label: "RabbitMQ" }, { value: "mqtt", label: "MQTT" }, { value: "nacos", label: "Nacos" }, + { value: "consul", label: "Consul" }, { value: "influxdb", label: "InfluxDB" }, { value: "victoriametrics", label: "VictoriaMetrics" }, { value: "iris", label: "IRIS" }, @@ -2805,7 +2903,7 @@ const dbCategoryDefinitions: Array<{ { key: "registry_config", titleKey: "connection.databaseCategoryRegistryConfig", - optionValues: ["etcd", "zookeeper", "nacos"], + optionValues: ["etcd", "zookeeper", "nacos", "consul"], }, ]; @@ -2921,6 +3019,7 @@ const tlsCapableDatabaseTypes = new Set([ "dameng", "redis", "etcd", + "consul", "clickhouse", "elasticsearch", "easysearch", @@ -3279,6 +3378,7 @@ const hasRequiredConnectionTarget = computed(() => { if (form.value.db_type === "zookeeper") return !!(form.value.host || form.value.connection_string || connectionUrlInput.value.trim()); if (form.value.db_type === "mqtt") return !!mqttHost.value.trim() && mqttPort.value > 0; if (form.value.db_type === "nacos") return !!nacosServerAddr.value.trim(); + if (form.value.db_type === "consul") return !!consulServerAddr.value.trim(); if (isCloudflareD1Connection(form.value)) return hasCloudflareD1Credentials(form.value); 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()); @@ -3679,6 +3779,20 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo config.database = undefined; config.connection_string = undefined; config.url_params = ""; + } else if (config.db_type === "consul") { + const consulConfig = buildConsulExternalConfig(); + config.external_config = consulConfig; + applyConsulServerAddr(config, String(consulConfig.serverAddr)); + config.client_cert_path = config.client_cert_path?.trim() || ""; + config.client_key_path = config.client_key_path?.trim() || ""; + if ((config.client_cert_path && !config.client_key_path) || (!config.client_cert_path && config.client_key_path)) { + throw new Error(t("connection.etcdClientCertPairRequired")); + } + config.username = ""; + config.password = config.password.trim(); + config.database = undefined; + config.connection_string = undefined; + config.url_params = ""; } else if (config.db_type === "mqtt") { const mqttConfig = buildMqttExternalConfig(); config.external_config = mqttConfig; @@ -3813,12 +3927,12 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo if ((config.client_cert_path && !config.client_key_path) || (!config.client_cert_path && config.client_key_path)) { throw new Error(t("connection.etcdClientCertPairRequired")); } - } else { + } else if (form.value.db_type !== "consul") { config.etcd_endpoints = undefined; config.client_cert_path = undefined; config.client_key_path = undefined; } - if (config.db_type !== "mysql" && config.db_type !== "clickhouse" && config.db_type !== "etcd" && config.db_type !== "starrocks" && config.db_type !== "mongodb" && config.db_type !== "victoriametrics") { + if (config.db_type !== "mysql" && config.db_type !== "clickhouse" && config.db_type !== "etcd" && config.db_type !== "consul" && config.db_type !== "starrocks" && config.db_type !== "mongodb" && config.db_type !== "victoriametrics") { config.ca_cert_path = undefined; } else { config.ca_cert_path = config.ca_cert_path?.trim() || ""; @@ -5442,7 +5556,7 @@ function openExternalUrl(url: string) {
-
+
@@ -6174,6 +6288,80 @@ function openExternalUrl(url: string) {
+ + +