main
js 382 lines 14.3 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 // Reach styles need to come before any component styles.
11 // This makes overriding the styles simpler.
12 import '@reach/menu-button/styles.css';
13 import '@reach/tooltip/styles.css';
14
15 import * as React from 'react';
16 import {useCallback, useEffect, useLayoutEffect, useMemo, useRef} from 'react';
17 import Store from '../store';
18 import {
19 BridgeContext,
20 ContextMenuContext,
21 StoreContext,
22 OptionsContext,
23 } from './context';
24 import Components from './Components/Components';
25 import Profiler from './Profiler/Profiler';
26 import SuspenseTab from './SuspenseTab/SuspenseTab';
27 import TabBar from './TabBar';
28 import EditorPane from './Editor/EditorPane';
29 import InspectedElementPane from './InspectedElement/InspectedElementPane';
30 import {SettingsContextController} from './Settings/SettingsContext';
31 import {TreeContextController} from './Components/TreeContext';
32 import ViewElementSourceContext from './Components/ViewElementSourceContext';
33 import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext';
34 import {InspectedElementContextController} from './Components/InspectedElementContext';
35 import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
36 import {ProfilerContextController} from './Profiler/ProfilerContext';
37 import {SuspenseTreeContextController} from './SuspenseTab/SuspenseTreeContext';
38 import {ModalDialogContextController} from './ModalDialog';
39 import ReactLogo from './ReactLogo';
40 import UnsupportedBridgeProtocolDialog from './UnsupportedBridgeProtocolDialog';
41 import UnsupportedVersionDialog from './UnsupportedVersionDialog';
42 import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected';
43 import {useLocalStorage} from './hooks';
44 import ThemeProvider from './ThemeProvider';
45 import {LOCAL_STORAGE_DEFAULT_TAB_KEY} from '../../constants';
46 import {logEvent} from '../../Logger';
47
48 import styles from './DevTools.css';
49
50 import './root.css';
51
52 import type {FetchFileWithCaching} from './Components/FetchFileWithCachingContext';
53 import type {HookNamesModuleLoaderFunction} from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
54 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
55 import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types';
56 import type {ReactFunctionLocation, ReactCallSite} from 'shared/ReactTypes';
57 import type {SourceSelection} from './Editor/EditorPane';
58
59 export type TabID = 'components' | 'profiler' | 'suspense';
60
61 export type ViewElementSource = (
62 source: ReactFunctionLocation | ReactCallSite,
63 symbolicatedSource: ReactFunctionLocation | ReactCallSite | null,
64 ) => void;
65 export type ViewAttributeSource = (
66 id: number,
67 path: Array<string | number>,
68 ) => void;
69 export type CanViewElementSource = (
70 source: ReactFunctionLocation | ReactCallSite,
71 symbolicatedSource: ReactFunctionLocation | ReactCallSite | null,
72 ) => boolean;
73
74 export type Props = {
75 bridge: FrontendBridge,
76 browserTheme?: BrowserTheme,
77 canViewElementSourceFunction?: ?CanViewElementSource,
78 defaultTab?: TabID,
79 enabledInspectedElementContextMenu?: boolean,
80 showTabBar?: boolean,
81 store: Store,
82 warnIfLegacyBackendDetected?: boolean,
83 warnIfUnsupportedVersionDetected?: boolean,
84 viewAttributeSourceFunction?: ?ViewAttributeSource,
85 viewElementSourceFunction?: ?ViewElementSource,
86 readOnly?: boolean,
87 hideSettings?: boolean,
88 hideToggleErrorAction?: boolean,
89 hideToggleSuspenseAction?: boolean,
90 hideLogAction?: boolean,
91 hideViewSourceAction?: boolean,
92
93 // This property is used only by the web extension target.
94 // The built-in tab UI is hidden in that case, in favor of the browser's own panel tabs.
95 // This is done to save space within the app.
96 // Because of this, the extension needs to be able to change which tab is active/rendered.
97 overrideTab?: TabID,
98
99 // To avoid potential multi-root trickiness, the web extension uses portals to render tabs.
100 // The root <DevTools> app is rendered in the top-level extension window,
101 // but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels.
102 componentsPortalContainer?: Element,
103 inspectedElementPortalContainer?: Element,
104 profilerPortalContainer?: Element,
105 suspensePortalContainer?: Element,
106 editorPortalContainer?: Element,
107
108 currentSelectedSource?: null | SourceSelection,
109
110 // Loads and parses source maps for function components
111 // and extracts hook "names" based on the variables the hook return values get assigned to.
112 // Not every DevTools build can load source maps, so this property is optional.
113 fetchFileWithCaching?: ?FetchFileWithCaching,
114 // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
115 hookNamesModuleLoaderFunction?: ?HookNamesModuleLoaderFunction,
116 };
117
118 const componentsTab = {
119 id: 'components' as TabID,
120 icon: 'components',
121 label: 'Components',
122 title: 'React Components',
123 };
124 const profilerTab = {
125 id: 'profiler' as TabID,
126 icon: 'profiler',
127 label: 'Profiler',
128 title: 'React Profiler',
129 };
130 const suspenseTab = {
131 id: 'suspense' as TabID,
132 icon: 'suspense',
133 label: 'Suspense',
134 title: 'React Suspense',
135 };
136
137 const tabs = [componentsTab, profilerTab, suspenseTab];
138
139 export default function DevTools({
140 bridge,
141 browserTheme = 'light',
142 canViewElementSourceFunction,
143 componentsPortalContainer,
144 editorPortalContainer,
145 inspectedElementPortalContainer,
146 profilerPortalContainer,
147 suspensePortalContainer,
148 currentSelectedSource,
149 defaultTab = 'components',
150 enabledInspectedElementContextMenu = false,
151 fetchFileWithCaching,
152 hookNamesModuleLoaderFunction,
153 overrideTab,
154 showTabBar = false,
155 store,
156 warnIfLegacyBackendDetected = false,
157 warnIfUnsupportedVersionDetected = false,
158 viewAttributeSourceFunction,
159 viewElementSourceFunction,
160 readOnly,
161 hideSettings,
162 hideToggleErrorAction,
163 hideToggleSuspenseAction,
164 hideLogAction,
165 hideViewSourceAction,
166 }: Props): React.Node {
167 const [currentTab, setTab] = useLocalStorage<TabID>(
168 LOCAL_STORAGE_DEFAULT_TAB_KEY,
169 defaultTab,
170 );
171
172 let tab = currentTab;
173
174 if (overrideTab != null) {
175 tab = overrideTab;
176 }
177
178 const selectTab = useCallback(
179 (tabId: TabID) => {
180 // We show the TabBar when DevTools is NOT rendered as a browser extension.
181 // In this case, we want to capture when people select tabs with the TabBar.
182 // When DevTools is rendered as an extension, we capture this event when
183 // the browser devtools panel changes.
184 if (showTabBar === true) {
185 if (tabId === 'components') {
186 logEvent({event_name: 'selected-components-tab'});
187 } else if (tabId === 'suspense') {
188 logEvent({event_name: 'selected-suspense-tab'});
189 } else {
190 logEvent({event_name: 'selected-profiler-tab'});
191 }
192 }
193 setTab(tabId);
194 },
195 [setTab, showTabBar],
196 );
197
198 const options = useMemo(
199 () => ({
200 readOnly: readOnly || false,
201 hideSettings: hideSettings || false,
202 hideToggleErrorAction: hideToggleErrorAction || false,
203 hideToggleSuspenseAction: hideToggleSuspenseAction || false,
204 hideLogAction: hideLogAction || false,
205 hideViewSourceAction: hideViewSourceAction || false,
206 }),
207 [
208 readOnly,
209 hideSettings,
210 hideToggleErrorAction,
211 hideToggleSuspenseAction,
212 hideLogAction,
213 hideViewSourceAction,
214 ],
215 );
216
217 const viewElementSource = useMemo(
218 () => ({
219 canViewElementSourceFunction: canViewElementSourceFunction || null,
220 viewElementSourceFunction: viewElementSourceFunction || null,
221 }),
222 [canViewElementSourceFunction, viewElementSourceFunction],
223 );
224
225 const contextMenu = useMemo(
226 () => ({
227 isEnabledForInspectedElement: enabledInspectedElementContextMenu,
228 viewAttributeSourceFunction: viewAttributeSourceFunction || null,
229 }),
230 [enabledInspectedElementContextMenu, viewAttributeSourceFunction],
231 );
232
233 const devToolsRef = useRef<HTMLElement | null>(null);
234
235 useEffect(() => {
236 if (!showTabBar) {
237 return;
238 }
239
240 const div = devToolsRef.current;
241 if (div === null) {
242 return;
243 }
244
245 const ownerWindow = div.ownerDocument.defaultView;
246 const handleKeyDown = (event: KeyboardEvent) => {
247 if (event.ctrlKey || event.metaKey) {
248 switch (event.key) {
249 case '1':
250 selectTab(tabs[0].id);
251 event.preventDefault();
252 event.stopPropagation();
253 break;
254 case '2':
255 selectTab(tabs[1].id);
256 event.preventDefault();
257 event.stopPropagation();
258 break;
259 case '3':
260 if (tabs.length > 2) {
261 selectTab(tabs[2].id);
262 event.preventDefault();
263 event.stopPropagation();
264 }
265 break;
266 }
267 }
268 };
269 ownerWindow.addEventListener('keydown', handleKeyDown);
270 return () => {
271 ownerWindow.removeEventListener('keydown', handleKeyDown);
272 };
273 }, [showTabBar]);
274
275 useLayoutEffect(() => {
276 return () => {
277 // Shut the Bridge down synchronously (during unmount).
278 bridge.shutdown();
279 };
280 }, [bridge]);
281
282 useEffect(() => {
283 logEvent({event_name: 'loaded-dev-tools'});
284 }, []);
285
286 return (
287 <BridgeContext.Provider value={bridge}>
288 <StoreContext.Provider value={store}>
289 <OptionsContext.Provider value={options}>
290 <ContextMenuContext.Provider value={contextMenu}>
291 <ModalDialogContextController>
292 <SettingsContextController
293 browserTheme={browserTheme}
294 componentsPortalContainer={componentsPortalContainer}
295 profilerPortalContainer={profilerPortalContainer}>
296 <ViewElementSourceContext.Provider value={viewElementSource}>
297 <HookNamesModuleLoaderContext.Provider
298 value={hookNamesModuleLoaderFunction || null}>
299 <FetchFileWithCachingContext.Provider
300 value={fetchFileWithCaching || null}>
301 <TreeContextController>
302 <ProfilerContextController>
303 <InspectedElementContextController>
304 <SuspenseTreeContextController>
305 <ThemeProvider>
306 <div
307 className={styles.DevTools}
308 ref={devToolsRef}
309 data-react-devtools-portal-root={true}>
310 {showTabBar && (
311 <div className={styles.TabBar}>
312 <ReactLogo />
313 <span className={styles.DevToolsVersion}>
314 {process.env.DEVTOOLS_VERSION}
315 </span>
316 <div className={styles.Spacer} />
317 <TabBar
318 currentTab={tab}
319 id="DevTools"
320 selectTab={selectTab}
321 tabs={tabs}
322 type="navigation"
323 />
324 </div>
325 )}
326 <div
327 className={styles.TabContent}
328 hidden={tab !== 'components'}>
329 <Components
330 portalContainer={
331 componentsPortalContainer
332 }
333 />
334 </div>
335 <div
336 className={styles.TabContent}
337 hidden={tab !== 'profiler'}>
338 <Profiler
339 portalContainer={profilerPortalContainer}
340 />
341 </div>
342 <div
343 className={styles.TabContent}
344 hidden={tab !== 'suspense'}>
345 <SuspenseTab
346 portalContainer={suspensePortalContainer}
347 />
348 </div>
349 </div>
350 {editorPortalContainer ? (
351 <EditorPane
352 selectedSource={currentSelectedSource}
353 portalContainer={editorPortalContainer}
354 />
355 ) : null}
356 {inspectedElementPortalContainer ? (
357 <InspectedElementPane
358 selectedSource={currentSelectedSource}
359 portalContainer={
360 inspectedElementPortalContainer
361 }
362 />
363 ) : null}
364 </ThemeProvider>
365 </SuspenseTreeContextController>
366 </InspectedElementContextController>
367 </ProfilerContextController>
368 </TreeContextController>
369 </FetchFileWithCachingContext.Provider>
370 </HookNamesModuleLoaderContext.Provider>
371 </ViewElementSourceContext.Provider>
372 </SettingsContextController>
373 <UnsupportedBridgeProtocolDialog />
374 {warnIfLegacyBackendDetected && <WarnIfLegacyBackendDetected />}
375 {warnIfUnsupportedVersionDetected && <UnsupportedVersionDialog />}
376 </ModalDialogContextController>
377 </ContextMenuContext.Provider>
378 </OptionsContext.Provider>
379 </StoreContext.Provider>
380 </BridgeContext.Provider>
381 );
382 }