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 {copy} from 'clipboard-js';
    
  11. import * as React from 'react';
    
  12. import {useCallback, useContext, useRef, useState} from 'react';
    
  13. import {BridgeContext, StoreContext} from '../context';
    
  14. import Button from '../Button';
    
  15. import ButtonIcon from '../ButtonIcon';
    
  16. import Toggle from '../Toggle';
    
  17. import ExpandCollapseToggle from './ExpandCollapseToggle';
    
  18. import KeyValue from './KeyValue';
    
  19. import {getMetaValueLabel, serializeHooksForCopy} from '../utils';
    
  20. import Store from '../../store';
    
  21. import styles from './InspectedElementHooksTree.css';
    
  22. import useContextMenu from '../../ContextMenu/useContextMenu';
    
  23. import {meta} from '../../../hydration';
    
  24. import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
    
  25. import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
    
  26. import isArray from 'react-devtools-shared/src/isArray';
    
  27. 
    
  28. import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
    
  29. import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
    
  30. import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
    
  31. import type {HookNames} from 'react-devtools-shared/src/frontend/types';
    
  32. import type {Element} from 'react-devtools-shared/src/frontend/types';
    
  33. import type {ToggleParseHookNames} from './InspectedElementContext';
    
  34. 
    
  35. type HooksTreeViewProps = {
    
  36.   bridge: FrontendBridge,
    
  37.   element: Element,
    
  38.   hookNames: HookNames | null,
    
  39.   inspectedElement: InspectedElement,
    
  40.   parseHookNames: boolean,
    
  41.   store: Store,
    
  42.   toggleParseHookNames: ToggleParseHookNames,
    
  43. };
    
  44. 
    
  45. export function InspectedElementHooksTree({
    
  46.   bridge,
    
  47.   element,
    
  48.   hookNames,
    
  49.   inspectedElement,
    
  50.   parseHookNames,
    
  51.   store,
    
  52.   toggleParseHookNames,
    
  53. }: HooksTreeViewProps): React.Node {
    
  54.   const {hooks, id} = inspectedElement;
    
  55. 
    
  56.   // Changing parseHookNames is done in a transition, because it suspends.
    
  57.   // This value is done outside of the transition, so the UI toggle feels responsive.
    
  58.   const [parseHookNamesOptimistic, setParseHookNamesOptimistic] =
    
  59.     useState(parseHookNames);
    
  60.   const handleChange = () => {
    
  61.     setParseHookNamesOptimistic(!parseHookNames);
    
  62.     toggleParseHookNames();
    
  63.   };
    
  64. 
    
  65.   const hookNamesModuleLoader = useContext(HookNamesModuleLoaderContext);
    
  66. 
    
  67.   const hookParsingFailed = parseHookNames && hookNames === null;
    
  68. 
    
  69.   let toggleTitle;
    
  70.   if (hookParsingFailed) {
    
  71.     toggleTitle = 'Hook parsing failed';
    
  72.   } else if (parseHookNames) {
    
  73.     toggleTitle = 'Parsing hook names ...';
    
  74.   } else {
    
  75.     toggleTitle = 'Parse hook names (may be slow)';
    
  76.   }
    
  77. 
    
  78.   const handleCopy = () => copy(serializeHooksForCopy(hooks));
    
  79. 
    
  80.   if (hooks === null) {
    
  81.     return null;
    
  82.   } else {
    
  83.     return (
    
  84.       <div
    
  85.         className={styles.HooksTreeView}
    
  86.         data-testname="InspectedElementHooksTree">
    
  87.         <div className={styles.HeaderRow}>
    
  88.           <div className={styles.Header}>hooks</div>
    
  89.           {typeof hookNamesModuleLoader === 'function' &&
    
  90.             (!parseHookNames || hookParsingFailed) && (
    
  91.               <Toggle
    
  92.                 className={hookParsingFailed ? styles.ToggleError : null}
    
  93.                 isChecked={parseHookNamesOptimistic}
    
  94.                 isDisabled={parseHookNamesOptimistic || hookParsingFailed}
    
  95.                 onChange={handleChange}
    
  96.                 testName="LoadHookNamesButton"
    
  97.                 title={toggleTitle}>
    
  98.                 <ButtonIcon type="parse-hook-names" />
    
  99.               </Toggle>
    
  100.             )}
    
  101.           <Button onClick={handleCopy} title="Copy to clipboard">
    
  102.             <ButtonIcon type="copy" />
    
  103.           </Button>
    
  104.         </div>
    
  105.         <InnerHooksTreeView
    
  106.           hookNames={hookNames}
    
  107.           hooks={hooks}
    
  108.           id={id}
    
  109.           element={element}
    
  110.           inspectedElement={inspectedElement}
    
  111.           path={[]}
    
  112.         />
    
  113.       </div>
    
  114.     );
    
  115.   }
    
  116. }
    
  117. 
    
  118. type InnerHooksTreeViewProps = {
    
  119.   element: Element,
    
  120.   hookNames: HookNames | null,
    
  121.   hooks: HooksTree,
    
  122.   id: number,
    
  123.   inspectedElement: InspectedElement,
    
  124.   path: Array<string | number>,
    
  125. };
    
  126. 
    
  127. export function InnerHooksTreeView({
    
  128.   element,
    
  129.   hookNames,
    
  130.   hooks,
    
  131.   id,
    
  132.   inspectedElement,
    
  133.   path,
    
  134. }: InnerHooksTreeViewProps): React.Node {
    
  135.   return hooks.map((hook, index) => (
    
  136.     <HookView
    
  137.       key={index}
    
  138.       element={element}
    
  139.       hook={hooks[index]}
    
  140.       hookNames={hookNames}
    
  141.       id={id}
    
  142.       inspectedElement={inspectedElement}
    
  143.       path={path.concat([index])}
    
  144.     />
    
  145.   ));
    
  146. }
    
  147. 
    
  148. type HookViewProps = {
    
  149.   element: Element,
    
  150.   hook: HooksNode,
    
  151.   hookNames: HookNames | null,
    
  152.   id: number,
    
  153.   inspectedElement: InspectedElement,
    
  154.   path: Array<string | number>,
    
  155. };
    
  156. 
    
  157. function HookView({
    
  158.   element,
    
  159.   hook,
    
  160.   hookNames,
    
  161.   id,
    
  162.   inspectedElement,
    
  163.   path,
    
  164. }: HookViewProps) {
    
  165.   const {canEditHooks, canEditHooksAndDeletePaths, canEditHooksAndRenamePaths} =
    
  166.     inspectedElement;
    
  167.   const {id: hookID, isStateEditable, subHooks, value} = hook;
    
  168. 
    
  169.   const isReadOnly = hookID == null || !isStateEditable;
    
  170. 
    
  171.   const canDeletePaths = !isReadOnly && canEditHooksAndDeletePaths;
    
  172.   const canEditValues = !isReadOnly && canEditHooks;
    
  173.   const canRenamePaths = !isReadOnly && canEditHooksAndRenamePaths;
    
  174. 
    
  175.   const bridge = useContext(BridgeContext);
    
  176.   const store = useContext(StoreContext);
    
  177. 
    
  178.   const [isOpen, setIsOpen] = useState<boolean>(false);
    
  179. 
    
  180.   const toggleIsOpen = useCallback(
    
  181.     () => setIsOpen(prevIsOpen => !prevIsOpen),
    
  182.     [],
    
  183.   );
    
  184. 
    
  185.   const contextMenuTriggerRef = useRef(null);
    
  186. 
    
  187.   useContextMenu({
    
  188.     data: {
    
  189.       path: ['hooks', ...path],
    
  190.       type:
    
  191.         hook !== null &&
    
  192.         typeof hook === 'object' &&
    
  193.         hook.hasOwnProperty(meta.type)
    
  194.           ? hook[(meta.type: any)]
    
  195.           : typeof value,
    
  196.     },
    
  197.     id: 'InspectedElement',
    
  198.     ref: contextMenuTriggerRef,
    
  199.   });
    
  200. 
    
  201.   if (hook.hasOwnProperty(meta.inspected)) {
    
  202.     // This Hook is too deep and hasn't been hydrated.
    
  203.     if (__DEV__) {
    
  204.       console.warn('Unexpected dehydrated hook; this is a DevTools error.');
    
  205.     }
    
  206.     return (
    
  207.       <div className={styles.Hook}>
    
  208.         <div className={styles.NameValueRow}>
    
  209.           <span className={styles.TruncationIndicator}>...</span>
    
  210.         </div>
    
  211.       </div>
    
  212.     );
    
  213.   }
    
  214. 
    
  215.   // Certain hooks are not editable at all (as identified by react-debug-tools).
    
  216.   // Primitive hook names (e.g. the "State" name for useState) are also never editable.
    
  217.   // $FlowFixMe[missing-local-annot]
    
  218.   const canRenamePathsAtDepth = depth => isStateEditable && depth > 1;
    
  219. 
    
  220.   const isCustomHook = subHooks.length > 0;
    
  221. 
    
  222.   let name = hook.name;
    
  223.   if (hookID !== null) {
    
  224.     name = (
    
  225.       <>
    
  226.         <span className={styles.PrimitiveHookNumber}>{hookID + 1}</span>
    
  227.         {name}
    
  228.       </>
    
  229.     );
    
  230.   }
    
  231. 
    
  232.   const type = typeof value;
    
  233. 
    
  234.   let displayValue;
    
  235.   let isComplexDisplayValue = false;
    
  236. 
    
  237.   const hookSource = hook.hookSource;
    
  238.   const hookName =
    
  239.     hookNames != null && hookSource != null
    
  240.       ? hookNames.get(getHookSourceLocationKey(hookSource))
    
  241.       : null;
    
  242.   const hookDisplayName = hookName ? (
    
  243.     <>
    
  244.       {name}
    
  245.       {!!hookName && <span className={styles.HookName}>({hookName})</span>}
    
  246.     </>
    
  247.   ) : (
    
  248.     name
    
  249.   );
    
  250. 
    
  251.   // Format data for display to mimic the props/state/context for now.
    
  252.   if (type === 'string') {
    
  253.     displayValue = `"${((value: any): string)}"`;
    
  254.   } else if (type === 'boolean') {
    
  255.     displayValue = value ? 'true' : 'false';
    
  256.   } else if (type === 'number') {
    
  257.     displayValue = value;
    
  258.   } else if (value === null) {
    
  259.     displayValue = 'null';
    
  260.   } else if (value === undefined) {
    
  261.     displayValue = null;
    
  262.   } else if (isArray(value)) {
    
  263.     isComplexDisplayValue = true;
    
  264.     displayValue = 'Array';
    
  265.   } else if (type === 'object') {
    
  266.     isComplexDisplayValue = true;
    
  267.     displayValue = 'Object';
    
  268.   }
    
  269. 
    
  270.   if (isCustomHook) {
    
  271.     const subHooksView = isArray(subHooks) ? (
    
  272.       <InnerHooksTreeView
    
  273.         element={element}
    
  274.         hooks={subHooks}
    
  275.         hookNames={hookNames}
    
  276.         id={id}
    
  277.         inspectedElement={inspectedElement}
    
  278.         path={path.concat(['subHooks'])}
    
  279.       />
    
  280.     ) : (
    
  281.       <KeyValue
    
  282.         alphaSort={false}
    
  283.         bridge={bridge}
    
  284.         canDeletePaths={canDeletePaths}
    
  285.         canEditValues={canEditValues}
    
  286.         canRenamePaths={canRenamePaths}
    
  287.         canRenamePathsAtDepth={canRenamePathsAtDepth}
    
  288.         depth={1}
    
  289.         element={element}
    
  290.         hookID={hookID}
    
  291.         hookName={hookName}
    
  292.         inspectedElement={inspectedElement}
    
  293.         name="subHooks"
    
  294.         path={path.concat(['subHooks'])}
    
  295.         store={store}
    
  296.         type="hooks"
    
  297.         value={subHooks}
    
  298.       />
    
  299.     );
    
  300. 
    
  301.     if (isComplexDisplayValue) {
    
  302.       return (
    
  303.         <div className={styles.Hook}>
    
  304.           <div ref={contextMenuTriggerRef} className={styles.NameValueRow}>
    
  305.             <ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
    
  306.             <span
    
  307.               onClick={toggleIsOpen}
    
  308.               className={name !== '' ? styles.Name : styles.NameAnonymous}>
    
  309.               {hookDisplayName || 'Anonymous'}
    
  310.             </span>
    
  311.             <span className={styles.Value} onClick={toggleIsOpen}>
    
  312.               {isOpen || getMetaValueLabel(value)}
    
  313.             </span>
    
  314.           </div>
    
  315.           <div className={styles.Children} hidden={!isOpen}>
    
  316.             <KeyValue
    
  317.               alphaSort={false}
    
  318.               bridge={bridge}
    
  319.               canDeletePaths={canDeletePaths}
    
  320.               canEditValues={canEditValues}
    
  321.               canRenamePaths={canRenamePaths}
    
  322.               canRenamePathsAtDepth={canRenamePathsAtDepth}
    
  323.               depth={1}
    
  324.               element={element}
    
  325.               hookID={hookID}
    
  326.               hookName={hookName}
    
  327.               inspectedElement={inspectedElement}
    
  328.               name="DebugValue"
    
  329.               path={path.concat(['value'])}
    
  330.               pathRoot="hooks"
    
  331.               store={store}
    
  332.               value={value}
    
  333.             />
    
  334.             {subHooksView}
    
  335.           </div>
    
  336.         </div>
    
  337.       );
    
  338.     } else {
    
  339.       return (
    
  340.         <div className={styles.Hook}>
    
  341.           <div ref={contextMenuTriggerRef} className={styles.NameValueRow}>
    
  342.             <ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
    
  343.             <span
    
  344.               onClick={toggleIsOpen}
    
  345.               className={name !== '' ? styles.Name : styles.NameAnonymous}>
    
  346.               {hookDisplayName || 'Anonymous'}
    
  347.             </span>{' '}
    
  348.             <span className={styles.Value} onClick={toggleIsOpen}>
    
  349.               {displayValue}
    
  350.             </span>
    
  351.           </div>
    
  352.           <div className={styles.Children} hidden={!isOpen}>
    
  353.             {subHooksView}
    
  354.           </div>
    
  355.         </div>
    
  356.       );
    
  357.     }
    
  358.   } else {
    
  359.     if (isComplexDisplayValue) {
    
  360.       return (
    
  361.         <div className={styles.Hook}>
    
  362.           <KeyValue
    
  363.             alphaSort={false}
    
  364.             bridge={bridge}
    
  365.             canDeletePaths={canDeletePaths}
    
  366.             canEditValues={canEditValues}
    
  367.             canRenamePaths={canRenamePaths}
    
  368.             canRenamePathsAtDepth={canRenamePathsAtDepth}
    
  369.             depth={1}
    
  370.             element={element}
    
  371.             hookID={hookID}
    
  372.             hookName={hookName}
    
  373.             inspectedElement={inspectedElement}
    
  374.             name={name}
    
  375.             path={path.concat(['value'])}
    
  376.             pathRoot="hooks"
    
  377.             store={store}
    
  378.             value={value}
    
  379.           />
    
  380.         </div>
    
  381.       );
    
  382.     } else {
    
  383.       return (
    
  384.         <div className={styles.Hook}>
    
  385.           <KeyValue
    
  386.             alphaSort={false}
    
  387.             bridge={bridge}
    
  388.             canDeletePaths={false}
    
  389.             canEditValues={canEditValues}
    
  390.             canRenamePaths={false}
    
  391.             depth={1}
    
  392.             element={element}
    
  393.             hookID={hookID}
    
  394.             hookName={hookName}
    
  395.             inspectedElement={inspectedElement}
    
  396.             name={name}
    
  397.             path={[]}
    
  398.             pathRoot="hooks"
    
  399.             store={store}
    
  400.             value={value}
    
  401.           />
    
  402.         </div>
    
  403.       );
    
  404.     }
    
  405.   }
    
  406. }
    
  407. 
    
  408. export default (React.memo(
    
  409.   InspectedElementHooksTree,
    
  410. ): React.ComponentType<HookViewProps>);