From da378ac1aff7dd38f83bfd924d71ada0f46103c1 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 16 Mar 2026 17:55:37 +0000 Subject: [PATCH 1/2] Connectors: Stream connection status checks to avoid blocking page render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The connectors page synchronously checks each provider's connection status during PHP page rendering, which blocks the entire page load when validation involves slow HTTP requests. Move the connection checks to a streaming handler hooked into admin_footer. The page is flushed to the browser first, allowing React to mount immediately with a "Checking…" state. Each provider's status is then streamed as an inline ', + wp_json_encode( $connector_id ), + $is_connected ? 'true' : 'false', + wp_json_encode( $connector_id ), + $is_connected ? 'true' : 'false' + ); + + // Flush each result so the browser processes it immediately. + flush(); + } +} +add_action( 'admin_footer-settings_page_options-connectors-wp-admin', '_gutenberg_stream_connector_statuses' ); diff --git a/routes/connectors-home/ai-plugin-callout.tsx b/routes/connectors-home/ai-plugin-callout.tsx index e008cd5ec4ed1f..62e0c8326e60d2 100644 --- a/routes/connectors-home/ai-plugin-callout.tsx +++ b/routes/connectors-home/ai-plugin-callout.tsx @@ -34,13 +34,13 @@ export function AiPluginCallout() { const [ isBusy, setIsBusy ] = useState( false ); const [ justActivated, setJustActivated ] = useState( false ); - // Server-side initial state — true if any provider was already connected at page load. + // Server-side initial state — true if any provider has a configured key at page load. const initialHasConnectedProvider = useRef( connectorDataValues.some( ( c ) => c.type === 'ai_provider' && c.authentication.method === 'api_key' && - c.authentication.isConnected + c.authentication.keySource !== 'none' ) ).current; diff --git a/routes/connectors-home/default-connectors.tsx b/routes/connectors-home/default-connectors.tsx index d12be5228ca6f7..8668576706f7fd 100644 --- a/routes/connectors-home/default-connectors.tsx +++ b/routes/connectors-home/default-connectors.tsx @@ -29,7 +29,6 @@ type ConnectorAuthentication = settingName: string; credentialsUrl: string | null; keySource?: ApiKeySource; - isConnected?: boolean; } | { method: 'none' }; @@ -102,6 +101,7 @@ const ConnectedBadge = () => ( const UnavailableActionBadge = () => { __( 'Not available' ) }; interface ApiKeyConnectorConfig { + connectorId: string; pluginSlug?: string; settingName: string; helpUrl?: string; @@ -109,12 +109,12 @@ interface ApiKeyConnectorConfig { isInstalled?: boolean; isActivated?: boolean; keySource?: ApiKeySource; - initialIsConnected?: boolean; } function ApiKeyConnector( { label, description, + connectorId, pluginSlug, settingName, helpUrl, @@ -122,7 +122,6 @@ function ApiKeyConnector( { isInstalled, isActivated, keySource: initialKeySource, - initialIsConnected, }: ConnectorRenderProps & ApiKeyConnectorConfig ) { let helpLabel: string | undefined; try { @@ -141,6 +140,7 @@ function ApiKeyConnector( { setIsExpanded, isBusy, isConnected, + isCheckingConnection, currentApiKey, keySource, handleButtonClick, @@ -148,12 +148,12 @@ function ApiKeyConnector( { saveApiKey, removeApiKey, } = useConnectorPlugin( { + connectorId, pluginSlug, settingName, isInstalled, isActivated, keySource: initialKeySource, - initialIsConnected, } ); const isExternallyConfigured = keySource === 'env' || keySource === 'constant'; @@ -187,7 +187,11 @@ function ApiKeyConnector( { : 'compact' } onClick={ handleButtonClick } - disabled={ pluginStatus === 'checking' || isBusy } + disabled={ + pluginStatus === 'checking' || + isCheckingConnection || + isBusy + } isBusy={ isBusy } aria-expanded={ isExpanded } > @@ -247,6 +251,7 @@ export function registerDefaultConnectors() { render: ( props ) => ( ), } ); diff --git a/routes/connectors-home/use-connector-plugin.ts b/routes/connectors-home/use-connector-plugin.ts index 9287f4abc5234c..34d2d4f51ad708 100644 --- a/routes/connectors-home/use-connector-plugin.ts +++ b/routes/connectors-home/use-connector-plugin.ts @@ -3,7 +3,7 @@ */ import { store as coreStore } from '@wordpress/core-data'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useState } from '@wordpress/element'; +import { useState, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import type { __experimentalApiKeySource as ApiKeySource } from '@wordpress/connectors'; @@ -11,12 +11,12 @@ import type { __experimentalApiKeySource as ApiKeySource } from '@wordpress/conn export type PluginStatus = 'checking' | 'not-installed' | 'inactive' | 'active'; interface UseConnectorPluginOptions { + connectorId: string; pluginSlug?: string; settingName: string; isInstalled?: boolean; isActivated?: boolean; keySource?: ApiKeySource; - initialIsConnected?: boolean; } interface UseConnectorPluginReturn { @@ -27,6 +27,7 @@ interface UseConnectorPluginReturn { setIsExpanded: ( expanded: boolean ) => void; isBusy: boolean; isConnected: boolean; + isCheckingConnection: boolean; currentApiKey: string; keySource: ApiKeySource; handleButtonClick: () => void; @@ -36,17 +37,53 @@ interface UseConnectorPluginReturn { } export function useConnectorPlugin( { + connectorId, pluginSlug, settingName, isInstalled, isActivated, keySource = 'none', - initialIsConnected = false, }: UseConnectorPluginOptions ): UseConnectorPluginReturn { const [ isExpanded, setIsExpanded ] = useState( false ); const [ isBusy, setIsBusy ] = useState( false ); - const [ connectedState, setConnectedState ] = - useState( initialIsConnected ); + const [ connectedState, setConnectedState ] = useState< boolean | null >( + () => { + // Check if streaming already delivered the result before React mounted. + const streamed = ( + window as unknown as { + __connectorStatuses?: Record< string, boolean >; + } + ).__connectorStatuses?.[ connectorId ]; + return streamed !== undefined ? streamed : null; + } + ); + + // Listen for streamed connector-status events from the server. + useEffect( () => { + if ( connectedState !== null ) { + return; + } + const handler = ( e: Event ) => { + const detail = ( e as CustomEvent ).detail; + if ( detail.id === connectorId ) { + setConnectedState( detail.connected ); + } + }; + document.addEventListener( 'connector-status', handler ); + + // Check again in case the event fired between initial state and effect registration. + const streamed = ( + window as unknown as { + __connectorStatuses?: Record< string, boolean >; + } + ).__connectorStatuses?.[ connectorId ]; + if ( streamed !== undefined ) { + setConnectedState( streamed ); + } + + return () => + document.removeEventListener( 'connector-status', handler ); + }, [ connectorId, connectedState ] ); // Local override for immediate UI feedback after install/activate. const [ pluginStatusOverride, setPluginStatusOverride ] = useState< PluginStatus | null >( null ); @@ -144,8 +181,11 @@ export function useConnectorPlugin( { // Use canManagePlugins (from plugin entity resolution) for activation capability. const canActivatePlugins = canManagePlugins; + const isCheckingConnection = + pluginStatus === 'active' && connectedState === null; + const isConnected = - ( pluginStatus === 'active' && connectedState ) || + ( pluginStatus === 'active' && connectedState === true ) || // After install/activate, if settings re-fetch reveals an existing key, // update connected state (mirrors what the server would report on page load). ( pluginStatusOverride === 'active' && !! currentApiKey ); @@ -226,6 +266,9 @@ export function useConnectorPlugin( { if ( isConnected ) { return __( 'Edit' ); } + if ( isCheckingConnection ) { + return __( 'Checking…' ); + } switch ( pluginStatus ) { case 'checking': return __( 'Checking…' ); @@ -298,6 +341,7 @@ export function useConnectorPlugin( { setIsExpanded, isBusy, isConnected, + isCheckingConnection, currentApiKey, keySource, handleButtonClick, From 31d329b9301f65ae84ce16e8f70166c757cba551 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 16 Mar 2026 18:07:56 +0000 Subject: [PATCH 2/2] Remove unused $registry variable from script module data function --- lib/experimental/connectors/default-connectors.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/experimental/connectors/default-connectors.php b/lib/experimental/connectors/default-connectors.php index 70ccf9eaddf1b6..fe3c805fc32b37 100644 --- a/lib/experimental/connectors/default-connectors.php +++ b/lib/experimental/connectors/default-connectors.php @@ -427,8 +427,6 @@ function _gutenberg_get_connector_script_module_data( array $data ): array { return $data; } - $registry = \WordPress\AiClient\AiClient::defaultRegistry(); - // Build a slug-to-file map for plugin installation status. if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php';