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 {
    
  12.   useCallback,
    
  13.   useContext,
    
  14.   useEffect,
    
  15.   useMemo,
    
  16.   useRef,
    
  17.   useState,
    
  18. } from 'react';
    
  19. import {
    
  20.   LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
    
  21.   LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
    
  22. } from '../../../constants';
    
  23. import {useLocalStorage, useSubscription} from '../hooks';
    
  24. import {StoreContext} from '../context';
    
  25. import Button from '../Button';
    
  26. import ButtonIcon from '../ButtonIcon';
    
  27. import Toggle from '../Toggle';
    
  28. import {SettingsContext} from '../Settings/SettingsContext';
    
  29. import {
    
  30.   ComponentFilterDisplayName,
    
  31.   ComponentFilterElementType,
    
  32.   ComponentFilterHOC,
    
  33.   ComponentFilterLocation,
    
  34.   ElementTypeClass,
    
  35.   ElementTypeContext,
    
  36.   ElementTypeFunction,
    
  37.   ElementTypeForwardRef,
    
  38.   ElementTypeHostComponent,
    
  39.   ElementTypeMemo,
    
  40.   ElementTypeOtherOrUnknown,
    
  41.   ElementTypeProfiler,
    
  42.   ElementTypeSuspense,
    
  43. } from 'react-devtools-shared/src/frontend/types';
    
  44. import {getDefaultOpenInEditorURL} from 'react-devtools-shared/src/utils';
    
  45. 
    
  46. import styles from './SettingsShared.css';
    
  47. 
    
  48. import type {
    
  49.   BooleanComponentFilter,
    
  50.   ComponentFilter,
    
  51.   ComponentFilterType,
    
  52.   ElementType,
    
  53.   ElementTypeComponentFilter,
    
  54.   RegExpComponentFilter,
    
  55. } from 'react-devtools-shared/src/frontend/types';
    
  56. 
    
  57. const vscodeFilepath = 'vscode://file/{path}:{line}';
    
  58. 
    
  59. export default function ComponentsSettings(_: {}): React.Node {
    
  60.   const store = useContext(StoreContext);
    
  61.   const {parseHookNames, setParseHookNames} = useContext(SettingsContext);
    
  62. 
    
  63.   const collapseNodesByDefaultSubscription = useMemo(
    
  64.     () => ({
    
  65.       getCurrentValue: () => store.collapseNodesByDefault,
    
  66.       subscribe: (callback: Function) => {
    
  67.         store.addListener('collapseNodesByDefault', callback);
    
  68.         return () => store.removeListener('collapseNodesByDefault', callback);
    
  69.       },
    
  70.     }),
    
  71.     [store],
    
  72.   );
    
  73.   const collapseNodesByDefault = useSubscription<boolean>(
    
  74.     collapseNodesByDefaultSubscription,
    
  75.   );
    
  76. 
    
  77.   const updateCollapseNodesByDefault = useCallback(
    
  78.     ({currentTarget}: $FlowFixMe) => {
    
  79.       store.collapseNodesByDefault = !currentTarget.checked;
    
  80.     },
    
  81.     [store],
    
  82.   );
    
  83. 
    
  84.   const updateParseHookNames = useCallback(
    
  85.     ({currentTarget}: $FlowFixMe) => {
    
  86.       setParseHookNames(currentTarget.checked);
    
  87.     },
    
  88.     [setParseHookNames],
    
  89.   );
    
  90. 
    
  91.   const [openInEditorURLPreset, setOpenInEditorURLPreset] = useLocalStorage<
    
  92.     'vscode' | 'custom',
    
  93.   >(LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET, 'custom');
    
  94. 
    
  95.   const [openInEditorURL, setOpenInEditorURL] = useLocalStorage<string>(
    
  96.     LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
    
  97.     getDefaultOpenInEditorURL(),
    
  98.   );
    
  99. 
    
  100.   const [componentFilters, setComponentFilters] = useState<
    
  101.     Array<ComponentFilter>,
    
  102.   >(() => [...store.componentFilters]);
    
  103. 
    
  104.   const addFilter = useCallback(() => {
    
  105.     setComponentFilters(prevComponentFilters => {
    
  106.       return [
    
  107.         ...prevComponentFilters,
    
  108.         {
    
  109.           type: ComponentFilterElementType,
    
  110.           value: ElementTypeHostComponent,
    
  111.           isEnabled: true,
    
  112.         },
    
  113.       ];
    
  114.     });
    
  115.   }, []);
    
  116. 
    
  117.   const changeFilterType = useCallback(
    
  118.     (componentFilter: ComponentFilter, type: ComponentFilterType) => {
    
  119.       setComponentFilters(prevComponentFilters => {
    
  120.         const cloned: Array<ComponentFilter> = [...prevComponentFilters];
    
  121.         const index = prevComponentFilters.indexOf(componentFilter);
    
  122.         if (index >= 0) {
    
  123.           if (type === ComponentFilterElementType) {
    
  124.             cloned[index] = {
    
  125.               type: ComponentFilterElementType,
    
  126.               isEnabled: componentFilter.isEnabled,
    
  127.               value: ElementTypeHostComponent,
    
  128.             };
    
  129.           } else if (type === ComponentFilterDisplayName) {
    
  130.             cloned[index] = {
    
  131.               type: ComponentFilterDisplayName,
    
  132.               isEnabled: componentFilter.isEnabled,
    
  133.               isValid: true,
    
  134.               value: '',
    
  135.             };
    
  136.           } else if (type === ComponentFilterLocation) {
    
  137.             cloned[index] = {
    
  138.               type: ComponentFilterLocation,
    
  139.               isEnabled: componentFilter.isEnabled,
    
  140.               isValid: true,
    
  141.               value: '',
    
  142.             };
    
  143.           } else if (type === ComponentFilterHOC) {
    
  144.             cloned[index] = {
    
  145.               type: ComponentFilterHOC,
    
  146.               isEnabled: componentFilter.isEnabled,
    
  147.               isValid: true,
    
  148.             };
    
  149.           }
    
  150.         }
    
  151.         return cloned;
    
  152.       });
    
  153.     },
    
  154.     [],
    
  155.   );
    
  156. 
    
  157.   const updateFilterValueElementType = useCallback(
    
  158.     (componentFilter: ComponentFilter, value: ElementType) => {
    
  159.       if (componentFilter.type !== ComponentFilterElementType) {
    
  160.         throw Error('Invalid value for element type filter');
    
  161.       }
    
  162. 
    
  163.       setComponentFilters(prevComponentFilters => {
    
  164.         const cloned: Array<ComponentFilter> = [...prevComponentFilters];
    
  165.         if (componentFilter.type === ComponentFilterElementType) {
    
  166.           const index = prevComponentFilters.indexOf(componentFilter);
    
  167.           if (index >= 0) {
    
  168.             cloned[index] = {
    
  169.               ...componentFilter,
    
  170.               value,
    
  171.             };
    
  172.           }
    
  173.         }
    
  174.         return cloned;
    
  175.       });
    
  176.     },
    
  177.     [],
    
  178.   );
    
  179. 
    
  180.   const updateFilterValueRegExp = useCallback(
    
  181.     (componentFilter: ComponentFilter, value: string) => {
    
  182.       if (componentFilter.type === ComponentFilterElementType) {
    
  183.         throw Error('Invalid value for element type filter');
    
  184.       }
    
  185. 
    
  186.       setComponentFilters(prevComponentFilters => {
    
  187.         const cloned: Array<ComponentFilter> = [...prevComponentFilters];
    
  188.         if (
    
  189.           componentFilter.type === ComponentFilterDisplayName ||
    
  190.           componentFilter.type === ComponentFilterLocation
    
  191.         ) {
    
  192.           const index = prevComponentFilters.indexOf(componentFilter);
    
  193.           if (index >= 0) {
    
  194.             let isValid = true;
    
  195.             try {
    
  196.               new RegExp(value); // eslint-disable-line no-new
    
  197.             } catch (error) {
    
  198.               isValid = false;
    
  199.             }
    
  200.             cloned[index] = {
    
  201.               ...componentFilter,
    
  202.               isValid,
    
  203.               value,
    
  204.             };
    
  205.           }
    
  206.         }
    
  207.         return cloned;
    
  208.       });
    
  209.     },
    
  210.     [],
    
  211.   );
    
  212. 
    
  213.   const removeFilter = useCallback((index: number) => {
    
  214.     setComponentFilters(prevComponentFilters => {
    
  215.       const cloned: Array<ComponentFilter> = [...prevComponentFilters];
    
  216.       cloned.splice(index, 1);
    
  217.       return cloned;
    
  218.     });
    
  219.   }, []);
    
  220. 
    
  221.   const removeAllFilter = () => {
    
  222.     setComponentFilters([]);
    
  223.   };
    
  224. 
    
  225.   const toggleFilterIsEnabled = useCallback(
    
  226.     (componentFilter: ComponentFilter, isEnabled: boolean) => {
    
  227.       setComponentFilters(prevComponentFilters => {
    
  228.         const cloned: Array<ComponentFilter> = [...prevComponentFilters];
    
  229.         const index = prevComponentFilters.indexOf(componentFilter);
    
  230.         if (index >= 0) {
    
  231.           if (componentFilter.type === ComponentFilterElementType) {
    
  232.             cloned[index] = {
    
  233.               ...((cloned[index]: any): ElementTypeComponentFilter),
    
  234.               isEnabled,
    
  235.             };
    
  236.           } else if (
    
  237.             componentFilter.type === ComponentFilterDisplayName ||
    
  238.             componentFilter.type === ComponentFilterLocation
    
  239.           ) {
    
  240.             cloned[index] = {
    
  241.               ...((cloned[index]: any): RegExpComponentFilter),
    
  242.               isEnabled,
    
  243.             };
    
  244.           } else if (componentFilter.type === ComponentFilterHOC) {
    
  245.             cloned[index] = {
    
  246.               ...((cloned[index]: any): BooleanComponentFilter),
    
  247.               isEnabled,
    
  248.             };
    
  249.           }
    
  250.         }
    
  251.         return cloned;
    
  252.       });
    
  253.     },
    
  254.     [],
    
  255.   );
    
  256. 
    
  257.   // Filter updates are expensive to apply (since they impact the entire tree).
    
  258.   // Only apply them on unmount.
    
  259.   // The Store will avoid doing any expensive work unless they've changed.
    
  260.   // We just want to batch the work in the event that they do change.
    
  261.   const componentFiltersRef = useRef<Array<ComponentFilter>>(componentFilters);
    
  262.   useEffect(() => {
    
  263.     componentFiltersRef.current = componentFilters;
    
  264.     return () => {};
    
  265.   }, [componentFilters]);
    
  266.   useEffect(
    
  267.     () => () => {
    
  268.       store.componentFilters = [...componentFiltersRef.current];
    
  269.     },
    
  270.     [store],
    
  271.   );
    
  272. 
    
  273.   return (
    
  274.     <div className={styles.Settings}>
    
  275.       <label className={styles.Setting}>
    
  276.         <input
    
  277.           type="checkbox"
    
  278.           checked={!collapseNodesByDefault}
    
  279.           onChange={updateCollapseNodesByDefault}
    
  280.         />{' '}
    
  281.         Expand component tree by default
    
  282.       </label>
    
  283. 
    
  284.       <label className={styles.Setting}>
    
  285.         <input
    
  286.           type="checkbox"
    
  287.           checked={parseHookNames}
    
  288.           onChange={updateParseHookNames}
    
  289.         />{' '}
    
  290.         Always parse hook names from source{' '}
    
  291.         <span className={styles.Warning}>(may be slow)</span>
    
  292.       </label>
    
  293. 
    
  294.       <label className={styles.OpenInURLSetting}>
    
  295.         Open in Editor URL:{' '}
    
  296.         <select
    
  297.           className={styles.Select}
    
  298.           value={openInEditorURLPreset}
    
  299.           onChange={({currentTarget}) => {
    
  300.             const selectedValue = currentTarget.value;
    
  301.             setOpenInEditorURLPreset(selectedValue);
    
  302.             if (selectedValue === 'vscode') {
    
  303.               setOpenInEditorURL(vscodeFilepath);
    
  304.             } else if (selectedValue === 'custom') {
    
  305.               setOpenInEditorURL('');
    
  306.             }
    
  307.           }}>
    
  308.           <option value="vscode">VS Code</option>
    
  309.           <option value="custom">Custom</option>
    
  310.         </select>
    
  311.         {openInEditorURLPreset === 'custom' && (
    
  312.           <input
    
  313.             className={styles.Input}
    
  314.             type="text"
    
  315.             placeholder={process.env.EDITOR_URL ? process.env.EDITOR_URL : ''}
    
  316.             value={openInEditorURL}
    
  317.             onChange={event => {
    
  318.               setOpenInEditorURL(event.target.value);
    
  319.             }}
    
  320.           />
    
  321.         )}
    
  322.       </label>
    
  323. 
    
  324.       <div className={styles.Header}>Hide components where...</div>
    
  325. 
    
  326.       <table className={styles.Table}>
    
  327.         <tbody>
    
  328.           {componentFilters.length === 0 && (
    
  329.             <tr className={styles.TableRow}>
    
  330.               <td className={styles.NoFiltersCell}>
    
  331.                 No filters have been added.
    
  332.               </td>
    
  333.             </tr>
    
  334.           )}
    
  335.           {componentFilters.map((componentFilter, index) => (
    
  336.             <tr className={styles.TableRow} key={index}>
    
  337.               <td className={styles.TableCell}>
    
  338.                 <Toggle
    
  339.                   className={
    
  340.                     componentFilter.isValid !== false
    
  341.                       ? ''
    
  342.                       : styles.InvalidRegExp
    
  343.                   }
    
  344.                   isChecked={componentFilter.isEnabled}
    
  345.                   onChange={isEnabled =>
    
  346.                     toggleFilterIsEnabled(componentFilter, isEnabled)
    
  347.                   }
    
  348.                   title={
    
  349.                     componentFilter.isValid === false
    
  350.                       ? 'Filter invalid'
    
  351.                       : componentFilter.isEnabled
    
  352.                       ? 'Filter enabled'
    
  353.                       : 'Filter disabled'
    
  354.                   }>
    
  355.                   <ToggleIcon
    
  356.                     isEnabled={componentFilter.isEnabled}
    
  357.                     isValid={
    
  358.                       componentFilter.isValid == null ||
    
  359.                       componentFilter.isValid === true
    
  360.                     }
    
  361.                   />
    
  362.                 </Toggle>
    
  363.               </td>
    
  364.               <td className={styles.TableCell}>
    
  365.                 <select
    
  366.                   className={styles.Select}
    
  367.                   value={componentFilter.type}
    
  368.                   onChange={({currentTarget}) =>
    
  369.                     changeFilterType(
    
  370.                       componentFilter,
    
  371.                       ((parseInt(
    
  372.                         currentTarget.value,
    
  373.                         10,
    
  374.                       ): any): ComponentFilterType),
    
  375.                     )
    
  376.                   }>
    
  377.                   <option value={ComponentFilterLocation}>location</option>
    
  378.                   <option value={ComponentFilterDisplayName}>name</option>
    
  379.                   <option value={ComponentFilterElementType}>type</option>
    
  380.                   <option value={ComponentFilterHOC}>hoc</option>
    
  381.                 </select>
    
  382.               </td>
    
  383.               <td className={styles.TableCell}>
    
  384.                 {componentFilter.type === ComponentFilterElementType &&
    
  385.                   'equals'}
    
  386.                 {(componentFilter.type === ComponentFilterLocation ||
    
  387.                   componentFilter.type === ComponentFilterDisplayName) &&
    
  388.                   'matches'}
    
  389.               </td>
    
  390.               <td className={styles.TableCell}>
    
  391.                 {componentFilter.type === ComponentFilterElementType && (
    
  392.                   <select
    
  393.                     className={styles.Select}
    
  394.                     value={componentFilter.value}
    
  395.                     onChange={({currentTarget}) =>
    
  396.                       updateFilterValueElementType(
    
  397.                         componentFilter,
    
  398.                         ((parseInt(currentTarget.value, 10): any): ElementType),
    
  399.                       )
    
  400.                     }>
    
  401.                     <option value={ElementTypeClass}>class</option>
    
  402.                     <option value={ElementTypeContext}>context</option>
    
  403.                     <option value={ElementTypeFunction}>function</option>
    
  404.                     <option value={ElementTypeForwardRef}>forward ref</option>
    
  405.                     <option value={ElementTypeHostComponent}>
    
  406.                       dom nodes (e.g. &lt;div&gt;)
    
  407.                     </option>
    
  408.                     <option value={ElementTypeMemo}>memo</option>
    
  409.                     <option value={ElementTypeOtherOrUnknown}>other</option>
    
  410.                     <option value={ElementTypeProfiler}>profiler</option>
    
  411.                     <option value={ElementTypeSuspense}>suspense</option>
    
  412.                   </select>
    
  413.                 )}
    
  414.                 {(componentFilter.type === ComponentFilterLocation ||
    
  415.                   componentFilter.type === ComponentFilterDisplayName) && (
    
  416.                   <input
    
  417.                     className={styles.Input}
    
  418.                     type="text"
    
  419.                     placeholder="Regular expression"
    
  420.                     onChange={({currentTarget}) =>
    
  421.                       updateFilterValueRegExp(
    
  422.                         componentFilter,
    
  423.                         currentTarget.value,
    
  424.                       )
    
  425.                     }
    
  426.                     value={componentFilter.value}
    
  427.                   />
    
  428.                 )}
    
  429.               </td>
    
  430.               <td className={styles.TableCell}>
    
  431.                 <Button
    
  432.                   onClick={() => removeFilter(index)}
    
  433.                   title="Delete filter">
    
  434.                   <ButtonIcon type="delete" />
    
  435.                 </Button>
    
  436.               </td>
    
  437.             </tr>
    
  438.           ))}
    
  439.         </tbody>
    
  440.       </table>
    
  441.       <Button onClick={addFilter} title="Add filter">
    
  442.         <ButtonIcon className={styles.ButtonIcon} type="add" />
    
  443.         Add filter
    
  444.       </Button>
    
  445.       {componentFilters.length > 0 && (
    
  446.         <Button onClick={removeAllFilter} title="Delete all filters">
    
  447.           <ButtonIcon className={styles.ButtonIcon} type="delete" />
    
  448.           Delete all filters
    
  449.         </Button>
    
  450.       )}
    
  451.     </div>
    
  452.   );
    
  453. }
    
  454. 
    
  455. type ToggleIconProps = {
    
  456.   isEnabled: boolean,
    
  457.   isValid: boolean,
    
  458. };
    
  459. function ToggleIcon({isEnabled, isValid}: ToggleIconProps) {
    
  460.   let className;
    
  461.   if (isValid) {
    
  462.     className = isEnabled ? styles.ToggleOn : styles.ToggleOff;
    
  463.   } else {
    
  464.     className = isEnabled ? styles.ToggleOnInvalid : styles.ToggleOffInvalid;
    
  465.   }
    
  466.   return (
    
  467.     <div className={className}>
    
  468.       <div
    
  469.         className={isEnabled ? styles.ToggleInsideOn : styles.ToggleInsideOff}
    
  470.       />
    
  471.     </div>
    
  472.   );
    
  473. }