mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
Addresses https://github.com/facebook/react/issues/32244. ### Chromium We will use [chrome.permissions](https://developer.chrome.com/docs/extensions/reference/api/permissions) for checking / requesting `clipboardWrite` permission before copying something to the clipboard. ### Firefox We will keep `clipboardWrite` as a required permission, because there is no reliable and working API for requesting optional permissions for extensions that are extending browser DevTools: - `chrome.permissions` is unavailable for devtools pages - https://bugzilla.mozilla.org/show_bug.cgi?id=1796933 - You can't call `chrome.permissions.request` from background, because this instruction has to be executed inside user-event callback, basically only initiated by user. I don't really want to come up with solutions like opening a new tab with a button that user has to click.
183 lines
4.5 KiB
JavaScript
183 lines
4.5 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 * as React from 'react';
|
|
import {useMemo} from 'react';
|
|
import {copy} from 'clipboard-js';
|
|
import prettyMilliseconds from 'pretty-ms';
|
|
|
|
import ContextMenuContainer from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenuContainer';
|
|
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
|
|
|
import {getBatchRange} from './utils/getBatchRange';
|
|
import {moveStateToRange} from './view-base/utils/scrollState';
|
|
import {MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL} from './view-base/constants';
|
|
|
|
import type {
|
|
ContextMenuItem,
|
|
ContextMenuRef,
|
|
} from 'react-devtools-shared/src/devtools/ContextMenu/types';
|
|
import type {
|
|
ReactEventInfo,
|
|
ReactMeasure,
|
|
TimelineData,
|
|
ViewState,
|
|
} from './types';
|
|
|
|
function zoomToBatch(
|
|
data: TimelineData,
|
|
measure: ReactMeasure,
|
|
viewState: ViewState,
|
|
width: number,
|
|
) {
|
|
const {batchUID} = measure;
|
|
const [rangeStart, rangeEnd] = getBatchRange(batchUID, data);
|
|
|
|
// Convert from time range to ScrollState
|
|
const scrollState = moveStateToRange({
|
|
state: viewState.horizontalScrollState,
|
|
rangeStart,
|
|
rangeEnd,
|
|
contentLength: data.duration,
|
|
|
|
minContentLength: data.duration * MIN_ZOOM_LEVEL,
|
|
maxContentLength: data.duration * MAX_ZOOM_LEVEL,
|
|
containerLength: width,
|
|
});
|
|
|
|
viewState.updateHorizontalScrollState(scrollState);
|
|
}
|
|
|
|
function copySummary(data: TimelineData, measure: ReactMeasure) {
|
|
const {batchUID, duration, timestamp, type} = measure;
|
|
|
|
const [startTime, stopTime] = getBatchRange(batchUID, data);
|
|
|
|
copy(
|
|
JSON.stringify({
|
|
type,
|
|
timestamp: prettyMilliseconds(timestamp),
|
|
duration: prettyMilliseconds(duration),
|
|
batchDuration: prettyMilliseconds(stopTime - startTime),
|
|
}),
|
|
);
|
|
}
|
|
|
|
type Props = {
|
|
canvasRef: {current: HTMLCanvasElement | null},
|
|
hoveredEvent: ReactEventInfo | null,
|
|
timelineData: TimelineData,
|
|
viewState: ViewState,
|
|
canvasWidth: number,
|
|
closedMenuStub: React.Node,
|
|
ref: ContextMenuRef,
|
|
};
|
|
|
|
export default function CanvasPageContextMenu({
|
|
canvasRef,
|
|
timelineData,
|
|
hoveredEvent,
|
|
viewState,
|
|
canvasWidth,
|
|
closedMenuStub,
|
|
ref,
|
|
}: Props): React.Node {
|
|
const menuItems = useMemo<ContextMenuItem[]>(() => {
|
|
if (hoveredEvent == null) {
|
|
return [];
|
|
}
|
|
|
|
const {
|
|
componentMeasure,
|
|
flamechartStackFrame,
|
|
measure,
|
|
networkMeasure,
|
|
schedulingEvent,
|
|
suspenseEvent,
|
|
} = hoveredEvent;
|
|
const items: ContextMenuItem[] = [];
|
|
|
|
if (componentMeasure != null) {
|
|
items.push({
|
|
onClick: () => copy(componentMeasure.componentName),
|
|
content: 'Copy component name',
|
|
});
|
|
}
|
|
|
|
if (networkMeasure != null) {
|
|
items.push({
|
|
onClick: () => copy(networkMeasure.url),
|
|
content: 'Copy URL',
|
|
});
|
|
}
|
|
|
|
if (schedulingEvent != null) {
|
|
items.push({
|
|
onClick: () => copy(schedulingEvent.componentName),
|
|
content: 'Copy component name',
|
|
});
|
|
}
|
|
|
|
if (suspenseEvent != null) {
|
|
items.push({
|
|
onClick: () => copy(suspenseEvent.componentName),
|
|
content: 'Copy component name',
|
|
});
|
|
}
|
|
|
|
if (measure != null) {
|
|
items.push(
|
|
{
|
|
onClick: () =>
|
|
zoomToBatch(timelineData, measure, viewState, canvasWidth),
|
|
content: 'Zoom to batch',
|
|
},
|
|
{
|
|
onClick: withPermissionsCheck({permissions: ['clipboardWrite']}, () =>
|
|
copySummary(timelineData, measure),
|
|
),
|
|
content: 'Copy summary',
|
|
},
|
|
);
|
|
}
|
|
|
|
if (flamechartStackFrame != null) {
|
|
items.push(
|
|
{
|
|
onClick: withPermissionsCheck({permissions: ['clipboardWrite']}, () =>
|
|
copy(flamechartStackFrame.scriptUrl),
|
|
),
|
|
content: 'Copy file path',
|
|
},
|
|
{
|
|
onClick: withPermissionsCheck({permissions: ['clipboardWrite']}, () =>
|
|
copy(
|
|
`line ${flamechartStackFrame.locationLine ?? ''}, column ${
|
|
flamechartStackFrame.locationColumn ?? ''
|
|
}`,
|
|
),
|
|
),
|
|
content: 'Copy location',
|
|
},
|
|
);
|
|
}
|
|
|
|
return items;
|
|
}, [hoveredEvent, viewState, canvasWidth]);
|
|
|
|
return (
|
|
<ContextMenuContainer
|
|
anchorElementRef={canvasRef}
|
|
items={menuItems}
|
|
closedMenuStub={closedMenuStub}
|
|
ref={ref}
|
|
/>
|
|
);
|
|
}
|