mirror of
https://github.com/facebook/react.git
synced 2026-02-26 18:58:05 +00:00
## Summary - While rolling out RDT 5.2.0 on Fusebox, we've discovered that context menus don't work well with this environment. The reason for it is the context menu state implementation - in a global context we define a map of registered context menus, basically what is shown at the moment (see deleted Contexts.js file). These maps are not invalidated on each re-initialization of DevTools frontend, since the bundle (react-devtools-fusebox module) is not reloaded, and this results into RDT throwing an error that some context menu was already registered. - We should not keep such data in a global state, since there is no guarantee that this will be invalidated with each re-initialization of DevTools (like with browser extension, for example). - The new implementation is based on a `ContextMenuContainer` component, which will add all required `contextmenu` event listeners to the anchor-element. This component will also receive a list of `items` that will be displayed in the shown context menu. - The `ContextMenuContainer` component is also using `useImperativeHandle` hook to extend the instance of the component, so context menus can be managed imperatively via `ref`: `contextMenu.current?.hide()`, for example. - **Changed**: The option for copying value to clipboard is now hidden for functions. The reasons for it are: - It is broken in the current implementation, because we call `JSON.stringify` on the value, see `packages/react-devtools-shared/src/backend/utils.js`. - I don't see any reasonable value in doing this for the user, since `Go to definition` option is available and you can inspect the real code and then copy it. - We already filter out fields from objects, if their value is a function, because the whole object is passed to `JSON.stringify`. ## How did you test this change? ### Works with element props and hooks: - All context menu items work reliably for props items - All context menu items work reliably or hooks items https://github.com/facebook/react/assets/28902667/5e2d58b0-92fa-4624-ad1e-2bbd7f12678f ### Works with timeline profiler: - All context menu items work reliably: copying, zooming, ... - Context menu automatically closes on the scroll event https://github.com/facebook/react/assets/28902667/de744cd0-372a-402a-9fa0-743857048d24 ### Works with Fusebox: - Produces no errors - Copy to clipboard context menu item works reliably https://github.com/facebook/react/assets/28902667/0288f5bf-0d44-435c-8842-6b57bc8a7a24
729 lines
21 KiB
JavaScript
729 lines
21 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 {Interaction, Point} from './view-base';
|
|
import type {ReactEventInfo, TimelineData, ViewState} from './types';
|
|
|
|
import * as React from 'react';
|
|
import {
|
|
Fragment,
|
|
useContext,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useRef,
|
|
useState,
|
|
useCallback,
|
|
} from 'react';
|
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
|
|
|
import {
|
|
HorizontalPanAndZoomView,
|
|
ResizableView,
|
|
VerticalScrollOverflowView,
|
|
Surface,
|
|
VerticalScrollView,
|
|
View,
|
|
useCanvasInteraction,
|
|
verticallyStackedLayout,
|
|
zeroPoint,
|
|
} from './view-base';
|
|
import {
|
|
ComponentMeasuresView,
|
|
FlamechartView,
|
|
NativeEventsView,
|
|
NetworkMeasuresView,
|
|
ReactMeasuresView,
|
|
SchedulingEventsView,
|
|
SnapshotsView,
|
|
SuspenseEventsView,
|
|
ThrownErrorsView,
|
|
TimeAxisMarkersView,
|
|
UserTimingMarksView,
|
|
} from './content-views';
|
|
import {COLORS} from './content-views/constants';
|
|
import {clampState, moveStateToRange} from './view-base/utils/scrollState';
|
|
import EventTooltip from './EventTooltip';
|
|
import {MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL} from './view-base/constants';
|
|
import {TimelineSearchContext} from './TimelineSearchContext';
|
|
import {TimelineContext} from './TimelineContext';
|
|
import CanvasPageContextMenu from './CanvasPageContextMenu';
|
|
|
|
import type {ContextMenuRef} from 'react-devtools-shared/src/devtools/ContextMenu/types';
|
|
|
|
import styles from './CanvasPage.css';
|
|
|
|
type Props = {
|
|
profilerData: TimelineData,
|
|
viewState: ViewState,
|
|
};
|
|
|
|
function CanvasPage({profilerData, viewState}: Props): React.Node {
|
|
return (
|
|
<div
|
|
className={styles.CanvasPage}
|
|
style={{backgroundColor: COLORS.BACKGROUND}}>
|
|
<AutoSizer>
|
|
{({height, width}: {height: number, width: number}) => (
|
|
<AutoSizedCanvas
|
|
data={profilerData}
|
|
height={height}
|
|
viewState={viewState}
|
|
width={width}
|
|
/>
|
|
)}
|
|
</AutoSizer>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const EMPTY_CONTEXT_INFO: ReactEventInfo = {
|
|
componentMeasure: null,
|
|
flamechartStackFrame: null,
|
|
measure: null,
|
|
nativeEvent: null,
|
|
networkMeasure: null,
|
|
schedulingEvent: null,
|
|
snapshot: null,
|
|
suspenseEvent: null,
|
|
thrownError: null,
|
|
userTimingMark: null,
|
|
};
|
|
|
|
type AutoSizedCanvasProps = {
|
|
data: TimelineData,
|
|
height: number,
|
|
viewState: ViewState,
|
|
width: number,
|
|
};
|
|
|
|
function AutoSizedCanvas({
|
|
data,
|
|
height,
|
|
viewState,
|
|
width,
|
|
}: AutoSizedCanvasProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
|
|
const [mouseLocation, setMouseLocation] = useState<Point>(zeroPoint); // DOM coordinates
|
|
const [hoveredEvent, setHoveredEvent] = useState<ReactEventInfo | null>(null);
|
|
const [lastHoveredEvent, setLastHoveredEvent] =
|
|
useState<ReactEventInfo | null>(null);
|
|
|
|
const contextMenuRef: ContextMenuRef = useRef(null);
|
|
|
|
const resetHoveredEvent = useCallback(
|
|
() => setHoveredEvent(EMPTY_CONTEXT_INFO),
|
|
[],
|
|
);
|
|
const updateHoveredEvent = useCallback(
|
|
(event: ReactEventInfo) => {
|
|
setHoveredEvent(event);
|
|
|
|
// If menu is already open, don't update the hovered event data
|
|
// So the same set of menu items is preserved until the current context menu is closed
|
|
if (contextMenuRef.current?.isShown()) {
|
|
return;
|
|
}
|
|
|
|
const {
|
|
componentMeasure,
|
|
flamechartStackFrame,
|
|
measure,
|
|
networkMeasure,
|
|
schedulingEvent,
|
|
suspenseEvent,
|
|
} = event;
|
|
|
|
// We have to keep track of last non-empty hovered event, since this will be the input for context menu items
|
|
// We can't just pass hoveredEvent to ContextMenuContainer,
|
|
// since it will be reset each time user moves mouse away from event object on the canvas
|
|
if (
|
|
componentMeasure != null ||
|
|
flamechartStackFrame != null ||
|
|
measure != null ||
|
|
networkMeasure != null ||
|
|
schedulingEvent != null ||
|
|
suspenseEvent != null
|
|
) {
|
|
setLastHoveredEvent(event);
|
|
}
|
|
},
|
|
[contextMenuRef],
|
|
);
|
|
|
|
const {searchIndex, searchRegExp, searchResults} = useContext(
|
|
TimelineSearchContext,
|
|
);
|
|
|
|
// This effect searches timeline data and scrolls to the next match wen search criteria change.
|
|
useLayoutEffect(() => {
|
|
viewState.updateSearchRegExpState(searchRegExp);
|
|
|
|
const componentMeasureSearchResult =
|
|
searchResults.length > 0 ? searchResults[searchIndex] : null;
|
|
if (componentMeasureSearchResult != null) {
|
|
const scrollState = moveStateToRange({
|
|
state: viewState.horizontalScrollState,
|
|
rangeStart: componentMeasureSearchResult.timestamp,
|
|
rangeEnd:
|
|
componentMeasureSearchResult.timestamp +
|
|
componentMeasureSearchResult.duration,
|
|
contentLength: data.duration,
|
|
minContentLength: data.duration * MIN_ZOOM_LEVEL,
|
|
maxContentLength: data.duration * MAX_ZOOM_LEVEL,
|
|
containerLength: width,
|
|
});
|
|
|
|
viewState.updateHorizontalScrollState(scrollState);
|
|
}
|
|
|
|
surfaceRef.current.displayIfNeeded();
|
|
}, [searchIndex, searchRegExp, searchResults, viewState]);
|
|
|
|
const surfaceRef = useRef(new Surface(resetHoveredEvent));
|
|
const userTimingMarksViewRef = useRef<null | UserTimingMarksView>(null);
|
|
const nativeEventsViewRef = useRef<null | NativeEventsView>(null);
|
|
const schedulingEventsViewRef = useRef<null | SchedulingEventsView>(null);
|
|
const suspenseEventsViewRef = useRef<null | SuspenseEventsView>(null);
|
|
const componentMeasuresViewRef = useRef<null | ComponentMeasuresView>(null);
|
|
const reactMeasuresViewRef = useRef<null | ReactMeasuresView>(null);
|
|
const flamechartViewRef = useRef<null | FlamechartView>(null);
|
|
const networkMeasuresViewRef = useRef<null | NetworkMeasuresView>(null);
|
|
const snapshotsViewRef = useRef<null | SnapshotsView>(null);
|
|
const thrownErrorsViewRef = useRef<null | ThrownErrorsView>(null);
|
|
|
|
useLayoutEffect(() => {
|
|
const surface = surfaceRef.current;
|
|
const defaultFrame = {origin: zeroPoint, size: {width, height}};
|
|
|
|
// Auto hide context menu when panning.
|
|
viewState.onHorizontalScrollStateChange(scrollState => {
|
|
contextMenuRef.current?.hide();
|
|
});
|
|
|
|
// Initialize horizontal view state
|
|
viewState.updateHorizontalScrollState(
|
|
clampState({
|
|
state: viewState.horizontalScrollState,
|
|
minContentLength: data.duration * MIN_ZOOM_LEVEL,
|
|
maxContentLength: data.duration * MAX_ZOOM_LEVEL,
|
|
containerLength: defaultFrame.size.width,
|
|
}),
|
|
);
|
|
|
|
function createViewHelper(
|
|
view: View,
|
|
label: string,
|
|
shouldScrollVertically: boolean = false,
|
|
shouldResizeVertically: boolean = false,
|
|
): View {
|
|
let verticalScrollView = null;
|
|
if (shouldScrollVertically) {
|
|
verticalScrollView = new VerticalScrollView(
|
|
surface,
|
|
defaultFrame,
|
|
view,
|
|
viewState,
|
|
label,
|
|
);
|
|
}
|
|
|
|
const horizontalPanAndZoomView = new HorizontalPanAndZoomView(
|
|
surface,
|
|
defaultFrame,
|
|
verticalScrollView !== null ? verticalScrollView : view,
|
|
data.duration,
|
|
viewState,
|
|
);
|
|
|
|
let resizableView = null;
|
|
if (shouldResizeVertically) {
|
|
resizableView = new ResizableView(
|
|
surface,
|
|
defaultFrame,
|
|
horizontalPanAndZoomView,
|
|
viewState,
|
|
canvasRef,
|
|
label,
|
|
);
|
|
}
|
|
|
|
return resizableView || horizontalPanAndZoomView;
|
|
}
|
|
|
|
const axisMarkersView = new TimeAxisMarkersView(
|
|
surface,
|
|
defaultFrame,
|
|
data.duration,
|
|
);
|
|
const axisMarkersViewWrapper = createViewHelper(axisMarkersView, 'time');
|
|
|
|
let userTimingMarksViewWrapper = null;
|
|
if (data.otherUserTimingMarks.length > 0) {
|
|
const userTimingMarksView = new UserTimingMarksView(
|
|
surface,
|
|
defaultFrame,
|
|
data.otherUserTimingMarks,
|
|
data.duration,
|
|
);
|
|
userTimingMarksViewRef.current = userTimingMarksView;
|
|
userTimingMarksViewWrapper = createViewHelper(
|
|
userTimingMarksView,
|
|
'user timing api',
|
|
);
|
|
}
|
|
|
|
let nativeEventsViewWrapper = null;
|
|
if (data.nativeEvents.length > 0) {
|
|
const nativeEventsView = new NativeEventsView(
|
|
surface,
|
|
defaultFrame,
|
|
data,
|
|
);
|
|
nativeEventsViewRef.current = nativeEventsView;
|
|
nativeEventsViewWrapper = createViewHelper(
|
|
nativeEventsView,
|
|
'events',
|
|
true,
|
|
true,
|
|
);
|
|
}
|
|
|
|
let thrownErrorsViewWrapper = null;
|
|
if (data.thrownErrors.length > 0) {
|
|
const thrownErrorsView = new ThrownErrorsView(
|
|
surface,
|
|
defaultFrame,
|
|
data,
|
|
);
|
|
thrownErrorsViewRef.current = thrownErrorsView;
|
|
thrownErrorsViewWrapper = createViewHelper(
|
|
thrownErrorsView,
|
|
'thrown errors',
|
|
);
|
|
}
|
|
|
|
let schedulingEventsViewWrapper = null;
|
|
if (data.schedulingEvents.length > 0) {
|
|
const schedulingEventsView = new SchedulingEventsView(
|
|
surface,
|
|
defaultFrame,
|
|
data,
|
|
);
|
|
schedulingEventsViewRef.current = schedulingEventsView;
|
|
schedulingEventsViewWrapper = createViewHelper(
|
|
schedulingEventsView,
|
|
'react updates',
|
|
);
|
|
}
|
|
|
|
let suspenseEventsViewWrapper = null;
|
|
if (data.suspenseEvents.length > 0) {
|
|
const suspenseEventsView = new SuspenseEventsView(
|
|
surface,
|
|
defaultFrame,
|
|
data,
|
|
);
|
|
suspenseEventsViewRef.current = suspenseEventsView;
|
|
suspenseEventsViewWrapper = createViewHelper(
|
|
suspenseEventsView,
|
|
'suspense',
|
|
true,
|
|
true,
|
|
);
|
|
}
|
|
|
|
const reactMeasuresView = new ReactMeasuresView(
|
|
surface,
|
|
defaultFrame,
|
|
data,
|
|
);
|
|
reactMeasuresViewRef.current = reactMeasuresView;
|
|
const reactMeasuresViewWrapper = createViewHelper(
|
|
reactMeasuresView,
|
|
'react scheduling',
|
|
true,
|
|
true,
|
|
);
|
|
|
|
let componentMeasuresViewWrapper = null;
|
|
if (data.componentMeasures.length > 0) {
|
|
const componentMeasuresView = new ComponentMeasuresView(
|
|
surface,
|
|
defaultFrame,
|
|
data,
|
|
viewState,
|
|
);
|
|
componentMeasuresViewRef.current = componentMeasuresView;
|
|
componentMeasuresViewWrapper = createViewHelper(
|
|
componentMeasuresView,
|
|
'react components',
|
|
);
|
|
}
|
|
|
|
let snapshotsViewWrapper = null;
|
|
if (data.snapshots.length > 0) {
|
|
const snapshotsView = new SnapshotsView(surface, defaultFrame, data);
|
|
snapshotsViewRef.current = snapshotsView;
|
|
snapshotsViewWrapper = createViewHelper(
|
|
snapshotsView,
|
|
'snapshots',
|
|
true,
|
|
true,
|
|
);
|
|
}
|
|
|
|
let networkMeasuresViewWrapper = null;
|
|
if (data.snapshots.length > 0) {
|
|
const networkMeasuresView = new NetworkMeasuresView(
|
|
surface,
|
|
defaultFrame,
|
|
data,
|
|
);
|
|
networkMeasuresViewRef.current = networkMeasuresView;
|
|
networkMeasuresViewWrapper = createViewHelper(
|
|
networkMeasuresView,
|
|
'network',
|
|
true,
|
|
true,
|
|
);
|
|
}
|
|
|
|
let flamechartViewWrapper = null;
|
|
if (data.flamechart.length > 0) {
|
|
const flamechartView = new FlamechartView(
|
|
surface,
|
|
defaultFrame,
|
|
data.flamechart,
|
|
data.internalModuleSourceToRanges,
|
|
data.duration,
|
|
);
|
|
flamechartViewRef.current = flamechartView;
|
|
flamechartViewWrapper = createViewHelper(
|
|
flamechartView,
|
|
'flamechart',
|
|
true,
|
|
true,
|
|
);
|
|
}
|
|
|
|
// Root view contains all of the sub views defined above.
|
|
// The order we add them below determines their vertical position.
|
|
const rootView = new View(
|
|
surface,
|
|
defaultFrame,
|
|
verticallyStackedLayout,
|
|
defaultFrame,
|
|
COLORS.BACKGROUND,
|
|
);
|
|
rootView.addSubview(axisMarkersViewWrapper);
|
|
if (userTimingMarksViewWrapper !== null) {
|
|
rootView.addSubview(userTimingMarksViewWrapper);
|
|
}
|
|
if (nativeEventsViewWrapper !== null) {
|
|
rootView.addSubview(nativeEventsViewWrapper);
|
|
}
|
|
if (schedulingEventsViewWrapper !== null) {
|
|
rootView.addSubview(schedulingEventsViewWrapper);
|
|
}
|
|
if (thrownErrorsViewWrapper !== null) {
|
|
rootView.addSubview(thrownErrorsViewWrapper);
|
|
}
|
|
if (suspenseEventsViewWrapper !== null) {
|
|
rootView.addSubview(suspenseEventsViewWrapper);
|
|
}
|
|
if (reactMeasuresViewWrapper !== null) {
|
|
rootView.addSubview(reactMeasuresViewWrapper);
|
|
}
|
|
if (componentMeasuresViewWrapper !== null) {
|
|
rootView.addSubview(componentMeasuresViewWrapper);
|
|
}
|
|
if (snapshotsViewWrapper !== null) {
|
|
rootView.addSubview(snapshotsViewWrapper);
|
|
}
|
|
if (networkMeasuresViewWrapper !== null) {
|
|
rootView.addSubview(networkMeasuresViewWrapper);
|
|
}
|
|
if (flamechartViewWrapper !== null) {
|
|
rootView.addSubview(flamechartViewWrapper);
|
|
}
|
|
|
|
const verticalScrollOverflowView = new VerticalScrollOverflowView(
|
|
surface,
|
|
defaultFrame,
|
|
rootView,
|
|
viewState,
|
|
);
|
|
|
|
surfaceRef.current.rootView = verticalScrollOverflowView;
|
|
}, [data]);
|
|
|
|
useLayoutEffect(() => {
|
|
if (canvasRef.current) {
|
|
surfaceRef.current.setCanvas(canvasRef.current, {width, height});
|
|
}
|
|
}, [width, height]);
|
|
|
|
const interactor = useCallback((interaction: Interaction) => {
|
|
const canvas = canvasRef.current;
|
|
if (canvas === null) {
|
|
return;
|
|
}
|
|
|
|
const surface = surfaceRef.current;
|
|
surface.handleInteraction(interaction);
|
|
|
|
// Flush any display work that got queued up as part of the previous interaction.
|
|
// Typically there should be no work, but certain interactions may need a second pass.
|
|
// For example, the ResizableView may collapse/expand its contents,
|
|
// which requires a second layout pass for an ancestor VerticalScrollOverflowView.
|
|
//
|
|
// TODO It would be nice to remove this call for performance reasons.
|
|
// To do that, we'll need to address the UX bug with VerticalScrollOverflowView.
|
|
// For more info see: https://github.com/facebook/react/pull/22005#issuecomment-896953399
|
|
surface.displayIfNeeded();
|
|
|
|
canvas.style.cursor = surface.getCurrentCursor() || 'default';
|
|
|
|
// Defer drawing to canvas until React's commit phase, to avoid drawing
|
|
// twice and to ensure that both the canvas and DOM elements managed by
|
|
// React are in sync.
|
|
setMouseLocation({
|
|
x: interaction.payload.event.x,
|
|
y: interaction.payload.event.y,
|
|
});
|
|
}, []);
|
|
|
|
useCanvasInteraction(canvasRef, interactor);
|
|
|
|
const {selectEvent} = useContext(TimelineContext);
|
|
|
|
useEffect(() => {
|
|
const {current: userTimingMarksView} = userTimingMarksViewRef;
|
|
if (userTimingMarksView) {
|
|
userTimingMarksView.onHover = userTimingMark => {
|
|
if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) {
|
|
updateHoveredEvent({
|
|
...EMPTY_CONTEXT_INFO,
|
|
userTimingMark,
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
const {current: nativeEventsView} = nativeEventsViewRef;
|
|
if (nativeEventsView) {
|
|
nativeEventsView.onHover = nativeEvent => {
|
|
if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) {
|
|
updateHoveredEvent({
|
|
...EMPTY_CONTEXT_INFO,
|
|
nativeEvent,
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
const {current: schedulingEventsView} = schedulingEventsViewRef;
|
|
if (schedulingEventsView) {
|
|
schedulingEventsView.onHover = schedulingEvent => {
|
|
if (!hoveredEvent || hoveredEvent.schedulingEvent !== schedulingEvent) {
|
|
updateHoveredEvent({
|
|
...EMPTY_CONTEXT_INFO,
|
|
schedulingEvent,
|
|
});
|
|
}
|
|
};
|
|
schedulingEventsView.onClick = schedulingEvent => {
|
|
selectEvent({
|
|
...EMPTY_CONTEXT_INFO,
|
|
schedulingEvent,
|
|
});
|
|
};
|
|
}
|
|
|
|
const {current: suspenseEventsView} = suspenseEventsViewRef;
|
|
if (suspenseEventsView) {
|
|
suspenseEventsView.onHover = suspenseEvent => {
|
|
if (!hoveredEvent || hoveredEvent.suspenseEvent !== suspenseEvent) {
|
|
updateHoveredEvent({
|
|
...EMPTY_CONTEXT_INFO,
|
|
suspenseEvent,
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
const {current: reactMeasuresView} = reactMeasuresViewRef;
|
|
if (reactMeasuresView) {
|
|
reactMeasuresView.onHover = measure => {
|
|
if (!hoveredEvent || hoveredEvent.measure !== measure) {
|
|
updateHoveredEvent({
|
|
...EMPTY_CONTEXT_INFO,
|
|
measure,
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
const {current: componentMeasuresView} = componentMeasuresViewRef;
|
|
if (componentMeasuresView) {
|
|
componentMeasuresView.onHover = componentMeasure => {
|
|
if (
|
|
!hoveredEvent ||
|
|
hoveredEvent.componentMeasure !== componentMeasure
|
|
) {
|
|
updateHoveredEvent({
|
|
...EMPTY_CONTEXT_INFO,
|
|
componentMeasure,
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
const {current: snapshotsView} = snapshotsViewRef;
|
|
if (snapshotsView) {
|
|
snapshotsView.onHover = snapshot => {
|
|
if (!hoveredEvent || hoveredEvent.snapshot !== snapshot) {
|
|
updateHoveredEvent({
|
|
...EMPTY_CONTEXT_INFO,
|
|
snapshot,
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
const {current: flamechartView} = flamechartViewRef;
|
|
if (flamechartView) {
|
|
flamechartView.setOnHover(flamechartStackFrame => {
|
|
if (
|
|
!hoveredEvent ||
|
|
hoveredEvent.flamechartStackFrame !== flamechartStackFrame
|
|
) {
|
|
updateHoveredEvent({
|
|
...EMPTY_CONTEXT_INFO,
|
|
flamechartStackFrame,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
const {current: networkMeasuresView} = networkMeasuresViewRef;
|
|
if (networkMeasuresView) {
|
|
networkMeasuresView.onHover = networkMeasure => {
|
|
if (!hoveredEvent || hoveredEvent.networkMeasure !== networkMeasure) {
|
|
updateHoveredEvent({
|
|
...EMPTY_CONTEXT_INFO,
|
|
networkMeasure,
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
const {current: thrownErrorsView} = thrownErrorsViewRef;
|
|
if (thrownErrorsView) {
|
|
thrownErrorsView.onHover = thrownError => {
|
|
if (!hoveredEvent || hoveredEvent.thrownError !== thrownError) {
|
|
updateHoveredEvent({
|
|
...EMPTY_CONTEXT_INFO,
|
|
thrownError,
|
|
});
|
|
}
|
|
};
|
|
}
|
|
}, [
|
|
hoveredEvent,
|
|
data, // Attach onHover callbacks when views are re-created on data change
|
|
]);
|
|
|
|
useLayoutEffect(() => {
|
|
const userTimingMarksView = userTimingMarksViewRef.current;
|
|
if (userTimingMarksView) {
|
|
userTimingMarksView.setHoveredMark(
|
|
hoveredEvent ? hoveredEvent.userTimingMark : null,
|
|
);
|
|
}
|
|
|
|
const nativeEventsView = nativeEventsViewRef.current;
|
|
if (nativeEventsView) {
|
|
nativeEventsView.setHoveredEvent(
|
|
hoveredEvent ? hoveredEvent.nativeEvent : null,
|
|
);
|
|
}
|
|
|
|
const schedulingEventsView = schedulingEventsViewRef.current;
|
|
if (schedulingEventsView) {
|
|
schedulingEventsView.setHoveredEvent(
|
|
hoveredEvent ? hoveredEvent.schedulingEvent : null,
|
|
);
|
|
}
|
|
|
|
const suspenseEventsView = suspenseEventsViewRef.current;
|
|
if (suspenseEventsView) {
|
|
suspenseEventsView.setHoveredEvent(
|
|
hoveredEvent ? hoveredEvent.suspenseEvent : null,
|
|
);
|
|
}
|
|
|
|
const reactMeasuresView = reactMeasuresViewRef.current;
|
|
if (reactMeasuresView) {
|
|
reactMeasuresView.setHoveredMeasure(
|
|
hoveredEvent ? hoveredEvent.measure : null,
|
|
);
|
|
}
|
|
|
|
const flamechartView = flamechartViewRef.current;
|
|
if (flamechartView) {
|
|
flamechartView.setHoveredFlamechartStackFrame(
|
|
hoveredEvent ? hoveredEvent.flamechartStackFrame : null,
|
|
);
|
|
}
|
|
|
|
const networkMeasuresView = networkMeasuresViewRef.current;
|
|
if (networkMeasuresView) {
|
|
networkMeasuresView.setHoveredEvent(
|
|
hoveredEvent ? hoveredEvent.networkMeasure : null,
|
|
);
|
|
}
|
|
}, [hoveredEvent]);
|
|
|
|
// Draw to canvas in React's commit phase
|
|
useLayoutEffect(() => {
|
|
surfaceRef.current.displayIfNeeded();
|
|
});
|
|
|
|
return (
|
|
<Fragment>
|
|
<canvas ref={canvasRef} height={height} width={width} />
|
|
|
|
<CanvasPageContextMenu
|
|
canvasRef={canvasRef}
|
|
hoveredEvent={lastHoveredEvent}
|
|
timelineData={data}
|
|
viewState={viewState}
|
|
canvasWidth={width}
|
|
closedMenuStub={
|
|
!surfaceRef.current.hasActiveView() ? (
|
|
<EventTooltip
|
|
canvasRef={canvasRef}
|
|
data={data}
|
|
height={height}
|
|
hoveredEvent={hoveredEvent}
|
|
origin={mouseLocation}
|
|
width={width}
|
|
/>
|
|
) : null
|
|
}
|
|
ref={contextMenuRef}
|
|
/>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
export default CanvasPage;
|