function DataInspectorImpl()

in desktop/flipper-plugin/src/ui/data-inspector/DataInspectorNode.tsx [286:621]


  function DataInspectorImpl({
    data,
    depth,
    diff,
    expandRoot,
    parentPath,
    onExpanded,
    onDelete,
    onRenderName,
    onRenderDescription,
    extractValue: extractValueProp,
    expanded: expandedPaths,
    name,
    parentAncestry,
    collapsed,
    tooltips,
    setValue: setValueProp,
  }) {
    const highlighter = useHighlighter();
    const getRoot = useContext(RootDataContext);
    const isUnitTest = useInUnitTest();

    const shouldExpand = useRef(false);
    const expandHandle = useRef(undefined as any);
    const [renderExpanded, setRenderExpanded] = useState(false);
    const path = useMemo(
      () => (name === undefined ? parentPath : parentPath.concat([name])),
      [parentPath, name],
    );

    const extractValue = useCallback(
      (data: any, depth: number, path: string[]) => {
        let res;
        if (extractValueProp) {
          res = extractValueProp(data, depth, path);
        }
        if (!res) {
          res = defaultValueExtractor(data, depth, path);
        }
        return res;
      },
      [extractValueProp],
    );

    const res = useMemo(
      () => extractValue(data, depth, path),
      [extractValue, data, depth, path],
    );
    const resDiff = useMemo(
      () => extractValue(diff, depth, path),
      [extractValue, diff, depth, path],
    );
    const ancestry = useMemo(
      () => (res ? parentAncestry!.concat([res.value]) : []),
      [parentAncestry, res?.value],
    );

    let isExpandable = false;
    if (!res) {
      shouldExpand.current = false;
    } else {
      isExpandable = isValueExpandable(res.value);
    }

    if (isExpandable) {
      if (
        expandRoot === true ||
        shouldBeExpanded(expandedPaths, path, collapsed)
      ) {
        shouldExpand.current = true;
      } else if (resDiff) {
        shouldExpand.current = isComponentExpanded(
          res!.value,
          resDiff.type,
          resDiff.value,
        );
      }
    }

    useEffect(() => {
      if (!shouldExpand.current) {
        setRenderExpanded(false);
      } else {
        if (isUnitTest) {
          setRenderExpanded(true);
        } else {
          expandHandle.current = requestIdleCallback(() => {
            setRenderExpanded(true);
          });
        }
      }
      return () => {
        if (!isUnitTest) {
          cancelIdleCallback(expandHandle.current);
        }
      };
    }, [shouldExpand.current, isUnitTest]);

    const setExpanded = useCallback(
      (pathParts: Array<string>, isExpanded: boolean) => {
        if (!onExpanded || !expandedPaths) {
          return;
        }
        const path = pathParts.join('.');
        onExpanded(path, isExpanded);
      },
      [onExpanded, expandedPaths],
    );

    const handleClick = useCallback(() => {
      if (!isUnitTest) {
        cancelIdleCallback(expandHandle.current);
      }
      const isExpanded = shouldBeExpanded(expandedPaths, path, collapsed);
      setExpanded(path, !isExpanded);
    }, [expandedPaths, path, collapsed, isUnitTest]);

    const handleDelete = useCallback(
      (path: Array<string>) => {
        if (!onDelete) {
          return;
        }
        onDelete(path);
      },
      [onDelete],
    );

    /**
     * RENDERING
     */
    if (!res) {
      return null;
    }

    // the data inspector makes values read only when setValue isn't set so we just need to set it
    // to null and the readOnly status will be propagated to all children
    const setValue = res.mutable ? setValueProp : null;
    const {value, type, extra} = res;

    if (parentAncestry!.includes(value)) {
      return recursiveMarker;
    }

    let expandGlyph = '';
    if (isExpandable) {
      if (shouldExpand.current) {
        expandGlyph = '▼';
      } else {
        expandGlyph = '▶';
      }
    } else {
      if (depth !== 0) {
        expandGlyph = ' ';
      }
    }

    let propertyNodesContainer = null;
    if (isExpandable && renderExpanded) {
      const propertyNodes = [];

      const diffValue = diff && resDiff ? resDiff.value : null;

      const keys = getSortedKeys({...value, ...diffValue});

      for (const key of keys) {
        const diffMetadataArr = diffMetadataExtractor(value, key, diffValue);
        for (const [index, metadata] of diffMetadataArr.entries()) {
          const metaKey = key + index;
          const dataInspectorNode = (
            <DataInspectorNode
              parentAncestry={ancestry}
              extractValue={extractValue}
              setValue={setValue}
              expanded={expandedPaths}
              collapsed={collapsed}
              onExpanded={onExpanded}
              onDelete={onDelete}
              onRenderName={onRenderName}
              onRenderDescription={onRenderDescription}
              parentPath={path}
              depth={depth + 1}
              key={metaKey}
              name={key}
              data={metadata.data}
              diff={metadata.diff}
              tooltips={tooltips}
            />
          );

          switch (metadata.status) {
            case 'added':
              propertyNodes.push(
                <Added key={metaKey}>{dataInspectorNode}</Added>,
              );
              break;
            case 'removed':
              propertyNodes.push(
                <Removed key={metaKey}>{dataInspectorNode}</Removed>,
              );
              break;
            default:
              propertyNodes.push(dataInspectorNode);
          }
        }
      }

      propertyNodesContainer = propertyNodes;
    }

    if (expandRoot === true) {
      return <>{propertyNodesContainer}</>;
    }

    // create name components
    const nameElems = [];
    if (typeof name !== 'undefined') {
      const text = onRenderName
        ? onRenderName(path, name, highlighter)
        : highlighter.render(name);
      nameElems.push(
        <Tooltip
          title={tooltips != null && tooltips[name]}
          key="name"
          placement="left">
          <InspectorName>{text}</InspectorName>
        </Tooltip>,
      );
      nameElems.push(<span key="sep">: </span>);
    }

    // create description or preview
    let descriptionOrPreview;
    if (renderExpanded || !isExpandable) {
      descriptionOrPreview = (
        <DataDescription
          path={path}
          setValue={setValue}
          type={type}
          value={value}
          extra={extra}
        />
      );

      descriptionOrPreview = onRenderDescription
        ? onRenderDescription(descriptionOrPreview)
        : descriptionOrPreview;
    } else {
      descriptionOrPreview = (
        <DataPreview
          path={path}
          type={type}
          value={value}
          extractValue={extractValue}
          depth={depth}
        />
      );
    }

    descriptionOrPreview = (
      <span>
        {nameElems}
        {descriptionOrPreview}
      </span>
    );

    let wrapperStart;
    let wrapperEnd;
    if (renderExpanded) {
      if (type === 'object') {
        wrapperStart = <Wrapper>{'{'}</Wrapper>;
        wrapperEnd = <Wrapper>{'}'}</Wrapper>;
      }

      if (type === 'array') {
        wrapperStart = <Wrapper>{'['}</Wrapper>;
        wrapperEnd = <Wrapper>{']'}</Wrapper>;
      }
    }

    function getContextMenu() {
      const lib = tryGetFlipperLibImplementation();
      return (
        <Menu>
          <Menu.Item
            key="copyClipboard"
            onClick={() => {
              lib?.writeTextToClipboard(safeStringify(getRoot()));
            }}>
            Copy tree
          </Menu.Item>
          {lib?.isFB && (
            <Menu.Item
              key="createPaste"
              onClick={() => {
                lib?.createPaste(safeStringify(getRoot()));
              }}>
              Create paste from tree
            </Menu.Item>
          )}
          <Menu.Divider />
          <Menu.Item
            key="copyValue"
            onClick={() => {
              lib?.writeTextToClipboard(safeStringify(data));
            }}>
            Copy value
          </Menu.Item>
          {!isExpandable && onDelete ? (
            <Menu.Item
              key="delete"
              onClick={() => {
                handleDelete(path);
              }}>
              Delete
            </Menu.Item>
          ) : null}
        </Menu>
      );
    }

    return (
      <Dropdown overlay={getContextMenu} trigger={contextMenuTrigger}>
        <BaseContainer
          depth={depth}
          disabled={!!setValueProp && !!setValue === false}>
          <PropertyContainer onClick={isExpandable ? handleClick : undefined}>
            {expandedPaths && <ExpandControl>{expandGlyph}</ExpandControl>}
            {descriptionOrPreview}
            {wrapperStart}
          </PropertyContainer>
          {propertyNodesContainer}
          {wrapperEnd}
        </BaseContainer>
      </Dropdown>
    );
  },