From 3ebfae05a291acdcaedf56ec46b8b0a09b84366a 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, 24 Jun 2026 01:19:18 +0800 Subject: [PATCH] feat(nacos): add Nacos management console (#1456) --- apps/desktop/public/icons/database/nacos.png | Bin 0 -> 1529 bytes apps/desktop/src/App.vue | 14 + .../connection/ConnectionDialog.vue | 186 +- .../src/components/diff/DataCompareDialog.vue | 2 +- .../components/diff/SchemaDiffConfigStep.vue | 2 +- .../components/editor/EditorSearchPanel.vue | 245 ++- .../export/DatabaseExportDialog.vue | 2 +- .../src/components/icons/DatabaseIcon.vue | 1 + .../src/components/layout/AppTabBar.vue | 4 +- .../src/components/layout/ContentArea.vue | 7 + .../src/components/layout/EditorToolbar.vue | 2 +- .../components/nacos/NacosAdminConsole.vue | 1166 +++++++++++ .../nacos/NacosConfigDiffDialog.vue | 209 ++ .../nacos/NacosConfigHistoryDialog.vue | 224 +++ .../src/components/sidebar/ConnectionTree.vue | 6 +- .../src/components/sidebar/TreeItem.vue | 160 +- .../sql-file/SqlFileExecutionDialog.vue | 2 +- apps/desktop/src/i18n/locales/en.ts | 131 ++ apps/desktop/src/i18n/locales/es.ts | 119 ++ apps/desktop/src/i18n/locales/it.ts | 119 ++ apps/desktop/src/i18n/locales/ja.ts | 119 ++ apps/desktop/src/i18n/locales/pt-BR.ts | 119 ++ apps/desktop/src/i18n/locales/zh-CN.ts | 131 ++ apps/desktop/src/i18n/locales/zh-TW.ts | 119 ++ .../__tests__/connectionOpenTarget.spec.ts | 4 + .../src/lib/__tests__/nacosAdmin.spec.ts | 51 + apps/desktop/src/lib/api.ts | 17 + apps/desktop/src/lib/connectionOpenTarget.ts | 5 +- apps/desktop/src/lib/http.ts | 86 + apps/desktop/src/lib/nacos-tauri.ts | 83 + apps/desktop/src/lib/nacosAdmin.ts | 323 ++++ apps/desktop/src/lib/openTabsPersistence.ts | 4 + .../desktop/src/lib/sidebarActiveTabTarget.ts | 13 + apps/desktop/src/lib/tabPresentation.ts | 1 + apps/desktop/src/lib/tauri.ts | 1 + apps/desktop/src/stores/connectionStore.ts | 46 + apps/desktop/src/stores/queryStore.ts | 34 + apps/desktop/src/types/database.ts | 10 +- apps/desktop/src/types/nacos.ts | 200 ++ crates/dbx-core/src/connection.rs | 73 +- crates/dbx-core/src/lib.rs | 1 + crates/dbx-core/src/models/connection.rs | 14 + crates/dbx-core/src/nacos/config.rs | 193 ++ crates/dbx-core/src/nacos/http.rs | 1697 +++++++++++++++++ crates/dbx-core/src/nacos/mod.rs | 85 + crates/dbx-core/src/nacos/port.rs | 22 + crates/dbx-core/src/nacos/service.rs | 287 +++ crates/dbx-core/src/nacos/types.rs | 334 ++++ crates/dbx-core/src/query.rs | 3 +- crates/dbx-web/src/main.rs | 16 + crates/dbx-web/src/routes/connection.rs | 23 + crates/dbx-web/src/routes/mod.rs | 1 + crates/dbx-web/src/routes/nacos.rs | 245 +++ docs/public/icons/database/nacos.svg | 11 + package.json | 4 + .../app-tests/openTabsPersistence.test.ts | 15 + .../app-tests/sidebarActiveTabTarget.test.ts | 28 + pnpm-lock.yaml | 163 ++ src-tauri/src/commands/connection.rs | 33 + src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/nacos_cmd.rs | 138 ++ src-tauri/src/lib.rs | 15 + 62 files changed, 7314 insertions(+), 55 deletions(-) create mode 100644 apps/desktop/public/icons/database/nacos.png create mode 100644 apps/desktop/src/components/nacos/NacosAdminConsole.vue create mode 100644 apps/desktop/src/components/nacos/NacosConfigDiffDialog.vue create mode 100644 apps/desktop/src/components/nacos/NacosConfigHistoryDialog.vue create mode 100644 apps/desktop/src/lib/__tests__/nacosAdmin.spec.ts create mode 100644 apps/desktop/src/lib/nacos-tauri.ts create mode 100644 apps/desktop/src/lib/nacosAdmin.ts create mode 100644 apps/desktop/src/types/nacos.ts create mode 100644 crates/dbx-core/src/nacos/config.rs create mode 100644 crates/dbx-core/src/nacos/http.rs create mode 100644 crates/dbx-core/src/nacos/mod.rs create mode 100644 crates/dbx-core/src/nacos/port.rs create mode 100644 crates/dbx-core/src/nacos/service.rs create mode 100644 crates/dbx-core/src/nacos/types.rs create mode 100644 crates/dbx-web/src/routes/nacos.rs create mode 100644 docs/public/icons/database/nacos.svg create mode 100644 src-tauri/src/commands/nacos_cmd.rs diff --git a/apps/desktop/public/icons/database/nacos.png b/apps/desktop/public/icons/database/nacos.png new file mode 100644 index 0000000000000000000000000000000000000000..0d7626a5144e49a00446304fb8f34f40d06202d7 GIT binary patch literal 1529 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeRz^suO5>XQ2>tmIipR1RclAn~S zSCLx)lxJYDv9BmdOwLX%QAkQn&&;z`dcS+Wl0s&Rtx~wDuYqrYb81GWM^#a3aFt(3 za#eP+Wr~u$oq|n;70`g()RIJnirk#MVyg;UC9t_xKsHENUr7P1q$Jx`$q{6sLWFOC zf^&XRs)DJWnQpS7v4w)UrJkXwrG=4+j)IYap_#scrM{twu7RPIfu)s!p#l^r0c|Tv zNwW%aaf8|g1^l#~=$>Fbx5m+O@q>*W`v>l<2HTIw4Z=^Gj80#)c1SLT%@R_Nvx zE5l51Ni9w;$}A|!%+FH*nV6WAUs__T1av9H3%LbwWAlok!2}F2{ffi_eM3D1ke6Tz zeSPsO&CP|YE-nd5MYtEM!Nnn!1*!T$sm1xFMaii^<;ozd3ap%qQWHz^i$e1Ab6}wu zkda@KU!0L&py2Eb3N>>D4d2A%%sh~wCRmrRua#$BaY^x=oo!a^ddz!ObD2UKumbz z1#;lYKQ#}S=8J%ddvo;Nd|<|qEOCt}3C>R|DNig)Wq_n%&)mdxVEiPeR2H60wE-$h z2=EDUbuaL-ne|U+&u^Lg-(_!q*WUljeCc1U!@rEz|FNF?4~R4m{Zc>vQ{%`lpfcmN ze;g+Mm%aPlbk$$w^FI`>eV2at-C@FijiW#1Zhi-fXz%-NJM*8>`ahNn|C+7%tH1qE z)owLQpk1ORL4LsurQ0@#$xXH64>@14=+ORKullvlKZ`GU&&j~Rc+1npF(l&f+o>nF zH7f|XnEGi1E@ms}`u9KHE|bmipibPhZ(7e)N`p(xT?OuxUXLyRyYH64+O9=jhp&C_ z=CMD$u6ve6Va?3&%VF|y-czmX8VbyLobA8tS@h$Cp2myD4LaTvWxsx$&6{(9Lnu8{NYOpE?9 zY;oBAcnRBqJF?=NII^m&7L zuYyWa%NDD*vAR`1BJTcDs1P>n;9_`wH+SO&<^-G9OIzKx9Vo6h`}^u5`^KCm28HP| zS7Y|?`nsw2dP9*Y-=+5q{{?q2f7@Zvw7!#5r!i+y_kvk3vg;TMb{fX4%~9l^U4iiGGOp@^>bP0l+XkK_54L> literal 0 HcmV?d00001 diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index effe31860..08b757ed9 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -765,6 +765,20 @@ async function openConnectionQuery(connectionId: string) { queryStore.openMqAdmin(connectionId); return; } + if (initialTarget.kind === "nacos-admin") { + try { + await connectionStore.ensureConnected(connectionId); + await connectionStore.loadNacosNamespaces(connectionId); + } catch (e: any) { + toast( + t("connection.connectFailed", { + message: translateBackendError(t, e?.message || String(e)), + }), + 5000, + ); + } + return; + } const tabId = queryStore.createTab(connectionId, initialTarget.database); try { await connectionStore.ensureConnected(connectionId); diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index e11b058b6..e1e193c11 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -15,6 +15,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Switch } from "@/components/ui/switch"; import type { ConnectionConfig, DatabaseType, JdbcDriverInfo, JdbcMavenBundleInfo, ProxyTunnelConfig, SshTunnelConfig, TransportLayerConfig } from "@/types/database"; import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq"; +import type { NacosAdminConfig, NacosAuthConfig } from "@/types/nacos"; import { useConnectionStore } from "@/stores/connectionStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { useToast } from "@/composables/useToast"; @@ -42,12 +43,17 @@ type DialogStep = "select" | "config"; type DbPickerView = "icon" | "list"; type ConfigTab = "connection" | "advanced" | "tls" | "transport"; type MqTokenSigningMode = "none" | "hs256" | "rs256"; +type NacosAuthKind = NacosAuthConfig["kind"]; type JdbcDriverSelectItem = { id: string; label: string; paths: string[]; }; +const NACOS_DEFAULT_CONSOLE_URL = "http://127.0.0.1:8085"; +const NACOS_LEGACY_SERVER_PORT = "8848"; +const NACOS_DOCKER_CONSOLE_PORT = "8085"; + type LegacyTransportFields = { ssh_enabled?: boolean; ssh_host?: string; @@ -307,6 +313,14 @@ const mqTlsSkipVerify = ref(false); const mqPinnedVersion = ref(pinnedVersionToSelection(undefined)); const mqTokenSigningMode = ref("none"); const mqTokenSigningKey = ref(""); +const nacosServerAddr = ref(NACOS_DEFAULT_CONSOLE_URL); +const nacosNamespace = ref(""); +const nacosContextPath = ref(""); +const nacosAuthKind = ref("none"); +const nacosUsername = ref("nacos"); +const nacosPassword = ref(""); +const nacosTlsSkipVerify = ref(false); +const nacosPageSize = ref(20); const colorOptions = [ { value: "", class: "bg-transparent border-dashed", labelKey: "connection.colorNone" }, @@ -509,6 +523,7 @@ const driverProfiles: Record< iotdb: { type: "iotdb", port: 6667, user: "root", label: "Apache IoTDB", icon: "iotdb" }, etcd: { type: "etcd", port: 2379, user: "", label: "etcd", icon: "etcd" }, 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" }, influxdb: { type: "influxdb", port: 8086, user: "", label: "InfluxDB", icon: "InfluxDB" }, custom_mysql: { @@ -574,6 +589,26 @@ function hydrateMqFields(value: unknown) { resetMqFields(value as Partial); } +function resetNacosFields(config?: Partial) { + nacosServerAddr.value = config?.serverAddr?.trim() || NACOS_DEFAULT_CONSOLE_URL; + nacosNamespace.value = config?.namespace || ""; + nacosContextPath.value = config?.contextPath || ""; + nacosTlsSkipVerify.value = !!config?.tlsSkipVerify; + nacosPageSize.value = Number(config?.pageSize) > 0 ? Number(config?.pageSize) : 20; + const auth = (config?.auth || { kind: "none" }) as NacosAuthConfig; + nacosAuthKind.value = auth.kind || "none"; + nacosUsername.value = auth.username || "nacos"; + nacosPassword.value = auth.password || ""; +} + +function hydrateNacosFields(value: unknown) { + if (!value || typeof value !== "object") { + resetNacosFields(); + return; + } + resetNacosFields(value as Partial); +} + function requireMqField(value: string, message: string): string { const trimmed = value.trim(); if (!trimmed) throw new Error(message); @@ -629,6 +664,65 @@ function buildMqAdminConfig(): MqAdminConfig { }; } +function buildNacosAuth(): NacosAuthConfig { + if (nacosAuthKind.value === "usernamePassword") { + return { + kind: "usernamePassword", + username: requireMqField(nacosUsername.value, t("connection.nacosUsernameRequired")), + password: nacosPassword.value, + }; + } + return { kind: "none" }; +} + +function buildNacosAdminConfig(): NacosAdminConfig { + return { + serverAddr: requireMqField(nacosServerAddr.value, t("connection.nacosConsoleUrlRequired")), + namespace: nacosNamespace.value.trim() || undefined, + contextPath: nacosContextPath.value.trim(), + auth: buildNacosAuth(), + tlsSkipVerify: nacosTlsSkipVerify.value || undefined, + pageSize: Number(nacosPageSize.value) > 0 ? Number(nacosPageSize.value) : 20, + }; +} + +function dockerNacosConsoleFallbackUrl(serverAddr: string): string | null { + let parsed: URL; + try { + parsed = new URL(serverAddr); + } catch { + return null; + } + const port = parsed.port || (parsed.protocol === "https:" ? "443" : "80"); + const host = parsed.hostname.toLowerCase(); + if (port !== NACOS_LEGACY_SERVER_PORT || !["127.0.0.1", "localhost", "::1"].includes(host)) { + return null; + } + parsed.port = NACOS_DOCKER_CONSOLE_PORT; + return parsed.toString().replace(/\/$/, ""); +} + +function isNacosAdminEndpointNotFound(message: string): boolean { + return /Nacos admin endpoint was not found/i.test(message); +} + +async function tryNacosDockerConsoleFallback(config: ConnectionConfig, originalError: string): Promise { + if (config.db_type !== "nacos" || !isNacosAdminEndpointNotFound(originalError)) return null; + const fallbackUrl = dockerNacosConsoleFallbackUrl(nacosServerAddr.value); + if (!fallbackUrl || fallbackUrl === nacosServerAddr.value.trim()) return null; + + const previousUrl = nacosServerAddr.value; + nacosServerAddr.value = fallbackUrl; + try { + const fallbackConfig = connectionConfigForSubmit(config.id); + const message = await api.testConnection(fallbackConfig); + return `${message} ${t("connection.nacosConsoleUrlAutoAdjusted", { from: previousUrl.trim(), to: fallbackUrl })}`; + } catch { + nacosServerAddr.value = previousUrl; + return null; + } +} + function applyMqAdminUrl(config: LegacyConnectionConfig, adminUrl: string) { let parsed: URL; try { @@ -642,6 +736,19 @@ function applyMqAdminUrl(config: LegacyConnectionConfig, adminUrl: string) { config.ssl = parsed.protocol === "https:"; } +function applyNacosServerAddr(config: LegacyConnectionConfig, serverAddr: string) { + let parsed: URL; + try { + parsed = new URL(serverAddr); + } catch { + throw new Error("Nacos server address is invalid"); + } + const port = Number(parsed.port) || (parsed.protocol === "https:" ? 443 : 8848); + config.host = parsed.hostname; + config.port = port; + config.ssl = parsed.protocol === "https:"; +} + function isCustomCompatibleProfile() { return selectedType.value === "custom_mysql" || selectedType.value === "custom_postgres"; } @@ -691,6 +798,12 @@ function applyProfile(val: string, preserveConnectionFields = false) { form.value.database = undefined; form.value.connection_string = undefined; } + if (profile.type === "nacos") { + resetNacosFields(); + form.value.database = undefined; + form.value.connection_string = undefined; + form.value.url_params = ""; + } } } @@ -763,6 +876,11 @@ watch( } else { resetMqFields(); } + if (config.db_type === "nacos") { + hydrateNacosFields(config.external_config); + } else { + resetNacosFields(); + } h2ConnectionMode.value = h2ConnectionModeForConfig(config); customColorInput.value = config.color || ""; selectedTransportLayerId.value = form.value.transport_layers?.[0]?.id || null; @@ -786,6 +904,7 @@ watch( selectedType.value = "mysql"; customDriverName.value = ""; resetMqFields(); + resetNacosFields(); oceanbaseSubMode.value = "mysql"; h2ConnectionMode.value = "file"; dialogStep.value = "select"; @@ -922,6 +1041,7 @@ const iconTypeMap: Record = { iotdb: "iotdb", etcd: "etcd", mq: "mq", + nacos: "nacos", dm: "dm", h2: "h2", snowflake: "snowflake", @@ -1004,6 +1124,7 @@ const dbOptions: DbOption[] = [ { value: "iotdb", label: "Apache IoTDB" }, { value: "etcd", label: "etcd" }, { value: "mq", label: "Apache Pulsar" }, + { value: "nacos", label: "Nacos" }, { value: "influxdb", label: "InfluxDB" }, { value: "iris", label: "IRIS" }, { value: "jdbc", label: "JDBC" }, @@ -1160,6 +1281,7 @@ const testResultMessage = computed(() => { }); const hasRequiredConnectionTarget = computed(() => { if (form.value.db_type === "mq") return !!mqAdminUrl.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()); }); @@ -1209,8 +1331,8 @@ async function testConnection() { const runId = ++testRunId; isTesting.value = true; testResult.value = null; + const config = connectionConfigForSubmit(editingId.value || uuid()); try { - const config = connectionConfigForSubmit(editingId.value || uuid()); const msg = await api.testConnection(config); if (runId !== testRunId) return; if (config.db_type === "mongodb" && /legacy driver/i.test(msg)) { @@ -1219,7 +1341,10 @@ async function testConnection() { testResult.value = { ok: true, message: msg }; } catch (e: any) { if (runId !== testRunId) return; - testResult.value = { ok: false, message: mongodbAuthFailureHint(String(e)) }; + const message = mongodbAuthFailureHint(String(e)); + const fallbackMessage = await tryNacosDockerConsoleFallback(config, message); + if (runId !== testRunId) return; + testResult.value = fallbackMessage ? { ok: true, message: fallbackMessage } : { ok: false, message }; } finally { if (runId === testRunId) { isTesting.value = false; @@ -1301,6 +1426,15 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { config.database = undefined; config.connection_string = undefined; config.url_params = ""; + } else if (config.db_type === "nacos") { + const nacosConfig = buildNacosAdminConfig(); + config.external_config = nacosConfig; + applyNacosServerAddr(config, nacosConfig.serverAddr); + config.username = nacosAuthKind.value === "usernamePassword" ? nacosUsername.value.trim() : ""; + config.password = nacosAuthKind.value === "usernamePassword" ? nacosPassword.value : ""; + config.database = nacosConfig.namespace || undefined; + config.connection_string = undefined; + config.url_params = ""; } else { config.external_config = undefined; } @@ -2791,6 +2925,54 @@ function openExternalUrl(url: string) { + + + + +