diff --git a/Cargo.lock b/Cargo.lock index 3f976828ad..12f68ab564 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1568,10 +1568,29 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ + "percent-encoding", "time", "version_check", ] +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -2440,6 +2459,15 @@ dependencies = [ "const-random", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dom_query" version = "0.27.0" @@ -4603,6 +4631,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -6344,6 +6378,12 @@ dependencies = [ "regex", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + [[package]] name = "psm" version = "0.1.31" @@ -6374,6 +6414,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "pxfm" version = "0.1.30" @@ -6740,6 +6790,8 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", + "cookie", + "cookie_store", "encoding_rs", "futures-core", "futures-util", diff --git a/agents/drivers/duckdb/Cargo.lock b/agents/drivers/duckdb/Cargo.lock index 4c99eca22b..1efb07cbf0 100644 --- a/agents/drivers/duckdb/Cargo.lock +++ b/agents/drivers/duckdb/Cargo.lock @@ -875,6 +875,35 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1468,6 +1497,15 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "duckdb" version = "1.10505.0" @@ -1621,7 +1659,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2807,6 +2845,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -3231,7 +3275,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3817,6 +3861,12 @@ dependencies = [ "regex", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + [[package]] name = "psm" version = "0.1.31" @@ -3847,6 +3897,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "quick-xml" version = "0.37.5" @@ -3910,7 +3970,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4149,6 +4209,8 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", + "cookie", + "cookie_store", "futures-core", "futures-util", "http", @@ -4467,7 +4529,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5269,10 +5331,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6105,7 +6167,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index d2962ce240..e587111f26 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -19,7 +19,7 @@ import type { InfluxDbExternalConfig, InfluxDbVersion } from "@/types/influxdb"; import type { VictoriaMetricsExternalConfig } from "@/types/victoriametrics"; import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq"; import type { MqttConnectionConfig } from "@/types/mqtt"; -import type { NacosAdminConfig, NacosAuthConfig, NacosImplementation, NacosMetricsMode, NacosNamespaceInfo, NacosRNacosConsoleAuth, NacosVersionMode } from "@/types/nacos"; +import type { NacosAdminConfig, NacosApiPlane, NacosAuthConfig, NacosImplementation, NacosMetricsMode, NacosNamespaceInfo, NacosRNacosConsoleAuth, NacosVersionMode } from "@/types/nacos"; import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stores/connectionStore"; import { useTunnelProfileStore } from "@/stores/tunnelProfileStore"; import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSummary } from "@/lib/connection/tunnelProfiles"; @@ -298,6 +298,11 @@ const visibleNacosNamespaces = ref([]); const visibleNacosNamespaceSelection = ref>(new Set()); const visibleNacosNamespaceSearchText = ref(""); const visibleNacosNamespaceError = ref(""); +const visibleNacosNamespaceListingPermissionDenied = ref(false); +const isResolvingManualNacosNamespaces = ref(false); +const visibleNacosNamespaceAccessMode = ref<"automatic" | "manual">("automatic"); +const nacosDynamicAllNamespaces = ref(false); +const visibleNacosNamespaceDynamicAllSupported = ref(false); const showProductionDatabasesDialog = ref(false); const isLoadingProductionDatabases = ref(false); const productionDatabaseNames = ref([]); @@ -883,8 +888,9 @@ const nacosImplementation = ref("nacos"); // Nacos 2 and 3 expose different API planes. New connections must therefore // choose an explicit version instead of relying on endpoint-shape guessing. const nacosVersionMode = ref("v2"); +const nacosApiPlane = ref("admin"); const nacosServerAddr = ref(""); -const nacosOrdinaryAccount = ref(false); +const nacosContextPath = ref(""); const nacosManagedNamespacesText = ref(""); const nacosRNacosConsoleAddr = ref(""); const nacosHistoryEnabled = ref(false); @@ -898,6 +904,11 @@ const nacosTlsSkipVerify = ref(false); const nacosMetricsMode = ref("auto"); const nacosMetricsUrl = ref(""); const nacosPageSize = ref(20); +let nacosScopeFingerprintBaseline = ""; + +function currentNacosScopeFingerprint(): string { + return JSON.stringify([nacosImplementation.value, nacosVersionMode.value, nacosApiPlane.value, nacosServerAddr.value.trim().replace(/\/+$/, ""), nacosContextPath.value.trim(), nacosAuthKind.value, nacosUsername.value.trim()]); +} type ConsulConsistency = "default" | "stale" | "consistent"; const consulServerAddr = ref("http://127.0.0.1:8500"); const consulDatacenter = ref(""); @@ -935,15 +946,25 @@ const mqttConnectTimeoutSecs = ref(30); const mqttMaxPacketSizeBytes = ref(16 * 1024 * 1024); const mqttSavedTopics = ref([]); const nacosPrimaryAddressPlaceholder = computed(() => { + if (nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" && nacosApiPlane.value === "console") { + return "http://127.0.0.1:8080"; + } return "http://127.0.0.1:8848/nacos"; }); +const nacosServiceAddressHint = computed(() => { + if (nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" && nacosApiPlane.value === "console") { + return t("nacos.nacosConsoleServiceAddressHint"); + } + return t("nacos.nacosServiceAddressHint"); +}); const nacosV3AdminEndpointWarning = computed(() => { if (nacosImplementation.value !== "nacos" || nacosVersionMode.value !== "v3" || !nacosServerAddr.value.trim()) return ""; try { const url = new URL(nacosServerAddr.value.trim()); - if (url.port === "8080") { + if (nacosApiPlane.value === "admin" && url.port === "8080") { return t("nacos.nacosV3ConsolePortWarning"); } + if (nacosApiPlane.value === "console" && url.port === "8848") return t("nacos.nacosV3AdminPortWarning"); } catch { // The normal form validation will report an invalid URL on save. } @@ -971,6 +992,7 @@ function selectNacosConnectionProfile(profile: NacosConnectionProfile) { } nacosImplementation.value = "nacos"; nacosVersionMode.value = profile; + if (profile !== "v3") nacosApiPlane.value = "admin"; } watch(nacosImplementation, (implementation) => { @@ -1459,11 +1481,13 @@ function resetNacosFields(config?: Partial) { // Saved `auto` profiles are legacy records. The form always saves an // explicit selection and no longer relies on a separate Console address. nacosVersionMode.value = config?.versionMode === "v3" ? "v3" : "v2"; + nacosApiPlane.value = config?.versionMode === "v3" ? config?.apiPlane || "admin" : "admin"; const serverAddr = config?.serverAddr?.trim() || ""; const contextPath = config?.contextPath?.trim() || ""; - nacosServerAddr.value = serverAddr && contextPath && contextPath !== "/" && !serverAddr.endsWith(contextPath) ? `${serverAddr.replace(/\/+$/, "")}/${contextPath.replace(/^\/+/, "")}` : serverAddr; - nacosOrdinaryAccount.value = !!config?.managedNamespaces?.length; + nacosServerAddr.value = serverAddr; + nacosContextPath.value = contextPath; nacosManagedNamespacesText.value = (config?.managedNamespaces || []).join("\n"); + nacosDynamicAllNamespaces.value = !!config && !config.managedNamespaces?.length && !Array.isArray(form.value.visible_databases); nacosRNacosConsoleAddr.value = config?.rnacosConsoleAddr?.trim() || ""; nacosHistoryEnabled.value = config?.rnacosHistoryEnabled ?? !!config?.rnacosConsoleAddr; const consoleAuth = config?.rnacosConsoleAuth || { kind: "inherit" }; @@ -1478,6 +1502,7 @@ function resetNacosFields(config?: Partial) { nacosAuthKind.value = auth.kind || "none"; nacosUsername.value = auth.username || "nacos"; nacosPassword.value = auth.password || ""; + nacosScopeFingerprintBaseline = currentNacosScopeFingerprint(); } function hydrateNacosFields(value: unknown) { @@ -1805,7 +1830,8 @@ function buildNacosAdminConfig(): NacosAdminConfig { const normalized = normalizeNacosEndpoint(primaryAddress, { implementation: nacosImplementation.value, versionMode: nacosVersionMode.value, - contextPath: undefined, + apiPlane: nacosApiPlane.value, + contextPath: nacosContextPath.value, }); if (nacosImplementation.value === "rnacos" && normalized.warnings.length) { throw new Error(t("connection.nacosRNacosOpenApiRequired")); @@ -1815,11 +1841,7 @@ function buildNacosAdminConfig(): NacosAdminConfig { throw new Error(t("connection.nacosRNacosConsoleUrlRequired")); } let rnacosConsoleAuth: NacosRNacosConsoleAuth | undefined; - const usesManagedNamespaces = nacosImplementation.value === "nacos" && nacosAuthKind.value === "usernamePassword" && nacosOrdinaryAccount.value; - const managedNamespaces = usesManagedNamespaces ? parseNacosManagedNamespaces(nacosManagedNamespacesText.value) : []; - if (usesManagedNamespaces && managedNamespaces.length === 0) { - throw new Error(t("nacos.nacosOrdinaryNamespacesRequired")); - } + const managedNamespaces = nacosImplementation.value === "nacos" && nacosAuthKind.value === "usernamePassword" ? parseNacosManagedNamespaces(nacosManagedNamespacesText.value) : []; let metricsUrl: string | undefined; if (nacosMetricsMode.value === "custom") { try { @@ -1843,6 +1865,7 @@ function buildNacosAdminConfig(): NacosAdminConfig { return { implementation: nacosImplementation.value, versionMode: nacosImplementation.value === "nacos" ? nacosVersionMode.value : undefined, + apiPlane: nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" ? nacosApiPlane.value : undefined, serverAddr: normalized.serverAddr, contextPath: normalized.contextPath || undefined, managedNamespaces: managedNamespaces.length ? managedNamespaces : undefined, @@ -3461,6 +3484,24 @@ const shouldShowAgentDriverInstallHint = computed(() => showAgentDriverInstallHi const h2DriverMissing = computed(() => form.value.db_type === "h2" && isH2FileMode.value && agentDrivers.value.find((d) => d.db_type === "h2")?.installed !== true); const agentDriverFocus = computed(() => ({ target: "driver", driver: agentDriverInstallKey(form.value.db_type, form.value.driver_profile) })); const canChooseVisibleNacosNamespaces = computed(() => form.value.db_type === "nacos"); +const isNacosV3AdminPlane = computed(() => nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" && nacosApiPlane.value === "admin"); +const isNacosV3ConsolePlane = computed(() => nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" && nacosApiPlane.value === "console"); +const canDetectNacosNamespaceAccess = computed(() => form.value.db_type === "nacos" && nacosImplementation.value === "nacos" && nacosAuthKind.value === "usernamePassword"); +const nacosManualNamespaceLabelKey = computed(() => (isNacosV3AdminPlane.value ? "nacos.nacosManagedNamespaces" : "nacos.nacosManagedNamespacesNameOrId")); +const nacosManualNamespacePlaceholderKey = computed(() => (isNacosV3AdminPlane.value ? "nacos.nacosManagedNamespacesIdPlaceholder" : "nacos.nacosManagedNamespacesPlaceholder")); +const nacosManualNamespaceHintKey = computed(() => { + if (isNacosV3AdminPlane.value) return "nacos.nacosV3AdminManagedNamespacesHint"; + if (isNacosV3ConsolePlane.value) return "nacos.nacosV3ConsoleManagedNamespacesHint"; + return "nacos.nacosManagedNamespacesHint"; +}); +const nacosNamespacePickerTitleKey = computed(() => (canDetectNacosNamespaceAccess.value ? "nacos.nacosDetectAccessibleNamespaces" : "nacos.nacosVisibleNamespacesTitle")); +const nacosNamespacePickerDescriptionKey = computed(() => (canDetectNacosNamespaceAccess.value ? "nacos.nacosDetectAccessibleNamespacesHint" : "nacos.nacosVisibleNamespacesDescription")); +function hasNacosNamespaceScopeForSave(): boolean { + if (!canDetectNacosNamespaceAccess.value) return true; + if (nacosDynamicAllNamespaces.value) return true; + if (parseNacosManagedNamespaces(nacosManagedNamespacesText.value).length > 0) return true; + return Array.isArray(form.value.visible_databases) && form.value.visible_databases.length > 0; +} const canChooseVisibleDatabases = computed(() => !canChooseVisibleNacosNamespaces.value && connectionCanChooseVisibleDatabases(form.value)); const visibleFilterUsesSchemas = computed(() => connectionUsesVisibleSchemaFilter(form.value)); const hasVisibleDatabaseFilter = computed(() => Array.isArray(form.value.visible_databases)); @@ -3494,7 +3535,7 @@ const filteredVisibleNacosNamespaces = computed(() => { }); }); const visibleNacosNamespaceSelectedCount = computed(() => visibleNacosNamespaceSelection.value.size); -const visibleNacosNamespaceCanSave = computed(() => visibleNacosNamespaceSelection.value.size > 0); +const visibleNacosNamespaceCanSave = computed(() => (visibleNacosNamespaceAccessMode.value === "manual" ? parseNacosManagedNamespaces(nacosManagedNamespacesText.value).length > 0 : visibleNacosNamespaceSelection.value.size > 0)); const filteredProductionDatabaseNames = computed(() => { const query = productionDatabaseSearchText.value.trim().toLowerCase(); if (!query) return productionDatabaseNames.value; @@ -4632,6 +4673,10 @@ function resetVisibleNacosNamespaceDraftState() { visibleNacosNamespaceSelection.value = new Set(); visibleNacosNamespaceSearchText.value = ""; visibleNacosNamespaceError.value = ""; + visibleNacosNamespaceListingPermissionDenied.value = false; + isResolvingManualNacosNamespaces.value = false; + visibleNacosNamespaceAccessMode.value = "automatic"; + visibleNacosNamespaceDynamicAllSupported.value = false; } function resetProductionDatabaseDraftState() { @@ -4715,24 +4760,99 @@ function normalizeVisibleNacosNamespaceSelection(selected: Iterable, nam return normalizeNacosNamespaceSelection(selected, namespaces); } +function normalizeManualNacosNamespaceNames(namespaces: string[], availableNamespaces: NacosNamespaceInfo[]): string[] { + const namespaceIds = new Map(availableNamespaces.map((namespace) => [nacosNamespaceIdentity(namespace.namespace), namespace.namespace])); + const namespaceNames = new Map(); + for (const namespace of availableNamespaces) { + const name = nacosNamespaceLabel(namespace).trim(); + if (!name) continue; + namespaceNames.set(name, [...(namespaceNames.get(name) || []), namespace.namespace]); + } + + const normalized: string[] = []; + const seen = new Set(); + for (const namespace of namespaces) { + const namespaceId = namespaceIds.get(nacosNamespaceIdentity(namespace)); + const matchingNames = namespaceNames.get(namespace.trim()) || []; + // Display names are only safe to convert when the server returned one + // exact match. Keep unknown or ambiguous values intact so the regular + // permission check can report them instead of targeting a wrong namespace. + const value = namespaceId ?? (matchingNames.length === 1 ? matchingNames[0] : namespace); + const identity = nacosNamespaceIdentity(value); + if (!seen.has(identity)) { + seen.add(identity); + normalized.push(value); + } + } + return normalized; +} + +async function resolveManualNacosNamespaceNames(namespaces: string[]): Promise { + const draftId = buildDraftVisibleDatabasesConnectionId(uuid()); + try { + const submittedConfig = connectionConfigForSubmit(draftId); + const draftConfig = { + ...submittedConfig, + // Resolve display names against the authenticated directory rather than + // passing them as namespace IDs to the configuration and naming APIs. + external_config: submittedConfig.db_type === "nacos" ? { ...(submittedConfig.external_config as NacosAdminConfig), managedNamespaces: undefined } : submittedConfig.external_config, + id: draftId, + one_time: true, + }; + await api.connectDb(draftConfig); + return normalizeManualNacosNamespaceNames(namespaces, await loadReadableNacosNamespaces(draftId, api)); + } catch { + // Some Nacos deployments do not expose a readable namespace directory to + // ordinary users. Preserve manually entered IDs for the normal test path. + return namespaces; + } finally { + await api.disconnectDb(draftId).catch(() => undefined); + } +} + +function isNacosNamespaceListingPermissionError(error: unknown): boolean { + const message = errorMessage(error); + if (/NACOS_ERROR\[(?:v3ManagedNamespacesRequired|managedNamespacesRequired)\]/.test(message)) return true; + return /\/v3\/(?:admin|console)\/core\/namespace\/list/.test(message) && /\b403\b/.test(message) && /authorization failed/i.test(message); +} + async function openVisibleNacosNamespacesPicker() { if (!ensureConnectionHostResolvedFromUrl()) return; if (!canChooseVisibleNacosNamespaces.value || isLoadingVisibleNacosNamespaces.value) return; isLoadingVisibleNacosNamespaces.value = true; visibleNacosNamespaceError.value = ""; + visibleNacosNamespaceListingPermissionDenied.value = false; + visibleNacosNamespaceDynamicAllSupported.value = false; visibleNacosNamespaceSearchText.value = ""; + visibleNacosNamespaceAccessMode.value = "automatic"; const draftId = buildDraftVisibleDatabasesConnectionId(uuid()); try { + const submittedConfig = connectionConfigForSubmit(draftId); const draftConfig = { - ...connectionConfigForSubmit(draftId), + ...submittedConfig, + // Automatic detection must not inherit a previously saved manual scope. + // Otherwise it would only rediscover that old subset instead of checking + // what the currently configured account can really access. + external_config: canDetectNacosNamespaceAccess.value && submittedConfig.db_type === "nacos" ? { ...(submittedConfig.external_config as NacosAdminConfig), managedNamespaces: undefined } : submittedConfig.external_config, + visible_databases: undefined, id: draftId, one_time: true, }; await api.connectDb(draftConfig); const namespaces = await loadReadableNacosNamespaces(draftId, api); visibleNacosNamespaces.value = [...namespaces].sort((left, right) => nacosNamespaceLabel(left).localeCompare(nacosNamespaceLabel(right))); + try { + const sidebarSnapshot = await api.nacosSidebarSnapshot(draftId); + const readableIds = normalizeVisibleNacosNamespaceSelection(visibleNacosNamespaces.value.map(nacosNamespaceValue), visibleNacosNamespaces.value); + const sidebarIds = normalizeVisibleNacosNamespaceSelection(sidebarSnapshot.namespaces.map(nacosNamespaceValue), visibleNacosNamespaces.value); + const readableSet = new Set(readableIds.map(nacosNamespaceIdentity)); + const sidebarSet = new Set(sidebarIds.map(nacosNamespaceIdentity)); + visibleNacosNamespaceDynamicAllSupported.value = readableSet.size === sidebarSet.size && [...readableSet].every((namespace) => sidebarSet.has(namespace)); + } catch { + visibleNacosNamespaceDynamicAllSupported.value = false; + } const configured = form.value.visible_databases; const initialSelection = Array.isArray(configured) ? normalizeVisibleNacosNamespaceSelection(configured, visibleNacosNamespaces.value) : visibleNacosNamespaces.value.map(nacosNamespaceValue); visibleNacosNamespaceSelection.value = new Set(initialSelection); @@ -4740,7 +4860,8 @@ async function openVisibleNacosNamespacesPicker() { } catch (e: any) { visibleNacosNamespaces.value = []; visibleNacosNamespaceSelection.value = new Set(); - visibleNacosNamespaceError.value = errorMessage(e); + visibleNacosNamespaceListingPermissionDenied.value = isNacosNamespaceListingPermissionError(e); + visibleNacosNamespaceError.value = visibleNacosNamespaceListingPermissionDenied.value ? t("nacos.nacosManagedNamespacesRequired") : errorMessage(e); testResult.value = { ok: false, message: visibleNacosNamespaceError.value }; showVisibleNacosNamespacesDialog.value = true; } finally { @@ -4765,13 +4886,31 @@ function clearVisibleNacosNamespaceSelection() { } function showAllVisibleNacosNamespaces() { + nacosDynamicAllNamespaces.value = true; + nacosManagedNamespacesText.value = ""; form.value.visible_databases = undefined; resetVisibleNacosNamespaceDraftState(); } -function saveVisibleNacosNamespaceSelection() { +async function saveVisibleNacosNamespaceSelection() { if (!visibleNacosNamespaceCanSave.value) return; - form.value.visible_databases = normalizeVisibleNacosNamespaceSelection(visibleNacosNamespaceSelection.value, visibleNacosNamespaces.value); + if (visibleNacosNamespaceAccessMode.value === "manual") { + const manualNamespaces = parseNacosManagedNamespaces(nacosManagedNamespacesText.value); + isResolvingManualNacosNamespaces.value = true; + const resolvedNamespaces = isNacosV3AdminPlane.value ? manualNamespaces : await resolveManualNacosNamespaceNames(manualNamespaces); + isResolvingManualNacosNamespaces.value = false; + nacosManagedNamespacesText.value = resolvedNamespaces.join("\n"); + form.value.visible_databases = resolvedNamespaces; + nacosDynamicAllNamespaces.value = false; + } else { + nacosManagedNamespacesText.value = ""; + const selected = normalizeVisibleNacosNamespaceSelection(visibleNacosNamespaceSelection.value, visibleNacosNamespaces.value); + const all = normalizeVisibleNacosNamespaceSelection(visibleNacosNamespaces.value.map(nacosNamespaceValue), visibleNacosNamespaces.value); + const selectsEntireReadableList = selected.length === all.length && selected.every((namespace, index) => namespace === all[index]); + const useDynamicAll = selectsEntireReadableList && visibleNacosNamespaceDynamicAllSupported.value; + form.value.visible_databases = useDynamicAll ? undefined : selected; + nacosDynamicAllNamespaces.value = useDynamicAll; + } showVisibleNacosNamespacesDialog.value = false; } @@ -5214,6 +5353,26 @@ watch( }, ); +watch( + () => currentNacosScopeFingerprint(), + (current) => { + if (form.value.db_type !== "nacos") { + nacosScopeFingerprintBaseline = current; + return; + } + if (!nacosScopeFingerprintBaseline || current === nacosScopeFingerprintBaseline) { + nacosScopeFingerprintBaseline = current; + return; + } + nacosScopeFingerprintBaseline = current; + nacosManagedNamespacesText.value = ""; + nacosDynamicAllNamespaces.value = false; + form.value.visible_databases = undefined; + resetVisibleNacosNamespaceDraftState(); + resetTestState(); + }, +); + watch(visibleDatabaseShowSystem, (show) => { if (show) return; const connection = connectionConfigSnapshotForVisibleDatabases(); @@ -5404,6 +5563,11 @@ async function persistConnectionNoteVisibilityDraft() { async function save() { if (!ensureConnectionHostResolvedFromUrl()) return; if (isSaving.value) return; + if (!hasNacosNamespaceScopeForSave()) { + testResult.value = null; + await openVisibleNacosNamespacesPicker(); + return; + } const databaseInfoForSave = visibleTestDatabaseInfo.value ?? visibleSavedDatabaseInfo.value; isSaving.value = true; let connectionSaved = false; @@ -6619,11 +6783,28 @@ function openExternalUrl(url: string) {
+
+ +
+ +
+

- +

@@ -6655,29 +6836,12 @@ function openExternalUrl(url: string) {

-
-
- - {{ t("nacos.nacosOrdinaryAccount") }} - {{ t("nacos.nacosOrdinaryAccountHint") }} - - -
-
-
- - {{ t("nacos.nacosManagedNamespacesSeparator") }} -
-