diff --git a/packages/react-devtools-core/webpack.standalone.js b/packages/react-devtools-core/webpack.standalone.js index 8f5b4e6dcac93..199e6e2c46620 100644 --- a/packages/react-devtools-core/webpack.standalone.js +++ b/packages/react-devtools-core/webpack.standalone.js @@ -18,6 +18,15 @@ const __DEV__ = NODE_ENV === 'development'; const DEVTOOLS_VERSION = getVersionString(); +const babelOptions = { + configFile: resolve( + __dirname, + '..', + 'react-devtools-shared', + 'babel.config.js', + ), +}; + module.exports = { mode: __DEV__ ? 'development' : 'production', devtool: __DEV__ ? 'cheap-module-eval-source-map' : 'source-map', @@ -62,17 +71,25 @@ module.exports = { ], module: { rules: [ + { + test: /\.worker\.js$/, + use: [ + { + loader: 'worker-loader', + options: { + inline: 'no-fallback', + }, + }, + { + loader: 'babel-loader', + options: babelOptions, + }, + ], + }, { test: /\.js$/, loader: 'babel-loader', - options: { - configFile: resolve( - __dirname, - '..', - 'react-devtools-shared', - 'babel.config.js', - ), - }, + options: babelOptions, }, { test: /\.css$/, diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index a97422cedf86d..44327c8566f74 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -32,7 +32,7 @@ "devtools_page": "main.html", - "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", + "content_security_policy": "script-src 'self' 'unsafe-eval' blob:; object-src 'self'", "web_accessible_resources": [ "main.html", "panel.html", diff --git a/packages/react-devtools-extensions/package.json b/packages/react-devtools-extensions/package.json index da473f7613a68..d18f205c7c8a7 100644 --- a/packages/react-devtools-extensions/package.json +++ b/packages/react-devtools-extensions/package.json @@ -37,6 +37,7 @@ "chrome-launch": "^1.1.4", "crx": "^5.0.0", "css-loader": "^1.0.1", + "file-loader": "^6.1.0", "firefox-profile": "^1.0.2", "fs-extra": "^4.0.2", "jest-fetch-mock": "^3.0.3", @@ -55,7 +56,8 @@ "web-ext": "^3.0.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", - "webpack-dev-server": "^3.10.3" + "webpack-dev-server": "^3.10.3", + "worker-loader": "^3.0.3" }, "dependencies": { "web-ext": "^4" diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index 94a12ebdd1118..45ea0c3ab767d 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -19,6 +19,15 @@ const DEVTOOLS_VERSION = getVersionString(); const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss'; +const babelOptions = { + configFile: resolve( + __dirname, + '..', + 'react-devtools-shared', + 'babel.config.js', + ), +}; + module.exports = { mode: __DEV__ ? 'development' : 'production', devtool: __DEV__ ? 'cheap-module-eval-source-map' : false, @@ -81,17 +90,25 @@ module.exports = { ], rules: [ + { + test: /\.worker\.js$/, + use: [ + { + loader: 'worker-loader', + options: { + inline: 'no-fallback', + }, + }, + { + loader: 'babel-loader', + options: babelOptions, + }, + ], + }, { test: /\.js$/, loader: 'babel-loader', - options: { - configFile: resolve( - __dirname, - '..', - 'react-devtools-shared', - 'babel.config.js', - ), - }, + options: babelOptions, }, { test: /\.css$/, diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index a661fdf839957..21efaf2bbd9a3 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -34,10 +34,12 @@ "babel-loader": "^8.0.4", "cross-env": "^3.1.4", "css-loader": "^1.0.1", + "file-loader": "^6.1.0", "raw-loader": "^3.1.0", "style-loader": "^0.23.1", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", - "webpack-dev-server": "^3.10.3" + "webpack-dev-server": "^3.10.3", + "worker-loader": "^3.0.3" } } diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index 7011e7151c760..0397d4358c567 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -16,6 +16,15 @@ const __DEV__ = NODE_ENV === 'development'; const DEVTOOLS_VERSION = getVersionString(); +const babelOptions = { + configFile: resolve( + __dirname, + '..', + 'react-devtools-shared', + 'babel.config.js', + ), +}; + module.exports = { mode: __DEV__ ? 'development' : 'production', devtool: __DEV__ ? 'eval-cheap-source-map' : 'source-map', @@ -65,17 +74,25 @@ module.exports = { ], module: { rules: [ + { + test: /\.worker\.js$/, + use: [ + { + loader: 'worker-loader', + options: { + inline: 'no-fallback', + }, + }, + { + loader: 'babel-loader', + options: babelOptions, + }, + ], + }, { test: /\.js$/, loader: 'babel-loader', - options: { - configFile: resolve( - __dirname, - '..', - 'react-devtools-shared', - 'babel.config.js', - ), - }, + options: babelOptions, }, { test: /\.css$/, diff --git a/packages/react-devtools-scheduling-profiler/README.md b/packages/react-devtools-scheduling-profiler/README.md index 5ce54a3d7ea20..457bec25efcf2 100644 --- a/packages/react-devtools-scheduling-profiler/README.md +++ b/packages/react-devtools-scheduling-profiler/README.md @@ -1,15 +1,3 @@ -# Experimental React Concurrent Mode Profiler +# React Concurrent Mode Profiler -https://react-devtools-scheduling-profiler.vercel.app/ - -## Setting up continuous deployment with CircleCI and Vercel - -These instructions are intended for internal use, but may be useful if you are setting up a custom production deployment of the scheduling profiler. - -1. Create a Vercel token at https://vercel.com/account/tokens. -2. Configure CircleCI: - 1. In CircleCI, navigate to the repository's Project Settings. - 2. In the Advanced tab, ensure that "Pass secrets to builds from forked pull requests" is set to false. - 3. In the Environment Variables tab, add the Vercel token as a new `SCHEDULING_PROFILER_DEPLOY_VERCEL_TOKEN` environment variable. - -The Vercel project will be created when the deploy job runs. +This package contains the new/experimental "scheduling profiler" for React 18. This profiler exists as its own project because it was initially deployed as a standalone app. It has since been moved into the DevTools Profiler under the "Scheduling" tab. This package will likely eventually be moved into `react-devtools-shared`. \ No newline at end of file diff --git a/packages/react-devtools-scheduling-profiler/buildUtils.js b/packages/react-devtools-scheduling-profiler/buildUtils.js deleted file mode 100644 index b0971c4861112..0000000000000 --- a/packages/react-devtools-scheduling-profiler/buildUtils.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const {execSync} = require('child_process'); -const {readFileSync} = require('fs'); -const {resolve} = require('path'); - -function getGitCommit() { - try { - return execSync('git show -s --format=%h') - .toString() - .trim(); - } catch (error) { - // Mozilla runs this command from a git archive. - // In that context, there is no Git revision. - return null; - } -} - -function getVersionString() { - const packageVersion = JSON.parse( - readFileSync(resolve(__dirname, './package.json')), - ).version; - - const commit = getGitCommit(); - - return `${packageVersion}-${commit}`; -} - -module.exports = { - getVersionString, -}; diff --git a/packages/react-devtools-scheduling-profiler/package.json b/packages/react-devtools-scheduling-profiler/package.json index 705261279e304..e3b87c664fafd 100644 --- a/packages/react-devtools-scheduling-profiler/package.json +++ b/packages/react-devtools-scheduling-profiler/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "react-devtools-scheduling-profiler", - "version": "0.0.0", + "version": "4.14.0", "license": "MIT", "scripts": { "build": "cross-env NODE_ENV=production cross-env TARGET=remote webpack --config webpack.config.js", diff --git a/packages/react-devtools-scheduling-profiler/src/App.css b/packages/react-devtools-scheduling-profiler/src/App.css deleted file mode 100644 index 1ea3d75fcc595..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/App.css +++ /dev/null @@ -1,19 +0,0 @@ -.DevTools { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - background-color: var(--color-background); - color: var(--color-text); -} - -.TabContent { - flex: 1 1 100%; - overflow: auto; - -webkit-app-region: no-drag; -} - -.DevTools, .DevTools * { - box-sizing: border-box; - -webkit-font-smoothing: var(--font-smoothing); -} diff --git a/packages/react-devtools-scheduling-profiler/src/App.js b/packages/react-devtools-scheduling-profiler/src/App.js deleted file mode 100644 index 9a27253b6c032..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/App.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -// Reach styles need to come before any component styles. -// This makes overriding the styles simpler. -import '@reach/menu-button/styles.css'; -import '@reach/tooltip/styles.css'; - -import * as React from 'react'; - -import {SchedulingProfiler} from './SchedulingProfiler'; -import {useBrowserTheme, useDisplayDensity} from './hooks'; - -import styles from './App.css'; -import 'react-devtools-shared/src/devtools/views/root.css'; - -export default function App() { - useBrowserTheme(); - useDisplayDensity(); - - return ( -
-
- -
-
- ); -} diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.css b/packages/react-devtools-scheduling-profiler/src/CanvasPage.css index e5d238a0d9d2c..8c7633a1b8977 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.css +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.css @@ -1,7 +1,7 @@ .CanvasPage { position: absolute; - top: 0.5rem; - bottom: 0.5rem; - left: 0.5rem; - right: 0.5rem; + top: 0; + bottom: 0; + left: 0; + right: 0; } diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index 9b20cd2fb80d7..cd6a0d072f501 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -52,9 +52,9 @@ import { import {COLORS} from './content-views/constants'; import EventTooltip from './EventTooltip'; -import ContextMenu from './context/ContextMenu'; -import ContextMenuItem from './context/ContextMenuItem'; -import useContextMenu from './context/useContextMenu'; +import ContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenu'; +import ContextMenuItem from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem'; +import useContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/useContextMenu'; import {getBatchRange} from './utils/getBatchRange'; import styles from './CanvasPage.css'; @@ -243,6 +243,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { defaultFrame, reactMeasuresHorizontalPanAndZoomView, flamechartHorizontalPanAndZoomView, + canvasRef, ); const rootView = new View( diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css index f721295b2f2ba..b6503b7338c35 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css @@ -6,9 +6,10 @@ padding: 0.25rem; user-select: none; pointer-events: none; - background-color: #ffffff; - border: 1px solid #ccc; - box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); + background-color: var(--color-tooltip-background); + border: 1px solid var(border); + box-shadow: 1px 1px 2px var(--color-shadow); + color: var(--color-tooltip-text); font-size: 11px; } @@ -26,7 +27,7 @@ } .DetailsGridLabel { - color: #666; + color: var(--color-dim); text-align: right; } @@ -56,14 +57,14 @@ line-height: 1.5; -webkit-mask-image: linear-gradient( 180deg, - #fff, - #fff 5em, + var(--color-tooltip-background), + var(--color-tooltip-background) 5em, transparent ); mask-image: linear-gradient( 180deg, - #fff, - #fff 5em, + var(--color-tooltip-background), + var(--color-tooltip-background) 5em, transparent ); white-space: pre; diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.css b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.css index e16279192b83e..b5666e6bfca1e 100644 --- a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.css +++ b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.css @@ -1,21 +1,5 @@ -.SchedulingProfiler { - width: 100%; - height: 100%; - position: relative; - display: flex; - flex-direction: column; - font-family: var(--font-family-sans); - font-size: var(--font-size-sans-normal); - background-color: var(--color-background); - color: var(--color-text); -} - -.SchedulingProfiler, .SchedulingProfiler * { - box-sizing: border-box; - -webkit-font-smoothing: var(--font-smoothing); -} - .Content { + width: 100%; position: relative; flex: 1 1 auto; display: flex; @@ -51,58 +35,3 @@ font-size: var(--font-size-sans-large); margin-bottom: 0.5rem; } - -.Toolbar { - height: 2.25rem; - padding: 0 0.25rem; - flex: 0 0 auto; - display: flex; - align-items: center; - border-bottom: 1px solid var(--color-border); -} - -.VRule { - height: 20px; - width: 1px; - border-left: 1px solid var(--color-border); - padding-left: 0.25rem; - margin-left: 0.25rem; -} - -.Spacer { - flex: 1; -} - -.Link { - color: var(--color-button); -} - -.ScreenshotWrapper { - max-width: 30rem; - padding: 0 1rem; - margin-bottom: 2rem; -} - -.Screenshot { - width: 100%; - border-radius: 0.4em; - border: 2px solid var(--color-border); -} - -.AppName { - font-size: var(--font-size-sans-large); - margin-right: 0.5rem; - user-select: none; -} - -@media screen and (max-width: 350px) { - .AppName { - display: none; - } -} - -@media screen and (max-height: 600px) { - .ScreenshotWrapper { - display: none; - } -} \ No newline at end of file diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js index 2f232b6bbe183..a23f528258c00 100644 --- a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js +++ b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js @@ -7,95 +7,65 @@ * @flow */ -import type {Resource} from 'react-devtools-shared/src/devtools/cache'; -import type {ReactProfilerData} from './types'; -import type {ImportWorkerOutputData} from './import-worker/import.worker'; +import type {DataResource} from './createDataResourceFromImportedFile'; import * as React from 'react'; -import {Suspense, useCallback, useState} from 'react'; -import {createResource} from 'react-devtools-shared/src/devtools/cache'; -import ReactLogo from 'react-devtools-shared/src/devtools/views/ReactLogo'; +import { + Suspense, + useCallback, + useContext, + useDeferredValue, + useLayoutEffect, + useState, +} from 'react'; +import {SettingsContext} from 'react-devtools-shared/src/devtools/views/Settings/SettingsContext'; +import {updateColorsToMatchTheme} from './content-views/constants'; +import {SchedulingProfilerContext} from './SchedulingProfilerContext'; import ImportButton from './ImportButton'; import CanvasPage from './CanvasPage'; -import ImportWorker from './import-worker/import.worker'; -import profilerBrowser from './assets/profilerBrowser.png'; import styles from './SchedulingProfiler.css'; -type DataResource = Resource; - -function createDataResourceFromImportedFile(file: File): DataResource { - return createResource( - () => { - return new Promise((resolve, reject) => { - const worker: Worker = new (ImportWorker: any)(); - - worker.onmessage = function(event) { - const data = ((event.data: any): ImportWorkerOutputData); - switch (data.status) { - case 'SUCCESS': - resolve(data.processedData); - break; - case 'INVALID_PROFILE_ERROR': - resolve(data.error); - break; - case 'UNEXPECTED_ERROR': - reject(data.error); - break; - } - worker.terminate(); - }; - - worker.postMessage({file}); - }); - }, - () => file, - {useWeakMap: true}, - ); -} - export function SchedulingProfiler(_: {||}) { - const [dataResource, setDataResource] = useState(null); + const {importSchedulingProfilerData, schedulingProfilerData} = useContext( + SchedulingProfilerContext, + ); - const handleFileSelect = useCallback((file: File) => { - setDataResource(createDataResourceFromImportedFile(file)); - }, []); + // HACK: Canvas rendering uses an imperative API, + // but DevTools colors are stored in CSS variables (see root.css and SettingsContext). + // When the theme changes, we need to trigger update the imperative colors and re-draw the Canvas. + const {theme} = useContext(SettingsContext); + // HACK: SettingsContext also uses a useLayoutEffect to update styles; + // make sure the theme context in SettingsContext updates before this code. + const deferredTheme = useDeferredValue(theme); + // HACK: Schedule a re-render of the Canvas once colors have been updated. + // The easiest way to guarangee this happens is to recreate the inner Canvas component. + const [key, setKey] = useState(theme); + useLayoutEffect(() => { + updateColorsToMatchTheme(); + setKey(deferredTheme); + }, [deferredTheme]); return ( -
-
- - Concurrent Mode Profiler -
- -
-
-
- {dataResource ? ( - }> - - - ) : ( - - )} -
+
+ {schedulingProfilerData ? ( + }> + + + ) : ( + + )}
); } const Welcome = ({onFileSelect}: {|onFileSelect: (file: File) => void|}) => (
-
- Profiler screenshot -
Welcome!
Click the import button diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js b/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js new file mode 100644 index 0000000000000..11f66b7f16dc4 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; +import {createContext, useCallback, useContext, useMemo, useState} from 'react'; +import {unstable_batchedUpdates as batchedUpdates} from 'react-dom'; +import createDataResourceFromImportedFile from './createDataResourceFromImportedFile'; + +import type {DataResource} from './createDataResourceFromImportedFile'; + +export type Context = {| + importSchedulingProfilerData: (file: File) => void, + schedulingProfilerData: DataResource | null, +|}; + +const SchedulingProfilerContext = createContext( + ((null: any): Context), +); +SchedulingProfilerContext.displayName = 'SchedulingProfilerContext'; + +type State = {||}; + +type Props = {| + children: React$Node, +|}; + +function SchedulingProfilerContextController({children}: Props) { + const [ + schedulingProfilerData, + setSchedulingProfilerData, + ] = useState(null); + + const importSchedulingProfilerData = useCallback((file: File) => { + setSchedulingProfilerData(createDataResourceFromImportedFile(file)); + }, []); + + // TODO (scheduling profiler) Move more stuff into this context, like scroll and zoom. + + const value = useMemo( + () => ({ + importSchedulingProfilerData, + schedulingProfilerData, + }), + [importSchedulingProfilerData, schedulingProfilerData], + ); + + return ( + + {children} + + ); +} + +export {SchedulingProfilerContext, SchedulingProfilerContextController}; diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerFeatureFlags.js b/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerFeatureFlags.js deleted file mode 100644 index 7558576c31781..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerFeatureFlags.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -export const enableDarkMode = false; diff --git a/packages/react-devtools-scheduling-profiler/src/assets/logo.svg b/packages/react-devtools-scheduling-profiler/src/assets/logo.svg deleted file mode 100644 index 2e5df0d3ab2f2..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/assets/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/react-devtools-scheduling-profiler/src/assets/profilerBrowser.png b/packages/react-devtools-scheduling-profiler/src/assets/profilerBrowser.png deleted file mode 100644 index b0282be2f6828..0000000000000 Binary files a/packages/react-devtools-scheduling-profiler/src/assets/profilerBrowser.png and /dev/null differ diff --git a/packages/react-devtools-scheduling-profiler/src/assets/reactlogo.svg b/packages/react-devtools-scheduling-profiler/src/assets/reactlogo.svg deleted file mode 100644 index 6b60c1042f58d..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/assets/reactlogo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js index 00b022899f657..8b79a30916fed 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js @@ -203,7 +203,7 @@ class FlamechartStackLayerView extends View { ); if (trimmedName !== null) { - context.fillStyle = COLORS.PRIORITY_LABEL; + context.fillStyle = COLORS.FLAME_GRAPH_LABEL; // Prevent text from being drawn outside `viewableArea` const textOverflowsViewableArea = !rectEqualToRect( diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js index 7b0fb87260769..baaedebc64ca7 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -41,34 +41,129 @@ export const FLAMECHART_FONT_SIZE = 10; export const FLAMECHART_FRAME_HEIGHT = 16; export const FLAMECHART_TEXT_PADDING = 3; -export const COLORS = Object.freeze({ - BACKGROUND: '#ffffff', - PRIORITY_BACKGROUND: '#ededf0', - PRIORITY_BORDER: '#d7d7db', - PRIORITY_LABEL: '#272727', - USER_TIMING: '#c9cacd', - USER_TIMING_HOVER: '#93959a', - REACT_IDLE: '#edf6ff', - REACT_IDLE_SELECTED: '#EDF6FF', - REACT_IDLE_HOVER: '#EDF6FF', - REACT_RENDER: '#9fc3f3', - REACT_RENDER_SELECTED: '#64A9F5', - REACT_RENDER_HOVER: '#2683E2', - REACT_COMMIT: '#ff718e', - REACT_COMMIT_SELECTED: '#FF5277', - REACT_COMMIT_HOVER: '#ed0030', - REACT_LAYOUT_EFFECTS: '#c88ff0', - REACT_LAYOUT_EFFECTS_SELECTED: '#934FC1', - REACT_LAYOUT_EFFECTS_HOVER: '#601593', - REACT_PASSIVE_EFFECTS: '#c88ff0', - REACT_PASSIVE_EFFECTS_SELECTED: '#934FC1', - REACT_PASSIVE_EFFECTS_HOVER: '#601593', - REACT_SCHEDULE: '#9fc3f3', - REACT_SCHEDULE_HOVER: '#2683E2', - REACT_SCHEDULE_CASCADING: '#ff718e', - REACT_SCHEDULE_CASCADING_HOVER: '#ed0030', - REACT_SUSPEND: '#a6e59f', - REACT_SUSPEND_HOVER: '#13bc00', - REACT_WORK_BORDER: '#ffffff', - TIME_MARKER_LABEL: '#18212b', -}); +// TODO Replace this with "export let" vars +export let COLORS = { + BACKGROUND: '', + PRIORITY_BACKGROUND: '', + PRIORITY_BORDER: '', + PRIORITY_LABEL: '', + FLAME_GRAPH_LABEL: '', + USER_TIMING: '', + USER_TIMING_HOVER: '', + REACT_IDLE: '', + REACT_IDLE_SELECTED: '', + REACT_IDLE_HOVER: '', + REACT_RENDER: '', + REACT_RENDER_SELECTED: '', + REACT_RENDER_HOVER: '', + REACT_COMMIT: '', + REACT_COMMIT_SELECTED: '', + REACT_COMMIT_HOVER: '', + REACT_LAYOUT_EFFECTS: '', + REACT_LAYOUT_EFFECTS_SELECTED: '', + REACT_LAYOUT_EFFECTS_HOVER: '', + REACT_PASSIVE_EFFECTS: '', + REACT_PASSIVE_EFFECTS_SELECTED: '', + REACT_PASSIVE_EFFECTS_HOVER: '', + REACT_RESIZE_BAR: '', + REACT_SCHEDULE: '', + REACT_SCHEDULE_HOVER: '', + REACT_SCHEDULE_CASCADING: '', + REACT_SCHEDULE_CASCADING_HOVER: '', + REACT_SUSPEND: '', + REACT_SUSPEND_HOVER: '', + REACT_WORK_BORDER: '', + TIME_MARKER_LABEL: '', +}; + +export function updateColorsToMatchTheme(): void { + const computedStyle = getComputedStyle((document.body: any)); + + COLORS = { + BACKGROUND: computedStyle.getPropertyValue('--color-background'), + PRIORITY_BACKGROUND: computedStyle.getPropertyValue( + '--color-scheduling-profiler-priority-background', + ), + PRIORITY_BORDER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-priority-border', + ), + PRIORITY_LABEL: computedStyle.getPropertyValue('--color-text'), + FLAME_GRAPH_LABEL: computedStyle.getPropertyValue( + '--color-scheduling-profiler-flame-graph-label', + ), + USER_TIMING: computedStyle.getPropertyValue( + '--color-scheduling-profiler-user-timing', + ), + USER_TIMING_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-user-timing-hover', + ), + REACT_IDLE: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-idle', + ), + REACT_IDLE_SELECTED: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-idle-selected', + ), + REACT_IDLE_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-idle-hover', + ), + REACT_RENDER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-render', + ), + REACT_RENDER_SELECTED: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-render-selected', + ), + REACT_RENDER_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-render-hover', + ), + REACT_COMMIT: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-commit', + ), + REACT_COMMIT_SELECTED: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-commit-selected', + ), + REACT_COMMIT_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-commit-hover', + ), + REACT_LAYOUT_EFFECTS: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-layout-effects', + ), + REACT_LAYOUT_EFFECTS_SELECTED: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-layout-effects-selected', + ), + REACT_LAYOUT_EFFECTS_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-layout-effects-hover', + ), + REACT_PASSIVE_EFFECTS: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-passive-effects', + ), + REACT_PASSIVE_EFFECTS_SELECTED: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-passive-effects-selected', + ), + REACT_PASSIVE_EFFECTS_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-passive-effects-hover', + ), + REACT_RESIZE_BAR: computedStyle.getPropertyValue('--color-resize-bar'), + REACT_SCHEDULE: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-schedule', + ), + REACT_SCHEDULE_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-schedule-hover', + ), + REACT_SCHEDULE_CASCADING: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-schedule-cascading', + ), + REACT_SCHEDULE_CASCADING_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-schedule-cascading-hover', + ), + REACT_SUSPEND: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspend', + ), + REACT_SUSPEND_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspend-hover', + ), + REACT_WORK_BORDER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-work-border', + ), + TIME_MARKER_LABEL: computedStyle.getPropertyValue('--color-text'), + }; +} diff --git a/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.css b/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.css deleted file mode 100644 index 60848641f4949..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.css +++ /dev/null @@ -1,10 +0,0 @@ -.ContextMenu { - position: absolute; - border-radius: 0.125rem; - background-color: #ffffff; - border: 1px solid #ccc; - box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); - font-size: 11px; - overflow: hidden; - z-index: 10000002; -} diff --git a/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.js b/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.js deleted file mode 100644 index 8b09ef1510dcf..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.js +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {RegistryContextType} from './Contexts'; - -import * as React from 'react'; -import {useContext, useEffect, useLayoutEffect, useRef, useState} from 'react'; -import {createPortal} from 'react-dom'; -import {RegistryContext} from './Contexts'; - -import styles from './ContextMenu.css'; - -function repositionToFit(element: HTMLElement, pageX: number, pageY: number) { - const ownerWindow = element.ownerDocument.defaultView; - if (element !== null) { - if (pageY + element.offsetHeight >= ownerWindow.innerHeight) { - if (pageY - element.offsetHeight > 0) { - element.style.top = `${pageY - element.offsetHeight}px`; - } else { - element.style.top = '0px'; - } - } else { - element.style.top = `${pageY}px`; - } - - if (pageX + element.offsetWidth >= ownerWindow.innerWidth) { - if (pageX - element.offsetWidth > 0) { - element.style.left = `${pageX - element.offsetWidth}px`; - } else { - element.style.left = '0px'; - } - } else { - element.style.left = `${pageX}px`; - } - } -} - -const HIDDEN_STATE = { - data: null, - isVisible: false, - pageX: 0, - pageY: 0, -}; - -type Props = {| - children: (data: Object) => React$Node, - id: string, -|}; - -export default function ContextMenu({children, id}: Props) { - const {hideMenu, registerMenu} = useContext( - RegistryContext, - ); - - const [state, setState] = useState(HIDDEN_STATE); - - const bodyAccessorRef = useRef(null); - const containerRef = useRef(null); - const menuRef = useRef(null); - - useEffect(() => { - if (!bodyAccessorRef.current) { - return; - } - const ownerDocument = bodyAccessorRef.current.ownerDocument; - containerRef.current = ownerDocument.createElement('div'); - if (ownerDocument.body) { - ownerDocument.body.appendChild(containerRef.current); - } - return () => { - if (ownerDocument.body && containerRef.current) { - ownerDocument.body.removeChild(containerRef.current); - } - }; - }, [bodyAccessorRef, containerRef]); - - useEffect(() => { - const showMenuFn = ({data, pageX, pageY}) => { - setState({data, isVisible: true, pageX, pageY}); - }; - const hideMenuFn = () => setState(HIDDEN_STATE); - return registerMenu(id, showMenuFn, hideMenuFn); - }, [id]); - - useLayoutEffect(() => { - if (!state.isVisible || !containerRef.current) { - return; - } - - const menu = menuRef.current; - if (!menu) { - return; - } - - const hideUnlessContains: MouseEventHandler & - TouchEventHandler & - KeyboardEventHandler = event => { - if (event.target instanceof HTMLElement && !menu.contains(event.target)) { - hideMenu(); - } - }; - - const ownerDocument = containerRef.current.ownerDocument; - ownerDocument.addEventListener('mousedown', hideUnlessContains); - ownerDocument.addEventListener('touchstart', hideUnlessContains); - ownerDocument.addEventListener('keydown', hideUnlessContains); - - const ownerWindow = ownerDocument.defaultView; - ownerWindow.addEventListener('resize', hideMenu); - - repositionToFit(menu, state.pageX, state.pageY); - - return () => { - ownerDocument.removeEventListener('mousedown', hideUnlessContains); - ownerDocument.removeEventListener('touchstart', hideUnlessContains); - ownerDocument.removeEventListener('keydown', hideUnlessContains); - - ownerWindow.removeEventListener('resize', hideMenu); - }; - }, [state]); - - if (!state.isVisible) { - return
; - } else { - const container = containerRef.current; - if (container !== null) { - return createPortal( -
- {children(state.data)} -
, - container, - ); - } else { - return null; - } - } -} diff --git a/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.css b/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.css deleted file mode 100644 index 19fd8284a47cb..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.css +++ /dev/null @@ -1,20 +0,0 @@ -.ContextMenuItem { - display: flex; - align-items: center; - color: #333; - padding: 0.5rem 0.75rem; - cursor: default; - border-top: 1px solid #ccc; -} -.ContextMenuItem:first-of-type { - border-top: none; -} -.ContextMenuItem:hover, -.ContextMenuItem:focus { - outline: 0; - background-color: rgba(0, 136, 250, 0.1); -} -.ContextMenuItem:active { - background-color: #0088fa; - color: #fff; -} diff --git a/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.js b/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.js deleted file mode 100644 index 5750bd90cd18f..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {RegistryContextType} from './Contexts'; - -import * as React from 'react'; -import {useContext} from 'react'; -import {RegistryContext} from './Contexts'; - -import styles from './ContextMenuItem.css'; - -type Props = {| - children: React$Node, - onClick: () => void, - title: string, -|}; - -export default function ContextMenuItem({children, onClick, title}: Props) { - const {hideMenu} = useContext(RegistryContext); - - const handleClick: MouseEventHandler = event => { - onClick(); - hideMenu(); - }; - - return ( -
- {children} -
- ); -} diff --git a/packages/react-devtools-scheduling-profiler/src/context/Contexts.js b/packages/react-devtools-scheduling-profiler/src/context/Contexts.js deleted file mode 100644 index 46c742e06d0b8..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/Contexts.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import {createContext} from 'react'; - -export type ShowFn = ({|data: Object, pageX: number, pageY: number|}) => void; -export type HideFn = () => void; -export type OnChangeFn = boolean => void; - -const idToShowFnMap = new Map(); -const idToHideFnMap = new Map(); - -let currentHideFn: ?HideFn = null; -let currentOnChange: ?OnChangeFn = null; - -function hideMenu() { - if (typeof currentHideFn === 'function') { - currentHideFn(); - - if (typeof currentOnChange === 'function') { - currentOnChange(false); - } - } - - currentHideFn = null; - currentOnChange = null; -} - -function showMenu({ - data, - id, - onChange, - pageX, - pageY, -}: {| - data: Object, - id: string, - onChange?: OnChangeFn, - pageX: number, - pageY: number, -|}) { - const showFn = idToShowFnMap.get(id); - if (typeof showFn === 'function') { - // Prevent open menus from being left hanging. - hideMenu(); - - currentHideFn = idToHideFnMap.get(id); - showFn({data, pageX, pageY}); - - if (typeof onChange === 'function') { - currentOnChange = onChange; - onChange(true); - } - } -} - -function registerMenu(id: string, showFn: ShowFn, hideFn: HideFn) { - if (idToShowFnMap.has(id)) { - throw Error(`Context menu with id "${id}" already registered.`); - } - - idToShowFnMap.set(id, showFn); - idToHideFnMap.set(id, hideFn); - - return function unregisterMenu() { - idToShowFnMap.delete(id); - idToHideFnMap.delete(id); - }; -} - -export type RegistryContextType = {| - hideMenu: typeof hideMenu, - showMenu: typeof showMenu, - registerMenu: typeof registerMenu, -|}; - -export const RegistryContext = createContext({ - hideMenu, - showMenu, - registerMenu, -}); diff --git a/packages/react-devtools-scheduling-profiler/src/context/index.js b/packages/react-devtools-scheduling-profiler/src/context/index.js deleted file mode 100644 index c903d4f886409..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import {RegistryContext} from './Contexts'; -import ContextMenu from './ContextMenu'; -import ContextMenuItem from './ContextMenuItem'; -import useContextMenu from './useContextMenu'; - -export {RegistryContext, ContextMenu, ContextMenuItem, useContextMenu}; diff --git a/packages/react-devtools-scheduling-profiler/src/context/useContextMenu.js b/packages/react-devtools-scheduling-profiler/src/context/useContextMenu.js deleted file mode 100644 index 467c138f62d87..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/useContextMenu.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {OnChangeFn, RegistryContextType} from './Contexts'; - -import {useContext, useEffect} from 'react'; -import {RegistryContext} from './Contexts'; - -export default function useContextMenu({ - data, - id, - onChange, - ref, -}: {| - data: T, - id: string, - onChange: OnChangeFn, - ref: {+current: HTMLElement | null}, -|}) { - const {showMenu} = useContext(RegistryContext); - - useEffect(() => { - if (ref.current !== null) { - const handleContextMenu = (event: MouseEvent | TouchEvent) => { - event.preventDefault(); - event.stopPropagation(); - - const pageX = - (event: any).pageX || - (event.touches && (event: any).touches[0].pageX); - const pageY = - (event: any).pageY || - (event.touches && (event: any).touches[0].pageY); - - showMenu({data, id, onChange, pageX, pageY}); - }; - - const trigger = ref.current; - trigger.addEventListener('contextmenu', handleContextMenu); - - return () => { - trigger.removeEventListener('contextmenu', handleContextMenu); - }; - } - }, [data, id, showMenu]); -} diff --git a/packages/react-devtools-scheduling-profiler/src/createDataResourceFromImportedFile.js b/packages/react-devtools-scheduling-profiler/src/createDataResourceFromImportedFile.js new file mode 100644 index 0000000000000..5d5d7939d4cab --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/createDataResourceFromImportedFile.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {createResource} from 'react-devtools-shared/src/devtools/cache'; +import ImportWorker from './import-worker/import.worker'; + +import type {Resource} from 'react-devtools-shared/src/devtools/cache'; +import type {ReactProfilerData} from './types'; +import type {ImportWorkerOutputData} from './import-worker/import.worker'; + +export type DataResource = Resource; + +export default function createDataResourceFromImportedFile( + file: File, +): DataResource { + return createResource( + () => { + return new Promise((resolve, reject) => { + const worker: Worker = new (ImportWorker: any)(); + worker.onmessage = function(event) { + const data = ((event.data: any): ImportWorkerOutputData); + switch (data.status) { + case 'SUCCESS': + resolve(data.processedData); + break; + case 'INVALID_PROFILE_ERROR': + resolve(data.error); + break; + case 'UNEXPECTED_ERROR': + reject(data.error); + break; + } + worker.terminate(); + }; + + worker.postMessage({file}); + }); + }, + () => file, + {useWeakMap: true}, + ); +} diff --git a/packages/react-devtools-scheduling-profiler/src/hooks.js b/packages/react-devtools-scheduling-profiler/src/hooks.js deleted file mode 100644 index a9692010bed2c..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/hooks.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import { - // $FlowFixMe - unstable_createMutableSource as createMutableSource, - useLayoutEffect, - // $FlowFixMe - unstable_useMutableSource as useMutableSource, -} from 'react'; - -import { - updateDisplayDensity, - updateThemeVariables, -} from 'react-devtools-shared/src/devtools/views/Settings/SettingsContext'; -import {enableDarkMode} from './SchedulingProfilerFeatureFlags'; - -export type BrowserTheme = 'dark' | 'light'; - -const DARK_MODE_QUERY = '(prefers-color-scheme: dark)'; - -const getSnapshot = window => - window.matchMedia(DARK_MODE_QUERY).matches ? 'dark' : 'light'; - -const darkModeMutableSource = createMutableSource( - window, - () => window.matchMedia(DARK_MODE_QUERY).matches, -); - -const subscribe = (window, callback) => { - const mediaQueryList = window.matchMedia(DARK_MODE_QUERY); - mediaQueryList.addEventListener('change', callback); - return () => { - mediaQueryList.removeEventListener('change', callback); - }; -}; - -export function useBrowserTheme(): void { - const theme = useMutableSource(darkModeMutableSource, getSnapshot, subscribe); - - useLayoutEffect(() => { - const documentElements = [((document.documentElement: any): HTMLElement)]; - if (enableDarkMode) { - switch (theme) { - case 'light': - updateThemeVariables('light', documentElements); - break; - case 'dark': - updateThemeVariables('dark', documentElements); - break; - default: - throw Error(`Unsupported theme value "${theme}"`); - } - } else { - updateThemeVariables('light', documentElements); - } - }, [theme]); -} - -export function useDisplayDensity(): void { - useLayoutEffect(() => { - const documentElements = [((document.documentElement: any): HTMLElement)]; - updateDisplayDensity('comfortable', documentElements); - }, []); -} diff --git a/packages/react-devtools-scheduling-profiler/src/index.css b/packages/react-devtools-scheduling-profiler/src/index.css deleted file mode 100644 index 0e798eef50713..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/index.css +++ /dev/null @@ -1,21 +0,0 @@ -html { - height: 100%; -} - -body { - height: 100%; - margin: 0; - font-family: var(--font-family-sans); - font-size: var(--font-size-sans-normal); - background-color: var(--color-background); - color: var(--color-text); -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} - -.Container { - height: 100%; -} \ No newline at end of file diff --git a/packages/react-devtools-scheduling-profiler/src/index.js b/packages/react-devtools-scheduling-profiler/src/index.js deleted file mode 100644 index b10a2b07efd6f..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/index.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import 'regenerator-runtime/runtime'; - -import * as React from 'react'; -// $FlowFixMe Flow does not yet know about createRoot() -import {createRoot} from 'react-dom'; -import nullthrows from 'nullthrows'; -import App from './App'; - -import styles from './index.css'; - -const container = document.createElement('div'); -container.className = styles.Container; -container.id = 'root'; - -const body = nullthrows(document.body, 'Expect document.body to exist'); -body.appendChild(container); - -createRoot(container).render( - - - , -); diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js index 314be68e2888b..c69b982c47cdc 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js @@ -15,6 +15,7 @@ import type { } from './useCanvasInteraction'; import type {Rect, Size} from './geometry'; +import {COLORS} from '../content-views/constants'; import nullthrows from 'nullthrows'; import {Surface} from './Surface'; import {View} from './View'; @@ -38,14 +39,11 @@ type LayoutState = $ReadOnly<{| |}>; function getColorForBarState(state: ResizeBarState): string { - // Colors obtained from Firefox Profiler switch (state) { case 'normal': - return '#ccc'; case 'hovered': - return '#bbb'; case 'dragging': - return '#aaa'; + return COLORS.REACT_RESIZE_BAR; } throw new Error(`Unknown resize bar state ${state}`); } @@ -131,6 +129,7 @@ class ResizeBar extends View { } export class ResizableSplitView extends View { + _canvasRef: {current: HTMLCanvasElement | null}; _resizingState: ResizingState | null = null; _layoutState: LayoutState; @@ -139,9 +138,12 @@ export class ResizableSplitView extends View { frame: Rect, topSubview: View, bottomSubview: View, + canvasRef: {current: HTMLCanvasElement | null}, ) { super(surface, frame, noopLayout); + this._canvasRef = canvasRef; + this.addSubview(topSubview); this.addSubview(new ResizeBar(surface, frame)); this.addSubview(bottomSubview); @@ -279,6 +281,18 @@ export class ResizableSplitView extends View { } _handleMouseMove(interaction: MouseMoveInteraction) { + const cursorLocation = interaction.payload.location; + const resizeBarFrame = this._getResizeBar().frame; + + const canvas = this._canvasRef.current; + if (canvas !== null) { + if (rectContainsPoint(cursorLocation, resizeBarFrame)) { + canvas.style.cursor = 'ns-resize'; + } else { + canvas.style.cursor = 'default'; + } + } + const {_resizingState} = this; if (_resizingState) { this._resizingState = { diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js b/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js index b08d9bbbbb466..3b374aebbee79 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js @@ -175,15 +175,16 @@ export function useCanvasInteraction( return false; }; - document.addEventListener('mousemove', onDocumentMouseMove); - document.addEventListener('mouseup', onDocumentMouseUp); + const ownerDocument = canvas.ownerDocument; + ownerDocument.addEventListener('mousemove', onDocumentMouseMove); + ownerDocument.addEventListener('mouseup', onDocumentMouseUp); canvas.addEventListener('mousedown', onCanvasMouseDown); canvas.addEventListener('wheel', onCanvasWheel); return () => { - document.removeEventListener('mousemove', onDocumentMouseMove); - document.removeEventListener('mouseup', onDocumentMouseUp); + ownerDocument.removeEventListener('mousemove', onDocumentMouseMove); + ownerDocument.removeEventListener('mouseup', onDocumentMouseUp); canvas.removeEventListener('mousedown', onCanvasMouseDown); canvas.removeEventListener('wheel', onCanvasWheel); diff --git a/packages/react-devtools-scheduling-profiler/webpack.config.js b/packages/react-devtools-scheduling-profiler/webpack.config.js index e30d4fda13db2..683e4bb547b3e 100644 --- a/packages/react-devtools-scheduling-profiler/webpack.config.js +++ b/packages/react-devtools-scheduling-profiler/webpack.config.js @@ -4,7 +4,6 @@ const {resolve} = require('path'); const {DefinePlugin} = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); -const {getVersionString} = require('./buildUtils'); const NODE_ENV = process.env.NODE_ENV; if (!NODE_ENV) { @@ -22,8 +21,6 @@ const shouldUseDevServer = TARGET === 'local'; const builtModulesDir = resolve(__dirname, '..', '..', 'build', 'node_modules'); -const DEVTOOLS_VERSION = getVersionString(); - const imageInlineSizeLimit = 10000; const babelOptions = { @@ -58,7 +55,6 @@ const config = { __PROFILE__: false, __EXPERIMENTAL__: true, __VARIANT__: false, - 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, }), new HtmlWebpackPlugin({ title: 'React Concurrent Mode Profiler', diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.css b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.css index 20af7c096f059..674346621b102 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.css +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.css @@ -4,4 +4,5 @@ border-radius: 0.25rem; overflow: hidden; z-index: 10000002; + user-select: none; } \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/Contexts.js b/packages/react-devtools-shared/src/devtools/ContextMenu/Contexts.js index 0d2e55106c89f..46c742e06d0b8 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/Contexts.js +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/Contexts.js @@ -11,33 +11,52 @@ import {createContext} from 'react'; export type ShowFn = ({|data: Object, pageX: number, pageY: number|}) => void; export type HideFn = () => void; +export type OnChangeFn = boolean => void; const idToShowFnMap = new Map(); const idToHideFnMap = new Map(); -let currentHideFn = null; +let currentHideFn: ?HideFn = null; +let currentOnChange: ?OnChangeFn = null; function hideMenu() { if (typeof currentHideFn === 'function') { currentHideFn(); + + if (typeof currentOnChange === 'function') { + currentOnChange(false); + } } + + currentHideFn = null; + currentOnChange = null; } function showMenu({ data, id, + onChange, pageX, pageY, }: {| data: Object, id: string, + onChange?: OnChangeFn, pageX: number, pageY: number, |}) { const showFn = idToShowFnMap.get(id); if (typeof showFn === 'function') { + // Prevent open menus from being left hanging. + hideMenu(); + currentHideFn = idToHideFnMap.get(id); showFn({data, pageX, pageY}); + + if (typeof onChange === 'function') { + currentOnChange = onChange; + onChange(true); + } } } @@ -56,14 +75,9 @@ function registerMenu(id: string, showFn: ShowFn, hideFn: HideFn) { } export type RegistryContextType = {| - hideMenu: () => void, - showMenu: ({| - data: Object, - id: string, - pageX: number, - pageY: number, - |}) => void, - registerMenu: (string, ShowFn, HideFn) => Function, + hideMenu: typeof hideMenu, + showMenu: typeof showMenu, + registerMenu: typeof registerMenu, |}; export const RegistryContext = createContext({ diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js b/packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js index 1c713bae73dd5..f61cde4b0b535 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js @@ -10,17 +10,19 @@ import {useContext, useEffect} from 'react'; import {RegistryContext} from './Contexts'; -import type {RegistryContextType} from './Contexts'; +import type {OnChangeFn, RegistryContextType} from './Contexts'; import type {ElementRef} from 'react'; export default function useContextMenu({ data, id, + onChange, ref, }: {| data: Object, id: string, - ref: {current: ElementRef<'div'> | null}, + onChange?: OnChangeFn, + ref: {current: ElementRef<*> | null}, |}) { const {showMenu} = useContext(RegistryContext); diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index 812ca916d8ebe..3c768c11ab97e 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -24,6 +24,7 @@ import {TreeContextController} from './Components/TreeContext'; import ViewElementSourceContext from './Components/ViewElementSourceContext'; import HookNamesContext from './Components/HookNamesContext'; import {ProfilerContextController} from './Profiler/ProfilerContext'; +import {SchedulingProfilerContextController} from 'react-devtools-scheduling-profiler/src/SchedulingProfilerContext'; import {ModalDialogContextController} from './ModalDialog'; import ReactLogo from './ReactLogo'; import UnsupportedBridgeProtocolDialog from './UnsupportedBridgeProtocolDialog'; @@ -218,36 +219,40 @@ export default function DevTools({ -
- {showTabBar && ( -
- - - {process.env.DEVTOOLS_VERSION} - -
- +
+ {showTabBar && ( +
+ + + {process.env.DEVTOOLS_VERSION} + +
+ +
+ )} + + - )} - - -
+ diff --git a/packages/react-devtools-shared/src/devtools/views/Icon.js b/packages/react-devtools-shared/src/devtools/views/Icon.js index ffa297610bdf5..c9ae931f5ee74 100644 --- a/packages/react-devtools-shared/src/devtools/views/Icon.js +++ b/packages/react-devtools-shared/src/devtools/views/Icon.js @@ -21,6 +21,7 @@ export type IconType = | 'flame-chart' | 'profiler' | 'ranked-chart' + | 'scheduling-profiler' | 'search' | 'settings' | 'store-as-global-variable' @@ -64,6 +65,9 @@ export default function Icon({className = '', type}: Props) { case 'ranked-chart': pathData = PATH_RANKED_CHART; break; + case 'scheduling-profiler': + pathData = PATH_SCHEDULING_PROFILER; + break; case 'search': pathData = PATH_SEARCH; break; @@ -136,6 +140,11 @@ const PATH_FLAME_CHART = ` const PATH_PROFILER = 'M5 9.2h3V19H5zM10.6 5h2.8v14h-2.8zm5.6 8H19v6h-2.8z'; +const PATH_SCHEDULING_PROFILER = ` + M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 + 16H5V9h14v10zm0-12H5V5h14v2zM7 11h5v5H7z +`; + const PATH_SEARCH = ` M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js index 9fda41499871e..70c87e618eb0d 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js @@ -16,6 +16,7 @@ import ClearProfilingDataButton from './ClearProfilingDataButton'; import CommitFlamegraph from './CommitFlamegraph'; import CommitRanked from './CommitRanked'; import RootSelector from './RootSelector'; +import {SchedulingProfiler} from 'react-devtools-scheduling-profiler/src/SchedulingProfiler'; import RecordToggle from './RecordToggle'; import ReloadAndProfileButton from './ReloadAndProfileButton'; import ProfilingImportExportButtons from './ProfilingImportExportButtons'; @@ -41,8 +42,10 @@ function Profiler(_: {||}) { supportsProfiling, } = useContext(ProfilerContext); + let showRightColumn = true; + let view = null; - if (didRecordCommits) { + if (didRecordCommits || selectedTabID === 'scheduling-profiler') { switch (selectedTabID) { case 'flame-chart': view = ; @@ -50,6 +53,10 @@ function Profiler(_: {||}) { case 'ranked-chart': view = ; break; + case 'scheduling-profiler': + view = ; + showRightColumn = false; + break; default: break; } @@ -119,7 +126,7 @@ function Profiler(_: {||}) {
-
{sidebar}
+ {showRightColumn &&
{sidebar}
}
@@ -139,6 +146,13 @@ const tabs = [ label: 'Ranked', title: 'Ranked chart', }, + null, // Divider/separator + { + id: 'scheduling-profiler', + icon: 'scheduling-profiler', + label: 'Scheduling', + title: 'Scheduling Profiler', + }, ]; const NoProfilingData = () => ( diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js index 48f9aa11eefdb..3206fcb28f74a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js @@ -19,7 +19,8 @@ import {StoreContext} from '../context'; import type {ProfilingDataFrontend} from './types'; -export type TabID = 'flame-chart' | 'ranked-chart'; +// TODO (scheduling profiler) Should this be its own context? +export type TabID = 'flame-chart' | 'ranked-chart' | 'scheduling-profiler'; export type Context = {| // Which tab is selected in the Profiler UI? diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js index d6570dd5ab34b..420bdbbd39021 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js @@ -19,13 +19,17 @@ import { prepareProfilingDataFrontendFromExport, } from './utils'; import {downloadFile} from '../utils'; +import {SchedulingProfilerContext} from 'react-devtools-scheduling-profiler/src/SchedulingProfilerContext'; import styles from './ProfilingImportExportButtons.css'; import type {ProfilingDataExport} from './types'; export default function ProfilingImportExportButtons() { - const {isProfiling, profilingData, rootID} = useContext(ProfilerContext); + const {isProfiling, profilingData, rootID, selectedTabID} = useContext( + ProfilerContext, + ); + const {importSchedulingProfilerData} = useContext(SchedulingProfilerContext); const store = useContext(StoreContext); const {profilerStore} = store; @@ -64,13 +68,13 @@ export default function ProfilingImportExportButtons() { } }, [rootID, profilingData]); - const uploadData = useCallback(() => { + const clickInputElement = useCallback(() => { if (inputRef.current !== null) { inputRef.current.click(); } }, []); - const handleFiles = useCallback(() => { + const importProfilerData = useCallback(() => { const input = inputRef.current; if (input !== null && input.files.length > 0) { const fileReader = new FileReader(); @@ -104,6 +108,11 @@ export default function ProfilingImportExportButtons() { } }, [modalDialogDispatch, profilerStore]); + const importSchedulingProfilerDataWrapper = event => { + const input = inputRef.current; + importSchedulingProfilerData(input.files[0]); + }; + return (
@@ -111,18 +120,26 @@ export default function ProfilingImportExportButtons() { ref={inputRef} className={styles.Input} type="file" - onChange={handleFiles} + onChange={ + selectedTabID === 'scheduling-profiler' + ? importSchedulingProfilerDataWrapper + : importProfilerData + } tabIndex={-1} />