From 0279a0903c7fbb8981b3f0f4141e64393bf7290c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Fri, 4 Sep 2026 00:29:41 +0800 Subject: [PATCH] feat(nacos): add browser console open, zen mode and metadata view --- apps/desktop/src/App.vue | 12 ++- .../connection/ConnectionDialog.vue | 26 +++++- .../src/components/layout/ContentArea.vue | 4 + .../components/nacos/NacosAdminConsole.vue | 92 ++++++++++++++++++- .../__tests__/NacosAdminConsoleLayout.spec.ts | 20 ++++ apps/desktop/src/i18n/locales/en.ts | 12 +++ apps/desktop/src/i18n/locales/es.ts | 12 +++ apps/desktop/src/i18n/locales/it.ts | 12 +++ apps/desktop/src/i18n/locales/ja.ts | 12 +++ apps/desktop/src/i18n/locales/ko.ts | 12 +++ apps/desktop/src/i18n/locales/pt-BR.ts | 12 +++ apps/desktop/src/i18n/locales/zh-CN.ts | 12 +++ apps/desktop/src/i18n/locales/zh-TW.ts | 12 +++ .../__tests__/app/rightSidebarPanels.spec.ts | 8 +- .../connection/nacosConnectionDialog.spec.ts | 5 + .../lib/__tests__/nacos/nacosAdmin.spec.ts | 23 +++++ apps/desktop/src/lib/nacos/nacosAdmin.ts | 61 +++++++++++- apps/desktop/src/types/nacos.ts | 2 + 18 files changed, 335 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index b02b6247ae..c124ba0abb 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -599,10 +599,14 @@ async function detachTab(tab: QueryTab, position?: { x: number; y: number }) { watch( () => activeTab.value?.mode, (mode) => { - if (mode !== "data") isZenMode.value = false; + if (!supportsZenMode(mode)) isZenMode.value = false; }, ); +function supportsZenMode(mode: QueryTab["mode"] | undefined) { + return mode === "data" || mode === "nacos"; +} + watch(activeOutputView, (view) => { if (isDetachedWindowContext && detachedContextTabId) { void queryStore.flushDetachedTabPersistence(detachedContextTabId, { activeOutputView: view }).catch(() => {}); @@ -610,7 +614,7 @@ watch(activeOutputView, (view) => { }); function toggleZenMode() { - if (activeTab.value?.mode !== "data") return; + if (!supportsZenMode(activeTab.value?.mode)) return; isZenMode.value = !isZenMode.value; } @@ -2953,7 +2957,7 @@ async function handleKeydown(e: KeyboardEvent) { setSidebarOpen(!sidebarOpen.value); return; } - if (isToggleZenModeShortcut(e, shortcuts) && activeTab.value?.mode === "data") { + if (isToggleZenModeShortcut(e, shortcuts) && supportsZenMode(activeTab.value?.mode)) { e.preventDefault(); e.stopPropagation(); toggleZenMode(); @@ -3487,6 +3491,7 @@ onUnmounted(() => { :selected-sql="selectedSql" :cursor-pos="cursorPos" :block-dangerous-redis-commands="blockDangerousRedisCommands" + :zen-mode="isZenMode" @update:active-output-view="activeOutputView = $event" @fix-with-ai="fixWithAi" @send-selection-to-ai="sendSelectionToAi" @@ -3544,6 +3549,7 @@ onUnmounted(() => { @structure-editor-close="activeTab && queryStore.closeTab(activeTab.id)" @open-settings="openSettings" @open-connection-settings="openConnectionSettings" + @toggle-zen-mode="toggleZenMode" /> diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 30d0636fbc..9af03a9c90 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -72,7 +72,7 @@ import { normalizeRabbitmqAddresses, parseRabbitmqAddress } from "@/lib/connecti import { detectMqUiAuthKind, isMqAuthKindAllowedForSystem, type MqUiAuthKind } from "@/lib/connection/mqAuth"; import { driverInstallProgressChannel, driverInstallProgressPercent, isDriverInstallProgressForOperation, requestAgentInstallCancellation, resolveAgentInstallOutcome, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi"; import { requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityConfig, sqlServerUsesLegacyCompatibility, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility"; -import { normalizeNacosEndpoint, normalizeNacosMetricsUrl, parseNacosManagedNamespaces } from "@/lib/nacos/nacosAdmin"; +import { normalizeNacosConsoleUrl, normalizeNacosEndpoint, normalizeNacosMetricsUrl, parseNacosManagedNamespaces } from "@/lib/nacos/nacosAdmin"; import { loadReadableNacosNamespaces, nacosNamespaceIdentity, normalizeNacosNamespaceSelection } from "@/lib/nacos/nacosNamespaceVisibility"; import { ArrowLeft, @@ -912,6 +912,7 @@ const nacosVersionMode = ref("v2"); const nacosApiPlane = ref("admin"); const nacosServerAddr = ref(""); const nacosContextPath = ref(""); +const nacosConsoleUrl = ref(""); const nacosManagedNamespacesText = ref(""); const nacosRNacosConsoleAddr = ref(""); const nacosHistoryEnabled = ref(false); @@ -972,6 +973,10 @@ const nacosPrimaryAddressPlaceholder = computed(() => { } return "http://127.0.0.1:8848/nacos"; }); +const nacosWebConsoleUrlPlaceholder = computed(() => { + if (nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3") 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"); @@ -1286,6 +1291,7 @@ function resetNacosFields(config?: Partial) { const contextPath = config?.contextPath?.trim() || ""; nacosServerAddr.value = serverAddr; nacosContextPath.value = contextPath; + nacosConsoleUrl.value = config?.consoleUrl?.trim() || ""; nacosManagedNamespacesText.value = (config?.managedNamespaces || []).join("\n"); nacosDynamicAllNamespaces.value = !!config && !config.managedNamespaces?.length && !Array.isArray(form.value.visible_databases); nacosRNacosConsoleAddr.value = config?.rnacosConsoleAddr?.trim() || ""; @@ -1656,6 +1662,15 @@ function buildNacosAdminConfig(): NacosAdminConfig { let rnacosConsoleAuth: NacosRNacosConsoleAuth | undefined; const managedNamespaces = nacosImplementation.value === "nacos" && nacosAuthKind.value === "usernamePassword" ? parseNacosManagedNamespaces(nacosManagedNamespacesText.value) : []; let metricsUrl: string | undefined; + let consoleUrl: string | undefined; + const usesIndependentConsoleUrl = nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" && nacosApiPlane.value === "admin"; + if (usesIndependentConsoleUrl && nacosConsoleUrl.value.trim()) { + try { + consoleUrl = normalizeNacosConsoleUrl(nacosConsoleUrl.value); + } catch { + throw new Error(t("connection.nacosWebConsoleUrlInvalid")); + } + } if (nacosMetricsMode.value === "custom") { try { metricsUrl = normalizeNacosMetricsUrl(nacosMetricsUrl.value); @@ -1681,6 +1696,7 @@ function buildNacosAdminConfig(): NacosAdminConfig { apiPlane: nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" ? nacosApiPlane.value : undefined, serverAddr: normalized.serverAddr, contextPath: normalized.contextPath || undefined, + consoleUrl, managedNamespaces: managedNamespaces.length ? managedNamespaces : undefined, rnacosConsoleAddr: rnacosConsoleConfigured ? nacosRNacosConsoleAddr.value.trim() : undefined, rnacosHistoryEnabled: nacosImplementation.value === "rnacos" ? nacosHistoryEnabled.value : undefined, @@ -8216,6 +8232,14 @@ function openExternalUrl(url: string) {

{{ t("nacos.nacosPageSizeHint") }}

+
+
+ +

{{ t("connection.nacosWebConsoleUrlHint") }}

+
+ +
+
diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 8e8df06995..d1cff8e280 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -209,6 +209,7 @@ const props = defineProps<{ selectedSql: string; cursorPos: number; blockDangerousRedisCommands: boolean; + zenMode?: boolean; }>(); const emit = defineEmits<{ @@ -243,6 +244,7 @@ const emit = defineEmits<{ structureEditorClose: []; openSettings: [initialTab?: string, initialSection?: string]; openConnectionSettings: [connectionId: string, initialTab: "advanced"]; + toggleZenMode: []; }>(); const { t, locale } = useI18n(); @@ -2364,6 +2366,8 @@ defineExpose({ :target-keyword="activeTab.nacosTargetKeyword" :target-request-id="activeTab.nacosTargetRequestId" :read-only="connectionIsEffectivelyReadOnly(activeConnection)" + :zen-mode="props.zenMode" + @toggle-zen-mode="emit('toggleZenMode')" />
diff --git a/apps/desktop/src/components/nacos/NacosAdminConsole.vue b/apps/desktop/src/components/nacos/NacosAdminConsole.vue index 8384a285dd..ec05e8dc8b 100644 --- a/apps/desktop/src/components/nacos/NacosAdminConsole.vue +++ b/apps/desktop/src/components/nacos/NacosAdminConsole.vue @@ -3,7 +3,7 @@ import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMoun import { Compartment, StateEffect, StateField, type Extension } from "@codemirror/state"; import { StreamLanguage, ensureSyntaxTree } from "@codemirror/language"; import { Decoration, EditorView } from "@codemirror/view"; -import { Archive, ArrowLeftRight, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, Clipboard, Columns3, Download, FileClock, FileInput, FileText, Loader2, Network, Plus, RefreshCw, Save, Search, Send, Server, Trash2, X } from "@lucide/vue"; +import { Archive, ArrowLeftRight, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, Clipboard, Columns3, Download, ExternalLink, FileClock, FileInput, FileText, Loader2, Maximize2, Minimize2, Network, Plus, RefreshCw, Save, Search, Send, Server, Trash2, X } from "@lucide/vue"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import ProductionContextBadge from "@/components/common/ProductionContextBadge.vue"; @@ -40,6 +40,8 @@ import { isNacosConfigDeleteSnapshotInScope, resolveNacosConfigCopyText, resolveNacosConfigSaveCompletion, + resolveNacosConsoleUrl, + nacosMetadataTableRows, type NacosConfigDeleteSnapshot, } from "@/lib/nacos/nacosAdmin"; import { createNacosNamespaceRequestGuard, subscribeNacosNamespacesChanged, type NacosNamespacesChangedDetail } from "@/lib/nacos/nacosNamespaceCache"; @@ -93,6 +95,11 @@ const props = defineProps<{ targetKeyword?: string; targetRequestId?: number; readOnly?: boolean; + zenMode?: boolean; +}>(); + +const emit = defineEmits<{ + toggleZenMode: []; }>(); type AdminTab = "configs" | "services"; @@ -241,6 +248,8 @@ const instanceEditorOpen = ref(false); const instanceEditorError = ref(""); const instanceEditorTarget = ref(null); const instanceEditor = ref({ weight: "1", metadata: "{}" }); +const instanceMetadataTableOpen = ref(false); +const instanceMetadataTableTarget = ref(null); const serviceEditorOpen = ref(false); const serviceEditorLoading = ref(false); const serviceEditorError = ref(""); @@ -289,6 +298,16 @@ function isSelectedConfigListItem(item: NacosConfigItem) { } const namespace = computed(() => props.namespace ?? connectionInfo.value?.namespace ?? ""); +const instanceMetadataTableRows = computed(() => nacosMetadataTableRows(instanceMetadataTableTarget.value?.metadata)); +const nacosConsoleUrl = computed(() => { + const config = connectionStore.getConfig(props.connectionId)?.external_config; + if (!config || typeof config !== "object" || Array.isArray(config)) return undefined; + try { + return resolveNacosConsoleUrl(config as import("@/types/nacos").NacosAdminConfig); + } catch { + return undefined; + } +}); const nacosProductionContext = computed(() => productionContextForDatabase(connectionStore.getConfig(props.connectionId), namespace.value)); const batchTargetConnections = computed(() => connectionStore.connections @@ -2152,6 +2171,11 @@ function openInstanceEditor(instance: NacosInstanceInfo) { instanceEditorOpen.value = true; } +function openInstanceMetadataTable(instance: NacosInstanceInfo) { + instanceMetadataTableTarget.value = instance; + instanceMetadataTableOpen.value = true; +} + function submitInstanceEditor() { const instance = instanceEditorTarget.value; const weight = Number(instanceEditor.value.weight); @@ -2645,6 +2669,19 @@ onBeforeUnmount(() => { pauseConfigEditorViewportWork(); destroyConfigEditor(); }); + +function openNacosConsole() { + const url = nacosConsoleUrl.value; + if (!url) { + toast(t("nacos.consoleUrlMissing"), 5000); + return; + } + if (isTauriRuntime()) { + void import("@tauri-apps/plugin-shell").then(({ open }) => open(url)).catch(() => toast(t("nacos.consoleOpenFailed"), 5000)); + return; + } + window.open(url, "_blank", "noopener,noreferrer"); +}