1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import * as React from 'react';
    
  11. import {useCallback, useContext, useSyncExternalStore} from 'react';
    
  12. import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
    
  13. import {BridgeContext, StoreContext, OptionsContext} from '../context';
    
  14. import Button from '../Button';
    
  15. import ButtonIcon from '../ButtonIcon';
    
  16. import Icon from '../Icon';
    
  17. import {ModalDialogContext} from '../ModalDialog';
    
  18. import ViewElementSourceContext from './ViewElementSourceContext';
    
  19. import Toggle from '../Toggle';
    
  20. import {ElementTypeSuspense} from 'react-devtools-shared/src/frontend/types';
    
  21. import CannotSuspendWarningMessage from './CannotSuspendWarningMessage';
    
  22. import InspectedElementView from './InspectedElementView';
    
  23. import {InspectedElementContext} from './InspectedElementContext';
    
  24. import {getOpenInEditorURL} from '../../../utils';
    
  25. import {LOCAL_STORAGE_OPEN_IN_EDITOR_URL} from '../../../constants';
    
  26. 
    
  27. import styles from './InspectedElement.css';
    
  28. 
    
  29. import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
    
  30. 
    
  31. export type Props = {};
    
  32. 
    
  33. // TODO Make edits and deletes also use transition API!
    
  34. 
    
  35. export default function InspectedElementWrapper(_: Props): React.Node {
    
  36.   const {inspectedElementID} = useContext(TreeStateContext);
    
  37.   const dispatch = useContext(TreeDispatcherContext);
    
  38.   const {canViewElementSourceFunction, viewElementSourceFunction} = useContext(
    
  39.     ViewElementSourceContext,
    
  40.   );
    
  41.   const bridge = useContext(BridgeContext);
    
  42.   const store = useContext(StoreContext);
    
  43.   const {
    
  44.     hideToggleErrorAction,
    
  45.     hideToggleSuspenseAction,
    
  46.     hideLogAction,
    
  47.     hideViewSourceAction,
    
  48.   } = useContext(OptionsContext);
    
  49.   const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
    
  50. 
    
  51.   const {hookNames, inspectedElement, parseHookNames, toggleParseHookNames} =
    
  52.     useContext(InspectedElementContext);
    
  53. 
    
  54.   const element =
    
  55.     inspectedElementID !== null
    
  56.       ? store.getElementByID(inspectedElementID)
    
  57.       : null;
    
  58. 
    
  59.   const highlightElement = useCallback(() => {
    
  60.     if (element !== null && inspectedElementID !== null) {
    
  61.       const rendererID = store.getRendererIDForElement(inspectedElementID);
    
  62.       if (rendererID !== null) {
    
  63.         bridge.send('highlightNativeElement', {
    
  64.           displayName: element.displayName,
    
  65.           hideAfterTimeout: true,
    
  66.           id: inspectedElementID,
    
  67.           openNativeElementsPanel: true,
    
  68.           rendererID,
    
  69.           scrollIntoView: true,
    
  70.         });
    
  71.       }
    
  72.     }
    
  73.   }, [bridge, element, inspectedElementID, store]);
    
  74. 
    
  75.   const logElement = useCallback(() => {
    
  76.     if (inspectedElementID !== null) {
    
  77.       const rendererID = store.getRendererIDForElement(inspectedElementID);
    
  78.       if (rendererID !== null) {
    
  79.         bridge.send('logElementToConsole', {
    
  80.           id: inspectedElementID,
    
  81.           rendererID,
    
  82.         });
    
  83.       }
    
  84.     }
    
  85.   }, [bridge, inspectedElementID, store]);
    
  86. 
    
  87.   const viewSource = useCallback(() => {
    
  88.     if (viewElementSourceFunction != null && inspectedElement !== null) {
    
  89.       viewElementSourceFunction(
    
  90.         inspectedElement.id,
    
  91.         ((inspectedElement: any): InspectedElement),
    
  92.       );
    
  93.     }
    
  94.   }, [inspectedElement, viewElementSourceFunction]);
    
  95. 
    
  96.   // In some cases (e.g. FB internal usage) the standalone shell might not be able to view the source.
    
  97.   // To detect this case, we defer to an injected helper function (if present).
    
  98.   const canViewSource =
    
  99.     inspectedElement !== null &&
    
  100.     inspectedElement.canViewSource &&
    
  101.     viewElementSourceFunction !== null &&
    
  102.     (canViewElementSourceFunction === null ||
    
  103.       canViewElementSourceFunction(inspectedElement));
    
  104. 
    
  105.   const isErrored = inspectedElement != null && inspectedElement.isErrored;
    
  106.   const targetErrorBoundaryID =
    
  107.     inspectedElement != null ? inspectedElement.targetErrorBoundaryID : null;
    
  108. 
    
  109.   const isSuspended =
    
  110.     element !== null &&
    
  111.     element.type === ElementTypeSuspense &&
    
  112.     inspectedElement != null &&
    
  113.     inspectedElement.state != null;
    
  114. 
    
  115.   const canToggleError =
    
  116.     !hideToggleErrorAction &&
    
  117.     inspectedElement != null &&
    
  118.     inspectedElement.canToggleError;
    
  119. 
    
  120.   const canToggleSuspense =
    
  121.     !hideToggleSuspenseAction &&
    
  122.     inspectedElement != null &&
    
  123.     inspectedElement.canToggleSuspense;
    
  124. 
    
  125.   const editorURL = useSyncExternalStore(
    
  126.     function subscribe(callback) {
    
  127.       window.addEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
    
  128.       return function unsubscribe() {
    
  129.         window.removeEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
    
  130.       };
    
  131.     },
    
  132.     function getState() {
    
  133.       return getOpenInEditorURL();
    
  134.     },
    
  135.   );
    
  136. 
    
  137.   const canOpenInEditor =
    
  138.     editorURL && inspectedElement != null && inspectedElement.source != null;
    
  139. 
    
  140.   const toggleErrored = useCallback(() => {
    
  141.     if (inspectedElement == null || targetErrorBoundaryID == null) {
    
  142.       return;
    
  143.     }
    
  144. 
    
  145.     const rendererID = store.getRendererIDForElement(targetErrorBoundaryID);
    
  146.     if (rendererID !== null) {
    
  147.       if (targetErrorBoundaryID !== inspectedElement.id) {
    
  148.         // Update tree selection so that if we cause a component to error,
    
  149.         // the nearest error boundary will become the newly selected thing.
    
  150.         dispatch({
    
  151.           type: 'SELECT_ELEMENT_BY_ID',
    
  152.           payload: targetErrorBoundaryID,
    
  153.         });
    
  154.       }
    
  155. 
    
  156.       // Toggle error.
    
  157.       bridge.send('overrideError', {
    
  158.         id: targetErrorBoundaryID,
    
  159.         rendererID,
    
  160.         forceError: !isErrored,
    
  161.       });
    
  162.     }
    
  163.   }, [bridge, dispatch, isErrored, targetErrorBoundaryID]);
    
  164. 
    
  165.   // TODO (suspense toggle) Would be nice to eventually use a two setState pattern here as well.
    
  166.   const toggleSuspended = useCallback(() => {
    
  167.     let nearestSuspenseElement = null;
    
  168.     let currentElement = element;
    
  169.     while (currentElement !== null) {
    
  170.       if (currentElement.type === ElementTypeSuspense) {
    
  171.         nearestSuspenseElement = currentElement;
    
  172.         break;
    
  173.       } else if (currentElement.parentID > 0) {
    
  174.         currentElement = store.getElementByID(currentElement.parentID);
    
  175.       } else {
    
  176.         currentElement = null;
    
  177.       }
    
  178.     }
    
  179. 
    
  180.     // If we didn't find a Suspense ancestor, we can't suspend.
    
  181.     // Instead we can show a warning to the user.
    
  182.     if (nearestSuspenseElement === null) {
    
  183.       modalDialogDispatch({
    
  184.         id: 'InspectedElement',
    
  185.         type: 'SHOW',
    
  186.         content: <CannotSuspendWarningMessage />,
    
  187.       });
    
  188.     } else {
    
  189.       const nearestSuspenseElementID = nearestSuspenseElement.id;
    
  190. 
    
  191.       // If we're suspending from an arbitrary (non-Suspense) component, select the nearest Suspense element in the Tree.
    
  192.       // This way when the fallback UI is shown and the current element is hidden, something meaningful is selected.
    
  193.       if (nearestSuspenseElement !== element) {
    
  194.         dispatch({
    
  195.           type: 'SELECT_ELEMENT_BY_ID',
    
  196.           payload: nearestSuspenseElementID,
    
  197.         });
    
  198.       }
    
  199. 
    
  200.       const rendererID = store.getRendererIDForElement(
    
  201.         nearestSuspenseElementID,
    
  202.       );
    
  203. 
    
  204.       // Toggle suspended
    
  205.       if (rendererID !== null) {
    
  206.         bridge.send('overrideSuspense', {
    
  207.           id: nearestSuspenseElementID,
    
  208.           rendererID,
    
  209.           forceFallback: !isSuspended,
    
  210.         });
    
  211.       }
    
  212.     }
    
  213.   }, [bridge, dispatch, element, isSuspended, modalDialogDispatch, store]);
    
  214. 
    
  215.   const onOpenInEditor = useCallback(() => {
    
  216.     const source = inspectedElement?.source;
    
  217.     if (source == null || editorURL == null) {
    
  218.       return;
    
  219.     }
    
  220. 
    
  221.     const url = new URL(editorURL);
    
  222.     url.href = url.href
    
  223.       .replace('{path}', source.fileName)
    
  224.       .replace('{line}', String(source.lineNumber))
    
  225.       .replace('%7Bpath%7D', source.fileName)
    
  226.       .replace('%7Bline%7D', String(source.lineNumber));
    
  227.     window.open(url);
    
  228.   }, [inspectedElement, editorURL]);
    
  229. 
    
  230.   if (element === null) {
    
  231.     return (
    
  232.       <div className={styles.InspectedElement}>
    
  233.         <div className={styles.TitleRow} />
    
  234.       </div>
    
  235.     );
    
  236.   }
    
  237. 
    
  238.   let strictModeBadge = null;
    
  239.   if (element.isStrictModeNonCompliant) {
    
  240.     strictModeBadge = (
    
  241.       <a
    
  242.         className={styles.StrictModeNonCompliant}
    
  243.         href="https://react.dev/reference/react/StrictMode"
    
  244.         rel="noopener noreferrer"
    
  245.         target="_blank"
    
  246.         title="This component is not running in StrictMode. Click to learn more.">
    
  247.         <Icon type="strict-mode-non-compliant" />
    
  248.       </a>
    
  249.     );
    
  250.   }
    
  251. 
    
  252.   return (
    
  253.     <div className={styles.InspectedElement}>
    
  254.       <div className={styles.TitleRow} data-testname="InspectedElement-Title">
    
  255.         {strictModeBadge}
    
  256. 
    
  257.         {element.key && (
    
  258.           <>
    
  259.             <div className={styles.Key} title={`key "${element.key}"`}>
    
  260.               {element.key}
    
  261.             </div>
    
  262.             <div className={styles.KeyArrow} />
    
  263.           </>
    
  264.         )}
    
  265. 
    
  266.         <div className={styles.SelectedComponentName}>
    
  267.           <div
    
  268.             className={
    
  269.               element.isStrictModeNonCompliant
    
  270.                 ? styles.StrictModeNonCompliant
    
  271.                 : styles.Component
    
  272.             }
    
  273.             title={element.displayName}>
    
  274.             {element.displayName}
    
  275.           </div>
    
  276.         </div>
    
  277.         {canOpenInEditor && (
    
  278.           <Button onClick={onOpenInEditor} title="Open in editor">
    
  279.             <ButtonIcon type="editor" />
    
  280.           </Button>
    
  281.         )}
    
  282.         {canToggleError && (
    
  283.           <Toggle
    
  284.             isChecked={isErrored}
    
  285.             onChange={toggleErrored}
    
  286.             title={
    
  287.               isErrored
    
  288.                 ? 'Clear the forced error'
    
  289.                 : 'Force the selected component into an errored state'
    
  290.             }>
    
  291.             <ButtonIcon type="error" />
    
  292.           </Toggle>
    
  293.         )}
    
  294.         {canToggleSuspense && (
    
  295.           <Toggle
    
  296.             isChecked={isSuspended}
    
  297.             onChange={toggleSuspended}
    
  298.             title={
    
  299.               isSuspended
    
  300.                 ? 'Unsuspend the selected component'
    
  301.                 : 'Suspend the selected component'
    
  302.             }>
    
  303.             <ButtonIcon type="suspend" />
    
  304.           </Toggle>
    
  305.         )}
    
  306.         {store.supportsNativeInspection && (
    
  307.           <Button
    
  308.             onClick={highlightElement}
    
  309.             title="Inspect the matching DOM element">
    
  310.             <ButtonIcon type="view-dom" />
    
  311.           </Button>
    
  312.         )}
    
  313.         {!hideLogAction && (
    
  314.           <Button
    
  315.             onClick={logElement}
    
  316.             title="Log this component data to the console">
    
  317.             <ButtonIcon type="log-data" />
    
  318.           </Button>
    
  319.         )}
    
  320.         {!hideViewSourceAction && (
    
  321.           <Button
    
  322.             disabled={!canViewSource}
    
  323.             onClick={viewSource}
    
  324.             title="View source for this element">
    
  325.             <ButtonIcon type="view-source" />
    
  326.           </Button>
    
  327.         )}
    
  328.       </div>
    
  329. 
    
  330.       {inspectedElement === null && (
    
  331.         <div className={styles.Loading}>Loading...</div>
    
  332.       )}
    
  333. 
    
  334.       {inspectedElement !== null && (
    
  335.         <InspectedElementView
    
  336.           key={
    
  337.             inspectedElementID /* Force reset when selected Element changes */
    
  338.           }
    
  339.           element={element}
    
  340.           hookNames={hookNames}
    
  341.           inspectedElement={inspectedElement}
    
  342.           parseHookNames={parseHookNames}
    
  343.           toggleParseHookNames={toggleParseHookNames}
    
  344.         />
    
  345.       )}
    
  346.     </div>
    
  347.   );
    
  348. }