2022-05-05 08:24:12 +00:00
|
|
|
import React from 'react';
|
|
|
|
|
import _ from 'lodash';
|
|
|
|
|
import cx from 'classnames';
|
|
|
|
|
import { ToolTip } from '@/_components/ToolTip';
|
|
|
|
|
import CopyToClipboardComponent from '@/_components/CopyToClipboard';
|
|
|
|
|
import { Popover } from 'react-bootstrap';
|
|
|
|
|
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
|
|
|
|
import JSONNodeObject from './JSONNodeObject';
|
|
|
|
|
import JSONNodeArray from './JSONNodeArray';
|
|
|
|
|
import JSONNodeValue from './JSONNodeValue';
|
|
|
|
|
import JSONNodeIndicator from './JSONNodeIndicator';
|
|
|
|
|
|
|
|
|
|
export const JSONNode = ({ data, ...restProps }) => {
|
|
|
|
|
const {
|
|
|
|
|
path,
|
|
|
|
|
shouldExpandNode,
|
|
|
|
|
currentNode,
|
|
|
|
|
selectedNode,
|
|
|
|
|
hoveredNode,
|
|
|
|
|
getCurrentPath,
|
|
|
|
|
getCurrentNodeType,
|
|
|
|
|
getLength,
|
|
|
|
|
toUseNodeIcons,
|
|
|
|
|
renderNodeIcons,
|
|
|
|
|
useIndentedBlock,
|
|
|
|
|
updateSelectedNode,
|
|
|
|
|
updateHoveredNode,
|
|
|
|
|
useActions,
|
|
|
|
|
enableCopyToClipboard,
|
|
|
|
|
getNodeShowHideComponents,
|
|
|
|
|
getOnSelectLabelDispatchActions,
|
|
|
|
|
expandWithLabels,
|
|
|
|
|
getAbsoluteNodePath,
|
|
|
|
|
actionsList,
|
|
|
|
|
updateParentState = () => null,
|
|
|
|
|
} = restProps;
|
|
|
|
|
|
|
|
|
|
const [expandable, set] = React.useState(() =>
|
|
|
|
|
typeof shouldExpandNode === 'function' ? shouldExpandNode(path, data) : shouldExpandNode
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const [showHiddenOptionsForNode, setShowHiddenOptionsForNode] = React.useState(false);
|
|
|
|
|
const [showHiddenOptionButtons, setShowHiddenOptionButtons] = React.useState([]);
|
|
|
|
|
const [onSelectDispatchActions, setOnSelectDispatchActions] = React.useState([]);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (showHiddenOptionButtons) {
|
|
|
|
|
setShowHiddenOptionButtons(() => getNodeShowHideComponents(currentNode, path));
|
|
|
|
|
}
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (useActions && currentNode) {
|
|
|
|
|
const onSelectDispatchActions = getOnSelectLabelDispatchActions(currentNode, path).filter(
|
|
|
|
|
(action) => action.onSelect
|
|
|
|
|
);
|
|
|
|
|
if (onSelectDispatchActions.length > 0) {
|
|
|
|
|
setOnSelectDispatchActions(onSelectDispatchActions);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [selectedNode]);
|
|
|
|
|
|
|
|
|
|
const toggleExpandNode = (node) => {
|
|
|
|
|
if (expandable) {
|
|
|
|
|
updateSelectedNode(null);
|
|
|
|
|
} else {
|
|
|
|
|
updateSelectedNode(node, path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set((prev) => !prev);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onSelect = (data, currentNode, path) => {
|
|
|
|
|
const actions = onSelectDispatchActions;
|
|
|
|
|
actions.forEach((action) => action.dispatchAction(data, currentNode));
|
|
|
|
|
|
|
|
|
|
if (!expandWithLabels) {
|
|
|
|
|
updateSelectedNode(currentNode, path);
|
|
|
|
|
set(true);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleOnClickLabels = (data, currentNode, path) => {
|
|
|
|
|
if (expandWithLabels) {
|
|
|
|
|
toggleExpandNode(currentNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (useActions) {
|
|
|
|
|
onSelect(data, currentNode, path);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const typeofCurrentNode = getCurrentNodeType(data);
|
|
|
|
|
const currentNodePath = getCurrentPath(path, currentNode);
|
|
|
|
|
const toExpandNode = (data instanceof Array || data instanceof Object) && !_.isEmpty(data);
|
|
|
|
|
const toShowNodeIndicator = (data instanceof Array || data instanceof Object) && typeofCurrentNode !== 'Function';
|
|
|
|
|
const numberOfEntries = getLength(typeofCurrentNode, data);
|
|
|
|
|
const toRenderSelector = (typeofCurrentNode === 'Object' || typeofCurrentNode === 'Array') && numberOfEntries > 0;
|
|
|
|
|
|
|
|
|
|
let $VALUE = null;
|
|
|
|
|
let $NODEType = null;
|
|
|
|
|
let $NODEIcon = null;
|
|
|
|
|
|
|
|
|
|
const checkSelectedNode = (_selectedNode, _currentNode, parent, toExpand) => {
|
|
|
|
|
if (selectedNode?.parent && parent) {
|
|
|
|
|
return _selectedNode.parent === parent && _selectedNode?.node === _currentNode && toExpand;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return toExpand && _selectedNode?.node === _currentNode;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const parent = path && typeof path?.length === 'number' ? path[path.length - 2] : null;
|
|
|
|
|
|
|
|
|
|
const applySelectedNodeStyles = toExpandNode
|
|
|
|
|
? checkSelectedNode(selectedNode, currentNode, parent, expandable)
|
|
|
|
|
: false;
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (!expandable) {
|
|
|
|
|
updateSelectedNode(null);
|
|
|
|
|
}
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [expandable]);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (selectedNode?.node === currentNode) {
|
|
|
|
|
set(true);
|
|
|
|
|
}
|
|
|
|
|
}, [selectedNode, currentNode]);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (hoveredNode?.node === currentNode && hoveredNode?.parent === parent) {
|
|
|
|
|
setShowHiddenOptionsForNode(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
setShowHiddenOptionsForNode(false);
|
|
|
|
|
};
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [hoveredNode]);
|
|
|
|
|
|
|
|
|
|
if (toUseNodeIcons && currentNode) {
|
|
|
|
|
$NODEIcon = renderNodeIcons(currentNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (typeofCurrentNode) {
|
|
|
|
|
case 'String':
|
|
|
|
|
case 'Boolean':
|
|
|
|
|
case 'Number':
|
|
|
|
|
case 'Null':
|
|
|
|
|
case 'Undefined':
|
|
|
|
|
case 'Function':
|
|
|
|
|
$VALUE = <JSONNodeValue data={data} type={typeofCurrentNode} />;
|
|
|
|
|
$NODEType = <JSONNode.DisplayNodeLabel type={typeofCurrentNode} />;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'Object':
|
|
|
|
|
$VALUE = <JSONNodeObject data={data} path={currentNodePath} {...restProps} />;
|
|
|
|
|
$NODEType = (
|
|
|
|
|
<JSONNode.DisplayNodeLabel type={'Object'}>
|
|
|
|
|
<span className="mx-1 fs-9 node-length-color">
|
|
|
|
|
{`${numberOfEntries} ${numberOfEntries > 1 ? 'entries' : 'entry'}`}{' '}
|
|
|
|
|
</span>
|
|
|
|
|
</JSONNode.DisplayNodeLabel>
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'Array':
|
|
|
|
|
$VALUE = <JSONNodeArray data={data} path={currentNodePath} {...restProps} />;
|
|
|
|
|
$NODEType = (
|
|
|
|
|
<JSONNode.DisplayNodeLabel type={'Array'}>
|
|
|
|
|
<span className="mx-1 fs-9 node-length-color">
|
|
|
|
|
{`${numberOfEntries} ${numberOfEntries > 1 ? 'items' : 'item'}`}{' '}
|
|
|
|
|
</span>
|
|
|
|
|
</JSONNode.DisplayNodeLabel>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
$VALUE = <span>{String(data)}</span>;
|
|
|
|
|
$NODEType = typeofCurrentNode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let $key = (
|
|
|
|
|
<span
|
|
|
|
|
onClick={() => toExpandNode && handleOnClickLabels(data, currentNode, path)}
|
|
|
|
|
style={{ marginTop: '1px', cursor: 'pointer', textTransform: 'none' }}
|
2022-05-05 14:31:57 +00:00
|
|
|
className={cx('node-key fs-12 mx-0 badge badge-outline', {
|
|
|
|
|
'color-primary': applySelectedNodeStyles && !showHiddenOptionsForNode,
|
|
|
|
|
'hovered-node': showHiddenOptionsForNode,
|
|
|
|
|
'node-key-outline': !applySelectedNodeStyles && !showHiddenOptionsForNode,
|
|
|
|
|
})}
|
2022-05-05 08:24:12 +00:00
|
|
|
>
|
|
|
|
|
{String(currentNode)}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!currentNode) {
|
|
|
|
|
return $VALUE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const shouldDisplayIntendedBlock =
|
|
|
|
|
useIndentedBlock && expandable && (typeofCurrentNode === 'Object' || typeofCurrentNode === 'Array');
|
|
|
|
|
|
|
|
|
|
function moreActionsPopover(actions) {
|
|
|
|
|
//Todo: For adding more actions to the menu popover!
|
|
|
|
|
const darkMode = localStorage.getItem('darkMode') === 'true';
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Popover
|
|
|
|
|
id="popover-basic popover-positioned-right json-tree-popover"
|
|
|
|
|
style={{ maxWidth: '350px', padding: '0px' }}
|
|
|
|
|
className={`shadow ${darkMode && 'popover-dark-themed theme-dark'}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="list-group">
|
|
|
|
|
{actions?.map((action, index) => (
|
|
|
|
|
<span
|
|
|
|
|
key={index}
|
|
|
|
|
type="button"
|
|
|
|
|
className="list-group-item list-group-item-action popover-more-actions"
|
|
|
|
|
aria-current="true"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
action.dispatchAction(data, currentNode);
|
|
|
|
|
updateParentState();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{action.name}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</Popover>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const renderHiddenOptionsForNode = () => {
|
|
|
|
|
const moreActions = actionsList.filter((action) => action.for === 'all')[0];
|
|
|
|
|
|
|
|
|
|
const renderOptions = () => {
|
|
|
|
|
if (!useActions || showHiddenOptionButtons?.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
return showHiddenOptionButtons?.map((actionOption, index) => {
|
|
|
|
|
const { name, icon, src, iconName, dispatchAction, width = 12, height = 12 } = actionOption;
|
|
|
|
|
if (icon) {
|
|
|
|
|
return (
|
|
|
|
|
<ToolTip key={`${name}-${index}`} message={`${name} ${currentNode}`}>
|
|
|
|
|
<span
|
|
|
|
|
style={{ height: '13px', width: '13px' }}
|
|
|
|
|
className="mx-1"
|
|
|
|
|
onClick={() => dispatchAction(data, currentNode)}
|
|
|
|
|
>
|
|
|
|
|
<img src={src ?? `/assets/images/icons/${iconName}.svg`} width={width} height={height} />
|
|
|
|
|
</span>
|
|
|
|
|
</ToolTip>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ fontSize: '9px', marginTop: '0px' }} className="d-flex end-0 position-absolute">
|
|
|
|
|
{enableCopyToClipboard && (
|
|
|
|
|
<CopyToClipboardComponent data={currentNodePath} path={true} callback={getAbsoluteNodePath} />
|
|
|
|
|
)}
|
|
|
|
|
{renderOptions()}
|
|
|
|
|
|
|
|
|
|
{moreActions.actions?.length > 0 && (
|
|
|
|
|
<OverlayTrigger
|
|
|
|
|
rootClose={true}
|
|
|
|
|
rootCloseEvent="mousedown"
|
|
|
|
|
trigger="click"
|
|
|
|
|
placement={'right'}
|
|
|
|
|
overlay={moreActionsPopover(moreActions?.actions)}
|
|
|
|
|
>
|
|
|
|
|
<span>
|
|
|
|
|
<ToolTip message={'More actions'}>
|
|
|
|
|
<svg
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
className="icon-tabler icon-tabler-dots-vertical"
|
|
|
|
|
width="13"
|
|
|
|
|
height="13"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
stroke="#2c3e50"
|
|
|
|
|
fill="none"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
|
|
|
<circle cx="12" cy="12" r="1" />
|
|
|
|
|
<circle cx="12" cy="19" r="1" />
|
|
|
|
|
<circle cx="12" cy="5" r="1" />
|
|
|
|
|
</svg>
|
|
|
|
|
</ToolTip>
|
|
|
|
|
</span>
|
|
|
|
|
</OverlayTrigger>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={cx('d-flex row-flex mt-1 font-monospace container-fluid px-1', {
|
|
|
|
|
'json-node-element': !expandable,
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
<div className={`json-tree-icon-container mx-2 ${applySelectedNodeStyles && 'selected-node'}`}>
|
|
|
|
|
<JSONNodeIndicator
|
|
|
|
|
toExpand={expandable}
|
|
|
|
|
toShowNodeIndicator={toShowNodeIndicator}
|
|
|
|
|
handleToggle={toggleExpandNode}
|
|
|
|
|
typeofCurrentNode={typeofCurrentNode}
|
|
|
|
|
currentNode={currentNode}
|
|
|
|
|
isSelected={selectedNode?.node === currentNode}
|
|
|
|
|
toExpandNode={toExpandNode}
|
|
|
|
|
data={data}
|
|
|
|
|
path={currentNodePath}
|
|
|
|
|
toExpandWithLabels={expandWithLabels}
|
|
|
|
|
toggleWithLabels={handleOnClickLabels}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
style={{ width: 'inherit' }}
|
|
|
|
|
className={`${shouldDisplayIntendedBlock && 'group-border'} ${applySelectedNodeStyles && 'selected-node'}`}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
onMouseEnter={() => updateHoveredNode(currentNode, currentNodePath)}
|
|
|
|
|
onMouseLeave={() => updateHoveredNode(null)}
|
|
|
|
|
className={cx('d-flex', {
|
|
|
|
|
'group-object-container': shouldDisplayIntendedBlock,
|
|
|
|
|
'mx-2': typeofCurrentNode !== 'Object' && typeofCurrentNode !== 'Array',
|
|
|
|
|
})}
|
|
|
|
|
>
|
2022-05-05 14:31:57 +00:00
|
|
|
{$NODEIcon && <div className="json-tree-icon-container">{$NODEIcon}</div>}
|
2022-05-05 08:24:12 +00:00
|
|
|
{$key} {$NODEType}
|
|
|
|
|
{!toExpandNode && !expandable && !toRenderSelector ? $VALUE : null}
|
|
|
|
|
<div className="action-icons-group">{showHiddenOptionsForNode && renderHiddenOptionsForNode()}</div>
|
|
|
|
|
</div>
|
|
|
|
|
{toRenderSelector && (toExpandNode && !expandable ? null : $VALUE)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const DisplayNodeLabel = ({ type = '', children }) => {
|
|
|
|
|
if (type === 'Null' || type === 'Undefined') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<span className="mx-1 fs-10 node-type">{type}</span>
|
|
|
|
|
{children}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
JSONNode.DisplayNodeLabel = DisplayNodeLabel;
|