flipper-plugin
Version:
Flipper Desktop plugin SDK and components
426 lines • 17.6 kB
JavaScript
"use strict";
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DataInspectorNode = exports.RootDataContext = void 0;
const DataDescription_1 = require("./DataDescription");
const react_1 = require("react");
const styled_1 = __importDefault(require("@emotion/styled"));
const DataPreview_1 = __importStar(require("./DataPreview"));
const utils_1 = require("./utils");
const react_2 = __importDefault(require("react"));
const Highlight_1 = require("../Highlight");
const antd_1 = require("antd");
const useInUnitTest_1 = require("../../utils/useInUnitTest");
const theme_1 = require("../theme");
exports.RootDataContext = (0, react_1.createContext)(() => ({}));
const BaseContainer = styled_1.default.div((props) => ({
fontFamily: 'Menlo, monospace',
fontSize: 11,
lineHeight: '17px',
filter: props.disabled ? 'grayscale(100%)' : '',
margin: props.depth === 0 ? '7.5px 0' : '0',
paddingLeft: 10,
userSelect: 'text',
width: '100%',
backgroundColor: props.hovered ? theme_1.theme.selectionBackgroundColor : '',
}));
BaseContainer.displayName = 'DataInspector:BaseContainer';
const RecursiveBaseWrapper = styled_1.default.span({
color: theme_1.theme.errorColor,
});
RecursiveBaseWrapper.displayName = 'DataInspector:RecursiveBaseWrapper';
const Wrapper = styled_1.default.span({
color: theme_1.theme.textColorSecondary,
});
Wrapper.displayName = 'DataInspector:Wrapper';
const PropertyContainer = styled_1.default.span({
paddingTop: '2px',
});
PropertyContainer.displayName = 'DataInspector:PropertyContainer';
const ExpandControl = styled_1.default.span({
color: theme_1.theme.textColorSecondary,
fontSize: 10,
marginLeft: -11,
marginRight: 5,
whiteSpace: 'pre',
});
ExpandControl.displayName = 'DataInspector:ExpandControl';
const Added = styled_1.default.div({
backgroundColor: theme_1.theme.semanticColors.diffAddedBackground,
});
const Removed = styled_1.default.div({
backgroundColor: theme_1.theme.semanticColors.diffRemovedBackground,
});
const defaultValueExtractor = (value) => {
const type = typeof value;
if (type === 'number') {
return { mutable: true, type: 'number', value };
}
if (type === 'bigint') {
return { mutable: true, type: 'bigint', value };
}
if (type === 'string') {
return { mutable: true, type: 'string', value };
}
if (type === 'boolean') {
return { mutable: true, type: 'boolean', value };
}
if (type === 'undefined') {
return { mutable: true, type: 'undefined', value };
}
if (value === null) {
return { mutable: true, type: 'null', value };
}
if (Array.isArray(value)) {
return { mutable: true, type: 'array', value };
}
if (Object.prototype.toString.call(value) === '[object Date]') {
return { mutable: true, type: 'date', value };
}
if (type === 'object') {
return { mutable: true, type: 'object', value };
}
};
function isPureObject(obj) {
return (obj !== null &&
Object.prototype.toString.call(obj) !== '[object Date]' &&
typeof obj === 'object');
}
const diffMetadataExtractor = (data, key, diff) => {
if (diff == null) {
return [{ data: data[key] }];
}
const val = data[key];
const diffVal = diff[key];
if (!data.hasOwnProperty(key)) {
return [{ data: diffVal, status: 'removed' }];
}
if (!diff.hasOwnProperty(key)) {
return [{ data: val, status: 'added' }];
}
if (isPureObject(diffVal) && isPureObject(val)) {
return [{ data: val, diff: diffVal }];
}
if (diffVal !== val) {
// Check if there's a difference between the original value and
// the value from the diff prop
// The property name still exists, but the values may be different.
return [
{ data: val, status: 'added' },
{ data: diffVal, status: 'removed' },
];
}
return Object.prototype.hasOwnProperty.call(data, key) ? [{ data: val }] : [];
};
function isComponentExpanded(data, diffType, diffValue) {
if (diffValue == null) {
return false;
}
if (diffType === 'object') {
const sortedDataValues = Object.keys(data)
.sort()
.map((key) => data[key]);
const sortedDiffValues = Object.keys(diffValue)
.sort()
.map((key) => diffValue[key]);
if (JSON.stringify(sortedDataValues) !== JSON.stringify(sortedDiffValues)) {
return true;
}
}
else {
if (data !== diffValue) {
return true;
}
}
return false;
}
const recursiveMarker = react_2.default.createElement(RecursiveBaseWrapper, null, "Recursive");
/**
* An expandable data inspector.
*
* This component is fairly low level. It's likely you're looking for
* [`<ManagedDataInspector>`](#manageddatainspector).
*/
exports.DataInspectorNode = (0, react_1.memo)(function DataInspectorImpl({ data, depth, diff, expandRoot, parentPath, onExpanded, onRenderName, onRenderDescription, extractValue: extractValueProp, expanded: expandedPaths, name, parentAncestry, collapsed, tooltips, setValue: setValueProp, hoveredNodePath, setHoveredNodePath, }) {
const highlighter = (0, Highlight_1.useHighlighter)();
const isUnitTest = (0, useInUnitTest_1.useInUnitTest)();
const shouldExpand = (0, react_1.useRef)(false);
const expandHandle = (0, react_1.useRef)(undefined);
const [renderExpanded, setRenderExpanded] = (0, react_1.useState)(false);
const path = (0, react_1.useMemo)(() => (name === undefined ? parentPath : parentPath.concat([name])), [parentPath, name]);
const extractValue = (0, react_1.useCallback)((data, depth, path) => {
let res;
if (extractValueProp) {
res = extractValueProp(data, depth, path);
}
if (!res) {
res = defaultValueExtractor(data, depth, path);
}
return res;
}, [extractValueProp]);
const res = (0, react_1.useMemo)(() => extractValue(data, depth, path), [extractValue, data, depth, path]);
const resDiff = (0, react_1.useMemo)(() => extractValue(diff, depth, path), [extractValue, diff, depth, path]);
const ancestry = (0, react_1.useMemo)(
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
() => (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(
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
res.value, resDiff.type, resDiff.value);
}
}
(0, react_1.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 = (0, react_1.useCallback)((pathParts, isExpanded) => {
if (!onExpanded || !expandedPaths) {
return;
}
const path = pathParts.join('.');
onExpanded(path, isExpanded);
}, [onExpanded, expandedPaths]);
const handleClick = (0, react_1.useCallback)((event) => {
if (!isUnitTest) {
cancelIdleCallback(expandHandle.current);
}
if (event.buttons !== 0) {
//only process left click
return;
}
const isExpanded = shouldBeExpanded(expandedPaths, path, collapsed);
setExpanded(path, !isExpanded);
}, [expandedPaths, path, collapsed, isUnitTest]);
/**
* 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;
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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 = (0, utils_1.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 = (react_2.default.createElement(exports.DataInspectorNode, { setHoveredNodePath: setHoveredNodePath, hoveredNodePath: hoveredNodePath, parentAncestry: ancestry, extractValue: extractValue, setValue: setValue, expanded: expandedPaths, collapsed: collapsed, onExpanded: onExpanded, 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(react_2.default.createElement(Added, { key: metaKey }, dataInspectorNode));
break;
case 'removed':
propertyNodes.push(react_2.default.createElement(Removed, { key: metaKey }, dataInspectorNode));
break;
default:
propertyNodes.push(dataInspectorNode);
}
}
}
propertyNodesContainer = propertyNodes;
}
if (expandRoot === true) {
return react_2.default.createElement(react_2.default.Fragment, null, propertyNodesContainer);
}
// create name components
const nameElems = [];
if (typeof name !== 'undefined') {
const text = onRenderName
? onRenderName(path, name, highlighter)
: highlighter.render(name);
nameElems.push(react_2.default.createElement(antd_1.Tooltip, { title: tooltips != null && tooltips[name], key: "name", placement: "left" },
react_2.default.createElement(DataPreview_1.InspectorName, null, text)));
nameElems.push(react_2.default.createElement("span", { key: "sep" }, ": "));
}
// create description or preview
let descriptionOrPreview;
if (renderExpanded || !isExpandable) {
descriptionOrPreview = (react_2.default.createElement(DataDescription_1.DataDescription, { path: path, setValue: setValue, type: type, value: value, extra: extra }));
descriptionOrPreview = onRenderDescription
? onRenderDescription(descriptionOrPreview)
: descriptionOrPreview;
}
else {
descriptionOrPreview = (react_2.default.createElement(DataPreview_1.default, { path: path, type: type, value: value, extractValue: extractValue, depth: depth }));
}
descriptionOrPreview = (react_2.default.createElement("span", null,
nameElems,
descriptionOrPreview));
let wrapperStart;
let wrapperEnd;
if (renderExpanded) {
if (type === 'object') {
wrapperStart = react_2.default.createElement(Wrapper, null, '{');
wrapperEnd = react_2.default.createElement(Wrapper, null, '}');
}
if (type === 'array') {
wrapperStart = react_2.default.createElement(Wrapper, null, '[');
wrapperEnd = react_2.default.createElement(Wrapper, null, ']');
}
}
const nodePath = path.join('.');
return (react_2.default.createElement(BaseContainer, { hovered: hoveredNodePath === nodePath, onMouseEnter: () => {
setHoveredNodePath(nodePath);
}, onMouseLeave: () => {
setHoveredNodePath(parentPath.join('.'));
}, depth: depth, disabled: !!setValueProp && !!setValue === false },
react_2.default.createElement(PropertyContainer, { onClick: isExpandable ? handleClick : undefined },
expandedPaths && react_2.default.createElement(ExpandControl, null, expandGlyph),
descriptionOrPreview,
wrapperStart),
propertyNodesContainer,
wrapperEnd));
}, dataInspectorPropsAreEqual);
function shouldBeExpanded(expanded, pathParts, collapsed) {
// if we have no expanded object then expand everything
if (expanded == null) {
return true;
}
const path = pathParts.join('.');
// check if there's a setting for this path
if (Object.prototype.hasOwnProperty.call(expanded, path)) {
return expanded[path];
}
// check if all paths are collapsed
if (collapsed === true) {
return false;
}
// by default all items are expanded
return true;
}
function dataInspectorPropsAreEqual(props, nextProps) {
// Optimization: it would be much faster to not pass the expanded tree
// down the tree, but rather introduce an ExpandStateManager, and subscribe per node
// check if any expanded paths effect this subtree
if (nextProps.expanded !== props.expanded) {
const path = !nextProps.name
? '' // root
: !nextProps.parentPath.length
? nextProps.name // root element
: `${nextProps.parentPath.join('.')}.${nextProps.name}`;
// we are being collapsed
if (props.expanded[path] !== nextProps.expanded[path]) {
return false;
}
// one of our children was expande
for (const key in nextProps.expanded) {
if (key.startsWith(path) === false) {
// this key doesn't effect us
continue;
}
if (nextProps.expanded[key] !== props.expanded[key]) {
return false;
}
}
}
const nodePath = (props.name === undefined
? props.parentPath
: props.parentPath.concat([props.name])).join('.');
//if the node is a prefix then we of the hovered path(s) then we *should* render this branch of the tree
//Otherwise we don't need to rerender since this node is not changing hover state
const nodePathIsPrefixOfCurrentOrNextHoverPath = nextProps.hoveredNodePath?.startsWith(nodePath) ||
props.hoveredNodePath?.startsWith(nodePath);
// basic equality checks for the rest
return (nextProps.data === props.data &&
nextProps.diff === props.diff &&
nextProps.name === props.name &&
nextProps.depth === props.depth &&
nextProps.parentPath === props.parentPath &&
nextProps.onExpanded === props.onExpanded &&
nextProps.setValue === props.setValue &&
nextProps.collapsed === props.collapsed &&
nextProps.expandRoot === props.expandRoot &&
!nodePathIsPrefixOfCurrentOrNextHoverPath);
}
function isValueExpandable(data) {
return (typeof data === 'object' && data !== null && Object.keys(data).length > 0);
}
//# sourceMappingURL=DataInspectorNode.js.map