From 3d1b84831f8e01002372c8115c0ea97a394a4d56 Mon Sep 17 00:00:00 2001 From: Jinpy Date: Mon, 14 Sep 2026 21:03:47 +0800 Subject: [PATCH] fix(plugins): stabilize plugin integration --- Cargo.lock | 1 + apps/desktop/src/App.vue | 44 +- .../components/config/DriverStoreDialog.vue | 6 +- .../connection/ConnectionDialog.vue | 363 ++++++++------- .../src/components/layout/AppTabBar.vue | 7 + .../src/components/layout/ContentArea.vue | 38 +- .../src/components/layout/EditorGroup.vue | 15 +- .../components/layout/SqlEditorWorkspace.vue | 44 +- .../layout/__tests__/AppTabBar.spec.ts | 5 + .../EditorGroupTabBar.groups.spec.ts | 34 ++ .../SqlEditorWorkspace.mount.spec.ts | 93 +++- .../plugins/PluginConnectionFields.spec.ts | 253 ++++++++++- .../plugins/PluginConnectionFields.vue | 159 ++++++- .../plugins/PluginWorkbenchHost.vue | 49 +- .../__tests__/SshHostKeyPromptDialog.spec.ts | 60 ++- .../src/components/ui/PasswordTextarea.vue | 35 ++ .../desktop/src/composables/useTauriEvents.ts | 35 ++ apps/desktop/src/composables/useTheme.ts | 8 +- apps/desktop/src/i18n/locales/en.ts | 3 + apps/desktop/src/i18n/locales/es.ts | 3 + apps/desktop/src/i18n/locales/it.ts | 3 + apps/desktop/src/i18n/locales/ja.ts | 3 + apps/desktop/src/i18n/locales/ko.ts | 3 + apps/desktop/src/i18n/locales/pt-BR.ts | 3 + apps/desktop/src/i18n/locales/zh-CN.ts | 3 + apps/desktop/src/i18n/locales/zh-TW.ts | 3 + .../connectionDialog.deepLinkServices.spec.ts | 7 +- .../connectionDialogProfileSwitch.spec.ts | 3 +- .../gbase8sConnectionDialog.spec.ts | 6 +- .../plugins/pluginCenterIntegration.spec.ts | 7 +- .../src/lib/app/openTabsPersistence.ts | 2 + apps/desktop/src/lib/backend/api.ts | 1 + apps/desktop/src/lib/backend/http.ts | 6 + apps/desktop/src/lib/backend/tauri.ts | 5 + .../lib/connection/connectionPassword.spec.ts | 73 +++ .../src/lib/connection/connectionPassword.ts | 26 +- .../lib/database/databaseFeatureSupport.ts | 3 +- .../src/lib/plugins/frontendPlugin.spec.ts | 58 +++ .../desktop/src/lib/plugins/frontendPlugin.ts | 11 +- .../src/lib/plugins/pluginAppearance.spec.ts | 34 ++ .../src/lib/plugins/pluginAppearance.ts | 84 ++++ .../lib/plugins/pluginFieldConditions.spec.ts | 87 ++++ .../src/lib/plugins/pluginFieldConditions.ts | 47 ++ .../src/lib/plugins/pluginHostBridge.spec.ts | 84 +++- .../src/lib/plugins/pluginHostBridge.ts | 84 +++- .../connectionStore.savePassword.spec.ts | 124 +++++ .../queryStore.openPluginWorkbench.spec.ts | 98 ++++ ...ueryStore.pluginTabCloseDisconnect.spec.ts | 60 +++ ...yStore.reconnectRestoredPluginTabs.spec.ts | 68 +++ apps/desktop/src/stores/connectionStore.ts | 56 ++- apps/desktop/src/stores/queryStore.ts | 137 +++++- apps/desktop/src/types/database.ts | 23 + .../src/types/generated/databaseTypes.ts | 2 +- crates/dbx-core/examples/install_plugin.rs | 29 ++ crates/dbx-core/src/cloud_sync.rs | 23 +- crates/dbx-core/src/connection.rs | 64 ++- crates/dbx-core/src/connection_secrets.rs | 65 ++- crates/dbx-core/src/plugins.rs | 129 +++++- crates/dbx-core/src/plugins/host.rs | 424 +++++++++++++++++- crates/dbx-core/src/plugins/manifest.rs | 146 ++++++ crates/dbx-core/src/query.rs | 17 +- crates/dbx-core/src/storage.rs | 73 ++- .../dbx-mcp/examples/verify_plugin_store.rs | 38 ++ crates/dbx-mcp/src/backend.rs | 67 ++- crates/dbx-mcp/tests/plugin_tools_bridge.rs | 66 +++ .../sidebarRuntimeDecomposition.test.ts | 3 +- plugins/manifest.schema.json | 13 +- plugins/sdk/cli/src/lib.rs | 69 ++- plugins/sdk/dev-host/browser-bridge.mjs | 15 +- plugins/sdk/dev-host/server.mjs | 2 +- scripts/sync-connection-types.mjs | 5 +- src-tauri/Cargo.toml | 1 + src-tauri/src/commands/mcp_bridge.rs | 291 +++++++++++- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/ssh_keys.rs | 207 +++++++++ src-tauri/src/lib.rs | 60 ++- 76 files changed, 3846 insertions(+), 431 deletions(-) create mode 100644 apps/desktop/src/components/ui/PasswordTextarea.vue create mode 100644 apps/desktop/src/lib/connection/connectionPassword.spec.ts create mode 100644 apps/desktop/src/lib/plugins/pluginAppearance.spec.ts create mode 100644 apps/desktop/src/lib/plugins/pluginAppearance.ts create mode 100644 apps/desktop/src/lib/plugins/pluginFieldConditions.spec.ts create mode 100644 apps/desktop/src/lib/plugins/pluginFieldConditions.ts create mode 100644 apps/desktop/src/stores/__tests__/queryStore.openPluginWorkbench.spec.ts create mode 100644 apps/desktop/src/stores/__tests__/queryStore.pluginTabCloseDisconnect.spec.ts create mode 100644 apps/desktop/src/stores/__tests__/queryStore.reconnectRestoredPluginTabs.spec.ts create mode 100644 crates/dbx-core/examples/install_plugin.rs create mode 100644 crates/dbx-mcp/examples/verify_plugin_store.rs create mode 100644 crates/dbx-mcp/tests/plugin_tools_bridge.rs create mode 100644 src-tauri/src/commands/ssh_keys.rs diff --git a/Cargo.lock b/Cargo.lock index 299bbd6ed3..b2fc4e7696 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2050,6 +2050,7 @@ dependencies = [ "tauri-plugin-single-instance", "tauri-plugin-updater", "tauri-plugin-window-state", + "tempfile", "tiberius", "tokio", "tokio-postgres", diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 35910ed76d..0f537d6160 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -115,7 +115,7 @@ import { normalizeSqlExecutionTarget, sqlExecutionTargetCapabilities } from "@/l import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard"; import { buildHistoryAiAnalysisPrompt } from "@/lib/history/historyAiAnalysis"; import { countAvailableAgentDriverUpdates } from "@/lib/connection/agentDriverUpdateBadge"; -import type { DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint"; +import type { DriverStoreFocus, DriverStoreTab } from "@/lib/connection/agentDriverInstallHint"; import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage"; import { apiUrl, webPath } from "@/lib/common/webPath"; import { shouldBlockAppNativeSelectAll } from "@/lib/common/clipboard"; @@ -152,6 +152,7 @@ import { resolveWindowContext } from "@/lib/app/windowContext"; import { openDetachedTabWindow } from "@/lib/app/detachedTabWindow"; const AiAssistant = defineAsyncComponent(() => import("@/components/editor/AiAssistant.vue")); +const PluginWorkbenchTab = defineAsyncComponent(() => import("@/components/plugins/PluginWorkbenchTab.vue")); const QueryHistory = defineAsyncComponent(() => import("@/components/editor/QueryHistory.vue")); const SqlLibraryPanel = defineAsyncComponent(() => import("@/components/layout/SqlLibraryPanel.vue")); const SqlFilePanel = defineAsyncComponent(() => import("@/components/layout/SqlFilePanel.vue")); @@ -416,6 +417,10 @@ const blockingAiRunCount = computed(() => (isDesktop ? blockingDesktopAiRunsForQ let aiRunsQuitConfirmed = false; const activeTab = computed(() => queryStore.tabs.find((t) => t.id === queryStore.activeTabId)); +// Plugin workbench tabs stay mounted once opened (hidden via v-show): an +// iframe moved out of the DOM reloads from scratch, so KeepAlive/ContentArea +// remounts flash the whole webview and drop its live session state. +const mountedPluginWorkbenchTabs = computed(() => queryStore.tabs.filter((tab) => tab.mode === "plugin-workbench" && tab.pluginWorkbench)); const tabNavigationHistory = ref(createTabNavigationHistory()); let pendingTabHistoryNavigationId: string | null = null; let detachedEventUnlisteners: Array<() => void> = []; @@ -870,10 +875,10 @@ function requestActiveEditorExecuteInNewResultTab() { const specialPageTabs = computed(() => ({ settingsOpen: settingsPageTabOpen.value, settingsActive: settingsStore.settingsPageActive, - driverStoreOpen: driverStoreTabOpen.value, - driverStoreActive: driverStoreActive.value, pluginCenterOpen: pluginCenterTabOpen.value, pluginCenterActive: pluginCenterActive.value, + driverStoreOpen: driverStoreTabOpen.value, + driverStoreActive: driverStoreActive.value, driverUpdateCount: toolbarAgentDriverUpdateCount.value, })); provide(GROUP_TAB_BAR_PORTAL, createGroupTabBarPortal(computed(() => !isDetachedWindowContext && (driverStoreActive.value || pluginCenterActive.value || settingsStore.settingsPageActive)))); @@ -903,10 +908,10 @@ provide(EDITOR_TOOLBAR_ACTIONS, { specialPageTabs, activateSettingsPage, closeSettingsPage, - activateDriverStore: () => openDriverStorePage(), - closeDriverStore: closeDriverStorePage, activatePluginCenter: () => openPluginCenterPage(pluginCenterFocus.value), closePluginCenter: closePluginCenterPage, + activateDriverStore: () => openDriverStorePage(), + closeDriverStore: closeDriverStorePage, }); // Upstream "preview changes" entry: dormant until the group toolbar wires the @@ -990,8 +995,8 @@ const { setupTauriListeners, cleanupTauriListeners } = useTauriEvents({ openSqlFilePath, openDbFilePath, openConnectionDeepLink, - openAiConfigDeepLink, closeActiveSurface, + openAiConfigDeepLink, }); const { showCloseActionPrompt, chooseQuit, chooseMinimize, cancelCloseActionPrompt, performCloseAction, setupCloseActionPromptListener, cleanupCloseActionPromptListener } = useCloseActionPrompt({ requestClose: requestAppClose }); useVisibilityChange(); @@ -1089,7 +1094,7 @@ function closeSettingsPage() { const driverStoreFocus = ref(null); -function openDriverStorePage(target?: "agent" | "jdbc" | "storage" | "runtime" | DriverStoreFocus | null) { +function openDriverStorePage(target?: DriverStoreTab | DriverStoreFocus | null) { if (typeof target === "string") { driverStoreActiveTab.value = target; driverStoreFocus.value = null; @@ -1101,6 +1106,7 @@ function openDriverStorePage(target?: "agent" | "jdbc" | "storage" | "runtime" | } driverStoreTabOpen.value = true; activateMainContentSurface("driverStore"); + pluginCenterActive.value = false; } function closeDriverStorePage() { @@ -1329,6 +1335,7 @@ watch( if (id) newQueryContextSource.value = "tab"; if (id) activateQuerySurface(); else if (previousId) activateOpenSpecialPageFallback(); + if (id && pluginCenterActive.value) pluginCenterActive.value = false; const tab = id ? queryStore.tabs.find((candidate) => candidate.id === id) : undefined; const selection = tab?.editorSelection; selectedSql.value = tab && selection && selection.anchor !== selection.head ? tab.sql.slice(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head)) : ""; @@ -2308,6 +2315,7 @@ async function openConnectionDeepLink(url: string) { if (!draft) return; connectionStore.stopEditing(); connectionStore.stopCreatingConnectionInGroup(); + connectionPluginProvider.value = null; connectionDialogPrefill.value = draft; showConnectionDialog.value = true; } catch (e: any) { @@ -2372,6 +2380,7 @@ function setConnectionDialogOpen(value: boolean) { function openConnectionSettings(connectionId: string, initialTab: ConfigTab = "connection") { if (!connectionStore.getConfig(connectionId)) return; + connectionPluginProvider.value = null; connectionDialogInitialTab.value = initialTab; connectionStore.startEditing(connectionId); showConnectionDialog.value = true; @@ -2398,6 +2407,8 @@ async function newQuery() { } else if (connectionTarget.kind === "nacos-admin") { await connectionStore.loadNacosNamespaces(target.connectionId); queryStore.openNacosAdmin(target.connectionId); + } else if (connectionTarget.kind === "plugin-workbench") { + await queryStore.openPluginConnection(target.connectionId); } else { queryStore.createTab(target.connectionId, "", `${conn.name}:keys`, connectionTarget.kind); } @@ -3038,6 +3049,7 @@ function activateQueryTab(tabId: string): boolean { dispatchBeforeTabSwitch(tabId); if (!queryStore.activateTab(tabId)) return false; activateQuerySurface(); + pluginCenterActive.value = false; return true; } @@ -3357,6 +3369,11 @@ async function initApp() { onOptionalStateError: (error) => console.error("[STARTUP] settingsStore.initAiConfigs failed", error), }); } + // Restored plugin tabs need the sidecar connection registry repopulated + // (see reconnectRestoredPluginTabs); kick it off before the heavier + // optional init so it races ahead of each plugin webview's first + // session/open. Fire-and-forget: it must never block startup. + void queryStore.reconnectRestoredPluginTabs(); await settingsStore.initDesktopSettings().catch(() => {}); if (isDesktop) { updateWindowReady = true; @@ -3651,6 +3668,7 @@ onUnmounted(() => { :tab-bar-width="tabBarWidth" :tab-bar-collapsed="tabBarCollapsed" @activate-driver-store="openDriverStorePage" + @activate-plugin-center="openPluginCenterPage(pluginCenterFocus)" @activate-settings-page="activateSettingsPage" @activate-tab="activateQueryTab" @close-driver-store="closeDriverStorePage" @@ -3707,9 +3725,10 @@ onUnmounted(() => {
-
+
+ +
diff --git a/apps/desktop/src/components/config/DriverStoreDialog.vue b/apps/desktop/src/components/config/DriverStoreDialog.vue index 8919ca0e85..3277edc2a1 100644 --- a/apps/desktop/src/components/config/DriverStoreDialog.vue +++ b/apps/desktop/src/components/config/DriverStoreDialog.vue @@ -1443,8 +1443,8 @@ watch(driverStoreTab, (tab) => {
-
- +
+ {{ t("driverStore.agentDrivers") }} @@ -2263,7 +2263,7 @@ watch(driverStoreTab, (tab) => { .driver-store-tabs { display: grid !important; - width: 360px !important; + width: 480px !important; grid-template-columns: repeat(3, minmax(0, 1fr)) !important; } diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index c7494b0599..bfa5682f98 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -92,6 +92,7 @@ import { assertCompleteDatabaseCategories, databaseSelectionForCategory } from " import { loadConnectionPickerView, saveConnectionPickerView, type DbPickerView } from "@/lib/connection/connectionPickerViewPreference"; import { normalizeRocketmqNamesrvAddr } from "@/lib/connection/rocketmqNamesrv"; import { normalizeRabbitmqAddresses, parseRabbitmqAddress } from "@/lib/connection/rabbitmqAddresses"; +import { pluginFieldIsRequired, pluginFieldIsVisible } from "@/lib/plugins/pluginFieldConditions"; 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"; @@ -255,6 +256,8 @@ type LegacyTransportFields = { type LegacyConnectionConfig = ConnectionConfig & LegacyTransportFields; type ConnectionForm = Omit; type ConnectionTestState = ConnectionTestResult & { ok: boolean; scope?: "connection" | "ssh" }; +type PluginActionStatus = { ok: boolean; message: string }; +type SaveConnectionOptions = { connectAfterSave?: boolean; closeOnSuccess?: boolean }; const { t, locale: appLocale } = useI18n(); const { toast } = useToast(); @@ -328,11 +331,9 @@ const connectionErrorDetail = ref(""); const testResultCopied = ref(false); const connectionErrorCopied = ref(false); const editingId = ref(null); -const installedPlugins = ref([]); const pluginFormValues = ref>({}); const pluginLoadError = ref(""); const runningPluginActionId = ref(null); -type PluginActionStatus = { ok: boolean; message: string }; const pluginActionStatus = ref(null); const draftTestConnectionId = ref(uuid()); const showVisibleDatabasesDialog = ref(false); @@ -365,6 +366,7 @@ const isLoadingVisibleSchemas = ref(false); const visibleSchemaNames = ref([]); const visibleSchemaInitialSelection = ref([]); const visibleSchemaError = ref(""); +const installedPlugins = ref([]); let testRunId = 0; let unlistenAgentInstallProgress: (() => void) | null = null; @@ -759,6 +761,13 @@ const keepaliveEnabled = computed({ const selectedTransportLayerId = ref(null); const draggedTransportLayerId = ref(null); const selectedType = ref("mysql"); +const pluginRegistry = computed(() => createFrontendPluginRegistry(installedPlugins.value, appLocale.value)); +const pluginConnectionProviders = computed(() => pluginRegistry.value.listConnectionProviders()); +const selectedPluginProvider = computed(() => pluginProviderEntryForOption(selectedType.value)); +const selectedPluginIcon = computed(() => (selectedPluginProvider.value ? pluginConnectionProviderIcon(selectedPluginProvider.value) : undefined)); +const isPluginConnection = computed(() => form.value.db_type === "plugin"); +const pluginFooterActions = computed(() => (selectedPluginProvider.value ? pluginConnectionActionsForDialog(selectedPluginProvider.value.contribution, !!editingId.value) : [])); +const pluginFooterBusy = computed(() => isTesting.value || isSaving.value || !!runningPluginActionId.value); const customDriverName = ref(""); const mongoUseUrl = ref(false); const jdbcDriverPathsInput = ref(""); @@ -1190,13 +1199,6 @@ function selectedProfile() { return driverProfiles[profile] ?? driverProfiles.mysql; } -const isPluginConnection = computed(() => form.value.db_type === "plugin"); -const pluginConnectionProviders = computed(() => createFrontendPluginRegistry(installedPlugins.value, appLocale.value).listConnectionProviders()); -const selectedPluginProvider = computed(() => pluginProviderEntryForOption(selectedType.value)); -const selectedPluginIcon = computed(() => (selectedPluginProvider.value ? pluginConnectionProviderIcon(selectedPluginProvider.value) : undefined)); -const pluginFooterActions = computed(() => (selectedPluginProvider.value ? pluginConnectionActionsForDialog(selectedPluginProvider.value.contribution, !!editingId.value) : [])); -const pluginFooterBusy = computed(() => isTesting.value || isSaving.value || !!runningPluginActionId.value); - function pluginProviderEntry(pluginId: string, providerId: string) { return pluginConnectionProviders.value.find((entry) => entry.plugin.manifest.id === pluginId && entry.contribution.id === providerId) || null; } @@ -1206,62 +1208,6 @@ function pluginProviderEntryForOption(value: string) { return target ? pluginProviderEntry(target.pluginId, target.providerId) : null; } -function applyPluginProvider(value: string, preserveConnectionFields = false, existing?: ConnectionConfig): boolean { - const target = parsePluginConnectionProviderOptionValue(value); - if (!target) return false; - const entry = pluginProviderEntry(target.pluginId, target.providerId); - if (!entry) return false; - - selectedType.value = value; - if (!preserveConnectionFields) { - form.value = { - ...defaultForm(), - name: entry.contribution.label, - db_type: "plugin", - driver_profile: "plugin", - driver_label: entry.contribution.label, - host: "", - port: 0, - username: "", - password: "", - query_timeout_secs: 60, - }; - } else { - form.value.db_type = "plugin"; - form.value.driver_profile = "plugin"; - form.value.driver_label = entry.contribution.label; - } - pluginFormValues.value = pluginConnectionFormValues(entry.contribution, existing); - if (!preserveConnectionFields) { - const nameField = entry.contribution.fields.find((field) => field.binding === "name"); - const defaultName = nameField ? pluginFormValues.value[nameField.key] : undefined; - if (typeof defaultName === "string" && defaultName.trim()) form.value.name = defaultName.trim(); - } - connectionUrlInput.value = ""; - appliedConnectionUrlInput.value = ""; - return true; -} - -async function loadInstalledPlugins() { - pluginLoadError.value = ""; - try { - installedPlugins.value = await api.listPlugins(); - } catch (cause) { - installedPlugins.value = []; - pluginLoadError.value = cause instanceof Error ? cause.message : String(cause); - } -} - -function applyRequestedPluginProvider(target: PluginCenterFocus | null | undefined) { - if (!target?.pluginId || !target.providerId) return false; - const value = pluginConnectionProviderOptionValue(target.pluginId, target.providerId); - if (!applyPluginProvider(value)) return false; - selectedDbCategory.value = "plugins"; - dialogStep.value = "config"; - configTab.value = "connection"; - return true; -} - function mqExtraRecord(config?: Partial): Record { const extra = config?.extra; return extra && typeof extra === "object" && !Array.isArray(extra) ? (extra as Record) : {}; @@ -2589,6 +2535,62 @@ function applyProfile(val: string, preserveConnectionFields = false) { } } +function applyPluginProvider(value: string, preserveConnectionFields = false, existing?: ConnectionConfig): boolean { + const target = parsePluginConnectionProviderOptionValue(value); + if (!target) return false; + const entry = pluginProviderEntry(target.pluginId, target.providerId); + if (!entry) return false; + + selectedType.value = value; + if (!preserveConnectionFields) { + form.value = { + ...defaultForm(), + name: entry.contribution.label, + db_type: "plugin", + driver_profile: "plugin", + driver_label: entry.contribution.label, + host: "", + port: 0, + username: "", + password: "", + query_timeout_secs: 60, + }; + } else { + form.value.db_type = "plugin"; + form.value.driver_profile = "plugin"; + form.value.driver_label = entry.contribution.label; + } + pluginFormValues.value = pluginConnectionFormValues(entry.contribution, existing); + if (!preserveConnectionFields) { + const nameField = entry.contribution.fields.find((field) => field.binding === "name"); + const defaultName = nameField ? pluginFormValues.value[nameField.key] : undefined; + if (typeof defaultName === "string" && defaultName.trim()) form.value.name = defaultName.trim(); + } + connectionUrlInput.value = ""; + appliedConnectionUrlInput.value = ""; + return true; +} + +async function loadInstalledPlugins() { + pluginLoadError.value = ""; + try { + installedPlugins.value = await api.listPlugins(); + } catch (cause) { + installedPlugins.value = []; + pluginLoadError.value = cause instanceof Error ? cause.message : String(cause); + } +} + +function applyRequestedPluginProvider(target: PluginCenterFocus | null | undefined) { + if (!target?.pluginId || !target.providerId) return false; + const value = pluginConnectionProviderOptionValue(target.pluginId, target.providerId); + if (!applyPluginProvider(value)) return false; + selectedDbCategory.value = "plugins"; + dialogStep.value = "config"; + configTab.value = "connection"; + return true; +} + function switchOceanbaseMode(mode: "mysql" | "oracle") { oceanbaseSubMode.value = mode; if (mode === "mysql") { @@ -2626,7 +2628,7 @@ watch( name: config.name, note: config.note || "", db_type: oceanbasePatch?.db_type || profileConfig?.type || config.db_type, - driver_profile: oceanbasePatch?.driver_profile || config.driver_profile || profile, + driver_profile: config.db_type === "plugin" ? "plugin" : oceanbasePatch?.driver_profile || config.driver_profile || profile, driver_label: config.driver_label || oceanbasePatch?.driver_label || driverProfiles[profile]?.label || config.db_type, url_params: config.url_params || "", agent_java_options: config.agent_java_options || [], @@ -2770,6 +2772,7 @@ watch( productionProtectionEnabled.value = false; selectedTransportLayerId.value = null; selectedType.value = "mysql"; + pluginFormValues.value = {}; customDriverName.value = ""; resetMqFields(); resetCassandraTlsFields(undefined); @@ -3013,10 +3016,10 @@ const dbCategoryMetadata: Array<{ key: DbCategoryKey; titleKey: string }> = [ { key: "registry_config", titleKey: "connection.databaseCategoryRegistryConfig" }, ]; -function jdbcProductCategory(profileId: string): DbCategoryKey { - const category = dbCategoryMetadata.find(({ key }) => jdbcProductProfileIdsForCategory(key).includes(profileId))?.key; - if (!category) throw new Error(`JDBC product profile ${profileId} has no connection picker category`); - return category; +function jdbcProductCategory(profileId: string): ConnectionProfileCategory { + const category: DbCategoryKey | undefined = dbCategoryMetadata.find(({ key }) => jdbcProductProfileIdsForCategory(key).includes(profileId))?.key; + if (category !== "plugins" && category) return category; + throw new Error(`JDBC product profile ${profileId} has no connection picker category`); } // `influxdb3` is presented as a version option inside the InfluxDB card @@ -3092,7 +3095,7 @@ const selectedDbOptionIsVisible = computed(() => visibleDbCategories.value.some( function selectDbCategory(category: DbCategoryKey) { selectedDbCategory.value = category; dbSearchQuery.value = ""; - const categoryOptions = dbCategoryDefinitions.find((definition) => definition.key === category)?.optionValues ?? []; + const categoryOptions = dbCategories.value.find((definition) => definition.key === category)?.options.map((option) => option.value) ?? []; const nextSelection = databaseSelectionForCategory(selectedType.value, categoryOptions); if (nextSelection && nextSelection !== selectedType.value) onDbTypeChange(nextSelection); } @@ -3573,6 +3576,32 @@ const pluginActionStatusMessage = computed(() => { if (!pluginActionStatus.value) return ""; return pluginActionStatus.value.ok ? pluginActionStatus.value.message : translateBackendError(t, pluginActionStatus.value.message); }); +const agentInstallPercent = computed(() => driverInstallProgressPercent(agentInstallProgress.value)); +const agentInstallProgressLabel = computed(() => { + const progress = agentInstallProgress.value; + if (agentInstallError.value) return t("connection.driverInstall.statusFailed"); + if (!agentInstallRunning.value) return t("connection.driverInstall.statusWaiting"); + if (!progress) return t("connection.driverInstall.statusPreparing"); + if (progress.step === "jre-extract") return t("connection.driverInstall.statusExtractingJre"); + const label = progress.step === "jre" ? t("connection.driverInstall.stepJre") : progress.step === "driver" ? t("connection.driverInstall.stepDriver") : progress.step || t("connection.driverInstall.stepDefault"); + if (!progress.total) return `${label}...`; + return `${label} ${formatInstallSize(progress.downloaded ?? 0)} / ${formatInstallSize(progress.total)} (${agentInstallPercent.value ?? 0}%)`; +}); +const canCloseAgentInstallDialog = computed(() => !agentInstallRunning.value || !!agentInstallError.value); +const sqlServerDriverMode = computed<"auto" | "legacy">(() => (sqlServerUsesLegacyCompatibility(form.value) ? "legacy" : "auto")); +const shouldUseWideConnectionDialog = computed( + () => dialogStep.value === "config" && (canChooseVisibleDatabases.value || canChooseVisibleNacosNamespaces.value || (canChooseVisibleSchemas.value && !visibleFilterUsesSchemas.value) || (selectedPluginProvider.value?.contribution.fields.length ?? 0) >= 6), +); +const connectionDialogContentClass = computed(() => { + if (dialogStep.value === "select") return "connection-dialog-content--picker sm:h-[720px] sm:max-w-[880px]"; + const widthClass = shouldUseWideConnectionDialog.value ? "connection-dialog-content--wide sm:max-w-[660px]" : "connection-dialog-content--standard sm:max-w-[560px]"; + const scrollableAdvancedNacos = form.value.db_type === "nacos" && configTab.value === "advanced"; + return `${widthClass} connection-dialog-content--config${scrollableAdvancedNacos ? " connection-dialog-content--scrollable" : ""}`; +}); +const connectionLabelClass = "justify-self-start text-left"; +const connectionLabelSmallClass = `${connectionLabelClass} text-xs`; +const connectionLabelTopClass = `${connectionLabelClass} mt-2`; +const connectionLabelSmallPaddedClass = `${connectionLabelClass} pt-2 text-xs`; function pluginFieldValue(field: PluginFormField): PluginFormFieldValue { if (field.binding === "name") return form.value.name; @@ -3584,6 +3613,19 @@ function pluginFieldHasValue(field: PluginFormField): boolean { return typeof value === "string" ? value.trim().length > 0 : value !== undefined; } +function pluginFieldValueByKey(key: string): PluginFormFieldValue { + const field = selectedPluginProvider.value?.contribution.fields.find((candidate) => candidate.key === key); + return field ? pluginFieldValue(field) : undefined; +} + +function pluginFieldVisible(field: PluginFormField): boolean { + return pluginFieldIsVisible(field, pluginFieldValueByKey, (key) => selectedPluginProvider.value?.contribution.fields.find((candidate) => candidate.key === key)); +} + +function pluginFieldRequired(field: PluginFormField): boolean { + return pluginFieldIsRequired(field, pluginFieldValueByKey); +} + function pluginActionLabel(action: PluginConnectionAction): string { if (action.label) return action.label; if (action.kind === "test") return t("connection.test"); @@ -3633,34 +3675,11 @@ function applyPluginActionFieldValues(values: Record driverInstallProgressPercent(agentInstallProgress.value)); -const agentInstallProgressLabel = computed(() => { - const progress = agentInstallProgress.value; - if (agentInstallError.value) return t("connection.driverInstall.statusFailed"); - if (!agentInstallRunning.value) return t("connection.driverInstall.statusWaiting"); - if (!progress) return t("connection.driverInstall.statusPreparing"); - if (progress.step === "jre-extract") return t("connection.driverInstall.statusExtractingJre"); - const label = progress.step === "jre" ? t("connection.driverInstall.stepJre") : progress.step === "driver" ? t("connection.driverInstall.stepDriver") : progress.step || t("connection.driverInstall.stepDefault"); - if (!progress.total) return `${label}...`; - return `${label} ${formatInstallSize(progress.downloaded ?? 0)} / ${formatInstallSize(progress.total)} (${agentInstallPercent.value ?? 0}%)`; -}); -const canCloseAgentInstallDialog = computed(() => !agentInstallRunning.value || !!agentInstallError.value); -const sqlServerDriverMode = computed<"auto" | "legacy">(() => (sqlServerUsesLegacyCompatibility(form.value) ? "legacy" : "auto")); -const shouldUseWideConnectionDialog = computed(() => dialogStep.value === "config" && (canChooseVisibleDatabases.value || canChooseVisibleNacosNamespaces.value || (canChooseVisibleSchemas.value && !visibleFilterUsesSchemas.value))); -const connectionDialogContentClass = computed(() => { - if (dialogStep.value === "select") return "connection-dialog-content--picker sm:h-[720px] sm:max-w-[880px]"; - const widthClass = shouldUseWideConnectionDialog.value ? "connection-dialog-content--wide sm:max-w-[660px]" : "connection-dialog-content--standard sm:max-w-[560px]"; - const scrollableAdvancedNacos = form.value.db_type === "nacos" && configTab.value === "advanced"; - return `${widthClass} connection-dialog-content--config${scrollableAdvancedNacos ? " connection-dialog-content--scrollable" : ""}`; -}); -const connectionLabelClass = "justify-self-start text-left"; -const connectionLabelSmallClass = `${connectionLabelClass} text-xs`; -const connectionLabelTopClass = `${connectionLabelClass} mt-2`; -const connectionLabelSmallPaddedClass = `${connectionLabelClass} pt-2 text-xs`; + const hasRequiredConnectionTarget = computed(() => { if (isPluginConnection.value) { - const provider = selectedPluginProvider.value?.contribution; - return !!provider && provider.fields.every((field) => !field.required || pluginFieldHasValue(field)); + const entry = selectedPluginProvider.value; + return !!entry && entry.contribution.fields.every((field) => !pluginFieldVisible(field) || !pluginFieldRequired(field) || pluginFieldHasValue(field)); } if (form.value.db_type === "mq") { if (mqSystemKind.value === "kafka") return mqKafkaConnectionSource.value === "zookeeper" ? !!mqKafkaZooKeeperServers.value.trim() : !!mqKafkaBootstrapServers.value.trim(); @@ -4061,11 +4080,12 @@ function connectionConfigForSshTunnelTest(id: string): ConnectionConfig { } function connectionConfigForSubmit(id: string, generatedName = "", validatePluginRequired = true): ConnectionConfig { + let config: LegacyConnectionConfig; if (isPluginConnection.value) { const entry = selectedPluginProvider.value; if (!entry) throw new Error(pluginLoadError.value || t("connection.pluginProviderUnavailable")); if (validatePluginRequired) { - const missingField = entry.contribution.fields.find((field) => field.required && !pluginFieldHasValue(field)); + const missingField = entry.contribution.fields.find((field) => pluginFieldVisible(field) && pluginFieldRequired(field) && !pluginFieldHasValue(field)); if (missingField) throw new Error(t("connection.pluginRequiredField", { field: missingField.label })); } const values = { ...pluginFormValues.value }; @@ -4073,10 +4093,23 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi if (field.binding === "name") values[field.key] = form.value.name; } const existing = props.editConfig?.db_type === "plugin" && props.editConfig.plugin_id === entry.plugin.manifest.id && props.editConfig.plugin_connection_provider === entry.contribution.id ? props.editConfig : undefined; - const config = buildPluginConnectionConfig(entry.plugin.manifest.id, entry.contribution, values, existing); - return { ...formValueForSubmit(), ...config, id, name: form.value.name.trim() || generatedName.trim() || config.name, note: form.value.note?.trim() || undefined }; + config = buildPluginConnectionConfig(entry.plugin.manifest.id, entry.contribution, values, existing) as LegacyConnectionConfig; + config.id = id; + config.name = form.value.name.trim() || config.name; + config.note = form.value.note; + config.color = form.value.color; + config.transport_layers = form.value.transport_layers || []; + config.connect_timeout_secs = form.value.connect_timeout_secs; + config.query_timeout_secs = form.value.query_timeout_secs; + config.idle_timeout_secs = form.value.idle_timeout_secs; + config.keepalive_interval_secs = form.value.keepalive_interval_secs; + config.read_only = form.value.read_only; + config.save_password = form.value.save_password; + config.is_production = form.value.is_production; + config.production_databases = form.value.production_databases; + } else { + config = { ...formValueForSubmit(), id } as LegacyConnectionConfig; } - const config = { ...formValueForSubmit(), id } as LegacyConnectionConfig; config.database_info = undefined; config.database = normalizeStoredConnectionDatabase(config.db_type, config.database); config.note = config.note?.trim() || undefined; @@ -4286,6 +4319,9 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi setGaussdbTargetServerType(config, targetServerType); setGaussdbCountQueryDop(config, countQueryDop); } else if (config.db_type !== "plugin" && !isDoltDriverProfile(config.driver_profile)) { + // Plugin connections keep `external_config`: the manifest-driven form + // fields land there via buildPluginConnectionConfig. Only the built-in + // drivers without an external-config payload get wiped here. config.external_config = undefined; } if (config.db_type === "mongodb" && !mongoUseUrl.value) { @@ -4382,12 +4418,6 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi config.port = firstEndpoint.port; } config.database = undefined; - config.ca_cert_path = config.ca_cert_path?.trim() || ""; - 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")); - } } if (config.db_type === "etcd") { config.etcd_endpoints = normalizeEndpointLines(config.etcd_endpoints || ""); @@ -4402,7 +4432,14 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi if ((config.client_cert_path && !config.client_key_path) || (!config.client_cert_path && config.client_key_path)) { throw new Error(t("connection.etcdClientCertPairRequired")); } - } else if (form.value.db_type !== "consul" && config.db_type !== "zookeeper" && config.db_type !== "elasticsearch" && config.db_type !== "easysearch") { + } else if (config.db_type === "zookeeper") { + config.etcd_endpoints = undefined; + 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")); + } + } else if (form.value.db_type !== "consul" && config.db_type !== "elasticsearch" && config.db_type !== "easysearch") { config.etcd_endpoints = undefined; config.client_cert_path = undefined; config.client_key_path = undefined; @@ -4751,6 +4788,7 @@ function resetTestState() { isTesting.value = false; isTestingSshTunnel.value = false; testResult.value = null; + pluginActionStatus.value = null; clearTestedConnectionInfo(); showConnectionErrorDialog.value = false; connectionErrorRawDetail.value = ""; @@ -5290,6 +5328,7 @@ function resetForm(options: { preservePickerState?: boolean } = {}) { selectedTransportLayerId.value = null; draggedTransportLayerId.value = null; selectedType.value = "mysql"; + pluginFormValues.value = {}; customDriverName.value = ""; mongoUseUrl.value = false; resetMqFields(); @@ -5425,6 +5464,19 @@ watch( resetForm(); if (props.prefillConfig) applyConnectionPrefill(props.prefillConfig); } + void loadInstalledPlugins().then(() => { + if (!open.value) return; + if (props.editConfig?.db_type === "plugin" && props.editConfig.plugin_id && props.editConfig.plugin_connection_provider) { + const entry = pluginProviderEntry(props.editConfig.plugin_id, props.editConfig.plugin_connection_provider); + if (entry) { + selectedType.value = pluginConnectionProviderOptionValue(props.editConfig.plugin_id, props.editConfig.plugin_connection_provider); + selectedDbCategory.value = "plugins"; + pluginFormValues.value = pluginConnectionFormValues(entry.contribution, props.editConfig); + } + } else if (!props.prefillConfig) { + applyRequestedPluginProvider(props.pluginProvider); + } + }); if (!props.prefillConfig?.oneTime) { void loadJdbcDrivers(); void loadAgentDrivers(); @@ -5713,83 +5765,74 @@ async function persistConnectionNoteVisibilityDraft() { await persistConnectionNoteVisibilityDraftState(connectionNoteVisibilityDraft, settingsStore.editorSettings.sidebarShowConnectionNotes, (value) => settingsStore.updateEditorSettingsAndPersist({ sidebarShowConnectionNotes: value })); } -async function save(options: { connectAfterSave?: boolean; closeOnSuccess?: boolean } = {}) { - if (!ensureConnectionHostResolvedFromUrl()) return; - if (isSaving.value) return; +function startSavedConnection(config: ConnectionConfig) { + emit("connectStarted", config.name); + void store + .connect(config) + .then(() => { + emit("connectSucceeded", config.name); + }) + .catch((e: any) => { + const message = String(e?.message || e); + if (message.includes(CONNECTION_ATTEMPT_CANCELLED_MESSAGE)) return; + // Keep failed one-time connections available for retry and error inspection. + // They are still removed by connectionStore.disconnect after a successful session. + emit("connectFailed", appendConnectionErrorHints(config, mongodbAuthFailureHint(message), t)); + }); +} + +async function save(options: SaveConnectionOptions = {}): Promise { + if (!ensureConnectionHostResolvedFromUrl()) return false; + if (isSaving.value) return false; if (!hasNacosNamespaceScopeForSave()) { testResult.value = null; await openVisibleNacosNamespacesPicker(); - return; + return false; } - const connectAfterSave = options.connectAfterSave ?? (!editingId.value && form.value.db_type !== "jdbc"); + const wasEditing = !!editingId.value; + const connectAfterSave = options.connectAfterSave ?? (!wasEditing && !isJdbcConnection.value); const closeOnSuccess = options.closeOnSuccess ?? true; const databaseInfoForSave = visibleTestDatabaseInfo.value ?? visibleSavedDatabaseInfo.value; isSaving.value = true; let connectionSaved = false; try { + let savedConfig: ConnectionConfig; if (editingId.value) { const updated = withSavedDatabaseInfo(connectionConfigForSubmit(editingId.value), databaseInfoForSave); await ensureRequiredAgentDriverInstalled(updated); await ensureRequiredGaussdbMJdbcRuntime(updated); await persistGlobalTimeoutDrafts(); await store.updateConnection(updated); + savedConfig = updated; connectionSaved = true; await persistConnectionNoteVisibilityDraft(); - store.stopEditing(); - if (connectAfterSave) { - if (closeOnSuccess) { - open.value = false; - await nextTick(); - } - emit("connectStarted", updated.name); - void store - .connect(updated) - .then(() => { - emit("connectSucceeded", updated.name); - }) - .catch((e: any) => { - const message = String(e?.message || e); - if (message.includes(CONNECTION_ATTEMPT_CANCELLED_MESSAGE)) return; - emit("connectFailed", appendConnectionErrorHints(updated, mongodbAuthFailureHint(message), t)); - }); - return; - } } else { const config = withSavedDatabaseInfo(connectionConfigForSubmit(draftTestConnectionId.value), databaseInfoForSave); await ensureRequiredAgentDriverInstalled(config); await ensureRequiredGaussdbMJdbcRuntime(config); await persistGlobalTimeoutDrafts(); await store.addConnection(config, selectedConnectionGroupId.value); + savedConfig = config; connectionSaved = true; await persistConnectionNoteVisibilityDraft(); draftTestConnectionId.value = uuid(); - if (!connectAfterSave) { - if (closeOnSuccess) open.value = false; - return; - } - if (closeOnSuccess) open.value = false; - await nextTick(); - emit("connectStarted", config.name); - void store - .connect(config) - .then(() => { - emit("connectSucceeded", config.name); - }) - .catch((e: any) => { - const message = String(e?.message || e); - if (message.includes(CONNECTION_ATTEMPT_CANCELLED_MESSAGE)) return; - // Keep failed one-time connections available for retry and error inspection. - // They are still removed by connectionStore.disconnect after a successful session. - emit("connectFailed", appendConnectionErrorHints(config, mongodbAuthFailureHint(message), t)); - }); - return; } - if (closeOnSuccess) open.value = false; + if (closeOnSuccess) { + if (wasEditing) store.stopEditing(); + open.value = false; + await nextTick(); + } else if (!wasEditing) { + editingId.value = savedConfig.id; + store.startEditing(savedConfig.id); + } + if (connectAfterSave && savedConfig.db_type !== "jdbc") startSavedConnection(savedConfig); + return true; } catch (e: any) { const cause = mongodbAuthFailureHint(String(e?.message || e)); const message = connectionSaved ? t("connection.savedSettingsFailed", { message: cause }) : cause; testResult.value = { ok: false, message }; showConnectionError(message); + return false; } finally { isSaving.value = false; } @@ -6442,7 +6485,15 @@ function openExternalUrl(url: string) {
diff --git a/apps/desktop/src/components/layout/AppTabBar.vue b/apps/desktop/src/components/layout/AppTabBar.vue index d6960c1c80..1506e359e6 100644 --- a/apps/desktop/src/components/layout/AppTabBar.vue +++ b/apps/desktop/src/components/layout/AppTabBar.vue @@ -28,6 +28,7 @@ const props = defineProps<{ const emit = defineEmits<{ "activate-driver-store": []; "close-driver-store": []; + "activate-plugin-center": []; "close-plugin-center": []; "activate-settings-page": []; "close-settings-page": []; @@ -130,6 +131,9 @@ function closeSpecialRegularSurfaces(keep?: SpecialRegularSurface) { if (keep !== "driverStore" && props.driverStoreOpen) { emit("close-driver-store"); } + if (keep !== "pluginCenter" && props.pluginCenterOpen) { + emit("close-plugin-center"); + } if (keep !== "settings" && props.settingsPageOpen) { emit("close-settings-page"); } @@ -201,6 +205,9 @@ function handleCancelClose() {
+
diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 3ef362649f..ce2260c0b5 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -61,8 +61,6 @@ import QueryLoadingState from "@/components/common/QueryLoadingState.vue"; import QueryErrorActions from "@/components/common/QueryErrorActions.vue"; import QueryMessagesView from "@/components/layout/QueryMessagesView.vue"; import QueryResultToolbarActions from "@/components/layout/QueryResultToolbarActions.vue"; -import PluginFilesystemTab from "@/components/plugins/PluginFilesystemTab.vue"; -import PluginWorkbenchTab from "@/components/plugins/PluginWorkbenchTab.vue"; import ResultSetNavigator from "@/components/layout/ResultSetNavigator.vue"; import QueryResultViewSwitcher from "@/components/layout/QueryResultViewSwitcher.vue"; import DataGridCopyFormatControl from "@/components/grid/DataGridCopyFormatControl.vue"; @@ -123,8 +121,10 @@ const MySqlDashboard = defineAsyncComponent(() => import("@/components/admin/MyS const PostgresDashboard = defineAsyncComponent(() => import("@/components/admin/PostgresDashboard.vue")); const XuguServerDashboard = defineAsyncComponent(() => import("@/components/admin/XuguServerDashboard.vue")); const DamengJobAdmin = defineAsyncComponent(() => import("@/components/admin/DamengJobAdmin.vue")); + const DamengUserAdmin = defineAsyncComponent(() => import("@/components/admin/DamengUserAdmin.vue")); const DamengRoleAdmin = defineAsyncComponent(() => import("@/components/admin/DamengRoleAdmin.vue")); +const PluginFilesystemTab = defineAsyncComponent(() => import("@/components/plugins/PluginFilesystemTab.vue")); const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explain/ExplainPlanViewer.vue")); const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryChart.vue")); import { useQueryStore } from "@/stores/queryStore"; @@ -210,10 +210,6 @@ type SearchableBrowserHandle = { executeCommand?: (command: string) => Promise; }; -type PluginTabHandle = { - refresh?: () => Promise | void; -}; - type ElasticsearchJsonResponsePanelHandle = { focusSearch: () => boolean; }; @@ -329,8 +325,7 @@ const consulOverviewRef = ref<{ refresh?: () => boolean }>(); const consulWorkspaceRef = ref(); const databaseBrowserRef = ref(); const objectBrowserRef = ref(); -const pluginWorkbenchRef = ref(); -const pluginFilesystemRef = ref(); +const pluginFilesystemTabRef = ref<{ refresh: () => Promise }>(); const activeTableMeta = computed(() => props.activeTab.tableMeta); const activeDataTabTableMeta = computed(() => tableMetaForDataTab(props.activeTab)); const activeResultExecutionTarget = computed(() => queryStore.activeResultExecutionTarget(props.activeTab.id)); @@ -988,10 +983,8 @@ function refreshData(): boolean { if (props.activeTab.mode === "consul-overview") return consulOverviewRef.value?.refresh?.() ?? false; if (props.activeTab.mode === "consul") return consulWorkspaceRef.value?.refresh?.() ?? false; if (props.activeTab.mode === "databases") return databaseBrowserRef.value?.refresh?.() ?? false; - if (props.activeTab.mode === "plugin-workbench" || props.activeTab.mode === "plugin-filesystem") { - const pluginTab = props.activeTab.mode === "plugin-workbench" ? pluginWorkbenchRef.value : pluginFilesystemRef.value; - if (!pluginTab?.refresh) return false; - void pluginTab.refresh(); + if (props.activeTab.mode === "plugin-filesystem") { + void pluginFilesystemTabRef.value?.refresh(); return true; } // Restored data tabs intentionally omit row data, so refresh must work before DataGrid mounts. @@ -1219,7 +1212,6 @@ function handleModRTarget(target: Element): boolean { if (target.closest("[data-cell-detail-editor-root]")) return dataGridRef.value?.openCellDetailSearch() ?? false; if (target.closest("[data-grid-root], [data-elasticsearch-json-response-root]")) return refreshData(); if (canReloadUnavailableDataTab(props.activeTab)) return refreshData(); - if (props.activeTab.mode === "plugin-workbench" || props.activeTab.mode === "plugin-filesystem") return refreshData(); return false; } @@ -2571,23 +2563,13 @@ defineExpose({
- +