From f3f4643a3af686b1e4d8711df17f6465a855925c Mon Sep 17 00:00:00 2001 From: Brian Vaughn <bvaughn@fb.com> Date: Sun, 17 Mar 2019 13:52:37 -0700 Subject: [PATCH 1/8] Experimenting with portals --- shells/browser/chrome/manifest.json | 8 +- shells/browser/firefox/manifest.json | 8 +- shells/browser/shared/build.js | 9 +- .../shared/{elements.html => panel.html} | 2 +- shells/browser/shared/profiler.html | 32 ----- shells/browser/shared/settings.html | 32 ----- shells/browser/shared/src/main.js | 127 +++++++++++------- shells/browser/shared/src/panel.js | 20 +++ shells/browser/shared/src/panels/elements.js | 3 - shells/browser/shared/src/panels/profiler.js | 3 - shells/browser/shared/src/panels/settings.js | 3 - shells/browser/shared/src/panels/utils.js | 72 ---------- shells/browser/shared/src/utils.js | 21 +++ shells/browser/shared/webpack.config.js | 4 +- src/devtools/views/DevTools.js | 15 ++- 15 files changed, 136 insertions(+), 223 deletions(-) rename shells/browser/shared/{elements.html => panel.html} (93%) delete mode 100644 shells/browser/shared/profiler.html delete mode 100644 shells/browser/shared/settings.html create mode 100644 shells/browser/shared/src/panel.js delete mode 100644 shells/browser/shared/src/panels/elements.js delete mode 100644 shells/browser/shared/src/panels/profiler.js delete mode 100644 shells/browser/shared/src/panels/settings.js delete mode 100644 shells/browser/shared/src/panels/utils.js diff --git a/shells/browser/chrome/manifest.json b/shells/browser/chrome/manifest.json index ccbf29aa3c6cd..c22e4b38490c0 100644 --- a/shells/browser/chrome/manifest.json +++ b/shells/browser/chrome/manifest.json @@ -27,13 +27,7 @@ "devtools_page": "main.html", "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", - "web_accessible_resources": [ - "elements.html", - "main.html", - "profiler.html", - "settings.html", - "build/backend.js" - ], + "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"], "background": { "scripts": ["build/background.js"], diff --git a/shells/browser/firefox/manifest.json b/shells/browser/firefox/manifest.json index a1653d50051fc..e0015a59ff7a6 100644 --- a/shells/browser/firefox/manifest.json +++ b/shells/browser/firefox/manifest.json @@ -33,13 +33,7 @@ "devtools_page": "main.html", "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", - "web_accessible_resources": [ - "elements.html", - "main.html", - "profiler.html", - "settings.html", - "build/backend.js" - ], + "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"], "background": { "scripts": ["build/background.js"], diff --git a/shells/browser/shared/build.js b/shells/browser/shared/build.js index 3923c1f128d23..846f73967094e 100644 --- a/shells/browser/shared/build.js +++ b/shells/browser/shared/build.js @@ -7,14 +7,7 @@ const { join } = require('path'); // These files are copied along with Webpack-bundled files // to produce the final web extension -const STATIC_FILES = [ - 'icons', - 'popups', - 'elements.html', - 'main.html', - 'profiler.html', - 'settings.html', -]; +const STATIC_FILES = ['icons', 'popups', 'main.html', 'panel.html']; const preProcess = async (destinationPath, tempPath) => { await remove(destinationPath); // Clean up from previously completed builds diff --git a/shells/browser/shared/elements.html b/shells/browser/shared/panel.html similarity index 93% rename from shells/browser/shared/elements.html rename to shells/browser/shared/panel.html index bbaa81a0f2f3a..60fd1bdf13ff2 100644 --- a/shells/browser/shared/elements.html +++ b/shells/browser/shared/panel.html @@ -27,6 +27,6 @@ <body> <!-- main react mount point --> <div id="container">Unable to find React on the page.</div> - <script src="./build/elements.js"></script> + <script src="./build/panel.js"></script> </body> </html> diff --git a/shells/browser/shared/profiler.html b/shells/browser/shared/profiler.html deleted file mode 100644 index 0281d9d2cc22b..0000000000000 --- a/shells/browser/shared/profiler.html +++ /dev/null @@ -1,32 +0,0 @@ -<!doctype html> -<html style="display: flex"> - <head> - <meta charset="utf8"> - <style> - html { - display: flex; - } - body { - margin: 0; - padding: 0; - flex: 1; - display: flex; - } - #container { - display: flex; - flex: 1; - width: 100%; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - } - </style> - </head> - <body> - <!-- main react mount point --> - <div id="container">Unable to find React on the page.</div> - <script src="./build/profiler.js"></script> - </body> -</html> diff --git a/shells/browser/shared/settings.html b/shells/browser/shared/settings.html deleted file mode 100644 index 16beaccceaca2..0000000000000 --- a/shells/browser/shared/settings.html +++ /dev/null @@ -1,32 +0,0 @@ -<!doctype html> -<html style="display: flex"> - <head> - <meta charset="utf8"> - <style> - html { - display: flex; - } - body { - margin: 0; - padding: 0; - flex: 1; - display: flex; - } - #container { - display: flex; - flex: 1; - width: 100%; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - } - </style> - </head> - <body> - <!-- main react mount point --> - <div id="container">Unable to find React on the page.</div> - <script src="./build/settings.js"></script> - </body> -</html> diff --git a/shells/browser/shared/src/main.js b/shells/browser/shared/src/main.js index 9183a0c4ce6da..d09dddf1bd98d 100644 --- a/shells/browser/shared/src/main.js +++ b/shells/browser/shared/src/main.js @@ -1,8 +1,16 @@ /* global chrome */ +import { createElement } from 'react'; +import { unstable_createRoot as createRoot } from 'react-dom'; import Bridge from 'src/bridge'; import Store from 'src/devtools/Store'; import inject from './inject'; +import { + createViewElementSource, + getBrowserName, + getBrowserTheme, +} from './utils'; +import DevTools from 'src/devtools/views/DevTools'; let panelCreated = false; @@ -22,6 +30,7 @@ function createPanelIfReactLoaded() { clearInterval(loadCheckInterval); + let renderRootToPortal = null; let bridge = null; let store = null; let elementsPanel = null; @@ -54,69 +63,85 @@ function createPanelIfReactLoaded() { // Otherwise the Store may miss important initial tree op codes. inject(chrome.runtime.getURL('build/backend.js')); + const viewElementSource = createViewElementSource(bridge, store); + + const container = document.createElement('div'); + const root = createRoot(container); + + renderRootToPortal = ({ overrideTab, portalContainer }) => { + root.render( + createElement(DevTools, { + bridge, + browserName: getBrowserName(), + browserTheme: getBrowserTheme(), + overrideTab, + portalContainer, + showTabBar: false, + store, + viewElementSource, + }) + ); + + const oldLinkTags = document.getElementsByTagName('link'); + const newLinkTags = []; + for (let oldLinkTag of oldLinkTags) { + if (oldLinkTag.rel === 'stylesheet') { + const newLinkTag = document.createElement('link'); + for (let attribute of oldLinkTag.attributes) { + newLinkTag.setAttribute( + attribute.nodeName, + attribute.nodeValue + ); + } + newLinkTags.push(newLinkTag); + } + } + + return newLinkTags; + }; + if (elementsPanel !== null) { - elementsPanel.injectBridgeAndStore(bridge, store); - } - if (profilerPanel !== null) { - profilerPanel.injectBridgeAndStore(bridge, store); - } - if (settingsPanel !== null) { - settingsPanel.injectBridgeAndStore(bridge, store); + elementsPanel.render(renderRootToPortal, 'elements'); } } initBridgeAndStore(); - chrome.devtools.panels.create( - '⚛ Elements', - '', - 'elements.html', - panel => { - panel.onShown.addListener(panel => { - if (elementsPanel === null) { - panel.injectBridgeAndStore(bridge, store); - } + chrome.devtools.panels.create('⚛ Elements', '', 'panel.html', panel => { + panel.onShown.addListener(panel => { + elementsPanel = panel; - elementsPanel = panel; + if (renderRootToPortal !== null) { + elementsPanel.render(renderRootToPortal, 'elements'); + } - // TODO: When the user switches to the panel, check for an Elements tab selection. - }); - panel.onHidden.addListener(() => { - // TODO: Stop highlighting and stuff. - }); - } - ); + // TODO: When the user switches to the panel, check for an Elements tab selection. + }); + panel.onHidden.addListener(() => { + // TODO: Stop highlighting and stuff. + }); + }); // TODO (profiling) Is there a way to detect profiling support and conditionally register this panel? - chrome.devtools.panels.create( - '⚛ Profiler', - '', - 'profiler.html', - panel => { - panel.onShown.addListener(panel => { - if (settingsPanel === null) { - panel.injectBridgeAndStore(bridge, store); - } + chrome.devtools.panels.create('⚛ Profiler', '', 'panel.html', panel => { + panel.onShown.addListener(panel => { + profilerPanel = panel; - profilerPanel = panel; - }); - } - ); - - chrome.devtools.panels.create( - '⚛ Settings', - '', - 'settings.html', - panel => { - panel.onShown.addListener(panel => { - if (settingsPanel === null) { - panel.injectBridgeAndStore(bridge, store); - } + if (renderRootToPortal !== null) { + profilerPanel.render(renderRootToPortal, 'profiler'); + } + }); + }); - settingsPanel = panel; - }); - } - ); + chrome.devtools.panels.create('⚛ Settings', '', 'panel.html', panel => { + panel.onShown.addListener(panel => { + settingsPanel = panel; + + if (renderRootToPortal !== null) { + settingsPanel.render(renderRootToPortal, 'settings'); + } + }); + }); chrome.devtools.network.onNavigated.removeListener(checkPageForReact); diff --git a/shells/browser/shared/src/panel.js b/shells/browser/shared/src/panel.js new file mode 100644 index 0000000000000..b7413918f5c2d --- /dev/null +++ b/shells/browser/shared/src/panel.js @@ -0,0 +1,20 @@ +const container = document.getElementById('container'); + +let hasInjectedStyles = false; + +window.render = (renderRootToPortal, tab) => { + container.innerHTML = ''; + + const linkTags = renderRootToPortal({ + overrideTab: tab, + portalContainer: container, + }); + + if (!hasInjectedStyles) { + hasInjectedStyles = true; + + for (let linkTag of linkTags) { + document.head.appendChild(linkTag); + } + } +}; diff --git a/shells/browser/shared/src/panels/elements.js b/shells/browser/shared/src/panels/elements.js deleted file mode 100644 index ef7f5e80df618..0000000000000 --- a/shells/browser/shared/src/panels/elements.js +++ /dev/null @@ -1,3 +0,0 @@ -import { createPanel } from './utils'; - -createPanel('elements'); diff --git a/shells/browser/shared/src/panels/profiler.js b/shells/browser/shared/src/panels/profiler.js deleted file mode 100644 index 23f028455b7e1..0000000000000 --- a/shells/browser/shared/src/panels/profiler.js +++ /dev/null @@ -1,3 +0,0 @@ -import { createPanel } from './utils'; - -createPanel('profiler'); diff --git a/shells/browser/shared/src/panels/settings.js b/shells/browser/shared/src/panels/settings.js deleted file mode 100644 index a8dd78b0504ce..0000000000000 --- a/shells/browser/shared/src/panels/settings.js +++ /dev/null @@ -1,3 +0,0 @@ -import { createPanel } from './utils'; - -createPanel('settings'); diff --git a/shells/browser/shared/src/panels/utils.js b/shells/browser/shared/src/panels/utils.js deleted file mode 100644 index 1f807fc922f19..0000000000000 --- a/shells/browser/shared/src/panels/utils.js +++ /dev/null @@ -1,72 +0,0 @@ -/* global chrome */ - -import { createElement } from 'react'; -import { unstable_createRoot as createRoot, flushSync } from 'react-dom'; -import DevTools from 'src/devtools/views/DevTools'; -import { getBrowserName, getBrowserTheme } from '../utils'; - -export function createPanel(defaultTab) { - let injectedBridge = null; - let injectedStore = null; - let root = null; - - // All DevTools panel share a single Bridge and Store instance. - // The main script will inject those shared instances using this method. - window.injectBridgeAndStore = (bridge, store) => { - injectedBridge = bridge; - injectedStore = store; - - if (root === null) { - injectAndInit(); - } else { - // It's easiest to recreate the DevTools panel (to clean up potential stale state). - // We can revisit this in the future as a small optimization. - flushSync(() => root.unmount(injectAndInit)); - } - }; - - function viewElementSource(id) { - if (injectedBridge == null || injectedStore == null) { - return; - } - - const rendererID = injectedStore.getRendererIDForElement(id); - if (rendererID != null) { - // Ask the renderer interface to determine the component function, - // and store it as a global variable on the window - injectedBridge.send('viewElementSource', { id, rendererID }); - - setTimeout(() => { - // Ask Chrome to display the location of the component function, - // assuming the renderer found one. - chrome.devtools.inspectedWindow.eval(` - if (window.$type != null) { - inspect(window.$type); - } - `); - }, 100); - } - } - - function injectAndInit() { - const container = ((document.getElementById( - 'container' - ): any): HTMLElement); - - // Clear the "React not found" initial message before rendering. - container.innerHTML = ''; - - root = createRoot(container); - root.render( - createElement(DevTools, { - bridge: injectedBridge, - browserName: getBrowserName(), - browserTheme: getBrowserTheme(), - defaultTab, - showTabBar: false, - store: injectedStore, - viewElementSource, - }) - ); - } -} diff --git a/shells/browser/shared/src/utils.js b/shells/browser/shared/src/utils.js index 7f4c2eaa068b2..0446ff9daf9a7 100644 --- a/shells/browser/shared/src/utils.js +++ b/shells/browser/shared/src/utils.js @@ -2,6 +2,27 @@ const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0; +export function createViewElementSource(bridge: Bridge, store: Store) { + return function viewElementSource(id) { + const rendererID = store.getRendererIDForElement(id); + if (rendererID != null) { + // Ask the renderer interface to determine the component function, + // and store it as a global variable on the window + bridge.send('viewElementSource', { id, rendererID }); + + setTimeout(() => { + // Ask Chrome to display the location of the component function, + // assuming the renderer found one. + chrome.devtools.inspectedWindow.eval(` + if (window.$type != null) { + inspect(window.$type); + } + `); + }, 100); + } + }; +} + export function getBrowserName() { return IS_CHROME ? 'Chrome' : 'Firefox'; } diff --git a/shells/browser/shared/webpack.config.js b/shells/browser/shared/webpack.config.js index e95c5cf418eee..44e3d5b5c28f4 100644 --- a/shells/browser/shared/webpack.config.js +++ b/shells/browser/shared/webpack.config.js @@ -16,9 +16,7 @@ module.exports = { contentScript: './src/contentScript.js', inject: './src/GlobalHook.js', main: './src/main.js', - elements: './src/panels/elements.js', - profiler: './src/panels/profiler.js', - settings: './src/panels/settings.js', + panel: './src/panel.js', }, output: { path: __dirname + '/build', diff --git a/src/devtools/views/DevTools.js b/src/devtools/views/DevTools.js index 3473582f04cba..2db0e2dcfb3ac 100644 --- a/src/devtools/views/DevTools.js +++ b/src/devtools/views/DevTools.js @@ -1,6 +1,7 @@ // @flow import React, { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; import Store from '../store'; import { BridgeContext, StoreContext } from './context'; import Elements from './Elements/Elements'; @@ -27,6 +28,8 @@ export type Props = {| browserName: BrowserName, defaultTab?: TabID, browserTheme: BrowserTheme, + overrideTab?: TabID, + portalContainer?: Element, showTabBar?: boolean, store: Store, viewElementSource?: ?Function, @@ -59,11 +62,17 @@ export default function DevTools({ browserName, defaultTab = 'elements', browserTheme = 'light', + overrideTab, + portalContainer, showTabBar = false, store, viewElementSource = null, }: Props) { const [tab, setTab] = useState(defaultTab); + if (overrideTab != null && overrideTab !== tab) { + setTab(overrideTab); + } + const [supportsProfiling, setSupportsProfiling] = useState( store.supportsProfiling ); @@ -100,7 +109,7 @@ export default function DevTools({ break; } - return ( + const children = ( <BridgeContext.Provider value={bridge}> <StoreContext.Provider value={store}> <SettingsContextController browserTheme={browserTheme}> @@ -135,4 +144,8 @@ export default function DevTools({ </StoreContext.Provider> </BridgeContext.Provider> ); + + return portalContainer != null + ? createPortal(children, portalContainer) + : children; } From 11573bf8d9d6346121492d97d2db8a443f4a22a3 Mon Sep 17 00:00:00 2001 From: Brian Vaughn <bvaughn@fb.com> Date: Mon, 18 Mar 2019 09:11:28 -0700 Subject: [PATCH 2/8] Refactored portaling and fixed disconnected CSS vars --- shells/browser/shared/src/main.js | 73 ++++--- shells/browser/shared/src/panel.js | 13 +- src/devtools/views/DevTools.js | 32 +-- src/devtools/views/Elements/Elements.js | 13 +- src/devtools/views/Profiler/Profiler.js | 16 +- src/devtools/views/Settings/Settings.js | 13 +- .../views/Settings/SettingsContext.js | 190 ++++++++++++------ 7 files changed, 222 insertions(+), 128 deletions(-) diff --git a/shells/browser/shared/src/main.js b/shells/browser/shared/src/main.js index d09dddf1bd98d..63859497b1d09 100644 --- a/shells/browser/shared/src/main.js +++ b/shells/browser/shared/src/main.js @@ -30,12 +30,13 @@ function createPanelIfReactLoaded() { clearInterval(loadCheckInterval); - let renderRootToPortal = null; let bridge = null; let store = null; - let elementsPanel = null; - let profilerPanel = null; - let settingsPanel = null; + let elementsPortalContainer = null; + let profilerPortalContainer = null; + let settingsPortalContainer = null; + let cloneStyleTags = null; + let render = null; function initBridgeAndStore() { let hasPortBeenDisconnected = false; @@ -68,51 +69,47 @@ function createPanelIfReactLoaded() { const container = document.createElement('div'); const root = createRoot(container); - renderRootToPortal = ({ overrideTab, portalContainer }) => { + render = overrideTab => { root.render( createElement(DevTools, { bridge, browserName: getBrowserName(), browserTheme: getBrowserTheme(), + elementsPortalContainer, overrideTab, - portalContainer, + profilerPortalContainer, + settingsPortalContainer, showTabBar: false, store, viewElementSource, }) ); + }; + } - const oldLinkTags = document.getElementsByTagName('link'); - const newLinkTags = []; - for (let oldLinkTag of oldLinkTags) { - if (oldLinkTag.rel === 'stylesheet') { - const newLinkTag = document.createElement('link'); - for (let attribute of oldLinkTag.attributes) { - newLinkTag.setAttribute( - attribute.nodeName, - attribute.nodeValue - ); - } - newLinkTags.push(newLinkTag); + cloneStyleTags = () => { + const linkTags = []; + for (let linkTag of document.getElementsByTagName('link')) { + if (linkTag.rel === 'stylesheet') { + const newLinkTag = document.createElement('link'); + for (let attribute of linkTag.attributes) { + newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue); } + linkTags.push(newLinkTag); } - - return newLinkTags; - }; - - if (elementsPanel !== null) { - elementsPanel.render(renderRootToPortal, 'elements'); } - } + return linkTags; + }; initBridgeAndStore(); chrome.devtools.panels.create('⚛ Elements', '', 'panel.html', panel => { panel.onShown.addListener(panel => { - elementsPanel = panel; - - if (renderRootToPortal !== null) { - elementsPanel.render(renderRootToPortal, 'elements'); + elementsPortalContainer = panel.container; + if (elementsPortalContainer != null) { + elementsPortalContainer.innerHTML = ''; + render('elements'); + panel.injectStyles(cloneStyleTags); } // TODO: When the user switches to the panel, check for an Elements tab selection. @@ -125,20 +122,22 @@ function createPanelIfReactLoaded() { // TODO (profiling) Is there a way to detect profiling support and conditionally register this panel? chrome.devtools.panels.create('⚛ Profiler', '', 'panel.html', panel => { panel.onShown.addListener(panel => { - profilerPanel = panel; - - if (renderRootToPortal !== null) { - profilerPanel.render(renderRootToPortal, 'profiler'); + profilerPortalContainer = panel.container; + if (profilerPortalContainer != null) { + profilerPortalContainer.innerHTML = ''; + render('profiler'); + panel.injectStyles(cloneStyleTags); } }); }); chrome.devtools.panels.create('⚛ Settings', '', 'panel.html', panel => { panel.onShown.addListener(panel => { - settingsPanel = panel; - - if (renderRootToPortal !== null) { - settingsPanel.render(renderRootToPortal, 'settings'); + settingsPortalContainer = panel.container; + if (settingsPortalContainer != null) { + settingsPortalContainer.innerHTML = ''; + render('settings'); + panel.injectStyles(cloneStyleTags); } }); }); diff --git a/shells/browser/shared/src/panel.js b/shells/browser/shared/src/panel.js index b7413918f5c2d..1ce574dfcf420 100644 --- a/shells/browser/shared/src/panel.js +++ b/shells/browser/shared/src/panel.js @@ -1,18 +1,13 @@ -const container = document.getElementById('container'); +window.container = document.getElementById('container'); let hasInjectedStyles = false; -window.render = (renderRootToPortal, tab) => { - container.innerHTML = ''; - - const linkTags = renderRootToPortal({ - overrideTab: tab, - portalContainer: container, - }); - +window.injectStyles = getLinkTags => { if (!hasInjectedStyles) { hasInjectedStyles = true; + const linkTags = getLinkTags(); + for (let linkTag of linkTags) { document.head.appendChild(linkTag); } diff --git a/src/devtools/views/DevTools.js b/src/devtools/views/DevTools.js index 2db0e2dcfb3ac..d80a0ecbe1197 100644 --- a/src/devtools/views/DevTools.js +++ b/src/devtools/views/DevTools.js @@ -1,7 +1,6 @@ // @flow import React, { useEffect, useState } from 'react'; -import { createPortal } from 'react-dom'; import Store from '../store'; import { BridgeContext, StoreContext } from './context'; import Elements from './Elements/Elements'; @@ -26,10 +25,12 @@ export type TabID = 'elements' | 'profiler' | 'settings'; export type Props = {| bridge: Bridge, browserName: BrowserName, - defaultTab?: TabID, browserTheme: BrowserTheme, + defaultTab?: TabID, + elementsPortalContainer?: Element, overrideTab?: TabID, - portalContainer?: Element, + profilerPortalContainer?: Element, + settingsPortalContainer?: Element, showTabBar?: boolean, store: Store, viewElementSource?: ?Function, @@ -60,10 +61,12 @@ const tabsWithoutProfiler = [elementTab, settingsTab]; export default function DevTools({ bridge, browserName, - defaultTab = 'elements', browserTheme = 'light', + defaultTab = 'elements', + elementsPortalContainer, overrideTab, - portalContainer, + profilerPortalContainer, + settingsPortalContainer, showTabBar = false, store, viewElementSource = null, @@ -98,21 +101,26 @@ export default function DevTools({ let tabElement; switch (tab) { case 'profiler': - tabElement = <Profiler />; + tabElement = <Profiler portalContainer={profilerPortalContainer} />; break; case 'settings': - tabElement = <Settings />; + tabElement = <Settings portalContainer={settingsPortalContainer} />; break; case 'elements': default: - tabElement = <Elements />; + tabElement = <Elements portalContainer={elementsPortalContainer} />; break; } - const children = ( + return ( <BridgeContext.Provider value={bridge}> <StoreContext.Provider value={store}> - <SettingsContextController browserTheme={browserTheme}> + <SettingsContextController + browserTheme={browserTheme} + elementsPortalContainer={elementsPortalContainer} + profilerPortalContainer={profilerPortalContainer} + settingsPortalContainer={settingsPortalContainer} + > <TreeContextController viewElementSource={viewElementSource}> <ProfilerContextController> <div className={styles.DevTools}> @@ -144,8 +152,4 @@ export default function DevTools({ </StoreContext.Provider> </BridgeContext.Provider> ); - - return portalContainer != null - ? createPortal(children, portalContainer) - : children; } diff --git a/src/devtools/views/Elements/Elements.js b/src/devtools/views/Elements/Elements.js index cb401ddfdba99..596a5c754a4a2 100644 --- a/src/devtools/views/Elements/Elements.js +++ b/src/devtools/views/Elements/Elements.js @@ -1,15 +1,18 @@ // @flow import React from 'react'; +import { createPortal } from 'react-dom'; import Tree from './Tree'; import SelectedElement from './SelectedElement'; import styles from './Elements.css'; -export type Props = {||}; +export type Props = {| + portalContainer?: Element, +|}; -export default function Elements(_: Props) { +export default function Elements({ portalContainer }: Props) { // TODO Flex wrappers below should be user resizable. - return ( + const children = ( <div className={styles.Elements}> <div className={styles.TreeWrapper}> <Tree /> @@ -19,4 +22,8 @@ export default function Elements(_: Props) { </div> </div> ); + + return portalContainer != null + ? createPortal(children, portalContainer) + : children; } diff --git a/src/devtools/views/Profiler/Profiler.js b/src/devtools/views/Profiler/Profiler.js index add792692297f..e5c2db601a9e0 100644 --- a/src/devtools/views/Profiler/Profiler.js +++ b/src/devtools/views/Profiler/Profiler.js @@ -1,6 +1,7 @@ // @flow import React, { Suspense, useCallback, useContext, useState } from 'react'; +import { createPortal } from 'react-dom'; import { ProfilerContext } from './ProfilerContext'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; @@ -13,21 +14,30 @@ import SnapshotSelector from './SnapshotSelector'; import styles from './Profiler.css'; -export default function Profiler(_: {||}) { +export type Props = {| + portalContainer?: Element, +|}; + +export default function Profiler({ portalContainer }: Props) { const { hasProfilingData, isProfiling, rootHasProfilingData } = useContext( ProfilerContext ); + let children = null; if (isProfiling || !rootHasProfilingData) { - return ( + children = ( <NonSuspendingProfiler hasProfilingData={hasProfilingData} isProfiling={isProfiling} /> ); } else { - return <SuspendingProfiler />; + children = <SuspendingProfiler />; } + + return portalContainer != null + ? createPortal(children, portalContainer) + : children; } // This view is rendered when there is no profiler data (either we haven't profiled yet or we're currently profiling). diff --git a/src/devtools/views/Settings/Settings.js b/src/devtools/views/Settings/Settings.js index 37e4dd9198bfa..d3a99360a0b53 100644 --- a/src/devtools/views/Settings/Settings.js +++ b/src/devtools/views/Settings/Settings.js @@ -1,13 +1,16 @@ // @flow import React, { useCallback, useContext } from 'react'; +import { createPortal } from 'react-dom'; import { SettingsContext } from './SettingsContext'; import styles from './Settings.css'; -export type Props = {||}; +export type Props = {| + portalContainer?: Element, +|}; -export default function Settings(_: Props) { +export default function Settings({ portalContainer }: Props) { const { displayDensity, setDisplayDensity, theme, setTheme } = useContext( SettingsContext ); @@ -26,7 +29,7 @@ export default function Settings(_: Props) { [setTheme] ); - return ( + const children = ( <div className={styles.Settings}> <div className={styles.Section}> <div className={styles.Header}>Theme</div> @@ -90,4 +93,8 @@ export default function Settings(_: Props) { </div> </div> ); + + return portalContainer != null + ? createPortal(children, portalContainer) + : children; } diff --git a/src/devtools/views/Settings/SettingsContext.js b/src/devtools/views/Settings/SettingsContext.js index 0c5458cbade74..f04af61f32128 100644 --- a/src/devtools/views/Settings/SettingsContext.js +++ b/src/devtools/views/Settings/SettingsContext.js @@ -23,18 +23,58 @@ type Context = {| const SettingsContext = createContext<Context>(((null: any): Context)); SettingsContext.displayName = 'SettingsContext'; +type DocumentElements = Array<HTMLElement>; + type Props = {| browserTheme: BrowserTheme, children: React$Node, + elementsPortalContainer?: Element, + profilerPortalContainer?: Element, + settingsPortalContainer?: Element, |}; -function SettingsContextController({ browserTheme, children }: Props) { +function SettingsContextController({ + browserTheme, + children, + elementsPortalContainer, + profilerPortalContainer, + settingsPortalContainer, +}: Props) { const [displayDensity, setDisplayDensity] = useLocalStorage<DisplayDensity>( 'displayDensity', 'compact' ); const [theme, setTheme] = useLocalStorage<Theme>('theme', 'auto'); + const documentElements = useMemo<DocumentElements>(() => { + const array: Array<HTMLElement> = [ + ((document.documentElement: any): HTMLElement), + ]; + if (elementsPortalContainer != null) { + array.push( + ((elementsPortalContainer.ownerDocument + .documentElement: any): HTMLElement) + ); + } + if (profilerPortalContainer != null) { + array.push( + ((profilerPortalContainer.ownerDocument + .documentElement: any): HTMLElement) + ); + } + if (settingsPortalContainer != null) { + array.push( + ((settingsPortalContainer.ownerDocument + .documentElement: any): HTMLElement) + ); + } + return array; + }, [ + elementsPortalContainer, + profilerPortalContainer, + settingsPortalContainer, + ]); + const comfortableLineHeight = parseInt( getComputedStyle((document.body: any)).getPropertyValue( '--comfortable-line-height-data' @@ -51,31 +91,31 @@ function SettingsContextController({ browserTheme, children }: Props) { useLayoutEffect(() => { switch (displayDensity) { case 'compact': - updateDisplayDensity('compact'); + updateDisplayDensity('compact', documentElements); break; case 'comfortable': - updateDisplayDensity('comfortable'); + updateDisplayDensity('comfortable', documentElements); break; default: throw Error(`Unsupported displayDensity value "${displayDensity}"`); } - }, [displayDensity]); + }, [displayDensity, documentElements]); useLayoutEffect(() => { switch (theme) { case 'light': - updateThemeVariables('light'); + updateThemeVariables('light', documentElements); break; case 'dark': - updateThemeVariables('dark'); + updateThemeVariables('dark', documentElements); break; case 'auto': - updateThemeVariables(browserTheme); + updateThemeVariables(browserTheme, documentElements); break; default: throw Error(`Unsupported theme value "${theme}"`); } - }, [browserTheme, theme]); + }, [browserTheme, theme, documentElements]); const value = useMemo( () => ({ @@ -105,63 +145,95 @@ function SettingsContextController({ browserTheme, children }: Props) { ); } -function setStyleVariable(name: string, value: string) { - (document.documentElement: any).style.setProperty(name, value); +function setStyleVariable( + name: string, + value: string, + documentElements: DocumentElements +) { + documentElements.forEach(documentElement => + documentElement.style.setProperty(name, value) + ); } -function updateStyleHelper(themeKey: string, style: string) { - setStyleVariable(`--${style}`, `var(--${themeKey}-${style})`); +function updateStyleHelper( + themeKey: string, + style: string, + documentElements: DocumentElements +) { + setStyleVariable( + `--${style}`, + `var(--${themeKey}-${style})`, + documentElements + ); } -function updateDisplayDensity(displayDensity: DisplayDensity): void { - updateStyleHelper(displayDensity, 'font-size-monospace-normal'); - updateStyleHelper(displayDensity, 'font-size-monospace-large'); - updateStyleHelper(displayDensity, 'font-size-sans-normal'); - updateStyleHelper(displayDensity, 'font-size-sans-large'); - updateStyleHelper(displayDensity, 'line-height-data'); +function updateDisplayDensity( + displayDensity: DisplayDensity, + documentElements: DocumentElements +): void { + updateStyleHelper( + displayDensity, + 'font-size-monospace-normal', + documentElements + ); + updateStyleHelper( + displayDensity, + 'font-size-monospace-large', + documentElements + ); + updateStyleHelper(displayDensity, 'font-size-sans-normal', documentElements); + updateStyleHelper(displayDensity, 'font-size-sans-large', documentElements); + updateStyleHelper(displayDensity, 'line-height-data', documentElements); } -function updateThemeVariables(theme: Theme): void { - updateStyleHelper(theme, 'color-attribute-name'); - updateStyleHelper(theme, 'color-attribute-value'); - updateStyleHelper(theme, 'color-attribute-editable-value'); - updateStyleHelper(theme, 'color-background'); - updateStyleHelper(theme, 'color-border'); - updateStyleHelper(theme, 'color-button-background'); - updateStyleHelper(theme, 'color-button-background-focus'); - updateStyleHelper(theme, 'color-button-background-hover'); - updateStyleHelper(theme, 'color-button'); - updateStyleHelper(theme, 'color-button-disabled'); - updateStyleHelper(theme, 'color-button-focus'); - updateStyleHelper(theme, 'color-button-hover'); - updateStyleHelper(theme, 'color-commit-did-not-render'); - updateStyleHelper(theme, 'color-commit-gradient-0'); - updateStyleHelper(theme, 'color-commit-gradient-1'); - updateStyleHelper(theme, 'color-commit-gradient-2'); - updateStyleHelper(theme, 'color-commit-gradient-3'); - updateStyleHelper(theme, 'color-commit-gradient-4'); - updateStyleHelper(theme, 'color-commit-gradient-5'); - updateStyleHelper(theme, 'color-commit-gradient-6'); - updateStyleHelper(theme, 'color-commit-gradient-7'); - updateStyleHelper(theme, 'color-commit-gradient-8'); - updateStyleHelper(theme, 'color-commit-gradient-9'); - updateStyleHelper(theme, 'color-commit-gradient-text'); - updateStyleHelper(theme, 'color-component-name'); - updateStyleHelper(theme, 'color-component-name-inverted'); - updateStyleHelper(theme, 'color-dim'); - updateStyleHelper(theme, 'color-dimmer'); - updateStyleHelper(theme, 'color-dimmest'); - updateStyleHelper(theme, 'color-jsx-arrow-brackets'); - updateStyleHelper(theme, 'color-jsx-arrow-brackets-inverted'); - updateStyleHelper(theme, 'color-modal-background'); - updateStyleHelper(theme, 'color-record-active'); - updateStyleHelper(theme, 'color-record-hover'); - updateStyleHelper(theme, 'color-record-inactive'); - updateStyleHelper(theme, 'color-tree-node-selected'); - updateStyleHelper(theme, 'color-tree-node-hover'); - updateStyleHelper(theme, 'color-search-match'); - updateStyleHelper(theme, 'color-search-match-current'); - updateStyleHelper(theme, 'color-text-color'); +function updateThemeVariables( + theme: Theme, + documentElements: DocumentElements +): void { + updateStyleHelper(theme, 'color-attribute-name', documentElements); + updateStyleHelper(theme, 'color-attribute-value', documentElements); + updateStyleHelper(theme, 'color-attribute-editable-value', documentElements); + updateStyleHelper(theme, 'color-background', documentElements); + updateStyleHelper(theme, 'color-border', documentElements); + updateStyleHelper(theme, 'color-button-background', documentElements); + updateStyleHelper(theme, 'color-button-background-focus', documentElements); + updateStyleHelper(theme, 'color-button-background-hover', documentElements); + updateStyleHelper(theme, 'color-button', documentElements); + updateStyleHelper(theme, 'color-button-disabled', documentElements); + updateStyleHelper(theme, 'color-button-focus', documentElements); + updateStyleHelper(theme, 'color-button-hover', documentElements); + updateStyleHelper(theme, 'color-commit-did-not-render', documentElements); + updateStyleHelper(theme, 'color-commit-gradient-0', documentElements); + updateStyleHelper(theme, 'color-commit-gradient-1', documentElements); + updateStyleHelper(theme, 'color-commit-gradient-2', documentElements); + updateStyleHelper(theme, 'color-commit-gradient-3', documentElements); + updateStyleHelper(theme, 'color-commit-gradient-4', documentElements); + updateStyleHelper(theme, 'color-commit-gradient-5', documentElements); + updateStyleHelper(theme, 'color-commit-gradient-6', documentElements); + updateStyleHelper(theme, 'color-commit-gradient-7', documentElements); + updateStyleHelper(theme, 'color-commit-gradient-8', documentElements); + updateStyleHelper(theme, 'color-commit-gradient-9', documentElements); + updateStyleHelper(theme, 'color-commit-gradient-text', documentElements); + updateStyleHelper(theme, 'color-component-name', documentElements); + updateStyleHelper(theme, 'color-component-name-inverted', documentElements); + updateStyleHelper(theme, 'color-dim', documentElements); + updateStyleHelper(theme, 'color-dimmer', documentElements); + updateStyleHelper(theme, 'color-dimmest', documentElements); + updateStyleHelper(theme, 'color-jsx-arrow-brackets', documentElements); + updateStyleHelper( + theme, + 'color-jsx-arrow-brackets-inverted', + documentElements + ); + updateStyleHelper(theme, 'color-modal-background', documentElements); + updateStyleHelper(theme, 'color-record-active', documentElements); + updateStyleHelper(theme, 'color-record-hover', documentElements); + updateStyleHelper(theme, 'color-record-inactive', documentElements); + updateStyleHelper(theme, 'color-tree-node-selected', documentElements); + updateStyleHelper(theme, 'color-tree-node-hover', documentElements); + updateStyleHelper(theme, 'color-search-match', documentElements); + updateStyleHelper(theme, 'color-search-match-current', documentElements); + updateStyleHelper(theme, 'color-text-color', documentElements); } export { SettingsContext, SettingsContextController }; From 2664036dbe42a137ecf0a345a0c15ab068982151 Mon Sep 17 00:00:00 2001 From: Brian Vaughn <bvaughn@fb.com> Date: Mon, 18 Mar 2019 09:18:22 -0700 Subject: [PATCH 3/8] Tweaked profiling did-not-render color to be dimmer --- src/devtools/views/root.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/devtools/views/root.css b/src/devtools/views/root.css index 65eb630d24221..a8d562ee16928 100644 --- a/src/devtools/views/root.css +++ b/src/devtools/views/root.css @@ -16,7 +16,7 @@ --light-color-button-focus: #3578e5; --light-color-button-hover: #3578e5; --light-color-border: #eeeeee; - --light-color-commit-did-not-render: #777d88; + --light-color-commit-did-not-render: #cfd1d5; --light-color-commit-gradient-0: #37afa9; --light-color-commit-gradient-1: #63b19e; --light-color-commit-gradient-2: #80b393; @@ -58,7 +58,7 @@ --dark-color-button-focus: #a2e9fc; --dark-color-button-hover: #a2e9fc; --dark-color-border: #3d424a; - --dark-color-commit-did-not-render: #8f949d; + --dark-color-commit-did-not-render: #777d88; --dark-color-commit-gradient-0: #37afa9; --dark-color-commit-gradient-1: #63b19e; --dark-color-commit-gradient-2: #80b393; From c9920f095457e39c4c8f060c5a6d14c7a8f0cd6f Mon Sep 17 00:00:00 2001 From: Brian Vaughn <bvaughn@fb.com> Date: Mon, 18 Mar 2019 09:20:00 -0700 Subject: [PATCH 4/8] Added a few inline comments --- shells/browser/shared/src/main.js | 2 ++ shells/browser/shared/src/panel.js | 3 +++ 2 files changed, 5 insertions(+) diff --git a/shells/browser/shared/src/main.js b/shells/browser/shared/src/main.js index 63859497b1d09..bf6726141ea3b 100644 --- a/shells/browser/shared/src/main.js +++ b/shells/browser/shared/src/main.js @@ -32,9 +32,11 @@ function createPanelIfReactLoaded() { let bridge = null; let store = null; + let elementsPortalContainer = null; let profilerPortalContainer = null; let settingsPortalContainer = null; + let cloneStyleTags = null; let render = null; diff --git a/shells/browser/shared/src/panel.js b/shells/browser/shared/src/panel.js index 1ce574dfcf420..f84ac11beb1e7 100644 --- a/shells/browser/shared/src/panel.js +++ b/shells/browser/shared/src/panel.js @@ -1,7 +1,10 @@ +// Portal target container. window.container = document.getElementById('container'); let hasInjectedStyles = false; +// DevTools styles are injected into the top-level document head (where the main React app is rendered). +// This method copies those styles to the child window where each panel (e.g. Elements, Profiler) is portaled. window.injectStyles = getLinkTags => { if (!hasInjectedStyles) { hasInjectedStyles = true; From e728ebc7b9c38770761fbd5f791c9334518f0131 Mon Sep 17 00:00:00 2001 From: Brian Vaughn <bvaughn@fb.com> Date: Mon, 18 Mar 2019 09:36:47 -0700 Subject: [PATCH 5/8] Unmount and remount when main URL changes to avoid staleness problems --- shells/browser/shared/src/main.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/shells/browser/shared/src/main.js b/shells/browser/shared/src/main.js index bf6726141ea3b..0a596e42caa27 100644 --- a/shells/browser/shared/src/main.js +++ b/shells/browser/shared/src/main.js @@ -1,7 +1,7 @@ /* global chrome */ import { createElement } from 'react'; -import { unstable_createRoot as createRoot } from 'react-dom'; +import { unstable_createRoot as createRoot, flushSync } from 'react-dom'; import Bridge from 'src/bridge'; import Store from 'src/devtools/Store'; import inject from './inject'; @@ -38,7 +38,9 @@ function createPanelIfReactLoaded() { let settingsPortalContainer = null; let cloneStyleTags = null; + let mostRecentOverrideTab = null; let render = null; + let root = null; function initBridgeAndStore() { let hasPortBeenDisconnected = false; @@ -68,10 +70,11 @@ function createPanelIfReactLoaded() { const viewElementSource = createViewElementSource(bridge, store); - const container = document.createElement('div'); - const root = createRoot(container); + root = createRoot(document.createElement('div')); + + render = (overrideTab = mostRecentOverrideTab) => { + mostRecentOverrideTab = overrideTab; - render = overrideTab => { root.render( createElement(DevTools, { bridge, @@ -87,6 +90,8 @@ function createPanelIfReactLoaded() { }) ); }; + + render(); } cloneStyleTags = () => { @@ -150,7 +155,9 @@ function createPanelIfReactLoaded() { chrome.devtools.network.onNavigated.addListener(function onNavigated() { bridge.send('shutdown'); - initBridgeAndStore(); + // It's easiest to recreate the DevTools panel (to clean up potential stale state). + // We can revisit this in the future as a small optimization. + flushSync(() => root.unmount(initBridgeAndStore)); }); } ); From 50b6b1d5f9e8a3284971ca0b464fb88ce3f925ba Mon Sep 17 00:00:00 2001 From: Brian Vaughn <bvaughn@fb.com> Date: Mon, 18 Mar 2019 09:57:08 -0700 Subject: [PATCH 6/8] Added some inline comments about portal props --- src/devtools/views/DevTools.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/devtools/views/DevTools.js b/src/devtools/views/DevTools.js index d80a0ecbe1197..4f839c368009b 100644 --- a/src/devtools/views/DevTools.js +++ b/src/devtools/views/DevTools.js @@ -27,13 +27,22 @@ export type Props = {| browserName: BrowserName, browserTheme: BrowserTheme, defaultTab?: TabID, - elementsPortalContainer?: Element, - overrideTab?: TabID, - profilerPortalContainer?: Element, - settingsPortalContainer?: Element, showTabBar?: boolean, store: Store, viewElementSource?: ?Function, + + // This property is used only by the web extension target. + // The built-in tab UI is hidden in that case, in favor of the browser's own panel tabs. + // This is done to save space within the app. + // Because of this, the extension needs to be able to change which tab is active/rendered. + overrideTab?: TabID, + + // To avoid potential multi-root trickiness, the web extension uses portals to render tabs. + // The root <DevTools> app is rendered in the top-level extension window, + // but individual tabs (e.g. Elements, Profiling) can be rendered into portals within their browser panels. + elementsPortalContainer?: Element, + profilerPortalContainer?: Element, + settingsPortalContainer?: Element, |}; const elementTab = { From be48150fc69a078e4b51cdaef635b6eacaa1b4db Mon Sep 17 00:00:00 2001 From: Brian Vaughn <bvaughn@fb.com> Date: Mon, 18 Mar 2019 10:09:09 -0700 Subject: [PATCH 7/8] innerTagName -> innerElementType --- src/devtools/views/Profiler/CommitRanked.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/devtools/views/Profiler/CommitRanked.js b/src/devtools/views/Profiler/CommitRanked.js index 1fb4ded3ee415..acdd0cb8841e6 100644 --- a/src/devtools/views/Profiler/CommitRanked.js +++ b/src/devtools/views/Profiler/CommitRanked.js @@ -104,7 +104,7 @@ function CommitRanked({ height, width }: {| height: number, width: number |}) { return ( <FixedSizeList height={height} - innerTagName="svg" + innerElementType="svg" itemCount={chartData.nodes.length} itemData={itemData} itemSize={barHeight} From 5f154b376e9658a6b7b7477dead472063be1541e Mon Sep 17 00:00:00 2001 From: Brian Vaughn <bvaughn@fb.com> Date: Mon, 18 Mar 2019 13:37:37 -0700 Subject: [PATCH 8/8] Added profiling-not-supported message for browser extension --- src/devtools/views/Button.css | 3 +- src/devtools/views/DevTools.js | 7 +++- src/devtools/views/Profiler/Profiler.js | 38 +++++++++++++++++++-- src/devtools/views/Profiler/RecordToggle.js | 7 ++-- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/devtools/views/Button.css b/src/devtools/views/Button.css index 31291d7f3bde1..a37af0ee30cc0 100644 --- a/src/devtools/views/Button.css +++ b/src/devtools/views/Button.css @@ -22,7 +22,8 @@ outline: none; box-shadow: 0 0 0 2px var(--color-button-background-focus) inset; } -.Button:disabled { +.Button:disabled, +.Button:disabled:active { background: var(--color-button-background); color: var(--color-button-disabled); cursor: default; diff --git a/src/devtools/views/DevTools.js b/src/devtools/views/DevTools.js index 4f839c368009b..56eee3b56edce 100644 --- a/src/devtools/views/DevTools.js +++ b/src/devtools/views/DevTools.js @@ -110,7 +110,12 @@ export default function DevTools({ let tabElement; switch (tab) { case 'profiler': - tabElement = <Profiler portalContainer={profilerPortalContainer} />; + tabElement = ( + <Profiler + portalContainer={profilerPortalContainer} + supportsProfiling={supportsProfiling} + /> + ); break; case 'settings': tabElement = <Settings portalContainer={settingsPortalContainer} />; diff --git a/src/devtools/views/Profiler/Profiler.js b/src/devtools/views/Profiler/Profiler.js index e5c2db601a9e0..4199efe1c25d6 100644 --- a/src/devtools/views/Profiler/Profiler.js +++ b/src/devtools/views/Profiler/Profiler.js @@ -16,9 +16,13 @@ import styles from './Profiler.css'; export type Props = {| portalContainer?: Element, + supportsProfiling: boolean, |}; -export default function Profiler({ portalContainer }: Props) { +export default function Profiler({ + portalContainer, + supportsProfiling, +}: Props) { const { hasProfilingData, isProfiling, rootHasProfilingData } = useContext( ProfilerContext ); @@ -29,6 +33,7 @@ export default function Profiler({ portalContainer }: Props) { <NonSuspendingProfiler hasProfilingData={hasProfilingData} isProfiling={isProfiling} + supportsProfiling={supportsProfiling} /> ); } else { @@ -47,12 +52,16 @@ export default function Profiler({ portalContainer }: Props) { function NonSuspendingProfiler({ hasProfilingData, isProfiling, + supportsProfiling, }: {| hasProfilingData: boolean, isProfiling: boolean, + supportsProfiling: boolean, |}) { let view = null; - if (isProfiling) { + if (!supportsProfiling) { + view = <ProfilingNotSupported />; + } else if (isProfiling) { view = <RecortdingInProgress />; } else if (!hasProfilingData) { view = <NoProfilingData />; @@ -64,7 +73,7 @@ function NonSuspendingProfiler({ <div className={styles.Profiler}> <div className={styles.LeftColumn}> <div className={styles.Toolbar}> - <RecordToggle /> + <RecordToggle disabled={!supportsProfiling} /> <Button disabled title="Reload and start profiling"> {/* TODO (profiling) Wire up reload button */} <ButtonIcon type="reload" /> @@ -200,6 +209,29 @@ const NoProfilingDataForRoot = () => ( </div> ); +const ProfilingNotSupported = () => ( + <div className={styles.Column}> + <div className={styles.Header}>Profiling not supported.</div> + <div className={styles.Column}> + <p> + Profiling support requires either a development or production-profiling + build of React v16.5+. + </p> + <p> + Learn more at{' '} + <a + href="https://fb.me/react-profiling" + rel="noopener noreferrer" + target="_blank" + > + fb.me/react-profiling + </a> + . + </p> + </div> + </div> +); + const RecortdingInProgress = () => ( <div className={styles.Column}> <div className={styles.Header}>Profiling is in progress...</div> diff --git a/src/devtools/views/Profiler/RecordToggle.js b/src/devtools/views/Profiler/RecordToggle.js index 4d1bb591d92f2..94074002ca1db 100644 --- a/src/devtools/views/Profiler/RecordToggle.js +++ b/src/devtools/views/Profiler/RecordToggle.js @@ -7,9 +7,11 @@ import { ProfilerContext } from './ProfilerContext'; import styles from './RecordToggle.css'; -export type Props = {||}; +export type Props = {| + disabled?: boolean, +|}; -export default function RecordToggle(_: Props) { +export default function RecordToggle({ disabled }: Props) { const { isProfiling, startProfiling, stopProfiling } = useContext( ProfilerContext ); @@ -19,6 +21,7 @@ export default function RecordToggle(_: Props) { className={ isProfiling ? styles.ActiveRecordToggle : styles.InactiveRecordToggle } + disabled={disabled} onClick={isProfiling ? stopProfiling : startProfiling} title={isProfiling ? 'Stop profiling' : 'Start profiling'} >