242 lines
7.4 KiB
JavaScript
242 lines
7.4 KiB
JavaScript
import PropTypes from 'prop-types'
|
|
import { Tree, Typography, Space, Tag } from 'antd'
|
|
import { useState, useMemo, useCallback } from 'react'
|
|
import XMarkIcon from '../../Icons/XMarkIcon'
|
|
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
|
|
import JsonStringIcon from '../../Icons/JsonStringIcon'
|
|
import JsonArrayIcon from '../../Icons/JsonArrayIcon'
|
|
import JsonObjectIcon from '../../Icons/JsonObjectIcon'
|
|
import JsonBoolIcon from '../../Icons/JsonBoolIcon'
|
|
import JsonNumberIcon from '../../Icons/JsonNumberIcon'
|
|
import CopyButton from './CopyButton'
|
|
|
|
const { Text } = Typography
|
|
|
|
const DataTree = ({
|
|
data,
|
|
showLine = true,
|
|
showValueCopy = true,
|
|
showKeyCopy = false,
|
|
defaultExpandAll = false,
|
|
onNodeSelect,
|
|
style = {}
|
|
}) => {
|
|
const [expandedKeys, setExpandedKeys] = useState([])
|
|
const [selectedKeys, setSelectedKeys] = useState([])
|
|
|
|
// Function to get data type and format value
|
|
const getDataTypeInfo = (value) => {
|
|
if (value === null)
|
|
return { type: 'null', color: 'default', icon: <XMarkIcon /> }
|
|
if (value === undefined)
|
|
return {
|
|
type: 'undefined',
|
|
color: 'default',
|
|
icon: <QuestionCircleIcon />
|
|
}
|
|
if (typeof value === 'boolean')
|
|
return { type: 'boolean', color: 'blue', icon: <JsonBoolIcon /> }
|
|
if (typeof value === 'number')
|
|
return { type: 'number', color: 'green', icon: <JsonNumberIcon /> }
|
|
if (typeof value === 'string')
|
|
return { type: 'string', color: 'orange', icon: <JsonStringIcon /> }
|
|
if (Array.isArray(value))
|
|
return { type: 'array', color: 'purple', icon: <JsonArrayIcon /> }
|
|
if (typeof value === 'object')
|
|
return { type: 'object', color: 'cyan', icon: <JsonObjectIcon /> }
|
|
return { type: 'unknown', color: 'default', icon: <QuestionCircleIcon /> }
|
|
}
|
|
|
|
// Function to format value for display
|
|
const formatValue = useCallback((value) => {
|
|
if (value === null) return 'null'
|
|
if (value === undefined) return 'undefined'
|
|
if (typeof value === 'boolean') return value.toString()
|
|
if (typeof value === 'number') return value.toString()
|
|
if (typeof value === 'string') {
|
|
// Truncate long strings
|
|
return value.length > 50 ? `${value.substring(0, 50)}...` : value
|
|
}
|
|
if (Array.isArray(value)) return `Array (${value.length} items)`
|
|
if (typeof value === 'object') {
|
|
const keys = Object.keys(value)
|
|
return `Object (${keys.length} properties)`
|
|
}
|
|
return String(value)
|
|
}, [])
|
|
|
|
// Function to get raw value for copying
|
|
const getCopyValue = useCallback((value) => {
|
|
if (value === null) return 'null'
|
|
if (value === undefined) return 'undefined'
|
|
if (typeof value === 'boolean') return value.toString()
|
|
if (typeof value === 'number') return value.toString()
|
|
if (typeof value === 'string') return value
|
|
if (Array.isArray(value)) return JSON.stringify(value, null, 2)
|
|
if (typeof value === 'object') return JSON.stringify(value, null, 2)
|
|
return String(value)
|
|
}, [])
|
|
|
|
// Recursive function to convert JSON to tree data
|
|
const convertToTreeData = useCallback(
|
|
(obj, key = 'root', path = '') => {
|
|
const currentPath = path ? `${path}.${key}` : key
|
|
const dataInfo = getDataTypeInfo(obj)
|
|
|
|
const node = {
|
|
title: (
|
|
<Space size='small'>
|
|
<Tag color={dataInfo.color} size='small' style={{ margin: 0 }}>
|
|
{dataInfo.icon}
|
|
</Tag>
|
|
<Text strong>{key}</Text>
|
|
{showKeyCopy && (
|
|
<CopyButton text={key} tooltip={`Copy key: ${key}`} />
|
|
)}
|
|
<Text type='secondary'>({dataInfo.type})</Text>
|
|
{dataInfo.type !== 'object' && dataInfo.type !== 'array' && (
|
|
<>
|
|
<Text code>{formatValue(obj)}</Text>
|
|
{showValueCopy && (
|
|
<CopyButton
|
|
text={getCopyValue(obj)}
|
|
tooltip={`Copy ${dataInfo.type} value`}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
{(dataInfo.type === 'object' || dataInfo.type === 'array') &&
|
|
showValueCopy && (
|
|
<CopyButton
|
|
text={getCopyValue(obj)}
|
|
tooltip={`Copy ${dataInfo.type} as JSON`}
|
|
/>
|
|
)}
|
|
</Space>
|
|
),
|
|
key: currentPath,
|
|
value: obj,
|
|
path: currentPath
|
|
}
|
|
|
|
// Add children for objects and arrays
|
|
if (typeof obj === 'object' && obj !== null) {
|
|
if (Array.isArray(obj)) {
|
|
node.children = obj.map((item, index) =>
|
|
convertToTreeData(item, `[${index}]`, currentPath)
|
|
)
|
|
} else {
|
|
node.children = Object.entries(obj).map(([childKey, childValue]) =>
|
|
convertToTreeData(childValue, childKey, currentPath)
|
|
)
|
|
}
|
|
}
|
|
|
|
return node
|
|
},
|
|
[showValueCopy, getCopyValue, formatValue, showKeyCopy]
|
|
)
|
|
|
|
// Convert data to tree structure
|
|
const treeData = useMemo(() => {
|
|
if (!data) return []
|
|
|
|
if (typeof data === 'object' && data !== null) {
|
|
if (Array.isArray(data)) {
|
|
return [convertToTreeData(data, 'root')]
|
|
} else {
|
|
return [convertToTreeData(data, 'root')]
|
|
}
|
|
} else {
|
|
// Handle primitive values
|
|
const dataInfo = getDataTypeInfo(data)
|
|
return [
|
|
{
|
|
title: (
|
|
<Space size='small'>
|
|
<Tag color={dataInfo.color} size='small' style={{ margin: 0 }}>
|
|
{dataInfo.icon}
|
|
</Tag>
|
|
<Text strong>Value</Text>
|
|
<Text type='secondary'>({dataInfo.type})</Text>
|
|
<Text code>{formatValue(data)}</Text>
|
|
{showValueCopy && (
|
|
<CopyButton
|
|
text={getCopyValue(data)}
|
|
tooltip={`Copy ${dataInfo.type} value`}
|
|
/>
|
|
)}
|
|
</Space>
|
|
),
|
|
key: 'root',
|
|
value: data,
|
|
path: 'root'
|
|
}
|
|
]
|
|
}
|
|
}, [data, showValueCopy, convertToTreeData, getCopyValue, formatValue])
|
|
|
|
// Handle node selection
|
|
const handleSelect = (selectedKeys, { selected, selectedNodes }) => {
|
|
setSelectedKeys(selectedKeys)
|
|
if (onNodeSelect && selected && selectedNodes.length > 0) {
|
|
const node = selectedNodes[0]
|
|
onNodeSelect({
|
|
key: node.key,
|
|
value: node.value,
|
|
path: node.path
|
|
})
|
|
}
|
|
}
|
|
|
|
// Handle expand/collapse
|
|
const handleExpand = (keys) => {
|
|
setExpandedKeys(keys)
|
|
}
|
|
|
|
// Auto-expand all if requested
|
|
const getExpandedKeys = () => {
|
|
if (defaultExpandAll) {
|
|
return treeData.length > 0 ? getAllKeys(treeData[0]) : []
|
|
}
|
|
return expandedKeys
|
|
}
|
|
|
|
// Helper function to get all keys for auto-expand
|
|
const getAllKeys = (node) => {
|
|
let keys = [node.key]
|
|
if (node.children) {
|
|
node.children.forEach((child) => {
|
|
keys = keys.concat(getAllKeys(child))
|
|
})
|
|
}
|
|
return keys
|
|
}
|
|
|
|
return (
|
|
<Tree
|
|
rootStyle={{ background: 'transparent', ...style }}
|
|
treeData={treeData}
|
|
expandedKeys={getExpandedKeys()}
|
|
selectedKeys={selectedKeys}
|
|
onExpand={handleExpand}
|
|
onSelect={handleSelect}
|
|
showLine={showLine}
|
|
showIcon={false}
|
|
blockNode
|
|
/>
|
|
)
|
|
}
|
|
|
|
DataTree.propTypes = {
|
|
data: PropTypes.any.isRequired,
|
|
showLine: PropTypes.bool,
|
|
showValueCopy: PropTypes.bool,
|
|
showKeyCopy: PropTypes.bool,
|
|
defaultExpandAll: PropTypes.bool,
|
|
onNodeSelect: PropTypes.func,
|
|
style: PropTypes.object
|
|
}
|
|
|
|
export default DataTree
|