mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge branch 'master' into continuous-select
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
.*node_modules/archiver-utils
|
||||
.*node_modules/babel.*
|
||||
.*node_modules/browserify-zlib/.*
|
||||
.*node_modules/classnames.*
|
||||
.*node_modules/gh-pages/.*
|
||||
.*node_modules/invariant/.*
|
||||
.*node_modules/json-loader.*
|
||||
|
||||
+2
-1
@@ -7,4 +7,5 @@ npm-debug.log
|
||||
yarn-error.log
|
||||
.DS_Store
|
||||
yarn-error.log
|
||||
.vscode
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
+3
-2
@@ -56,7 +56,6 @@
|
||||
"cli-spinners": "^1.0.0",
|
||||
"clipboard-js": "^0.3.6",
|
||||
"css-loader": "^1.0.1",
|
||||
"html2canvas": "^1.0.0-alpha.12",
|
||||
"error-stack-parser": "^2.0.2",
|
||||
"es6-symbol": "3.0.2",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
@@ -78,13 +77,15 @@
|
||||
"fbjs": "0.5.1",
|
||||
"fbjs-scripts": "0.7.0",
|
||||
"firefox-profile": "^1.0.2",
|
||||
"flow-bin": "^0.94.0",
|
||||
"flow-bin": "^0.96.0",
|
||||
"fs-extra": "^3.0.1",
|
||||
"gh-pages": "^1.0.0",
|
||||
"html2canvas": "^1.0.0-alpha.12",
|
||||
"immutable": "3.7.6",
|
||||
"jest": "22.1.4",
|
||||
"lerna": "^2.8.0",
|
||||
"lint-staged": "^7.0.5",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"log-update": "^2.0.0",
|
||||
"lru-cache": "^4.1.3",
|
||||
"memoize-one": "^3.1.1",
|
||||
|
||||
@@ -154,6 +154,14 @@ function createPanelIfReactLoaded() {
|
||||
|
||||
initBridgeAndStore();
|
||||
|
||||
function ensureInitialHTMLIsCleared(container) {
|
||||
if (container._hasInitialHTMLBeenCleared) {
|
||||
return;
|
||||
}
|
||||
container.innerHTML = '';
|
||||
container._hasInitialHTMLBeenCleared = true;
|
||||
}
|
||||
|
||||
let currentPanel = null;
|
||||
|
||||
chrome.devtools.panels.create('⚛ Components', '', 'panel.html', panel => {
|
||||
@@ -166,7 +174,7 @@ function createPanelIfReactLoaded() {
|
||||
componentsPortalContainer = panel.container;
|
||||
|
||||
if (componentsPortalContainer != null) {
|
||||
componentsPortalContainer.innerHTML = '';
|
||||
ensureInitialHTMLIsCleared(componentsPortalContainer);
|
||||
render('components');
|
||||
panel.injectStyles(cloneStyleTags);
|
||||
}
|
||||
@@ -188,7 +196,7 @@ function createPanelIfReactLoaded() {
|
||||
profilerPortalContainer = panel.container;
|
||||
|
||||
if (profilerPortalContainer != null) {
|
||||
profilerPortalContainer.innerHTML = '';
|
||||
ensureInitialHTMLIsCleared(profilerPortalContainer);
|
||||
render('profiler');
|
||||
panel.injectStyles(cloneStyleTags);
|
||||
}
|
||||
@@ -205,7 +213,7 @@ function createPanelIfReactLoaded() {
|
||||
settingsPortalContainer = panel.container;
|
||||
|
||||
if (settingsPortalContainer != null) {
|
||||
settingsPortalContainer.innerHTML = '';
|
||||
ensureInitialHTMLIsCleared(settingsPortalContainer);
|
||||
render('settings');
|
||||
panel.injectStyles(cloneStyleTags);
|
||||
}
|
||||
|
||||
+46
-4
@@ -66,13 +66,19 @@ export default class Agent extends EventEmitter {
|
||||
this._bridge = bridge;
|
||||
|
||||
bridge.addListener('captureScreenshot', this.captureScreenshot);
|
||||
bridge.addListener(
|
||||
'clearHighlightedElementInDOM',
|
||||
this.clearHighlightedElementInDOM
|
||||
);
|
||||
bridge.addListener('exportProfilingSummary', this.exportProfilingSummary);
|
||||
bridge.addListener('getCommitDetails', this.getCommitDetails);
|
||||
bridge.addListener('getFiberCommits', this.getFiberCommits);
|
||||
bridge.addListener('getInteractions', this.getInteractions);
|
||||
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
|
||||
bridge.addListener('getProfilingSummary', this.getProfilingSummary);
|
||||
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
|
||||
bridge.addListener('inspectElement', this.inspectElement);
|
||||
bridge.addListener('logElementToConsole', this.logElementToConsole);
|
||||
bridge.addListener('overrideContext', this.overrideContext);
|
||||
bridge.addListener('overrideHookState', this.overrideHookState);
|
||||
bridge.addListener('overrideProps', this.overrideProps);
|
||||
@@ -161,6 +167,26 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
getFiberCommits = ({
|
||||
fiberID,
|
||||
rendererID,
|
||||
rootID,
|
||||
}: {
|
||||
fiberID: number,
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}"`);
|
||||
} else {
|
||||
this._bridge.send(
|
||||
'fiberCommits',
|
||||
renderer.getFiberCommits(rootID, fiberID)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getInteractions = ({
|
||||
rendererID,
|
||||
rootID,
|
||||
@@ -198,14 +224,22 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
clearHighlightedElementInDOM = () => {
|
||||
hideOverlay();
|
||||
};
|
||||
|
||||
highlightElementInDOM = ({
|
||||
displayName,
|
||||
hideAfterTimeout,
|
||||
id,
|
||||
rendererID,
|
||||
scrollIntoView,
|
||||
}: {
|
||||
displayName: string,
|
||||
hideAfterTimeout: boolean,
|
||||
id: number,
|
||||
rendererID: number,
|
||||
scrollIntoView: boolean,
|
||||
}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
@@ -218,13 +252,12 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
|
||||
if (node != null) {
|
||||
if (typeof node.scrollIntoView === 'function') {
|
||||
if (scrollIntoView && typeof node.scrollIntoView === 'function') {
|
||||
// If the node isn't visible show it before highlighting it.
|
||||
// We may want to reconsider this; it might be a little disruptive.
|
||||
node.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
}
|
||||
|
||||
showOverlay(((node: any): HTMLElement), displayName);
|
||||
showOverlay(((node: any): HTMLElement), displayName, hideAfterTimeout);
|
||||
} else {
|
||||
hideOverlay();
|
||||
}
|
||||
@@ -239,6 +272,15 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
logElementToConsole = ({ id, rendererID }: InspectSelectParams) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
renderer.logElementToConsole(id);
|
||||
}
|
||||
};
|
||||
|
||||
reloadAndProfile = () => {
|
||||
localStorage.setItem(LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
|
||||
|
||||
@@ -452,7 +494,7 @@ export default class Agent extends EventEmitter {
|
||||
const target = ((event.target: any): HTMLElement);
|
||||
// Don't pass the name explicitly.
|
||||
// It will be inferred from DOM tag and Fiber owner.
|
||||
showOverlay(target);
|
||||
showOverlay(target, null, false);
|
||||
this._lastInspectedNode = target;
|
||||
};
|
||||
|
||||
|
||||
+93
-22
@@ -32,6 +32,7 @@ import type {
|
||||
CommitDetails,
|
||||
DevToolsHook,
|
||||
Fiber,
|
||||
FiberCommits,
|
||||
FiberData,
|
||||
Interaction,
|
||||
Interactions,
|
||||
@@ -810,21 +811,6 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function unmountFiberRecursively(fiber, traverseSiblings = false) {
|
||||
if (__DEBUG__) {
|
||||
debug('unmountFiberRecursively()', fiber, traverseSiblings);
|
||||
}
|
||||
if (!shouldFilterFiber(fiber)) {
|
||||
recordUnmount(fiber);
|
||||
}
|
||||
if (fiber.child !== null) {
|
||||
unmountFiberRecursively(fiber.child, true);
|
||||
}
|
||||
if (traverseSiblings && fiber.sibling !== null) {
|
||||
unmountFiberRecursively(fiber.sibling, true);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRecordUpdate(fiber: Fiber, hasChildOrderChanged: boolean) {
|
||||
if (__DEBUG__) {
|
||||
debug('maybeRecordUpdate()', fiber);
|
||||
@@ -1424,7 +1410,7 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function inspectElement(id: number): InspectedElement | null {
|
||||
function inspectElementRaw(id: number): InspectedElement | null {
|
||||
let fiber = idToFiberMap.get(id);
|
||||
|
||||
if (fiber == null) {
|
||||
@@ -1505,7 +1491,7 @@ export function attach(
|
||||
if (context !== null) {
|
||||
// To simplify hydration and display logic for context, wrap in a value object.
|
||||
// Otherwise simple values (e.g. strings, booleans) become harder to handle.
|
||||
context = cleanForBridge({ value: context });
|
||||
context = { value: context };
|
||||
}
|
||||
|
||||
let owners = null;
|
||||
@@ -1544,16 +1530,16 @@ export function attach(
|
||||
// Can view component source location.
|
||||
canViewSource,
|
||||
|
||||
displayName: getDataForFiber(fiber).displayName,
|
||||
|
||||
// Inspectable properties.
|
||||
// TODO Review sanitization approach for the below inspectable values.
|
||||
context,
|
||||
hooks: usesHooks
|
||||
? cleanForBridge(
|
||||
inspectHooksOfFiber(fiber, (renderer.currentDispatcherRef: any))
|
||||
)
|
||||
? inspectHooksOfFiber(fiber, (renderer.currentDispatcherRef: any))
|
||||
: null,
|
||||
props: cleanForBridge(memoizedProps),
|
||||
state: usesHooks ? null : cleanForBridge(memoizedState),
|
||||
props: memoizedProps,
|
||||
state: usesHooks ? null : memoizedState,
|
||||
|
||||
// List of owners
|
||||
owners,
|
||||
@@ -1563,6 +1549,56 @@ export function attach(
|
||||
};
|
||||
}
|
||||
|
||||
function inspectElement(id: number): InspectedElement | null {
|
||||
let result = inspectElementRaw(id);
|
||||
if (result === null) {
|
||||
return null;
|
||||
}
|
||||
// TODO Review sanitization approach for the below inspectable values.
|
||||
result.context = cleanForBridge(result.context);
|
||||
result.hooks = cleanForBridge(result.hooks);
|
||||
result.props = cleanForBridge(result.props);
|
||||
result.state = cleanForBridge(result.state);
|
||||
return result;
|
||||
}
|
||||
|
||||
function logElementToConsole(id) {
|
||||
const result = inspectElementRaw(id);
|
||||
if (result === null) {
|
||||
console.warn(`Could not find Fiber with id "${id}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
const supportsGroup = typeof console.groupCollapsed === 'function';
|
||||
const label =
|
||||
'[Click to expand] <' + (result.displayName || 'Component') + ' />';
|
||||
|
||||
if (supportsGroup) {
|
||||
console.groupCollapsed(label);
|
||||
}
|
||||
if (result.props !== null) {
|
||||
console.log('Props:', result.props);
|
||||
}
|
||||
if (result.state !== null) {
|
||||
console.log('State:', result.state);
|
||||
}
|
||||
if (result.hooks !== null) {
|
||||
console.log('Hooks:', result.hooks);
|
||||
}
|
||||
const nativeNode = findNativeByFiberID(id);
|
||||
if (nativeNode !== null) {
|
||||
console.log('Node:', nativeNode);
|
||||
}
|
||||
if (window.chrome || /firefox/i.test(navigator.userAgent)) {
|
||||
console.log(
|
||||
'Right-click any value to save it as a global variable for further inspection.'
|
||||
);
|
||||
}
|
||||
if (supportsGroup) {
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
function setInHook(
|
||||
id: number,
|
||||
index: number,
|
||||
@@ -1667,6 +1703,39 @@ export function attach(
|
||||
};
|
||||
}
|
||||
|
||||
function getFiberCommits(rootID: number, fiberID: number): FiberCommits {
|
||||
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
|
||||
rootID
|
||||
);
|
||||
if (commitProfilingMetadata != null) {
|
||||
const commitDurations = [];
|
||||
commitProfilingMetadata.forEach(({ actualDurations }, commitIndex) => {
|
||||
for (let i = 0; i < actualDurations.length; i += 2) {
|
||||
if (actualDurations[i] === fiberID) {
|
||||
commitDurations.push(commitIndex, actualDurations[i + 1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
commitDurations,
|
||||
fiberID,
|
||||
rootID,
|
||||
};
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`getFiberCommits(): No profiling info recorded for root "${rootID}"`
|
||||
);
|
||||
|
||||
return {
|
||||
commitDurations: [],
|
||||
fiberID,
|
||||
rootID,
|
||||
};
|
||||
}
|
||||
|
||||
function getInteractions(rootID: number): Interactions {
|
||||
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
|
||||
rootID
|
||||
@@ -1832,6 +1901,7 @@ export function attach(
|
||||
flushInitialOperations,
|
||||
getCommitDetails,
|
||||
getFiberIDFromNative,
|
||||
getFiberCommits,
|
||||
getInteractions,
|
||||
findNativeByFiberID,
|
||||
getProfilingDataForDownload,
|
||||
@@ -1839,6 +1909,7 @@ export function attach(
|
||||
handleCommitFiberRoot,
|
||||
handleCommitFiberUnmount,
|
||||
inspectElement,
|
||||
logElementToConsole,
|
||||
prepareViewElementSource,
|
||||
overrideSuspense,
|
||||
renderer,
|
||||
|
||||
@@ -66,6 +66,12 @@ export type CommitDetails = {|
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
export type FiberCommits = {|
|
||||
commitDurations: Array<number>,
|
||||
fiberID: number,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
export type InteractionWithCommits = {|
|
||||
...Interaction,
|
||||
commits: Array<number>,
|
||||
@@ -93,12 +99,14 @@ export type RendererInterface = {
|
||||
component: NativeType,
|
||||
findNearestUnfilteredAncestor?: boolean
|
||||
) => number | null,
|
||||
getFiberCommits: (rootID: number, fiberID: number) => FiberCommits,
|
||||
getInteractions: (rootID: number) => Interactions,
|
||||
getProfilingDataForDownload: (rootID: number) => Object,
|
||||
getProfilingSummary: (rootID: number) => ProfilingSummary,
|
||||
handleCommitFiberRoot: (fiber: Object) => void,
|
||||
handleCommitFiberUnmount: (fiber: Object) => void,
|
||||
inspectElement: (id: number) => InspectedElement | null,
|
||||
logElementToConsole: (id: number) => void,
|
||||
overrideSuspense: (id: number, forceFallback: boolean) => void,
|
||||
prepareViewElementSource: (id: number) => void,
|
||||
renderer: ReactRenderer | null,
|
||||
|
||||
@@ -18,7 +18,8 @@ export function hideOverlay() {
|
||||
|
||||
export function showOverlay(
|
||||
element: HTMLElement | null,
|
||||
componentName: string = ''
|
||||
componentName: string | null,
|
||||
hideAfterTimeout: boolean
|
||||
) {
|
||||
if (timeoutID !== null) {
|
||||
clearTimeout(timeoutID);
|
||||
@@ -34,5 +35,7 @@ export function showOverlay(
|
||||
|
||||
overlay.inspect(element, componentName);
|
||||
|
||||
timeoutID = setTimeout(hideOverlay, SHOW_DURATION);
|
||||
if (hideAfterTimeout) {
|
||||
timeoutID = setTimeout(hideOverlay, SHOW_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,21 +215,21 @@ function findTipPos(dims, win) {
|
||||
return { top, left: dims.left + margin + 'px' };
|
||||
}
|
||||
|
||||
function getElementDimensions(domElement) {
|
||||
export function getElementDimensions(domElement: Element) {
|
||||
const calculatedStyle = window.getComputedStyle(domElement);
|
||||
return {
|
||||
borderLeft: +calculatedStyle.borderLeftWidth.match(/[0-9]*/)[0],
|
||||
borderRight: +calculatedStyle.borderRightWidth.match(/[0-9]*/)[0],
|
||||
borderTop: +calculatedStyle.borderTopWidth.match(/[0-9]*/)[0],
|
||||
borderBottom: +calculatedStyle.borderBottomWidth.match(/[0-9]*/)[0],
|
||||
marginLeft: +calculatedStyle.marginLeft.match(/[0-9]*/)[0],
|
||||
marginRight: +calculatedStyle.marginRight.match(/[0-9]*/)[0],
|
||||
marginTop: +calculatedStyle.marginTop.match(/[0-9]*/)[0],
|
||||
marginBottom: +calculatedStyle.marginBottom.match(/[0-9]*/)[0],
|
||||
paddingLeft: +calculatedStyle.paddingLeft.match(/[0-9]*/)[0],
|
||||
paddingRight: +calculatedStyle.paddingRight.match(/[0-9]*/)[0],
|
||||
paddingTop: +calculatedStyle.paddingTop.match(/[0-9]*/)[0],
|
||||
paddingBottom: +calculatedStyle.paddingBottom.match(/[0-9]*/)[0],
|
||||
borderLeft: parseInt(calculatedStyle.borderLeftWidth, 10),
|
||||
borderRight: parseInt(calculatedStyle.borderRightWidth, 10),
|
||||
borderTop: parseInt(calculatedStyle.borderTopWidth, 10),
|
||||
borderBottom: parseInt(calculatedStyle.borderBottomWidth, 10),
|
||||
marginLeft: parseInt(calculatedStyle.marginLeft, 10),
|
||||
marginRight: parseInt(calculatedStyle.marginRight, 10),
|
||||
marginTop: parseInt(calculatedStyle.marginTop, 10),
|
||||
marginBottom: parseInt(calculatedStyle.marginBottom, 10),
|
||||
paddingLeft: parseInt(calculatedStyle.paddingLeft, 10),
|
||||
paddingRight: parseInt(calculatedStyle.paddingRight, 10),
|
||||
paddingTop: parseInt(calculatedStyle.paddingTop, 10),
|
||||
paddingBottom: parseInt(calculatedStyle.paddingBottom, 10),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,13 @@ import type { Resource } from './cache';
|
||||
import type { Bridge } from '../types';
|
||||
import type {
|
||||
CommitDetails as CommitDetailsBackend,
|
||||
FiberCommits as FiberCommitsBackend,
|
||||
Interactions as InteractionsBackend,
|
||||
ProfilingSummary as ProfilingSummaryBackend,
|
||||
} from 'src/backend/types';
|
||||
import type {
|
||||
CommitDetails as CommitDetailsFrontend,
|
||||
FiberCommits as FiberCommitsFrontend,
|
||||
Interactions as InteractionsFrontend,
|
||||
InteractionWithCommits,
|
||||
CommitTree as CommitTreeFrontend,
|
||||
@@ -39,13 +41,19 @@ import type { ChartData as RankedChartData } from 'src/devtools/views/Profiler/R
|
||||
|
||||
type CommitDetailsParams = {|
|
||||
commitIndex: number,
|
||||
rootID: number,
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
type FiberCommitsParams = {|
|
||||
fiberID: number,
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
type InteractionsParams = {|
|
||||
rootID: number,
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
type GetCommitTreeParams = {|
|
||||
@@ -54,8 +62,8 @@ type GetCommitTreeParams = {|
|
||||
|};
|
||||
|
||||
type ProfilingSummaryParams = {|
|
||||
rootID: number,
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
export default class ProfilingCache {
|
||||
@@ -67,6 +75,11 @@ export default class ProfilingCache {
|
||||
(commitDetails: CommitDetailsFrontend) => void
|
||||
> = new Map();
|
||||
|
||||
_pendingFiberCommitsMap: Map<
|
||||
string,
|
||||
(fiberCommits: FiberCommitsFrontend) => void
|
||||
> = new Map();
|
||||
|
||||
_pendingInteractionsMap: Map<
|
||||
number,
|
||||
(interactions: InteractionsFrontend) => void
|
||||
@@ -121,6 +134,38 @@ export default class ProfilingCache {
|
||||
`${rootID}-${commitIndex}`
|
||||
);
|
||||
|
||||
FiberCommits: Resource<
|
||||
FiberCommitsParams,
|
||||
FiberCommitsFrontend
|
||||
> = createResource(
|
||||
({ fiberID, rendererID, rootID }: FiberCommitsParams) => {
|
||||
return new Promise(resolve => {
|
||||
const importedProfilingData = this._store.importedProfilingData;
|
||||
if (importedProfilingData !== null) {
|
||||
// TODO (profiling) commit details
|
||||
// Copy from renderer getFiberCommits()
|
||||
} else if (this._store.profilingOperations.has(rootID)) {
|
||||
this._pendingFiberCommitsMap.set(`${rootID}-${fiberID}`, resolve);
|
||||
this._bridge.send('getFiberCommits', {
|
||||
fiberID,
|
||||
rendererID,
|
||||
rootID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If no profiling data was recorded for this root, skip the round trip.
|
||||
resolve({
|
||||
commitDurations: [],
|
||||
fiberID,
|
||||
rootID,
|
||||
});
|
||||
});
|
||||
},
|
||||
({ fiberID, rendererID, rootID }: FiberCommitsParams) =>
|
||||
`${rootID}-${fiberID}`
|
||||
);
|
||||
|
||||
Interactions: Resource<
|
||||
InteractionsParams,
|
||||
InteractionsFrontend
|
||||
@@ -192,6 +237,7 @@ export default class ProfilingCache {
|
||||
this._store = store;
|
||||
|
||||
bridge.addListener('commitDetails', this.onCommitDetails);
|
||||
bridge.addListener('fiberCommits', this.onFiberCommits);
|
||||
bridge.addListener('interactions', this.onInteractions);
|
||||
bridge.addListener('profilingSummary', this.onProfileSummary);
|
||||
}
|
||||
@@ -285,6 +331,24 @@ export default class ProfilingCache {
|
||||
}
|
||||
};
|
||||
|
||||
onFiberCommits = ({
|
||||
commitDurations,
|
||||
fiberID,
|
||||
rootID,
|
||||
}: FiberCommitsBackend) => {
|
||||
const key = `${rootID}-${fiberID}`;
|
||||
const resolve = this._pendingFiberCommitsMap.get(key);
|
||||
if (resolve != null) {
|
||||
this._pendingFiberCommitsMap.delete(key);
|
||||
|
||||
resolve({
|
||||
commitDurations,
|
||||
fiberID,
|
||||
rootID,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onInteractions = ({ interactions, rootID }: InteractionsBackend) => {
|
||||
const resolve = this._pendingInteractionsMap.get(rootID);
|
||||
if (resolve != null) {
|
||||
@@ -304,6 +368,7 @@ export default class ProfilingCache {
|
||||
const resolve = this._pendingProfileSummaryMap.get(rootID);
|
||||
if (resolve != null) {
|
||||
this._pendingProfileSummaryMap.delete(rootID);
|
||||
|
||||
const initialTreeBaseDurationsMap = new Map();
|
||||
for (let i = 0; i < initialTreeBaseDurations.length; i += 2) {
|
||||
initialTreeBaseDurationsMap.set(
|
||||
|
||||
+15
-5
@@ -1,6 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import memoize from 'memoize-one';
|
||||
import throttle from 'lodash.throttle';
|
||||
import {
|
||||
TREE_OPERATION_ADD,
|
||||
TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN,
|
||||
@@ -35,6 +37,8 @@ const debug = (methodName, ...args) => {
|
||||
const LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY =
|
||||
'React::DevTools::captureScreenshots';
|
||||
|
||||
const THROTTLE_CAPTURE_SCREENSHOT_DURATION = 500;
|
||||
|
||||
type Config = {|
|
||||
isProfiling?: boolean,
|
||||
supportsCaptureScreenshots?: boolean,
|
||||
@@ -129,8 +133,8 @@ export default class Store extends EventEmitter {
|
||||
if (supportsCaptureScreenshots) {
|
||||
this._supportsCaptureScreenshots = true;
|
||||
this._captureScreenshots =
|
||||
localStorage.getItem(LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY) !==
|
||||
'false';
|
||||
localStorage.getItem(LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY) ===
|
||||
'true';
|
||||
}
|
||||
if (supportsFileDownloads) {
|
||||
this._supportsFileDownloads = true;
|
||||
@@ -416,6 +420,13 @@ export default class Store extends EventEmitter {
|
||||
this.emit('isProfiling');
|
||||
}
|
||||
|
||||
_captureScreenshot = throttle(
|
||||
memoize((commitIndex: number) => {
|
||||
this._bridge.send('captureScreenshot', { commitIndex });
|
||||
}),
|
||||
THROTTLE_CAPTURE_SCREENSHOT_DURATION
|
||||
);
|
||||
|
||||
_takeProfilingSnapshotRecursive = (id: number) => {
|
||||
const element = this.getElementByID(id);
|
||||
if (element !== null) {
|
||||
@@ -454,10 +465,9 @@ export default class Store extends EventEmitter {
|
||||
profilingOperations.push(operations);
|
||||
}
|
||||
|
||||
const commitIndex = profilingOperations.length - 1;
|
||||
|
||||
if (this._captureScreenshots) {
|
||||
this._bridge.send('captureScreenshot', { commitIndex });
|
||||
const commitIndex = profilingOperations.length - 1;
|
||||
this._captureScreenshot(commitIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export type IconType =
|
||||
| 'export'
|
||||
| 'filter'
|
||||
| 'import'
|
||||
| 'log-data'
|
||||
| 'more'
|
||||
| 'next'
|
||||
| 'previous'
|
||||
@@ -54,6 +55,9 @@ export default function ButtonIcon({ type }: Props) {
|
||||
case 'import':
|
||||
pathData = PATH_IMPORT;
|
||||
break;
|
||||
case 'log-data':
|
||||
pathData = PATH_LOG_DATA;
|
||||
break;
|
||||
case 'more':
|
||||
pathData = PATH_MORE;
|
||||
break;
|
||||
@@ -130,11 +134,16 @@ const PATH_FILTER = 'M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z';
|
||||
|
||||
const PATH_IMPORT = 'M8.18,18.13v-7H3l9-8.95,9,9H15.82v7ZM3,20.13H21v1.73H3Z';
|
||||
|
||||
const PATH_LOG_DATA = `
|
||||
M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41
|
||||
3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04
|
||||
1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6
|
||||
8h-4v-2h4v2zm0-4h-4v-2h4v2z
|
||||
`;
|
||||
|
||||
const PATH_MORE = `
|
||||
M22 3H7c-.69 0-1.23.35-1.59.88L0 12l5.41 8.11c.36.53.97.89 1.66.89H22c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9
|
||||
13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm5 0c-.83
|
||||
0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm5 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5
|
||||
1.5.67 1.5 1.5-.67 1.5-1.5 1.5z
|
||||
M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9
|
||||
2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z
|
||||
`;
|
||||
|
||||
const PATH_NEXT = 'M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z';
|
||||
|
||||
@@ -4,24 +4,27 @@ import React, {
|
||||
Fragment,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { ElementTypeClass, ElementTypeFunction } from 'src/devtools/types';
|
||||
import { createRegExp } from '../utils';
|
||||
import { TreeContext } from './TreeContext';
|
||||
import { BridgeContext, StoreContext } from '../context';
|
||||
|
||||
import type { ItemData } from './Tree';
|
||||
import type { Element } from './types';
|
||||
|
||||
import styles from './Element.css';
|
||||
|
||||
type Props = {
|
||||
data: ItemData,
|
||||
index: number,
|
||||
style: Object,
|
||||
};
|
||||
|
||||
export default function ElementView({ index, style }: Props) {
|
||||
export default function ElementView({ data, index, style }: Props) {
|
||||
const {
|
||||
baseDepth,
|
||||
getElementAtIndex,
|
||||
@@ -29,11 +32,14 @@ export default function ElementView({ index, style }: Props) {
|
||||
selectedElementID,
|
||||
selectElementByID,
|
||||
} = useContext(TreeContext);
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const element = getElementAtIndex(index);
|
||||
|
||||
const id = element === null ? null : element.id;
|
||||
const isSelected = selectedElementID === id;
|
||||
const lastScrolledIDRef = data.lastScrolledIDRef;
|
||||
|
||||
const handleDoubleClick = useCallback(() => {
|
||||
if (id !== null) {
|
||||
@@ -43,8 +49,22 @@ export default function ElementView({ index, style }: Props) {
|
||||
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// The tree above has its own autoscrolling, but it only works for rows.
|
||||
// However, even when the row gets into the viewport, the component name
|
||||
// might be too far left or right on the screen. Adjust it in this case.
|
||||
useLayoutEffect(() => {
|
||||
if (isSelected) {
|
||||
// Don't select the same item twice.
|
||||
// A row may appear and disappear just by scrolling:
|
||||
// https://github.com/bvaughn/react-devtools-experimental/issues/67
|
||||
// It doesn't necessarily indicate a user action.
|
||||
// TODO: we might want to revamp the autoscroll logic
|
||||
// to only happen explicitly for user-initiated events.
|
||||
if (lastScrolledIDRef.current === id) {
|
||||
return;
|
||||
}
|
||||
lastScrolledIDRef.current = id;
|
||||
|
||||
if (ref.current !== null) {
|
||||
ref.current.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
@@ -53,7 +73,7 @@ export default function ElementView({ index, style }: Props) {
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isSelected]);
|
||||
}, [id, isSelected, lastScrolledIDRef]);
|
||||
|
||||
// TODO Add click and key handlers for toggling element open/close state.
|
||||
|
||||
@@ -66,6 +86,21 @@ export default function ElementView({ index, style }: Props) {
|
||||
[id, selectElementByID]
|
||||
);
|
||||
|
||||
const rendererID = id !== null ? store.getRendererIDForElement(id) : null;
|
||||
// Individual elements don't have a corresponding leave handler.
|
||||
// Instead, it's implemented on the tree level.
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
if (element !== null && id !== null && rendererID !== null) {
|
||||
bridge.send('highlightElementInDOM', {
|
||||
displayName: element.displayName,
|
||||
hideAfterTimeout: false,
|
||||
id,
|
||||
rendererID,
|
||||
scrollIntoView: false,
|
||||
});
|
||||
}
|
||||
}, [bridge, element, id, rendererID]);
|
||||
|
||||
// Handle elements that are removed from the tree while an async render is in progress.
|
||||
if (element == null) {
|
||||
console.warn(`<ElementView> Could not find element at index ${index}`);
|
||||
@@ -84,6 +119,7 @@ export default function ElementView({ index, style }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={isSelected ? styles.SelectedElement : styles.Element}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseDown={handleMouseDown}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
style={{
|
||||
|
||||
@@ -2,25 +2,21 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Component,
|
||||
.FocusedComponent {
|
||||
.SelectedComponent {
|
||||
padding: 0.25rem;
|
||||
margin-right: 0.5rem;
|
||||
color: var(--color-component-name);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
white-space: nowrap;
|
||||
border-radius: 0.125rem;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.Component {
|
||||
cursor: pointer;
|
||||
color: var(--color-component-name);
|
||||
text-align: left;
|
||||
}
|
||||
.Component:hover {
|
||||
background-color: var(--color-hover-background);
|
||||
@@ -30,17 +26,43 @@
|
||||
background-color: var(--color-hover-background);
|
||||
}
|
||||
|
||||
.FocusedComponent {
|
||||
.SelectedComponent {
|
||||
background-color: var(--color-selected-background);
|
||||
color: var(--color-selected-foreground);
|
||||
}
|
||||
.FocusedComponent:focus {
|
||||
.SelectedComponent:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.Bar {
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.Toggle {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.VRule {
|
||||
flex: 0 0 auto;
|
||||
height: 20px;
|
||||
width: 1px;
|
||||
background-color: var(--color-border);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.25rem);
|
||||
left: 2.5rem;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-background);
|
||||
padding: 0.5rem;
|
||||
padding-right: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.25rem;
|
||||
max-height: 10rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,53 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import React, {
|
||||
Fragment,
|
||||
useCallback,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Toggle from '../Toggle';
|
||||
import { TreeContext } from './TreeContext';
|
||||
import { StoreContext } from '../context';
|
||||
import { useIsOverflowing, useModalDismissSignal } from '../hooks';
|
||||
|
||||
import type { Element } from './types';
|
||||
|
||||
import styles from './OwnersStack.css';
|
||||
|
||||
export default function OwnerStack() {
|
||||
const { ownerStack, resetOwnerStack } = useContext(TreeContext);
|
||||
const { ownerStack, ownerStackIndex, resetOwnerStack } = useContext(
|
||||
TreeContext
|
||||
);
|
||||
|
||||
const elements = ownerStack.map((id, index) => (
|
||||
<ElementView key={id} id={id} index={index} />
|
||||
));
|
||||
const [elementsTotalWidth, setElementsTotalWidth] = useState(0);
|
||||
const elementsBarRef = useRef<HTMLDivElement | null>(null);
|
||||
const isOverflowing = useIsOverflowing(elementsBarRef, elementsTotalWidth);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// If we're already overflowing, then we don't need to re-measure items.
|
||||
// That's because once the owners stack is open, it can only get larger (by driling in).
|
||||
// A totally new stack can only be reached by exiting this mode and re-entering it.
|
||||
if (elementsBarRef.current === null || isOverflowing) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
let elementsTotalWidth = 0;
|
||||
for (let i = 0; i < ownerStack.length; i++) {
|
||||
const element = elementsBarRef.current.children[i];
|
||||
const computedStyle = getComputedStyle(element);
|
||||
|
||||
elementsTotalWidth +=
|
||||
element.offsetWidth +
|
||||
parseInt(computedStyle.marginLeft, 10) +
|
||||
parseInt(computedStyle.marginRight, 10);
|
||||
}
|
||||
|
||||
setElementsTotalWidth(elementsTotalWidth);
|
||||
}, [elementsBarRef, isOverflowing, ownerStack.length]);
|
||||
|
||||
return (
|
||||
<div className={styles.OwnerStack}>
|
||||
@@ -27,19 +59,97 @@ export default function OwnerStack() {
|
||||
<ButtonIcon type="close" />
|
||||
</Button>
|
||||
<div className={styles.VRule} />
|
||||
{elements}
|
||||
<div className={styles.Bar} ref={elementsBarRef}>
|
||||
{isOverflowing && (
|
||||
<ElementsDropdown
|
||||
ownerStack={ownerStack}
|
||||
ownerStackIndex={ownerStackIndex}
|
||||
/>
|
||||
)}
|
||||
{isOverflowing ? (
|
||||
<ElementView
|
||||
id={ownerStack[((ownerStackIndex: any): number)]}
|
||||
index={ownerStackIndex}
|
||||
/>
|
||||
) : (
|
||||
ownerStack.map((id, index) => (
|
||||
<ElementView key={id} id={id} index={index} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
id: number,
|
||||
index: number,
|
||||
type ElementsDropdownProps = {
|
||||
ownerStack: Array<number>,
|
||||
ownerStackIndex: number | null,
|
||||
};
|
||||
|
||||
function ElementView({ id, index }: Props) {
|
||||
const { ownerStackIndex, selectOwner } = useContext(TreeContext);
|
||||
function ElementsDropdown({
|
||||
ownerStack,
|
||||
ownerStackIndex,
|
||||
}: ElementsDropdownProps) {
|
||||
const store = useContext(StoreContext);
|
||||
const { selectOwner } = useContext(TreeContext);
|
||||
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
||||
|
||||
const handleDropdownButtonClick = useCallback(() => {
|
||||
setIsDropdownVisible(!isDropdownVisible);
|
||||
}, [isDropdownVisible, setIsDropdownVisible]);
|
||||
|
||||
const handleElementClick = useCallback(
|
||||
(id: number) => {
|
||||
selectOwner(id);
|
||||
setIsDropdownVisible(false);
|
||||
},
|
||||
[selectOwner, setIsDropdownVisible]
|
||||
);
|
||||
|
||||
const modalRef = useRef<HTMLDivElement | null>(null);
|
||||
const dismissModal = useCallback(() => setIsDropdownVisible(false));
|
||||
|
||||
useModalDismissSignal(modalRef, dismissModal);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Toggle
|
||||
className={styles.Toggle}
|
||||
isChecked={isDropdownVisible}
|
||||
onChange={handleDropdownButtonClick}
|
||||
title="Open elements dropdown"
|
||||
>
|
||||
<ButtonIcon type="more" />
|
||||
</Toggle>
|
||||
{isDropdownVisible && (
|
||||
<div className={styles.Modal} ref={modalRef}>
|
||||
{ownerStack.map((id, index) => (
|
||||
<button
|
||||
key={id}
|
||||
className={
|
||||
ownerStackIndex === index
|
||||
? styles.SelectedComponent
|
||||
: styles.Component
|
||||
}
|
||||
onClick={() => handleElementClick(id)}
|
||||
>
|
||||
{((store.getElementByID(id): any): Element).displayName}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
type ElementViewProps = {
|
||||
id: number,
|
||||
index: number | null,
|
||||
};
|
||||
function ElementView({ id, index }: ElementViewProps) {
|
||||
const store = useContext(StoreContext);
|
||||
const { ownerStackIndex, selectOwner } = useContext(TreeContext);
|
||||
|
||||
const { displayName } = ((store.getElementByID(id): any): Element);
|
||||
|
||||
const isSelected = ownerStackIndex === index;
|
||||
@@ -52,7 +162,7 @@ function ElementView({ id, index }: Props) {
|
||||
|
||||
return (
|
||||
<button
|
||||
className={isSelected ? styles.FocusedComponent : styles.Component}
|
||||
className={isSelected ? styles.SelectedComponent : styles.Component}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{displayName}
|
||||
|
||||
@@ -40,17 +40,30 @@ export default function SelectedElement(_: Props) {
|
||||
|
||||
const highlightElement = useCallback(() => {
|
||||
if (element !== null && selectedElementID !== null) {
|
||||
const rendererID =
|
||||
store.getRendererIDForElement(selectedElementID) || null;
|
||||
const rendererID = store.getRendererIDForElement(selectedElementID);
|
||||
if (rendererID !== null) {
|
||||
bridge.send('highlightElementInDOM', {
|
||||
displayName: element.displayName,
|
||||
hideAfterTimeout: true,
|
||||
id: selectedElementID,
|
||||
rendererID,
|
||||
scrollIntoView: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [bridge, element, selectedElementID, store]);
|
||||
|
||||
const logElement = useCallback(() => {
|
||||
if (selectedElementID !== null) {
|
||||
const rendererID = store.getRendererIDForElement(selectedElementID);
|
||||
if (rendererID !== null) {
|
||||
bridge.send('logElementToConsole', {
|
||||
id: selectedElementID,
|
||||
rendererID,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [bridge, element, selectedElementID, store]);
|
||||
}, [bridge, selectedElementID, store]);
|
||||
|
||||
const viewSource = useCallback(() => {
|
||||
if (viewElementSource != null && selectedElementID !== null) {
|
||||
@@ -87,6 +100,13 @@ export default function SelectedElement(_: Props) {
|
||||
>
|
||||
<ButtonIcon type="view-dom" />
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.IconButton}
|
||||
onClick={logElement}
|
||||
title="Log this component data to the console"
|
||||
>
|
||||
<ButtonIcon type="log-data" />
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.IconButton}
|
||||
disabled={!canViewSource}
|
||||
@@ -207,7 +227,7 @@ function InspectedElementView({
|
||||
|
||||
{ownerStack.length === 0 && owners !== null && owners.length > 0 && (
|
||||
<div className={styles.Owners}>
|
||||
<div className={styles.OwnersHeader}>owner stack</div>
|
||||
<div className={styles.OwnersHeader}>rendered by</div>
|
||||
{owners.map(owner => (
|
||||
<OwnerView
|
||||
key={owner.id}
|
||||
@@ -269,7 +289,7 @@ function useInspectedElement(id: number | null): InspectedElement | null {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const rendererID = store.getRendererIDForElement(id) || null;
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
|
||||
// Update the $r variable.
|
||||
bridge.send('selectElement', { id, rendererID });
|
||||
|
||||
@@ -24,10 +24,6 @@
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
line-height: var(--line-height-data);
|
||||
border: 0.25rem solid transparent;
|
||||
}
|
||||
.List:focus-within {
|
||||
border-color: var(--color-button-background-focus);
|
||||
}
|
||||
|
||||
.InnerElementType:focus {
|
||||
|
||||
@@ -12,13 +12,23 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { TreeContext } from './TreeContext';
|
||||
import { SettingsContext } from '../Settings/SettingsContext';
|
||||
import Element from './Element';
|
||||
import { BridgeContext } from '../context';
|
||||
import ElementView from './Element';
|
||||
import InspectHostNodesToggle from './InspectHostNodesToggle';
|
||||
import OwnersStack from './OwnersStack';
|
||||
import SearchInput from './SearchInput';
|
||||
|
||||
import styles from './Tree.css';
|
||||
|
||||
import type { Element } from './types';
|
||||
|
||||
export type ItemData = {|
|
||||
baseDepth: number,
|
||||
numElements: number,
|
||||
getElementAtIndex: (index: number) => Element | null,
|
||||
lastScrolledIDRef: { current: number | null },
|
||||
|};
|
||||
|
||||
type Props = {||};
|
||||
|
||||
export default function Tree(props: Props) {
|
||||
@@ -32,19 +42,30 @@ export default function Tree(props: Props) {
|
||||
selectParentElementInTree,
|
||||
selectPreviousElementInTree,
|
||||
} = useContext(TreeContext);
|
||||
const listRef = useRef<FixedSizeList<any> | null>(null);
|
||||
const bridge = useContext(BridgeContext);
|
||||
// $FlowFixMe https://github.com/facebook/flow/issues/7341
|
||||
const listRef = useRef<FixedSizeList<ItemData> | null>(null);
|
||||
const treeRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { lineHeight } = useContext(SettingsContext);
|
||||
|
||||
// Make sure a newly selected element is visible in the list.
|
||||
// This is helpful for things like the owners list.
|
||||
// This is helpful for things like the owners list and search.
|
||||
useLayoutEffect(() => {
|
||||
if (selectedElementIndex !== null && listRef.current != null) {
|
||||
listRef.current.scrollToItem(selectedElementIndex);
|
||||
// Note this autoscroll only works for rows.
|
||||
// There's another autoscroll inside the elements
|
||||
// that ensures the component name is visible horizontally.
|
||||
// It's too early to do it now because the row might not exist yet.
|
||||
}
|
||||
}, [listRef, selectedElementIndex]);
|
||||
|
||||
// This ref is passed down the context to elements.
|
||||
// It lets them avoid autoscrolling to the same item many times
|
||||
// when a selected virtual row goes in and out of the viewport.
|
||||
const lastScrolledIDRef = useRef<number | null>(null);
|
||||
|
||||
// Navigate the tree with up/down arrow keys.
|
||||
useEffect(() => {
|
||||
if (treeRef.current === null) {
|
||||
@@ -93,24 +114,30 @@ export default function Tree(props: Props) {
|
||||
|
||||
// Let react-window know to re-render any time the underlying tree data changes.
|
||||
// This includes the owner context, since it controls a filtered view of the tree.
|
||||
const itemData = useMemo(
|
||||
const itemData = useMemo<ItemData>(
|
||||
() => ({
|
||||
baseDepth,
|
||||
numElements,
|
||||
getElementAtIndex,
|
||||
lastScrolledIDRef,
|
||||
}),
|
||||
[baseDepth, numElements, getElementAtIndex]
|
||||
[baseDepth, numElements, getElementAtIndex, lastScrolledIDRef]
|
||||
);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
bridge.send('clearHighlightedElementInDOM');
|
||||
}, [bridge]);
|
||||
|
||||
return (
|
||||
<div className={styles.Tree} ref={treeRef}>
|
||||
<div className={styles.SearchInput}>
|
||||
{ownerStack.length > 0 ? <OwnersStack /> : <SearchInput />}
|
||||
<InspectHostNodesToggle />
|
||||
</div>
|
||||
<div className={styles.AutoSizerWrapper}>
|
||||
<div className={styles.AutoSizerWrapper} onMouseLeave={handleMouseLeave}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
// $FlowFixMe https://github.com/facebook/flow/issues/7341
|
||||
<FixedSizeList
|
||||
className={styles.List}
|
||||
height={height}
|
||||
@@ -118,10 +145,11 @@ export default function Tree(props: Props) {
|
||||
itemCount={numElements}
|
||||
itemData={itemData}
|
||||
itemSize={lineHeight}
|
||||
overscanCount={3}
|
||||
ref={listRef}
|
||||
width={width}
|
||||
>
|
||||
{Element}
|
||||
{ElementView}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
||||
@@ -383,7 +383,10 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
case 'RESET_OWNER_STACK':
|
||||
ownerStack = [];
|
||||
ownerStackIndex = null;
|
||||
selectedElementIndex = null;
|
||||
selectedElementIndex =
|
||||
selectedElementID !== null
|
||||
? store.getIndexOfElementID(selectedElementID)
|
||||
: null;
|
||||
_ownerFlatTree = null;
|
||||
break;
|
||||
case 'SELECT_ELEMENT_AT_INDEX':
|
||||
|
||||
@@ -35,6 +35,8 @@ export type Owner = {|
|
||||
export type InspectedElement = {|
|
||||
id: number,
|
||||
|
||||
displayName: string | null,
|
||||
|
||||
// Does the current renderer support editable hooks?
|
||||
canEditHooks: boolean,
|
||||
|
||||
|
||||
@@ -107,25 +107,6 @@ export default function DevTools({
|
||||
};
|
||||
}, [store, supportsProfiling]);
|
||||
|
||||
let tabElement;
|
||||
switch (tab) {
|
||||
case 'profiler':
|
||||
tabElement = (
|
||||
<Profiler
|
||||
portalContainer={profilerPortalContainer}
|
||||
supportsProfiling={supportsProfiling}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'settings':
|
||||
tabElement = <Settings portalContainer={settingsPortalContainer} />;
|
||||
break;
|
||||
case 'components':
|
||||
default:
|
||||
tabElement = <Components portalContainer={componentsPortalContainer} />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
@@ -158,7 +139,21 @@ export default function DevTools({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.TabContent}>{tabElement}</div>
|
||||
<div
|
||||
className={styles.TabContent}
|
||||
hidden={tab !== 'components'}
|
||||
>
|
||||
<Components portalContainer={componentsPortalContainer} />
|
||||
</div>
|
||||
<div className={styles.TabContent} hidden={tab !== 'profiler'}>
|
||||
<Profiler
|
||||
portalContainer={profilerPortalContainer}
|
||||
supportsProfiling={supportsProfiling}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.TabContent} hidden={tab !== 'settings'}>
|
||||
<Settings portalContainer={settingsPortalContainer} />
|
||||
</div>
|
||||
</div>
|
||||
</ProfilerContextController>
|
||||
</TreeContextController>
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: var(--font-family-sans);
|
||||
font-family: var(--font-size-sans-normal);
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-sans-normal);
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
line-height: 1.5;
|
||||
padding: 0 0 0;
|
||||
font-weight: 400;
|
||||
|
||||
@@ -20,7 +20,7 @@ export type ItemData = {|
|
||||
scaleX: (value: number, fallbackValue: number) => number,
|
||||
selectedChartNode: ChartNode,
|
||||
selectedChartNodeIndex: number,
|
||||
selectFiber: (id: number | null) => void,
|
||||
selectFiber: (id: number | null, name: string | null) => void,
|
||||
width: number,
|
||||
|};
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function CommitFlamegraphAutoSizer(_: {||}) {
|
||||
const deselectCurrentFiber = useCallback(
|
||||
event => {
|
||||
event.stopPropagation();
|
||||
selectFiber(null);
|
||||
selectFiber(null, null);
|
||||
},
|
||||
[selectFiber]
|
||||
);
|
||||
|
||||
@@ -26,9 +26,9 @@ function CommitFlamegraphListItem({ data, index, style }: Props) {
|
||||
const { maxSelfDuration, rows } = chartData;
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event: MouseEvent, id: number) => {
|
||||
(event: MouseEvent, id: number, name: string) => {
|
||||
event.stopPropagation();
|
||||
selectFiber(id);
|
||||
selectFiber(id, name);
|
||||
},
|
||||
[selectFiber]
|
||||
);
|
||||
@@ -50,6 +50,7 @@ function CommitFlamegraphListItem({ data, index, style }: Props) {
|
||||
didRender,
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
offset,
|
||||
selfDuration,
|
||||
treeBaseDuration,
|
||||
@@ -84,7 +85,7 @@ function CommitFlamegraphListItem({ data, index, style }: Props) {
|
||||
isDimmed={index < selectedChartNodeIndex}
|
||||
key={id}
|
||||
label={label}
|
||||
onClick={event => handleClick(event, id)}
|
||||
onClick={event => handleClick(event, id, name)}
|
||||
width={nodeWidth}
|
||||
x={nodeOffset - selectedNodeOffset}
|
||||
y={top}
|
||||
|
||||
@@ -20,7 +20,7 @@ export type ItemData = {|
|
||||
scaleX: (value: number, fallbackValue: number) => number,
|
||||
selectedFiberID: number | null,
|
||||
selectedFiberIndex: number,
|
||||
selectFiber: (id: number | null) => void,
|
||||
selectFiber: (id: number | null, name: string | null) => void,
|
||||
width: number,
|
||||
|};
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function CommitRankedAutoSizer(_: {||}) {
|
||||
const deselectCurrentFiber = useCallback(
|
||||
event => {
|
||||
event.stopPropagation();
|
||||
selectFiber(null);
|
||||
selectFiber(null, null);
|
||||
},
|
||||
[selectFiber]
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ function CommitRankedListItem({ data, index, style }: Props) {
|
||||
const handleClick = useCallback(
|
||||
event => {
|
||||
event.stopPropagation();
|
||||
selectFiber(node.id);
|
||||
selectFiber(node.id, node.name);
|
||||
},
|
||||
[node, selectFiber]
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ export type ChartNode = {|
|
||||
didRender: boolean,
|
||||
id: number,
|
||||
label: string,
|
||||
name: string,
|
||||
offset: number,
|
||||
selfDuration: number,
|
||||
treeBaseDuration: number,
|
||||
@@ -69,7 +70,7 @@ export function getChartData({
|
||||
|
||||
let label = `${name}${maybeKey}`;
|
||||
if (didRender) {
|
||||
label += ` (${selfDuration.toFixed(1)}ms) of ${actualDuration.toFixed(
|
||||
label += ` (${selfDuration.toFixed(1)}ms of ${actualDuration.toFixed(
|
||||
1
|
||||
)}ms)`;
|
||||
}
|
||||
@@ -82,6 +83,7 @@ export function getChartData({
|
||||
didRender,
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
offset: parentOffset,
|
||||
selfDuration,
|
||||
treeBaseDuration: node.treeBaseDuration,
|
||||
|
||||
@@ -19,6 +19,7 @@ import ProfilingImportExportButtons from './ProfilingImportExportButtons';
|
||||
import SnapshotSelector from './SnapshotSelector';
|
||||
import SidebarCommitInfo from './SidebarCommitInfo';
|
||||
import SidebarInteractions from './SidebarInteractions';
|
||||
import SidebarSelectedFiberInfo from './SidebarSelectedFiberInfo';
|
||||
import ToggleCommitFilterModalButton from './ToggleCommitFilterModalButton';
|
||||
|
||||
import styles from './Profiler.css';
|
||||
@@ -118,7 +119,9 @@ function SnapshotSelectorFallback() {
|
||||
// This view's subtree uses suspense to request profiler data from the backend.
|
||||
// NOTE that the structure of this UI should mirror NonSuspendingProfiler.
|
||||
function SuspendingProfiler() {
|
||||
const { selectedTabID, selectTab } = useContext(ProfilerContext);
|
||||
const { selectedFiberID, selectedTabID, selectTab } = useContext(
|
||||
ProfilerContext
|
||||
);
|
||||
const { isFilterModalShowing, setIsFilterModalShowing } = useContext(
|
||||
CommitFilterModalContext
|
||||
);
|
||||
@@ -147,7 +150,11 @@ function SuspendingProfiler() {
|
||||
break;
|
||||
case 'flame-chart':
|
||||
case 'ranked-chart':
|
||||
sidebar = <SidebarCommitInfo />;
|
||||
if (selectedFiberID !== null) {
|
||||
sidebar = <SidebarSelectedFiberInfo />;
|
||||
} else {
|
||||
sidebar = <SidebarCommitInfo />;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -56,7 +56,8 @@ type Context = {|
|
||||
|
||||
// Which fiber is currently selected in the Ranked or Flamegraph charts?
|
||||
selectedFiberID: number | null,
|
||||
selectFiber: (id: number | null) => void,
|
||||
selectedFiberName: string | null,
|
||||
selectFiber: (id: number | null, name: string | null) => void,
|
||||
|
||||
// Which interaction is currently selected in the Interactions graph?
|
||||
selectedInteractionID: number | null,
|
||||
@@ -141,13 +142,15 @@ function ProfilerContextController({ children }: Props) {
|
||||
);
|
||||
const [selectedTabID, selectTab] = useState<TabID>('flame-chart');
|
||||
const [selectedFiberID, selectFiberID] = useState<number | null>(null);
|
||||
const [selectedFiberName, selectFiberName] = useState<string | null>(null);
|
||||
const [selectedInteractionID, selectInteraction] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const selectFiber = useCallback(
|
||||
(id: number | null) => {
|
||||
(id: number | null, name: string | null) => {
|
||||
selectFiberID(id);
|
||||
selectFiberName(name);
|
||||
if (id !== null) {
|
||||
const index = store.getIndexOfElementID(id);
|
||||
if (index !== null) {
|
||||
@@ -155,7 +158,7 @@ function ProfilerContextController({ children }: Props) {
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectElementAtIndex, selectFiberID, store]
|
||||
[selectElementAtIndex, selectFiberID, selectFiberName, store]
|
||||
);
|
||||
|
||||
if (isProfiling) {
|
||||
@@ -165,6 +168,7 @@ function ProfilerContextController({ children }: Props) {
|
||||
}
|
||||
if (selectedFiberID !== null) {
|
||||
selectFiberID(null);
|
||||
selectFiberName(null);
|
||||
}
|
||||
if (selectedInteractionID !== null) {
|
||||
selectInteraction(null);
|
||||
@@ -195,6 +199,7 @@ function ProfilerContextController({ children }: Props) {
|
||||
selectCommitIndex,
|
||||
|
||||
selectedFiberID,
|
||||
selectedFiberName,
|
||||
selectFiber,
|
||||
|
||||
selectedInteractionID,
|
||||
@@ -222,6 +227,7 @@ function ProfilerContextController({ children }: Props) {
|
||||
selectCommitIndex,
|
||||
|
||||
selectedFiberID,
|
||||
selectedFiberName,
|
||||
selectFiber,
|
||||
|
||||
selectedInteractionID,
|
||||
|
||||
@@ -85,6 +85,7 @@ export default function ProfilingImportExportButtons() {
|
||||
className={styles.Input}
|
||||
type="file"
|
||||
onChange={handleFiles}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<Button
|
||||
disabled={isProfiling}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { CommitDetails, CommitTree, Node } from './types';
|
||||
export type ChartNode = {|
|
||||
id: number,
|
||||
label: string,
|
||||
name: string,
|
||||
value: number,
|
||||
|};
|
||||
|
||||
@@ -58,6 +59,7 @@ export function getChartData({
|
||||
chartNodes.push({
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
value: selfDuration,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,25 +12,35 @@
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.List,
|
||||
.InteractionList {
|
||||
.List {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.InteractionList {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.ListItem {
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.InteractionListItem {
|
||||
padding: 0.25rem 0.5rem;
|
||||
.NoInteractions {
|
||||
color: var(--color-dim);
|
||||
}
|
||||
.InteractionListItem:hover {
|
||||
|
||||
.Interactions {
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
.Interaction {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.Interaction:focus,
|
||||
.Interaction:hover {
|
||||
outline: none;
|
||||
background-color: var(--color-hover-background);
|
||||
}
|
||||
|
||||
@@ -79,3 +89,13 @@
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.NoScreenshot {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-color: var(--color-button-background-focus);
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,11 @@ export default function SidebarCommitInfo(_: Props) {
|
||||
selectTab,
|
||||
} = useContext(ProfilerContext);
|
||||
|
||||
const { profilingCache, profilingScreenshots } = useContext(StoreContext);
|
||||
const {
|
||||
captureScreenshots,
|
||||
profilingCache,
|
||||
profilingScreenshots,
|
||||
} = useContext(StoreContext);
|
||||
|
||||
const screenshot =
|
||||
selectedCommitIndex !== null
|
||||
@@ -80,31 +84,38 @@ export default function SidebarCommitInfo(_: Props) {
|
||||
ms
|
||||
</span>
|
||||
</li>
|
||||
<li className={styles.InteractionList}>
|
||||
<li className={styles.Interactions}>
|
||||
<label className={styles.Label}>Interactions</label>:
|
||||
<ul className={styles.InteractionList}>
|
||||
<div className={styles.InteractionList}>
|
||||
{interactions.length === 0 ? (
|
||||
<li className={styles.InteractionListItem}>None</li>
|
||||
<div className={styles.NoInteractions}>None</div>
|
||||
) : null}
|
||||
{interactions.map((interaction, index) => (
|
||||
<li
|
||||
<button
|
||||
key={index}
|
||||
className={styles.InteractionListItem}
|
||||
className={styles.Interaction}
|
||||
onClick={() => viewInteraction(interaction)}
|
||||
>
|
||||
{interaction.name}
|
||||
</li>
|
||||
</button>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
{screenshot != null && (
|
||||
{captureScreenshots && (
|
||||
<li>
|
||||
<img
|
||||
alt="Screenshot"
|
||||
className={styles.Screenshot}
|
||||
onClick={showScreenshotModal}
|
||||
src={screenshot}
|
||||
/>
|
||||
<label className={styles.Label}>Screenshot</label>:
|
||||
{screenshot != null ? (
|
||||
<img
|
||||
alt="Screenshot"
|
||||
className={styles.Screenshot}
|
||||
onClick={showScreenshotModal}
|
||||
src={screenshot}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.NoScreenshot}>
|
||||
No screenshot available
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
{screenshot != null && isScreenshotModalVisible && (
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
.Toolbar {
|
||||
height: 2.25rem;
|
||||
padding: 0 0.5rem;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Content {
|
||||
padding: 0.5rem;
|
||||
user-select: none;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.Component {
|
||||
flex: 1;
|
||||
color: var(--color-component-name);
|
||||
}
|
||||
.Component:before {
|
||||
white-space: nowrap;
|
||||
content: '<';
|
||||
color: var(--color-jsx-arrow-brackets);
|
||||
}
|
||||
.Component:after {
|
||||
white-space: nowrap;
|
||||
content: '>';
|
||||
color: var(--color-jsx-arrow-brackets);
|
||||
}
|
||||
|
||||
.Label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.CurrentCommit,
|
||||
.Commit {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.Commit:focus,
|
||||
.Commit:hover {
|
||||
outline: none;
|
||||
background-color: var(--color-hover-background);
|
||||
}
|
||||
|
||||
.CurrentCommit {
|
||||
background-color: var(--color-selected-background);
|
||||
color: var(--color-selected-foreground);
|
||||
}
|
||||
.CurrentCommit:focus {
|
||||
outline: none;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// @flow
|
||||
|
||||
import React, { Fragment, useContext } from 'react';
|
||||
import { ProfilerContext } from './ProfilerContext';
|
||||
import { formatDuration, formatTime } from './utils';
|
||||
import { StoreContext } from '../context';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
|
||||
import styles from './SidebarSelectedFiberInfo.css';
|
||||
|
||||
export type Props = {||};
|
||||
|
||||
export default function SidebarSelectedFiberInfo(_: Props) {
|
||||
const { profilingCache } = useContext(StoreContext);
|
||||
const {
|
||||
rendererID,
|
||||
rootID,
|
||||
selectCommitIndex,
|
||||
selectedCommitIndex,
|
||||
selectedFiberID,
|
||||
selectedFiberName,
|
||||
selectFiber,
|
||||
} = useContext(ProfilerContext);
|
||||
|
||||
const { commitTimes } = profilingCache.ProfilingSummary.read({
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
const { commitDurations } = profilingCache.FiberCommits.read({
|
||||
fiberID: ((selectedFiberID: any): number),
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
const listItems = [];
|
||||
for (let i = 0; i < commitDurations.length; i += 2) {
|
||||
const commitIndex = commitDurations[i];
|
||||
const duration = commitDurations[i + 1];
|
||||
const time = commitTimes[commitIndex];
|
||||
|
||||
listItems.push(
|
||||
<button
|
||||
key={commitIndex}
|
||||
className={
|
||||
selectedCommitIndex === commitIndex
|
||||
? styles.CurrentCommit
|
||||
: styles.Commit
|
||||
}
|
||||
onClick={() => selectCommitIndex(commitIndex)}
|
||||
>
|
||||
{formatTime(time)}s for {formatDuration(duration)}ms
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles.Toolbar}>
|
||||
<div className={styles.Component}>
|
||||
{selectedFiberName || 'Selected component'}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className={styles.IconButton}
|
||||
onClick={() => selectFiber(null, null)}
|
||||
title="Back to commit view"
|
||||
>
|
||||
<ButtonIcon type="close" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.Content}>
|
||||
<label className={styles.Label}>Rendered at</label>: {listItems}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
}
|
||||
.Commits:focus {
|
||||
outline: none;
|
||||
background-color: var(--color-button-background-focus);
|
||||
}
|
||||
|
||||
.IndexLabel {
|
||||
|
||||
@@ -34,6 +34,12 @@ export type CommitDetails = {|
|
||||
interactions: Array<Interaction>,
|
||||
|};
|
||||
|
||||
export type FiberCommits = {|
|
||||
commitDurations: Array<number>,
|
||||
fiberID: number,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
export type ProfilingSummary = {|
|
||||
rootID: number,
|
||||
|
||||
|
||||
@@ -51,3 +51,9 @@
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.ScreenshotThrottling {
|
||||
background-color: var(--color-hover-background);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -117,16 +117,20 @@ export default function Settings({ portalContainer }: Props) {
|
||||
{store.supportsCaptureScreenshots && (
|
||||
<div className={styles.Section}>
|
||||
<div className={styles.Header}>Profiler</div>
|
||||
<div className={styles.OptionGroup}>
|
||||
<label className={styles.Option}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={captureScreenshots}
|
||||
onChange={updateCaptureScreenshotsWhileProfiling}
|
||||
/>{' '}
|
||||
Capture screenshots while profiling
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={captureScreenshots}
|
||||
onChange={updateCaptureScreenshotsWhileProfiling}
|
||||
/>{' '}
|
||||
Capture screenshots while profiling
|
||||
{captureScreenshots && (
|
||||
<p className={styles.ScreenshotThrottling}>
|
||||
Screenshots will be throttled in order to reduce the negative
|
||||
impact on performance.
|
||||
</p>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
.TabCurrent:hover {
|
||||
background-color: var(--color-hover-background);
|
||||
}
|
||||
.Tab:focus-within,
|
||||
.TabCurrent:focus-within {
|
||||
background-color: var(--color-hover-background);
|
||||
}
|
||||
|
||||
.TabCurrent {
|
||||
border-bottom: 3px solid var(--color-selected-border);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { Fragment, useCallback } from 'react';
|
||||
import Icon from './Icon';
|
||||
|
||||
@@ -60,9 +61,11 @@ export default function TabBar({
|
||||
<Fragment>
|
||||
{tabs.map(({ icon, id, label, title }) => (
|
||||
<label
|
||||
className={`${tabClassName} ${
|
||||
disabled ? styles.TabDisabled : styles.Tab
|
||||
} ${!disabled && currentTab === id ? styles.TabCurrent : ''}`}
|
||||
className={classNames(
|
||||
tabClassName,
|
||||
disabled ? styles.TabDisabled : styles.Tab,
|
||||
!disabled && currentTab === id ? styles.TabCurrent : null
|
||||
)}
|
||||
key={id}
|
||||
onKeyDown={handleKeyDown}
|
||||
title={title || label}
|
||||
|
||||
@@ -10,6 +10,7 @@ type Props = {
|
||||
isChecked: boolean,
|
||||
isDisabled?: boolean,
|
||||
onChange: (isChecked: boolean) => void,
|
||||
title?: string,
|
||||
};
|
||||
|
||||
export default function Toggle({
|
||||
@@ -18,6 +19,7 @@ export default function Toggle({
|
||||
isDisabled = false,
|
||||
isChecked,
|
||||
onChange,
|
||||
title,
|
||||
}: Props) {
|
||||
let defaultClassName;
|
||||
if (isDisabled) {
|
||||
@@ -36,7 +38,7 @@ export default function Toggle({
|
||||
);
|
||||
|
||||
return (
|
||||
<label className={`${defaultClassName} ${className}`}>
|
||||
<label className={`${defaultClassName} ${className}`} title={title}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.Input}
|
||||
|
||||
@@ -1,7 +1,40 @@
|
||||
// @flow
|
||||
|
||||
import throttle from 'lodash.throttle';
|
||||
import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||
|
||||
export function useIsOverflowing(
|
||||
containerRef: { current: HTMLDivElement | null },
|
||||
totalChildWidth: number
|
||||
): boolean {
|
||||
const [isOverflowing, setIsOverflowing] = useState<boolean>(false);
|
||||
|
||||
// It's important to use a layout effect, so that we avoid showing a flash of overflowed content.
|
||||
useLayoutEffect(() => {
|
||||
if (containerRef.current === null) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const container = ((containerRef.current: any): HTMLDivElement);
|
||||
|
||||
const handleResize = throttle(
|
||||
() => setIsOverflowing(container.clientWidth <= totalChildWidth),
|
||||
100
|
||||
);
|
||||
|
||||
handleResize();
|
||||
|
||||
// It's important to listen to the ownerDocument.defaultView to support the browser extension.
|
||||
// Here we use portals to render individual tabs (e.g. Profiler),
|
||||
// and the root document might belong to a different window.
|
||||
const ownerWindow = container.ownerDocument.defaultView;
|
||||
ownerWindow.addEventListener('resize', handleResize);
|
||||
return () => ownerWindow.removeEventListener('resize', handleResize);
|
||||
}, [containerRef, totalChildWidth]);
|
||||
|
||||
return isOverflowing;
|
||||
}
|
||||
|
||||
// Forked from https://usehooks.com/useLocalStorage/
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
@@ -68,9 +101,15 @@ export function useModalDismissSignal(
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseOrTouch = ({ target }: any) => {
|
||||
const handleClick = (event: any) => {
|
||||
// $FlowFixMe
|
||||
if (modalRef.current !== null && !modalRef.current.contains(target)) {
|
||||
if (
|
||||
modalRef.current !== null &&
|
||||
!modalRef.current.contains(event.target)
|
||||
) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
dismissCallback();
|
||||
}
|
||||
};
|
||||
@@ -80,13 +119,11 @@ export function useModalDismissSignal(
|
||||
// and the root document might belong to a different window.
|
||||
const ownerDocument = modalRef.current.ownerDocument;
|
||||
ownerDocument.addEventListener('keydown', handleKeyDown);
|
||||
ownerDocument.addEventListener('mousedown', handleMouseOrTouch);
|
||||
ownerDocument.addEventListener('touchstart', handleMouseOrTouch);
|
||||
ownerDocument.addEventListener('click', handleClick);
|
||||
|
||||
return () => {
|
||||
ownerDocument.removeEventListener('keydown', handleKeyDown);
|
||||
ownerDocument.removeEventListener('mousedown', handleMouseOrTouch);
|
||||
ownerDocument.removeEventListener('touchstart', handleMouseOrTouch);
|
||||
ownerDocument.removeEventListener('click', handleClick);
|
||||
};
|
||||
}, [modalRef, dismissCallback]);
|
||||
}
|
||||
|
||||
@@ -4683,10 +4683,10 @@ flatstr@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.5.tgz#5b451b08cbd48e2eac54a2bbe0bf46165aa14be3"
|
||||
|
||||
flow-bin@^0.94.0:
|
||||
version "0.94.0"
|
||||
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.94.0.tgz#b5d58fe7559705b73a18229f97edfc3ab6ffffcb"
|
||||
integrity sha512-DYF7r9CJ/AksfmmB4+q+TyLMoeQPRnqtF1Pk7KY3zgfkB/nVuA3nXyzqgsIPIvnMSiFEXQcFK4z+iPxSLckZhQ==
|
||||
flow-bin@^0.96.0:
|
||||
version "0.96.0"
|
||||
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.96.0.tgz#3b0379d97304dc1879ae6db627cd2d6819998661"
|
||||
integrity sha512-OSxERs0EdhVxEVCst/HmlT/RcnXsQQIRqcfK9J9wC8/93JQj+xQz4RtlsmYe1PSRYaozuDLyPS5pIA81Zwzaww==
|
||||
|
||||
flush-write-stream@^1.0.0:
|
||||
version "1.1.1"
|
||||
@@ -7053,6 +7053,11 @@ lodash.templatesettings@^4.0.0:
|
||||
dependencies:
|
||||
lodash._reinterpolate "~3.0.0"
|
||||
|
||||
lodash.throttle@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
|
||||
integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
|
||||
|
||||
lodash@3.10.1, lodash@^3.10.0:
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
|
||||
|
||||
Reference in New Issue
Block a user