import React, { useState } from 'react';
import { ChevronLeft, Braces, List, Type, Hash, ToggleLeft, Minus, Eye, Code, Copy, Check } from 'lucide-react';
const PostmanTable = ({ data, title = "JSON Data" }) => {
const [currentPath, setCurrentPath] = useState([]);
const [renderHtml, setRenderHtml] = useState(true);
const [copiedItems, setCopiedItems] = useState(new Set());
// Get current data based on path
const getCurrentData = () => {
let current = data;
for (const pathSegment of currentPath) {
if (current && typeof current === 'object') {
current = current[pathSegment];
}
}
return current;
};
const currentData = getCurrentData();
// Check if current data is an array for horizontal table display
const isArrayView = Array.isArray(currentData);
const isObjectView = currentData && typeof currentData === 'object' && !Array.isArray(currentData);
// Generate table headers for array view
const getArrayHeaders = () => {
if (!isArrayView || currentData.length === 0) return [];
// Check if array contains objects or primitives
const firstItem = currentData[0];
if (firstItem && typeof firstItem === 'object' && !Array.isArray(firstItem)) {
// Array of objects - get all possible keys
const headers = new Set();
currentData.forEach(item => {
if (item && typeof item === 'object') {
Object.keys(item).forEach(key => headers.add(key));
}
});
return Array.from(headers);
} else {
// Array of primitives - use "Value" as header
return ['Value'];
}
};
const headers = getArrayHeaders();
// Check if value contains HTML
const isHtmlContent = (value) => {
return value && typeof value === 'string' &&
(value.includes('<') && value.includes('>')) &&
/<[^>]+>/.test(value);
};
// Copy value to clipboard
const copyToClipboard = async (value, itemId) => {
try {
const textValue = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
await navigator.clipboard.writeText(textValue);
// Show feedback
setCopiedItems(prev => new Set([...prev, itemId]));
setTimeout(() => {
setCopiedItems(prev => {
const newSet = new Set(prev);
newSet.delete(itemId);
return newSet;
});
}, 2500);
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
// Show feedback for fallback too
setCopiedItems(prev => new Set([...prev, itemId]));
setTimeout(() => {
setCopiedItems(prev => {
const newSet = new Set(prev);
newSet.delete(itemId);
return newSet;
});
}, 2500);
}
};
// Handle row click - navigate to item details
const handleRowClick = (index, key = null) => {
if (isArrayView) {
setCurrentPath([...currentPath, index]);
} else if (isObjectView && key) {
// Navigate into object property
setCurrentPath([...currentPath, key]);
}
};
// Handle back navigation
const handleBack = () => {
if (currentPath.length > 0) {
const newPath = currentPath.slice(0, -1);
setCurrentPath(newPath);
}
};
// Handle breadcrumb navigation
const handleBreadcrumbClick = (index) => {
if (index === 0) {
// Click on "Root" - go to root
setCurrentPath([]);
} else {
// Click on specific breadcrumb - navigate to that level
const newPath = currentPath.slice(0, index);
setCurrentPath(newPath);
}
};
// Generate breadcrumb
const getBreadcrumb = () => {
const parts = ['Root'];
currentPath.forEach((segment, index) => {
if (typeof segment === 'number') {
parts.push(segment.toString());
} else {
parts.push(segment);
}
});
return parts;
};
// Format value for display in table (truncated)
const formatValue = (value) => {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (typeof value === 'string') {
// Truncate long strings for table display
if (value.length > 100) {
return value.substring(0, 100) + '...';
}
// Replace newlines with spaces for single-line display
return value.replace(/\n/g, ' ').replace(/\s+/g, ' ');
}
if (typeof value === 'boolean') return value.toString();
if (typeof value === 'number') return value.toString();
if (typeof value === 'object') {
if (Array.isArray(value)) return `Array(${value.length})`;
return `Object(${Object.keys(value).length})`;
}
return String(value);
};
// Format value for display in details view (full text)
const formatFullValue = (value) => {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (typeof value === 'string') return value; // Show full string without truncation
if (typeof value === 'boolean') return value.toString();
if (typeof value === 'number') return value.toString();
if (typeof value === 'object') {
if (Array.isArray(value)) return `Array(${value.length})`;
return `Object(${Object.keys(value).length})`;
}
return String(value);
};
// Get type-specific styling and icon
const getTypeStyle = (value) => {
if (value === null) {
return {
color: 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-300',
icon: ,
type: 'null'
};
}
if (Array.isArray(value)) {
return {
color: 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300',
icon:
,
type: 'array'
};
}
switch (typeof value) {
case 'object':
return {
color: 'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300',
icon: ,
type: 'object'
};
case 'string':
return {
color: 'bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-300',
icon: ,
type: 'string'
};
case 'number':
return {
color: 'bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-300',
icon: ,
type: 'number'
};
case 'boolean':
return {
color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300',
icon: ,
type: 'boolean'
};
default:
return {
color: 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-300',
icon: ,
type: 'unknown'
};
}
};
// Render value with appropriate styling (for table view)
const renderValue = (value) => {
const typeStyle = getTypeStyle(value);
const formattedValue = formatValue(value);
return (
{typeStyle.icon}
{formattedValue}
);
};
// Render value with full text (for detail view)
const renderFullValue = (value) => {
const typeStyle = getTypeStyle(value);
const formattedValue = formatFullValue(value);
const hasHtml = isHtmlContent(value);
return (
{typeStyle.icon}
{hasHtml && renderHtml ? (
) : (
formattedValue
)}
{/* HTML Toggle Buttons */}
{hasHtml && (
)}
);
};
// Get value type
const getValueType = (value) => {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
return typeof value;
};
return (
{/* Header with Breadcrumb */}
{currentPath.length > 0 && (
)}
{getBreadcrumb().map((part, index) => (
{index > 0 && /}
))}
{isArrayView && `${currentData.length} items`}
{isObjectView && `${Object.keys(currentData).length} properties`}
{/* Content */}
{isArrayView ? (
// Horizontal table for arrays
|
#
|
{headers.map(header => (
{header}
|
))}
{currentData.map((item, index) => {
const isPrimitiveArray = headers.length === 1 && headers[0] === 'Value';
return (
handleRowClick(index)}
className="hover:bg-blue-50 dark:hover:bg-blue-900/20 cursor-pointer transition-colors duration-150"
>
|
{index}
|
{isPrimitiveArray ? (
{renderValue(item)}
|
) : (
headers.map(header => (
{renderValue(item?.[header])}
|
))
)}
);
})}
) : isObjectView ? (
// Vertical key-value table for objects
|
Key
|
Value
|
|
{Object.entries(currentData).map(([key, value]) => {
const isClickable = typeof value === 'object' && value !== null;
return (
isClickable && handleRowClick(null, key)}
className={`transition-colors duration-150 ${
isClickable
? 'hover:bg-blue-50 dark:hover:bg-blue-900/20 cursor-pointer'
: 'hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
|
{key}
|
{renderFullValue(value)}
|
|
);
})}
) : (
// Fallback for primitive values
{formatFullValue(currentData)}
Type: {getValueType(currentData)}
)}
{/* Footer */}
{isArrayView ? `Array with ${currentData.length} items` :
isObjectView ? `Object with ${Object.keys(currentData).length} properties` :
`Primitive value (${getValueType(currentData)})`}
Path: {currentPath.length === 0 ? 'Root' : currentPath.join(' → ')}
);
};
export default PostmanTable;