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>
);
},