/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import {copy} from 'clipboard-js'; import React, {Fragment, useCallback, useContext} from 'react'; import {TreeDispatcherContext, TreeStateContext} from './TreeContext'; import {BridgeContext, ContextMenuContext, StoreContext} from '../context'; import ContextMenu from '../../ContextMenu/ContextMenu'; import ContextMenuItem from '../../ContextMenu/ContextMenuItem'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import Icon from '../Icon'; import HooksTree from './HooksTree'; import {ModalDialogContext} from '../ModalDialog'; import HocBadges from './HocBadges'; import InspectedElementTree from './InspectedElementTree'; import {InspectedElementContext} from './InspectedElementContext'; import ViewElementSourceContext from './ViewElementSourceContext'; import NativeStyleEditor from './NativeStyleEditor'; import Toggle from '../Toggle'; import Badge from './Badge'; import { ComponentFilterElementType, ElementTypeClass, ElementTypeForwardRef, ElementTypeFunction, ElementTypeMemo, ElementTypeSuspense, } from 'react-devtools-shared/src/types'; import styles from './SelectedElement.css'; import type { CopyInspectedElementPath, GetInspectedElementPath, StoreAsGlobal, } from './InspectedElementContext'; import type {Element, InspectedElement} from './types'; import type {ElementType} from 'react-devtools-shared/src/types'; export type Props = {||}; export default function SelectedElement(_: Props) { const {inspectedElementID} = useContext(TreeStateContext); const dispatch = useContext(TreeDispatcherContext); const {canViewElementSourceFunction, viewElementSourceFunction} = useContext( ViewElementSourceContext, ); const bridge = useContext(BridgeContext); const store = useContext(StoreContext); const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext); const { copyInspectedElementPath, getInspectedElementPath, getInspectedElement, storeAsGlobal, viewInspectedElementPath, } = useContext(InspectedElementContext); const element = inspectedElementID !== null ? store.getElementByID(inspectedElementID) : null; const inspectedElement = inspectedElementID != null ? getInspectedElement(inspectedElementID) : null; const highlightElement = useCallback( () => { if (element !== null && inspectedElementID !== null) { const rendererID = store.getRendererIDForElement(inspectedElementID); if (rendererID !== null) { bridge.send('highlightNativeElement', { displayName: element.displayName, hideAfterTimeout: true, id: inspectedElementID, openNativeElementsPanel: true, rendererID, scrollIntoView: true, }); } } }, [bridge, element, inspectedElementID, store], ); const logElement = useCallback( () => { if (inspectedElementID !== null) { const rendererID = store.getRendererIDForElement(inspectedElementID); if (rendererID !== null) { bridge.send('logElementToConsole', { id: inspectedElementID, rendererID, }); } } }, [bridge, inspectedElementID, store], ); const viewSource = useCallback( () => { if (viewElementSourceFunction != null && inspectedElement !== null) { viewElementSourceFunction( inspectedElement.id, ((inspectedElement: any): InspectedElement), ); } }, [inspectedElement, viewElementSourceFunction], ); // In some cases (e.g. FB internal usage) the standalone shell might not be able to view the source. // To detect this case, we defer to an injected helper function (if present). const canViewSource = inspectedElement !== null && inspectedElement.canViewSource && viewElementSourceFunction !== null && (canViewElementSourceFunction === null || canViewElementSourceFunction(inspectedElement)); const isSuspended = element !== null && element.type === ElementTypeSuspense && inspectedElement != null && inspectedElement.state != null; const canToggleSuspense = inspectedElement != null && inspectedElement.canToggleSuspense; // TODO (suspense toggle) Would be nice to eventually use a two setState pattern here as well. const toggleSuspended = useCallback( () => { let nearestSuspenseElement = null; let currentElement = element; while (currentElement !== null) { if (currentElement.type === ElementTypeSuspense) { nearestSuspenseElement = currentElement; break; } else if (currentElement.parentID > 0) { currentElement = store.getElementByID(currentElement.parentID); } else { currentElement = null; } } // If we didn't find a Suspense ancestor, we can't suspend. // Instead we can show a warning to the user. if (nearestSuspenseElement === null) { modalDialogDispatch({ type: 'SHOW', content: , }); } else { const nearestSuspenseElementID = nearestSuspenseElement.id; // If we're suspending from an arbitary (non-Suspense) component, select the nearest Suspense element in the Tree. // This way when the fallback UI is shown and the current element is hidden, something meaningful is selected. if (nearestSuspenseElement !== element) { dispatch({ type: 'SELECT_ELEMENT_BY_ID', payload: nearestSuspenseElementID, }); } const rendererID = store.getRendererIDForElement( nearestSuspenseElementID, ); // Toggle suspended if (rendererID !== null) { bridge.send('overrideSuspense', { id: nearestSuspenseElementID, rendererID, forceFallback: !isSuspended, }); } } }, [bridge, dispatch, element, isSuspended, modalDialogDispatch, store], ); if (element === null) { return (
); } return (
{element.displayName}
{canToggleSuspense && ( )} {store.supportsNativeInspection && ( )}
{inspectedElement === null && (
Loading...
)} {inspectedElement !== null && ( )}
); } export type CopyPath = (path: Array) => void; export type InspectPath = (path: Array) => void; type InspectedElementViewProps = {| copyInspectedElementPath: CopyInspectedElementPath, element: Element, getInspectedElementPath: GetInspectedElementPath, inspectedElement: InspectedElement, storeAsGlobal: StoreAsGlobal, |}; const IS_SUSPENDED = 'Suspended'; function InspectedElementView({ copyInspectedElementPath, element, getInspectedElementPath, inspectedElement, storeAsGlobal, viewInspectedElementPath, }: InspectedElementViewProps) { const {id, type} = element; const { canEditFunctionProps, canEditHooks, canToggleSuspense, hasLegacyContext, context, hooks, owners, props, source, state, } = inspectedElement; const {ownerID} = useContext(TreeStateContext); const bridge = useContext(BridgeContext); const store = useContext(StoreContext); const { isEnabledForInspectedElement, supportsCopyOperation, viewAttributeSourceFunction, } = useContext(ContextMenuContext); const inspectContextPath = useCallback( (path: Array) => { getInspectedElementPath(id, ['context', ...path]); }, [getInspectedElementPath, id], ); const inspectPropsPath = useCallback( (path: Array) => { getInspectedElementPath(id, ['props', ...path]); }, [getInspectedElementPath, id], ); const inspectStatePath = useCallback( (path: Array) => { getInspectedElementPath(id, ['state', ...path]); }, [getInspectedElementPath, id], ); let overrideContextFn = null; let overridePropsFn = null; let overrideStateFn = null; let overrideSuspenseFn = null; if (type === ElementTypeClass) { overrideContextFn = (path: Array, value: any) => { const rendererID = store.getRendererIDForElement(id); if (rendererID !== null) { bridge.send('overrideContext', {id, path, rendererID, value}); } }; overridePropsFn = (path: Array, value: any) => { const rendererID = store.getRendererIDForElement(id); if (rendererID !== null) { bridge.send('overrideProps', {id, path, rendererID, value}); } }; overrideStateFn = (path: Array, value: any) => { const rendererID = store.getRendererIDForElement(id); if (rendererID !== null) { bridge.send('overrideState', {id, path, rendererID, value}); } }; } else if ( (type === ElementTypeFunction || type === ElementTypeMemo || type === ElementTypeForwardRef) && canEditFunctionProps ) { overridePropsFn = (path: Array, value: any) => { const rendererID = store.getRendererIDForElement(id); if (rendererID !== null) { bridge.send('overrideProps', {id, path, rendererID, value}); } }; } else if (type === ElementTypeSuspense && canToggleSuspense) { overrideSuspenseFn = (path: Array, value: boolean) => { if (path.length !== 1 && path !== IS_SUSPENDED) { throw new Error('Unexpected path.'); } const rendererID = store.getRendererIDForElement(id); if (rendererID !== null) { bridge.send('overrideSuspense', { id, rendererID, forceFallback: value, }); } }; } return (
{type === ElementTypeSuspense ? ( ) : ( )} {ownerID === null && owners !== null && owners.length > 0 && (
rendered by
{owners.map(owner => ( ))}
)} {source !== null && ( )}
{isEnabledForInspectedElement && ( {data => ( {supportsCopyOperation && ( copyInspectedElementPath(id, data.path)} title="Copy value to clipboard"> Copy value to clipboard )} storeAsGlobal(id, data.path)} title="Store as global variable"> {' '} Store as global variable {viewAttributeSourceFunction !== null && data.type === 'function' && ( viewAttributeSourceFunction(id, data.path)} title="Go to definition"> Go to definition )} )} )}
); } // This function is based on packages/shared/describeComponentFrame.js function formatSourceForDisplay(fileName: string, lineNumber: string) { const BEFORE_SLASH_RE = /^(.*)[\\\/]/; let nameOnly = fileName.replace(BEFORE_SLASH_RE, ''); // In DEV, include code for a common special case: // prefer "folder/index.js" instead of just "index.js". if (/^index\./.test(nameOnly)) { const match = fileName.match(BEFORE_SLASH_RE); if (match) { const pathBeforeSlash = match[1]; if (pathBeforeSlash) { const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, ''); nameOnly = folderName + '/' + nameOnly; } } } return `${nameOnly}:${lineNumber}`; } type SourceProps = {| fileName: string, lineNumber: string, |}; function Source({fileName, lineNumber}: SourceProps) { const handleCopy = () => copy(`${fileName}:${lineNumber}`); return (
source
{formatSourceForDisplay(fileName, lineNumber)}
); } type OwnerViewProps = {| displayName: string, hocDisplayNames: Array | null, id: number, isInStore: boolean, type: ElementType, |}; function OwnerView({ displayName, hocDisplayNames, id, isInStore, type, }: OwnerViewProps) { const dispatch = useContext(TreeDispatcherContext); const handleClick = useCallback( () => dispatch({ type: 'SELECT_ELEMENT_BY_ID', payload: id, }), [dispatch, id], ); return ( ); } function CannotSuspendWarningMessage() { const store = useContext(StoreContext); const areSuspenseElementsHidden = !!store.componentFilters.find( filter => filter.type === ComponentFilterElementType && filter.value === ElementTypeSuspense && filter.isEnabled, ); // Has the user filted out Suspense nodes from the tree? // If so, the selected element might actually be in a Suspense tree after all. if (areSuspenseElementsHidden) { return (
Suspended state cannot be toggled while Suspense components are hidden. Disable the filter and try agan.
); } else { return (
The selected element is not within a Suspense container. Suspending it would cause an error.
); } }