From efd0c381ad13e44ed192ec08588261219315bf50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Sat, 8 Aug 2026 10:55:21 +0800 Subject: [PATCH] feat(nacos): namespace production protection and sidebar visibility --- .../connection/ConnectionDialog.vue | 52 ++++-- .../components/nacos/NacosAdminConsole.vue | 31 +++- .../__tests__/NacosAdminConsoleLayout.spec.ts | 10 ++ .../src/components/sidebar/ConnectionTree.vue | 21 ++- .../sidebar/SidebarTreeRuntimeHost.vue | 18 ++ .../src/components/sidebar/TreeItem.vue | 6 +- .../sidebar/VisibleNacosNamespacesDialog.vue | 163 ++++++++++++++++++ .../TreeItem.visibleFilterDetail.spec.ts | 12 ++ .../components/sidebar/sidebarAsyncDialogs.ts | 1 + ...abaseSpecificMutationRuntime.nacos.spec.ts | 2 + ...eSidebarDatabaseSpecificMutationRuntime.ts | 20 ++- apps/desktop/src/i18n/locales/en.ts | 15 ++ apps/desktop/src/i18n/locales/es.ts | 15 ++ apps/desktop/src/i18n/locales/it.ts | 15 ++ apps/desktop/src/i18n/locales/ja.ts | 15 ++ apps/desktop/src/i18n/locales/ko.ts | 15 ++ apps/desktop/src/i18n/locales/pt-BR.ts | 15 ++ apps/desktop/src/i18n/locales/zh-CN.ts | 17 +- apps/desktop/src/i18n/locales/zh-TW.ts | 15 ++ .../connection/nacosConnectionDialog.spec.ts | 8 + .../nacos/nacosNamespaceVisibility.spec.ts | 10 ++ .../__tests__/productionSafety.spec.ts | 6 + .../src/lib/database/productionSafety.ts | 8 +- .../sidebar/sidebarVisibleFilterSummary.ts | 17 +- apps/desktop/src/stores/connectionStore.ts | 11 +- 25 files changed, 492 insertions(+), 26 deletions(-) create mode 100644 apps/desktop/src/components/sidebar/VisibleNacosNamespacesDialog.vue diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index f15a7d6a8..1186e4729 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -65,7 +65,7 @@ import { detectMqUiAuthKind, isMqAuthKindAllowedForSystem, type MqUiAuthKind } f import { driverInstallProgressChannel, driverInstallProgressPercent, isDriverInstallProgressForOperation, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi"; import { requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityConfig, sqlServerUsesLegacyCompatibility, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility"; import { normalizeNacosEndpoint, normalizeNacosMetricsUrl } from "@/lib/nacos/nacosAdmin"; -import { normalizeNacosNamespaceSelection, normalizeNacosNamespacesForDisplay } from "@/lib/nacos/nacosNamespaceVisibility"; +import { nacosNamespaceIdentity, normalizeNacosNamespaceSelection, normalizeNacosNamespacesForDisplay } from "@/lib/nacos/nacosNamespaceVisibility"; import { ArrowLeft, ArrowDown, @@ -3068,6 +3068,20 @@ const filteredProductionDatabaseNames = computed(() => { }); const productionDatabaseSelectedCount = computed(() => productionDatabaseSelection.value.size); const productionDatabaseCanSave = computed(() => productionDatabaseNames.value.length > 0 && productionDatabaseSelection.value.size > 0); +const usesNacosProductionNamespaces = computed(() => form.value.db_type === "nacos"); +const productionDisabledDescriptionKey = computed(() => (usesNacosProductionNamespaces.value ? "production.namespaceDisabledDescription" : "production.disabledDescription")); +const productionConnectionDescriptionKey = computed(() => (usesNacosProductionNamespaces.value ? "production.namespaceConnectionDescription" : "production.connectionDescription")); +const productionScopeAllLabelKey = computed(() => (usesNacosProductionNamespaces.value ? "production.allNamespaces" : "production.allDatabases")); +const productionScopeSelectedLabelKey = computed(() => (usesNacosProductionNamespaces.value ? "production.selectedNamespaces" : "production.selectedDatabases")); +const productionScopeResourceLabelKey = computed(() => (usesNacosProductionNamespaces.value ? "production.namespaces" : "production.databases")); +const productionScopeDescriptionKey = computed(() => (usesNacosProductionNamespaces.value ? "production.namespaceDescription" : "production.databaseDescription")); +const productionScopePickerLabelKey = computed(() => (usesNacosProductionNamespaces.value ? "production.selectNamespaces" : "production.selectDatabases")); +const productionPickerTitleKey = computed(() => (usesNacosProductionNamespaces.value ? "production.namespacePickerTitle" : "production.databasePickerTitle")); +const productionPickerDescriptionKey = computed(() => (usesNacosProductionNamespaces.value ? "production.namespacePickerDescription" : "production.databasePickerDescription")); +const productionPickerSearchPlaceholderKey = computed(() => (usesNacosProductionNamespaces.value ? "production.namespaceSearchPlaceholder" : "production.databaseSearchPlaceholder")); +const productionPickerSelectionRequiredKey = computed(() => (usesNacosProductionNamespaces.value ? "production.namespaceSelectionRequired" : "production.databaseSelectionRequired")); +const productionPickerLoadFailedKey = computed(() => (usesNacosProductionNamespaces.value ? "production.namespaceLoadFailed" : "production.databaseLoadFailed")); +const productionPickerEmptyKey = computed(() => (usesNacosProductionNamespaces.value ? "production.noNamespacesAvailable" : "production.noDatabasesAvailable")); const productionDatabaseSummary = computed(() => { const selected = form.value.production_databases?.length || 0; if (!selected) return t("production.noDatabasesSelected"); @@ -4242,6 +4256,15 @@ async function loadVisibleDatabaseNames(connectionId: string, config: Connection } function normalizeProductionDatabaseSelection(selectedNames: Iterable, databaseNames: string[]): string[] { + if (form.value.db_type === "nacos") { + const available = new Map(databaseNames.map((name) => [nacosNamespaceIdentity(name), name])); + const selected = new Set(); + for (const name of selectedNames) { + const canonicalName = available.get(nacosNamespaceIdentity(name)); + if (canonicalName !== undefined) selected.add(canonicalName); + } + return [...selected]; + } const available = new Map(databaseNames.map((name) => [name.toLowerCase(), name])); const selected = new Set(); for (const name of selectedNames) { @@ -4258,6 +4281,9 @@ function initialProductionDatabaseSelection(databaseNames: string[]): string[] { } async function loadProductionDatabaseNames(connectionId: string, config: ConnectionConfig): Promise { + if (config.db_type === "nacos") { + return normalizeNacosNamespacesForDisplay(await api.nacosListNamespaces(connectionId)).map((namespace) => namespace.namespace); + } if (config.db_type === "redis") { return (await api.redisListDatabases(connectionId)).map((database) => String(database.db)); } @@ -7427,25 +7453,25 @@ function openExternalUrl(url: string) { -

{{ t("production.disabledDescription") }}

+

{{ t(productionDisabledDescriptionKey) }}

@@ -7958,15 +7984,15 @@ function openExternalUrl(url: string) { - {{ t("production.databasePickerTitle") }} + {{ t(productionPickerTitleKey) }}

- {{ t("production.databasePickerDescription", { connection: form.name || selectedProfile().label }) }} + {{ t(productionPickerDescriptionKey, { connection: form.name || selectedProfile().label }) }}

- +
@@ -7981,7 +8007,7 @@ function openExternalUrl(url: string) {

- {{ t("production.databaseSelectionRequired") }} + {{ t(productionPickerSelectionRequiredKey) }}

@@ -7990,14 +8016,14 @@ function openExternalUrl(url: string) { {{ t("common.loading") }}
-

{{ t("production.databaseLoadFailed", { message: productionDatabaseError }) }}

+

{{ t(productionPickerLoadFailedKey, { message: productionDatabaseError }) }}

- {{ productionDatabaseNames.length ? t("grid.noSearchResults") : t("production.noDatabasesAvailable") }} + {{ productionDatabaseNames.length ? t("grid.noSearchResults") : t(productionPickerEmptyKey) }}