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 {Fragment, useCallback, useContext} from 'react';
    
  13. import {TreeDispatcherContext} from './TreeContext';
    
  14. import {BridgeContext, ContextMenuContext, StoreContext} from '../context';
    
  15. import ContextMenu from '../../ContextMenu/ContextMenu';
    
  16. import ContextMenuItem from '../../ContextMenu/ContextMenuItem';
    
  17. import Button from '../Button';
    
  18. import ButtonIcon from '../ButtonIcon';
    
  19. import Icon from '../Icon';
    
  20. import HocBadges from './HocBadges';
    
  21. import InspectedElementContextTree from './InspectedElementContextTree';
    
  22. import InspectedElementErrorsAndWarningsTree from './InspectedElementErrorsAndWarningsTree';
    
  23. import InspectedElementHooksTree from './InspectedElementHooksTree';
    
  24. import InspectedElementPropsTree from './InspectedElementPropsTree';
    
  25. import InspectedElementStateTree from './InspectedElementStateTree';
    
  26. import InspectedElementStyleXPlugin from './InspectedElementStyleXPlugin';
    
  27. import InspectedElementSuspenseToggle from './InspectedElementSuspenseToggle';
    
  28. import NativeStyleEditor from './NativeStyleEditor';
    
  29. import Badge from './Badge';
    
  30. import {useHighlightNativeElement} from '../hooks';
    
  31. import {
    
  32.   copyInspectedElementPath as copyInspectedElementPathAPI,
    
  33.   storeAsGlobal as storeAsGlobalAPI,
    
  34. } from 'react-devtools-shared/src/backendAPI';
    
  35. import {enableStyleXFeatures} from 'react-devtools-feature-flags';
    
  36. import {logEvent} from 'react-devtools-shared/src/Logger';
    
  37. 
    
  38. import styles from './InspectedElementView.css';
    
  39. 
    
  40. import type {ContextMenuContextType} from '../context';
    
  41. import type {
    
  42.   Element,
    
  43.   InspectedElement,
    
  44.   SerializedElement,
    
  45. } from 'react-devtools-shared/src/frontend/types';
    
  46. import type {
    
  47.   ElementType,
    
  48.   HookNames,
    
  49. } from 'react-devtools-shared/src/frontend/types';
    
  50. import type {ToggleParseHookNames} from './InspectedElementContext';
    
  51. 
    
  52. export type CopyPath = (path: Array<string | number>) => void;
    
  53. export type InspectPath = (path: Array<string | number>) => void;
    
  54. 
    
  55. type Props = {
    
  56.   element: Element,
    
  57.   hookNames: HookNames | null,
    
  58.   inspectedElement: InspectedElement,
    
  59.   parseHookNames: boolean,
    
  60.   toggleParseHookNames: ToggleParseHookNames,
    
  61. };
    
  62. 
    
  63. export default function InspectedElementView({
    
  64.   element,
    
  65.   hookNames,
    
  66.   inspectedElement,
    
  67.   parseHookNames,
    
  68.   toggleParseHookNames,
    
  69. }: Props): React.Node {
    
  70.   const {id} = element;
    
  71.   const {owners, rendererPackageName, rendererVersion, rootType, source} =
    
  72.     inspectedElement;
    
  73. 
    
  74.   const bridge = useContext(BridgeContext);
    
  75.   const store = useContext(StoreContext);
    
  76. 
    
  77.   const {
    
  78.     isEnabledForInspectedElement: isContextMenuEnabledForInspectedElement,
    
  79.     viewAttributeSourceFunction,
    
  80.   } = useContext<ContextMenuContextType>(ContextMenuContext);
    
  81. 
    
  82.   const rendererLabel =
    
  83.     rendererPackageName !== null && rendererVersion !== null
    
  84.       ? `${rendererPackageName}@${rendererVersion}`
    
  85.       : null;
    
  86.   const showOwnersList = owners !== null && owners.length > 0;
    
  87.   const showRenderedBy =
    
  88.     showOwnersList || rendererLabel !== null || rootType !== null;
    
  89. 
    
  90.   return (
    
  91.     <Fragment>
    
  92.       <div className={styles.InspectedElement}>
    
  93.         <HocBadges element={element} />
    
  94. 
    
  95.         <InspectedElementPropsTree
    
  96.           bridge={bridge}
    
  97.           element={element}
    
  98.           inspectedElement={inspectedElement}
    
  99.           store={store}
    
  100.         />
    
  101. 
    
  102.         <InspectedElementSuspenseToggle
    
  103.           bridge={bridge}
    
  104.           inspectedElement={inspectedElement}
    
  105.           store={store}
    
  106.         />
    
  107. 
    
  108.         <InspectedElementStateTree
    
  109.           bridge={bridge}
    
  110.           element={element}
    
  111.           inspectedElement={inspectedElement}
    
  112.           store={store}
    
  113.         />
    
  114. 
    
  115.         <InspectedElementHooksTree
    
  116.           bridge={bridge}
    
  117.           element={element}
    
  118.           hookNames={hookNames}
    
  119.           inspectedElement={inspectedElement}
    
  120.           parseHookNames={parseHookNames}
    
  121.           store={store}
    
  122.           toggleParseHookNames={toggleParseHookNames}
    
  123.         />
    
  124. 
    
  125.         <InspectedElementContextTree
    
  126.           bridge={bridge}
    
  127.           element={element}
    
  128.           inspectedElement={inspectedElement}
    
  129.           store={store}
    
  130.         />
    
  131. 
    
  132.         {enableStyleXFeatures && (
    
  133.           <InspectedElementStyleXPlugin
    
  134.             bridge={bridge}
    
  135.             element={element}
    
  136.             inspectedElement={inspectedElement}
    
  137.             store={store}
    
  138.           />
    
  139.         )}
    
  140. 
    
  141.         <InspectedElementErrorsAndWarningsTree
    
  142.           bridge={bridge}
    
  143.           element={element}
    
  144.           inspectedElement={inspectedElement}
    
  145.           store={store}
    
  146.         />
    
  147. 
    
  148.         <NativeStyleEditor />
    
  149. 
    
  150.         {showRenderedBy && (
    
  151.           <div
    
  152.             className={styles.Owners}
    
  153.             data-testname="InspectedElementView-Owners">
    
  154.             <div className={styles.OwnersHeader}>rendered by</div>
    
  155.             {showOwnersList &&
    
  156.               ((owners: any): Array<SerializedElement>).map(owner => (
    
  157.                 <OwnerView
    
  158.                   key={owner.id}
    
  159.                   displayName={owner.displayName || 'Anonymous'}
    
  160.                   hocDisplayNames={owner.hocDisplayNames}
    
  161.                   id={owner.id}
    
  162.                   isInStore={store.containsElement(owner.id)}
    
  163.                   type={owner.type}
    
  164.                 />
    
  165.               ))}
    
  166.             {rootType !== null && (
    
  167.               <div className={styles.OwnersMetaField}>{rootType}</div>
    
  168.             )}
    
  169.             {rendererLabel !== null && (
    
  170.               <div className={styles.OwnersMetaField}>{rendererLabel}</div>
    
  171.             )}
    
  172.           </div>
    
  173.         )}
    
  174. 
    
  175.         {source !== null && (
    
  176.           <Source fileName={source.fileName} lineNumber={source.lineNumber} />
    
  177.         )}
    
  178.       </div>
    
  179. 
    
  180.       {isContextMenuEnabledForInspectedElement && (
    
  181.         <ContextMenu id="InspectedElement">
    
  182.           {({path, type: pathType}) => {
    
  183.             const copyInspectedElementPath = () => {
    
  184.               const rendererID = store.getRendererIDForElement(id);
    
  185.               if (rendererID !== null) {
    
  186.                 copyInspectedElementPathAPI({
    
  187.                   bridge,
    
  188.                   id,
    
  189.                   path,
    
  190.                   rendererID,
    
  191.                 });
    
  192.               }
    
  193.             };
    
  194. 
    
  195.             const storeAsGlobal = () => {
    
  196.               const rendererID = store.getRendererIDForElement(id);
    
  197.               if (rendererID !== null) {
    
  198.                 storeAsGlobalAPI({
    
  199.                   bridge,
    
  200.                   id,
    
  201.                   path,
    
  202.                   rendererID,
    
  203.                 });
    
  204.               }
    
  205.             };
    
  206. 
    
  207.             return (
    
  208.               <Fragment>
    
  209.                 <ContextMenuItem
    
  210.                   onClick={copyInspectedElementPath}
    
  211.                   title="Copy value to clipboard">
    
  212.                   <Icon className={styles.ContextMenuIcon} type="copy" /> Copy
    
  213.                   value to clipboard
    
  214.                 </ContextMenuItem>
    
  215.                 <ContextMenuItem
    
  216.                   onClick={storeAsGlobal}
    
  217.                   title="Store as global variable">
    
  218.                   <Icon
    
  219.                     className={styles.ContextMenuIcon}
    
  220.                     type="store-as-global-variable"
    
  221.                   />{' '}
    
  222.                   Store as global variable
    
  223.                 </ContextMenuItem>
    
  224.                 {viewAttributeSourceFunction !== null &&
    
  225.                   pathType === 'function' && (
    
  226.                     <ContextMenuItem
    
  227.                       onClick={() => viewAttributeSourceFunction(id, path)}
    
  228.                       title="Go to definition">
    
  229.                       <Icon className={styles.ContextMenuIcon} type="code" /> Go
    
  230.                       to definition
    
  231.                     </ContextMenuItem>
    
  232.                   )}
    
  233.               </Fragment>
    
  234.             );
    
  235.           }}
    
  236.         </ContextMenu>
    
  237.       )}
    
  238.     </Fragment>
    
  239.   );
    
  240. }
    
  241. 
    
  242. // This function is based on describeComponentFrame() in packages/shared/ReactComponentStackFrame
    
  243. function formatSourceForDisplay(fileName: string, lineNumber: string) {
    
  244.   const BEFORE_SLASH_RE = /^(.*)[\\\/]/;
    
  245. 
    
  246.   let nameOnly = fileName.replace(BEFORE_SLASH_RE, '');
    
  247. 
    
  248.   // In DEV, include code for a common special case:
    
  249.   // prefer "folder/index.js" instead of just "index.js".
    
  250.   if (/^index\./.test(nameOnly)) {
    
  251.     const match = fileName.match(BEFORE_SLASH_RE);
    
  252.     if (match) {
    
  253.       const pathBeforeSlash = match[1];
    
  254.       if (pathBeforeSlash) {
    
  255.         const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, '');
    
  256.         nameOnly = folderName + '/' + nameOnly;
    
  257.       }
    
  258.     }
    
  259.   }
    
  260. 
    
  261.   return `${nameOnly}:${lineNumber}`;
    
  262. }
    
  263. 
    
  264. type SourceProps = {
    
  265.   fileName: string,
    
  266.   lineNumber: string,
    
  267. };
    
  268. 
    
  269. function Source({fileName, lineNumber}: SourceProps) {
    
  270.   const handleCopy = () => copy(`${fileName}:${lineNumber}`);
    
  271.   return (
    
  272.     <div className={styles.Source} data-testname="InspectedElementView-Source">
    
  273.       <div className={styles.SourceHeaderRow}>
    
  274.         <div className={styles.SourceHeader}>source</div>
    
  275.         <Button onClick={handleCopy} title="Copy to clipboard">
    
  276.           <ButtonIcon type="copy" />
    
  277.         </Button>
    
  278.       </div>
    
  279.       <div className={styles.SourceOneLiner}>
    
  280.         {formatSourceForDisplay(fileName, lineNumber)}
    
  281.       </div>
    
  282.     </div>
    
  283.   );
    
  284. }
    
  285. 
    
  286. type OwnerViewProps = {
    
  287.   displayName: string,
    
  288.   hocDisplayNames: Array<string> | null,
    
  289.   id: number,
    
  290.   isInStore: boolean,
    
  291.   type: ElementType,
    
  292. };
    
  293. 
    
  294. function OwnerView({
    
  295.   displayName,
    
  296.   hocDisplayNames,
    
  297.   id,
    
  298.   isInStore,
    
  299.   type,
    
  300. }: OwnerViewProps) {
    
  301.   const dispatch = useContext(TreeDispatcherContext);
    
  302.   const {highlightNativeElement, clearHighlightNativeElement} =
    
  303.     useHighlightNativeElement();
    
  304. 
    
  305.   const handleClick = useCallback(() => {
    
  306.     logEvent({
    
  307.       event_name: 'select-element',
    
  308.       metadata: {source: 'owner-view'},
    
  309.     });
    
  310.     dispatch({
    
  311.       type: 'SELECT_ELEMENT_BY_ID',
    
  312.       payload: id,
    
  313.     });
    
  314.   }, [dispatch, id]);
    
  315. 
    
  316.   const onMouseEnter = () => highlightNativeElement(id);
    
  317. 
    
  318.   const onMouseLeave = clearHighlightNativeElement;
    
  319. 
    
  320.   return (
    
  321.     <Button
    
  322.       key={id}
    
  323.       className={styles.OwnerButton}
    
  324.       disabled={!isInStore}
    
  325.       onClick={handleClick}
    
  326.       onMouseEnter={onMouseEnter}
    
  327.       onMouseLeave={onMouseLeave}>
    
  328.       <span className={styles.OwnerContent}>
    
  329.         <span
    
  330.           className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
    
  331.           title={displayName}>
    
  332.           {displayName}
    
  333.         </span>
    
  334.         <Badge hocDisplayNames={hocDisplayNames} type={type} />
    
  335.       </span>
    
  336.     </Button>
    
  337.   );
    
  338. }