|
| 1 | +// Copyright (c) Meta Platforms, Inc. and affiliates. |
| 2 | +// Copyright 2024 The Chromium Authors. All rights reserved. |
| 3 | +// Use of this source code is governed by a BSD-style license that can be |
| 4 | +// found in the LICENSE file. |
| 5 | + |
| 6 | +importtype*asCommonfrom'../../core/common/common.js'; |
| 7 | +import*asi18nfrom'../../core/i18n/i18n.js'; |
| 8 | +import*asRootfrom'../../core/root/root.js'; |
| 9 | +import*asSDKfrom'../../core/sdk/sdk.js'; |
| 10 | +importtype*asProtocolfrom'../../generated/protocol.js'; |
| 11 | +import*asUIfrom'../../ui/legacy/legacy.js'; |
| 12 | +import*asLitfrom'../../ui/lit/lit.js'; |
| 13 | +import*asVisualLoggingfrom'../../ui/visual_logging/visual_logging.js'; |
| 14 | + |
| 15 | +import{FuseboxWindowTitleManager}from'./FuseboxWindowTitleManager.js'; |
| 16 | + |
| 17 | +const{html, render}=Lit; |
| 18 | + |
| 19 | +constUIStrings={ |
| 20 | +/** |
| 21 | + * @description Message for the "settings changed" banner shown when a reload is required for the Network panel. |
| 22 | + */ |
| 23 | +reloadRequiredForNetworkPanelMessage: 'The Network panel is now available for dogfooding. Please reload to access it.', |
| 24 | +/** |
| 25 | + * @description Title shown when Network inspection is disabled due to multiple React Native hosts. |
| 26 | + */ |
| 27 | +networkInspectionUnavailable: 'Network inspection is unavailable', |
| 28 | +/** |
| 29 | + * @description Title shown when Performance profiling is disabled due to multiple React Native hosts. |
| 30 | + */ |
| 31 | +performanceProfilingUnavailable: 'Performance profiling is unavailable', |
| 32 | +/** |
| 33 | + * @description Title shown when a feature is unavailable due to multiple React Native hosts. |
| 34 | + */ |
| 35 | +multiHostFeatureUnavailableTitle: 'Feature is unavailable', |
| 36 | +/** |
| 37 | + * @description Detail message shown when a feature is disabled due to multiple React Native hosts. |
| 38 | + */ |
| 39 | +multiHostFeatureDisabledDetail: 'This feature is disabled as the app or framework has registered multiple React Native hosts, which is not currently supported.', |
| 40 | +}asconst; |
| 41 | + |
| 42 | +conststr_=i18n.i18n.registerUIStrings('entrypoints/rn_fusebox/FuseboxFeatureObserver.ts',UIStrings); |
| 43 | +consti18nString=i18n.i18n.getLocalizedString.bind(undefined,str_); |
| 44 | + |
| 45 | +/** |
| 46 | + * The set of features that are not guaranteed to behave safely with multiple |
| 47 | + * React Native hosts. |
| 48 | + */ |
| 49 | +constUNSAFE_MULTI_HOST_FEATURES=newSet([ |
| 50 | +'network', |
| 51 | +'timeline', |
| 52 | +]); |
| 53 | + |
| 54 | +/** |
| 55 | + * [RN] Model observer which configures available DevTools features and |
| 56 | + * experiments based on the target's capabilities. |
| 57 | + */ |
| 58 | +exportclassFuseboxFeatureObserverimplements |
| 59 | +SDK.TargetManager.SDKModelObserver<SDK.ReactNativeApplicationModel.ReactNativeApplicationModel>{ |
| 60 | + #singleHostFeaturesDisabled =false; |
| 61 | + |
| 62 | +constructor(targetManager: SDK.TargetManager.TargetManager){ |
| 63 | +targetManager.observeModels(SDK.ReactNativeApplicationModel.ReactNativeApplicationModel,this); |
| 64 | +} |
| 65 | + |
| 66 | +modelAdded(model: SDK.ReactNativeApplicationModel.ReactNativeApplicationModel): void{ |
| 67 | +model.ensureEnabled(); |
| 68 | +model.addEventListener(SDK.ReactNativeApplicationModel.Events.METADATA_UPDATED,this.#handleMetadataUpdated,this); |
| 69 | +model.addEventListener(SDK.ReactNativeApplicationModel.Events.SYSTEM_STATE_CHANGED,this.#handleSystemStateChanged,this); |
| 70 | +} |
| 71 | + |
| 72 | +modelRemoved(model: SDK.ReactNativeApplicationModel.ReactNativeApplicationModel): void{ |
| 73 | +model.removeEventListener( |
| 74 | +SDK.ReactNativeApplicationModel.Events.METADATA_UPDATED,this.#handleMetadataUpdated,this); |
| 75 | +model.removeEventListener( |
| 76 | +SDK.ReactNativeApplicationModel.Events.SYSTEM_STATE_CHANGED,this.#handleSystemStateChanged,this); |
| 77 | +} |
| 78 | + |
| 79 | + #handleMetadataUpdated( |
| 80 | +event: Common.EventTarget.EventTargetEvent<Protocol.ReactNativeApplication.MetadataUpdatedEvent>): void{ |
| 81 | +// eslint-disable-next-line @typescript-eslint/naming-convention |
| 82 | +const{unstable_isProfilingBuild, unstable_networkInspectionEnabled}=event.data; |
| 83 | + |
| 84 | +if(unstable_isProfilingBuild){ |
| 85 | +FuseboxWindowTitleManager.instance().setSuffix('[PROFILING]'); |
| 86 | +this.#hideUnsupportedFeaturesForProfilingBuilds(); |
| 87 | +} |
| 88 | + |
| 89 | +if(unstable_networkInspectionEnabled){ |
| 90 | +this.#ensureNetworkPanelEnabled(); |
| 91 | +} |
| 92 | +} |
| 93 | + |
| 94 | + #handleSystemStateChanged( |
| 95 | +event: Common.EventTarget.EventTargetEvent<Protocol.ReactNativeApplication.SystemStateChangedEvent>): void{ |
| 96 | +const{isSingleHost}=event.data; |
| 97 | +if(!isSingleHost){ |
| 98 | +this.#disableSingleHostOnlyFeatures(); |
| 99 | +} |
| 100 | +} |
| 101 | + |
| 102 | + #hideUnsupportedFeaturesForProfilingBuilds(): void{ |
| 103 | +UI.InspectorView.InspectorView.instance().closeDrawer(); |
| 104 | + |
| 105 | +constviewManager=UI.ViewManager.ViewManager.instance(); |
| 106 | +constpanelLocationPromise=viewManager.resolveLocation(UI.ViewManager.ViewLocationValues.PANEL); |
| 107 | +constdrawerLocationPromise=viewManager.resolveLocation(UI.ViewManager.ViewLocationValues.DRAWER_VIEW); |
| 108 | +voidPromise.all([panelLocationPromise,drawerLocationPromise]) |
| 109 | +.then(([panelLocation,drawerLocation])=>{ |
| 110 | +UI.ViewManager.getRegisteredViewExtensions().forEach(view=>{ |
| 111 | +if(view.location()===UI.ViewManager.ViewLocationValues.DRAWER_VIEW){ |
| 112 | +drawerLocation?.removeView(view); |
| 113 | +}else{ |
| 114 | +switch(view.viewId()){ |
| 115 | +case'console': |
| 116 | +case'heap-profiler': |
| 117 | +case'live-heap-profile': |
| 118 | +case'sources': |
| 119 | +case'network': |
| 120 | +case'react-devtools-components': |
| 121 | +case'react-devtools-profiler': |
| 122 | +panelLocation?.removeView(view); |
| 123 | +break; |
| 124 | +} |
| 125 | +} |
| 126 | +}); |
| 127 | +}); |
| 128 | +} |
| 129 | + |
| 130 | + #ensureNetworkPanelEnabled(): void{ |
| 131 | +if(Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.ENABLE_NETWORK_PANEL)){ |
| 132 | +return; |
| 133 | +} |
| 134 | + |
| 135 | +Root.Runtime.experiments.setEnabled( |
| 136 | +Root.Runtime.ExperimentName.ENABLE_NETWORK_PANEL, |
| 137 | +true, |
| 138 | +); |
| 139 | + |
| 140 | +UI.InspectorView?.InspectorView?.instance()?.displayReloadRequiredWarning( |
| 141 | +i18nString(UIStrings.reloadRequiredForNetworkPanelMessage), |
| 142 | +); |
| 143 | +} |
| 144 | + |
| 145 | + #disableSingleHostOnlyFeatures(): void{ |
| 146 | +if(this.#singleHostFeaturesDisabled){ |
| 147 | +return; |
| 148 | +} |
| 149 | + |
| 150 | +// Disable relevant CDP domains |
| 151 | +consttargetManager=SDK.TargetManager.TargetManager.instance(); |
| 152 | +for(consttargetoftargetManager.targets()){ |
| 153 | +voidtarget.networkAgent().invoke_disable(); |
| 154 | +} |
| 155 | + |
| 156 | +// Stop network recording if active |
| 157 | +voidthis.#disableNetworkRecording(); |
| 158 | + |
| 159 | +// Show in-panel overlay when disabled panels are selected |
| 160 | +constinspectorView=UI.InspectorView.InspectorView.instance(); |
| 161 | +constoverlaidPanels=newSet<string>(); |
| 162 | + |
| 163 | +constshowPanelOverlay=(panel: UI.Panel.Panel,panelId: string): void=>{ |
| 164 | +consttitleText= |
| 165 | +panelId==='network' |
| 166 | + ? i18nString(UIStrings.networkInspectionUnavailable) |
| 167 | + : panelId==='timeline' |
| 168 | + ? i18nString(UIStrings.performanceProfilingUnavailable) |
| 169 | + : i18nString(UIStrings.multiHostFeatureUnavailableTitle); |
| 170 | + |
| 171 | +// Dim the existing panel content and disable interaction |
| 172 | +for(constchildofpanel.element.children){ |
| 173 | +constelement=childasHTMLElement; |
| 174 | +element.style.opacity='0.5'; |
| 175 | +element.style.pointerEvents='none'; |
| 176 | +element.setAttribute('inert',''); |
| 177 | +element.setAttribute('aria-hidden','true'); |
| 178 | +} |
| 179 | + |
| 180 | +constalertBar=document.createElement('div'); |
| 181 | +render(html` |
| 182 | +<style> |
| 183 | + .alert-bar { |
| 184 | +background:var(--sys-color-tonal-container); |
| 185 | +color:var(--sys-color-on-tonal-container); |
| 186 | +padding:var(--sys-size-6) var(--sys-size-8); |
| 187 | +border-bottom:1px solid var(--sys-color-tonal-outline); |
| 188 | + } |
| 189 | + .alert-title { |
| 190 | +font:var(--sys-typescale-body2-medium); |
| 191 | +margin-bottom:var(--sys-size-3); |
| 192 | + } |
| 193 | + .alert-detail { |
| 194 | +font:var(--sys-typescale-body4-regular); |
| 195 | + } |
| 196 | +</style> |
| 197 | +<divclass="alert-bar"> |
| 198 | +<divclass="alert-title">${titleText}</div> |
| 199 | +<divclass="alert-detail"> |
| 200 | +${i18nString(UIStrings.multiHostFeatureDisabledDetail)} |
| 201 | + See <x-linkhref="https://github.com/react-native-community/discussions-and-proposals/discussions/954" class="devtools-link" jslog=${VisualLogging.link().track({click: true,keydown:'Enter|Space'}).context('multi-host-learn-more')}>discussions/954</x-link>. |
| 202 | +</div> |
| 203 | +</div> |
| 204 | + `,alertBar,{host: this}); |
| 205 | + |
| 206 | +panel.element.insertBefore(alertBar,panel.element.firstChild); |
| 207 | +}; |
| 208 | + |
| 209 | +inspectorView.tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected,event=>{ |
| 210 | +consttabId=event.data.tabId; |
| 211 | +if(UNSAFE_MULTI_HOST_FEATURES.has(tabId)&&!overlaidPanels.has(tabId)){ |
| 212 | +overlaidPanels.add(tabId); |
| 213 | +voidinspectorView.panel(tabId).then(panel=>{ |
| 214 | +if(panel){ |
| 215 | +showPanelOverlay(panel,tabId); |
| 216 | +} |
| 217 | +}); |
| 218 | +} |
| 219 | +}); |
| 220 | + |
| 221 | +// Show overlay if a disabled panel is currently selected |
| 222 | +constcurrentTabId=inspectorView.tabbedPane.selectedTabId; |
| 223 | +if(currentTabId&&UNSAFE_MULTI_HOST_FEATURES.has(currentTabId)){ |
| 224 | +overlaidPanels.add(currentTabId); |
| 225 | +voidinspectorView.panel(currentTabId).then(panel=>{ |
| 226 | +if(panel){ |
| 227 | +showPanelOverlay(panel,currentTabId); |
| 228 | +} |
| 229 | +}); |
| 230 | +} |
| 231 | + |
| 232 | +this.#singleHostFeaturesDisabled =true; |
| 233 | +} |
| 234 | + |
| 235 | +async #disableNetworkRecording(): Promise<void>{ |
| 236 | +constinspectorView=UI.InspectorView.InspectorView.instance(); |
| 237 | +try{ |
| 238 | +constnetworkPanel=awaitinspectorView.panel('network'); |
| 239 | +if(networkPanel&&'toggleRecord'innetworkPanel){ |
| 240 | +(networkPanelasUI.Panel.Panel&{toggleRecord: (toggled: boolean)=>void}).toggleRecord(false); |
| 241 | +} |
| 242 | +}catch{ |
| 243 | +} |
| 244 | +} |
| 245 | +} |
0 commit comments