506 lines
19 KiB
TypeScript
506 lines
19 KiB
TypeScript
import { useState } from 'react';
|
|
import { useLanguage } from '../../../contexts/LanguageContext';
|
|
import styles from '../FilePreview.module.css';
|
|
|
|
interface JsonRendererProps {
|
|
previewContent: string;
|
|
fileName: string;
|
|
}
|
|
|
|
export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) {
|
|
const { t } = useLanguage();
|
|
const [collapsedRows, setCollapsedRows] = useState<Set<string>>(new Set());
|
|
|
|
const handleCopyJson = () => {
|
|
try {
|
|
const parsedJson = JSON.parse(previewContent);
|
|
const formattedJson = JSON.stringify(parsedJson, null, 2);
|
|
navigator.clipboard.writeText(formattedJson);
|
|
} catch (error) {
|
|
navigator.clipboard.writeText(previewContent);
|
|
}
|
|
};
|
|
|
|
const formatTimestamp = (value: string | number): string => {
|
|
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
|
|
|
if (!isNaN(numValue) && numValue > 1000000000 && numValue < 4102444800) {
|
|
const date = new Date(numValue * 1000);
|
|
if (!isNaN(date.getTime())) {
|
|
return date.toLocaleString('en-US', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: false
|
|
});
|
|
}
|
|
}
|
|
|
|
return String(value);
|
|
};
|
|
|
|
const preprocessJson = (obj: any, parentKey = ''): {keys: string[], values: any[], types: (string | 'timestamp')[], isNested: boolean[]} => {
|
|
const keys: string[] = [];
|
|
const values: any[] = [];
|
|
const types: (string | 'timestamp')[] = [];
|
|
const isNested: boolean[] = [];
|
|
|
|
if (obj === null || obj === undefined) {
|
|
keys.push(parentKey);
|
|
values.push(String(obj));
|
|
types.push(typeof obj);
|
|
isNested.push(false);
|
|
} else if (typeof obj === 'boolean' || typeof obj === 'number') {
|
|
keys.push(parentKey);
|
|
if (typeof obj === 'number' && (parentKey.toLowerCase().includes('timestamp') || parentKey.toLowerCase().includes('time'))) {
|
|
values.push(formatTimestamp(obj));
|
|
types.push('timestamp');
|
|
} else {
|
|
values.push(obj);
|
|
types.push(typeof obj);
|
|
}
|
|
isNested.push(false);
|
|
} else if (typeof obj === 'string') {
|
|
keys.push(parentKey);
|
|
const numValue = parseFloat(obj);
|
|
if (!isNaN(numValue) && (parentKey.toLowerCase().includes('timestamp') || parentKey.toLowerCase().includes('time'))) {
|
|
values.push(formatTimestamp(obj));
|
|
types.push('timestamp');
|
|
} else {
|
|
try {
|
|
const parsedString = JSON.parse(obj);
|
|
if (typeof parsedString === 'object' && parsedString !== null) {
|
|
// For stringified JSON objects, process them recursively
|
|
const nestedData = preprocessJson(parsedString, '');
|
|
values.push(nestedData);
|
|
types.push(Array.isArray(parsedString) ? 'array' : 'object');
|
|
isNested.push(true);
|
|
} else {
|
|
values.push(obj);
|
|
types.push('string');
|
|
isNested.push(false);
|
|
}
|
|
} catch (e) {
|
|
values.push(obj);
|
|
types.push('string');
|
|
isNested.push(false);
|
|
}
|
|
}
|
|
} else if (Array.isArray(obj)) {
|
|
if (obj.length === 0) {
|
|
keys.push(parentKey);
|
|
values.push('');
|
|
types.push('array');
|
|
isNested.push(false);
|
|
} else {
|
|
const allPrimitive = obj.every(item =>
|
|
item === null ||
|
|
typeof item !== 'object' ||
|
|
(typeof item === 'object' && !Array.isArray(item) && Object.keys(item).length === 0)
|
|
);
|
|
|
|
if (allPrimitive) {
|
|
if (parentKey) {
|
|
keys.push(parentKey);
|
|
values.push({
|
|
isArray: true,
|
|
items: obj,
|
|
length: obj.length
|
|
});
|
|
types.push('array');
|
|
isNested.push(true);
|
|
} else {
|
|
// For root-level arrays, always use compact array display
|
|
keys.push('');
|
|
values.push({
|
|
isArray: true,
|
|
items: obj,
|
|
length: obj.length
|
|
});
|
|
types.push('array');
|
|
isNested.push(true);
|
|
}
|
|
} else {
|
|
const nestedKeys: string[] = [];
|
|
const nestedValues: any[] = [];
|
|
const nestedTypes: (string | 'timestamp')[] = [];
|
|
const nestedIsNested: boolean[] = [];
|
|
|
|
obj.forEach((item, index) => {
|
|
if (typeof item === 'object' && item !== null) {
|
|
const nestedData = preprocessJson(item, `Item ${index + 1}`);
|
|
nestedKeys.push(...nestedData.keys);
|
|
nestedValues.push(...nestedData.values);
|
|
nestedTypes.push(...nestedData.types);
|
|
nestedIsNested.push(...nestedData.isNested);
|
|
} else {
|
|
nestedKeys.push(`Item ${index + 1}`);
|
|
nestedValues.push(String(item));
|
|
nestedTypes.push(typeof item);
|
|
nestedIsNested.push(false);
|
|
}
|
|
});
|
|
|
|
if (parentKey) {
|
|
keys.push(parentKey);
|
|
values.push({
|
|
keys: nestedKeys,
|
|
values: nestedValues,
|
|
types: nestedTypes,
|
|
isNested: nestedIsNested
|
|
});
|
|
types.push('array');
|
|
isNested.push(true);
|
|
} else {
|
|
keys.push(...nestedKeys);
|
|
values.push(...nestedValues);
|
|
types.push(...nestedTypes);
|
|
isNested.push(...nestedIsNested);
|
|
}
|
|
}
|
|
}
|
|
} else if (typeof obj === 'object') {
|
|
const entries = Object.entries(obj);
|
|
if (entries.length === 0) {
|
|
keys.push(parentKey);
|
|
values.push('{}');
|
|
types.push('object');
|
|
isNested.push(false);
|
|
} else {
|
|
const nestedKeys: string[] = [];
|
|
const nestedValues: any[] = [];
|
|
const nestedTypes: (string | 'timestamp')[] = [];
|
|
const nestedIsNested: boolean[] = [];
|
|
|
|
entries.forEach(([key, value]) => {
|
|
if (typeof value === 'object' && value !== null) {
|
|
const nestedData = preprocessJson(value, key);
|
|
nestedKeys.push(...nestedData.keys);
|
|
nestedValues.push(...nestedData.values);
|
|
nestedTypes.push(...nestedData.types);
|
|
nestedIsNested.push(...nestedData.isNested);
|
|
} else {
|
|
let processedValue = value;
|
|
let processedType: string | 'timestamp' = typeof value;
|
|
|
|
if (typeof value === 'number' && (key.toLowerCase().includes('timestamp') || key.toLowerCase().includes('time'))) {
|
|
processedValue = formatTimestamp(value);
|
|
processedType = 'timestamp';
|
|
} else if (typeof value === 'string') {
|
|
const numValue = parseFloat(value);
|
|
if (!isNaN(numValue) && (key.toLowerCase().includes('timestamp') || key.toLowerCase().includes('time'))) {
|
|
processedValue = formatTimestamp(value);
|
|
processedType = 'timestamp';
|
|
}
|
|
}
|
|
|
|
nestedKeys.push(key);
|
|
nestedValues.push(processedValue);
|
|
nestedTypes.push(processedType);
|
|
nestedIsNested.push(false);
|
|
}
|
|
});
|
|
|
|
if (parentKey) {
|
|
keys.push(parentKey);
|
|
values.push({
|
|
keys: nestedKeys,
|
|
values: nestedValues,
|
|
types: nestedTypes,
|
|
isNested: nestedIsNested
|
|
});
|
|
types.push('object');
|
|
isNested.push(true);
|
|
} else {
|
|
keys.push(...nestedKeys);
|
|
values.push(...nestedValues);
|
|
types.push(...nestedTypes);
|
|
isNested.push(...nestedIsNested);
|
|
}
|
|
}
|
|
}
|
|
|
|
return { keys, values, types, isNested };
|
|
};
|
|
|
|
const renderTable = (data: {keys: string[], values: any[], types: (string | 'timestamp')[], isNested: boolean[]}, level = 0, parentPath = '') => {
|
|
if (!data || data.keys.length === 0) return null;
|
|
|
|
const toggleCollapse = (rowPath: string) => {
|
|
const newCollapsed = new Set(collapsedRows);
|
|
if (newCollapsed.has(rowPath)) {
|
|
newCollapsed.delete(rowPath);
|
|
} else {
|
|
newCollapsed.add(rowPath);
|
|
}
|
|
setCollapsedRows(newCollapsed);
|
|
};
|
|
|
|
const isLongContent = (value: any, type: string): boolean => {
|
|
if (typeof value === 'string') {
|
|
return value.includes('\n') || value.length > 80;
|
|
}
|
|
if (type === 'array' && Array.isArray(value)) {
|
|
return value.length > 8;
|
|
}
|
|
if (typeof value === 'object' && value !== null && 'keys' in value) {
|
|
return value.keys.length > 1;
|
|
}
|
|
if (typeof value === 'object' && value !== null && 'isArray' in value) {
|
|
return value.length > 8;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const getPreview = (value: any, type: string): string => {
|
|
if (typeof value === 'string') {
|
|
if (value.includes('\n')) {
|
|
const firstLine = value.split('\n')[0];
|
|
return firstLine.length > 60 ? firstLine.substring(0, 60) + '...' : firstLine + '...';
|
|
}
|
|
if (value.length > 80) {
|
|
return value.substring(0, 80) + '...';
|
|
}
|
|
return value;
|
|
}
|
|
if (type === 'array' && Array.isArray(value)) {
|
|
return `[${value.slice(0, 3).map(item => typeof item === 'string' ? `"${item}"` : String(item)).join(', ')}${value.length > 3 ? '...' : ''}]`;
|
|
}
|
|
if (typeof value === 'object' && value !== null && 'isArray' in value) {
|
|
return `[${value.items.slice(0, 3).map((item: any) => typeof item === 'string' ? `"${item}"` : String(item)).join(', ')}${value.length > 3 ? '...' : ''}]`;
|
|
}
|
|
if (typeof value === 'object' && value !== null && 'keys' in value) {
|
|
if (type === 'array') {
|
|
return `[${value.keys.slice(0, 3).join(', ')}${value.keys.length > 3 ? '...' : ''}]`;
|
|
} else {
|
|
return `{${value.keys.slice(0, 3).join(', ')}${value.keys.length > 3 ? '...' : ''}}`;
|
|
}
|
|
}
|
|
return String(value);
|
|
};
|
|
|
|
return (
|
|
<div className={`${level > 0 ? styles.nestedTable : styles.jsonTable} ${level > 0 ? styles.nestedTableIndented : ''}`}>
|
|
<div className={level > 0 ? styles.nestedTableBody : styles.jsonTableBody}>
|
|
{data.keys.map((key, index) => {
|
|
const typeClass = data.types[index] ? `jsonValue${data.types[index].charAt(0).toUpperCase() + data.types[index].slice(1)}` : '';
|
|
const typeClassName = styles[typeClass] || '';
|
|
const rowPath = `${parentPath}.${key}`;
|
|
const isCollapsed = collapsedRows.has(rowPath);
|
|
const shouldShowCollapse = isLongContent(data.values[index], data.types[index]);
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
className={`${level > 0 ? styles.nestedTableRow : styles.jsonTableRow} ${isCollapsed ? styles.collapsedRow : styles.notCollapsedRow}`}
|
|
>
|
|
<div className={level > 0 ? styles.nestedTableKey : styles.jsonTableKey}>
|
|
<span className={styles.jsonKey}>{key}</span>
|
|
{shouldShowCollapse && (
|
|
<button
|
|
className={styles.collapseButton}
|
|
onClick={() => toggleCollapse(rowPath)}
|
|
title={isCollapsed ? 'Expand' : 'Collapse'}
|
|
>
|
|
{isCollapsed ? '▶' : '▼'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className={`${level > 0 ? styles.nestedTableValue : styles.jsonTableValue} ${typeClassName}`}>
|
|
{data.isNested[index] ? (
|
|
<div>
|
|
{shouldShowCollapse && isCollapsed ? (
|
|
<span className={styles.jsonValuePreview}>
|
|
{getPreview(data.values[index], data.types[index])}
|
|
</span>
|
|
) : (
|
|
data.values[index].isArray ? (
|
|
<div className={data.keys[index] === '' ? styles.arrayItemsFullWidth : styles.arrayItems}>
|
|
{isCollapsed ? (
|
|
<div className={styles.arrayPreview}>
|
|
{data.values[index].items.slice(0, 10).map((item: any) => String(item)).join(', ')}
|
|
{data.values[index].length > 10 && `, ... (${data.values[index].length} items)`}
|
|
</div>
|
|
) : (
|
|
<>
|
|
{data.values[index].items.map((item: any, itemIndex: number) => (
|
|
<div key={itemIndex} className={styles.arrayItem}>
|
|
<span className={`${styles.arrayValue} ${typeof item === 'number' ? styles.jsonValueNumber : typeof item === 'string' ? styles.jsonValueString : styles.jsonValue}`}>
|
|
{String(item)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</>
|
|
)}
|
|
</div>
|
|
) : (
|
|
typeof data.values[index] === 'object' && data.values[index] !== null && 'keys' in data.values[index] ?
|
|
renderTable(data.values[index], level + 1, rowPath) :
|
|
<span className={styles.jsonValue}>Error: Invalid nested data</span>
|
|
)
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className={styles.valueContainer}>
|
|
{shouldShowCollapse && isCollapsed ? (
|
|
<span className={styles.jsonValuePreview}>
|
|
{getPreview(data.values[index], data.types[index])}
|
|
</span>
|
|
) : (
|
|
<span
|
|
className={`${styles.jsonValue} ${
|
|
data.types[index] === 'number' ? styles.jsonValueNumber :
|
|
data.types[index] === 'boolean' ? styles.jsonValueBoolean :
|
|
data.types[index] === 'string' ? styles.jsonValueString : ''
|
|
}`}
|
|
>
|
|
{String(data.values[index])}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
try {
|
|
const parsedJson = JSON.parse(previewContent);
|
|
|
|
let preprocessedData;
|
|
if (Array.isArray(parsedJson)) {
|
|
preprocessedData = preprocessJson(parsedJson, 'root');
|
|
} else if (typeof parsedJson === 'object' && parsedJson !== null) {
|
|
if (parsedJson.result && typeof parsedJson.result === 'string') {
|
|
try {
|
|
const parsedResult = JSON.parse(parsedJson.result);
|
|
parsedJson.result = parsedResult;
|
|
} catch (e) {
|
|
// If parsing fails, keep as string - it will be handled by the string processing logic
|
|
}
|
|
}
|
|
|
|
const entries = Object.entries(parsedJson);
|
|
|
|
preprocessedData = {
|
|
keys: entries.map(([key]) => key),
|
|
values: entries.map(([key, value]) => {
|
|
if (typeof value === 'object' && value !== null) {
|
|
const processed = preprocessJson(value, '');
|
|
return processed;
|
|
} else if (typeof value === 'string') {
|
|
try {
|
|
const parsedString = JSON.parse(value);
|
|
if (typeof parsedString === 'object' && parsedString !== null) {
|
|
return preprocessJson(parsedString, '');
|
|
} else {
|
|
let processedValue = value.replace(/\\n/g, ' ').replace(/\n/g, ' ');
|
|
const numValue = parseFloat(value);
|
|
if (!isNaN(numValue) && (key.toLowerCase().includes('timestamp') || key.toLowerCase().includes('time'))) {
|
|
processedValue = formatTimestamp(value);
|
|
}
|
|
return String(processedValue);
|
|
}
|
|
} catch (e) {
|
|
let processedValue = value.replace(/\\n/g, ' ').replace(/\n/g, ' ');
|
|
const numValue = parseFloat(value);
|
|
if (!isNaN(numValue) && (key.toLowerCase().includes('timestamp') || key.toLowerCase().includes('time'))) {
|
|
processedValue = formatTimestamp(value);
|
|
}
|
|
return String(processedValue);
|
|
}
|
|
} else {
|
|
let processedValue = value;
|
|
|
|
if (typeof value === 'number' && (key.toLowerCase().includes('timestamp') || key.toLowerCase().includes('time'))) {
|
|
processedValue = formatTimestamp(value);
|
|
return String(processedValue);
|
|
}
|
|
|
|
return processedValue;
|
|
}
|
|
}),
|
|
types: entries.map(([, value]) => {
|
|
if (typeof value === 'object' && value !== null) {
|
|
return Array.isArray(value) ? 'array' : 'object';
|
|
} else if (typeof value === 'string') {
|
|
try {
|
|
const parsedString = JSON.parse(value);
|
|
if (typeof parsedString === 'object' && parsedString !== null) {
|
|
return Array.isArray(parsedString) ? 'array' : 'object';
|
|
}
|
|
} catch (e) {
|
|
// Ignore parsing errors
|
|
}
|
|
return 'string';
|
|
}
|
|
return typeof value;
|
|
}),
|
|
isNested: entries.map(([, value]) => {
|
|
if (typeof value === 'object' && value !== null) {
|
|
return true;
|
|
} else if (typeof value === 'string') {
|
|
try {
|
|
const parsedString = JSON.parse(value);
|
|
return typeof parsedString === 'object' && parsedString !== null;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
})
|
|
};
|
|
console.log(preprocessedData);
|
|
} else {
|
|
preprocessedData = {
|
|
keys: ['value'],
|
|
values: [String(parsedJson)],
|
|
types: [typeof parsedJson],
|
|
isNested: [false]
|
|
};
|
|
}
|
|
|
|
return (
|
|
<div className={styles.jsonContainer}>
|
|
<div className={styles.jsonHeader}>
|
|
<div className={styles.jsonHeaderRight}>
|
|
<span className={styles.jsonSize}>{preprocessedData.keys.length} {t('files.preview.json.properties', 'properties')}</span>
|
|
</div>
|
|
</div>
|
|
{renderTable(preprocessedData, 0, 'root')}
|
|
</div>
|
|
);
|
|
} catch (parseError) {
|
|
const rawData = {
|
|
keys: ['Raw Content'],
|
|
values: [previewContent],
|
|
types: ['string'],
|
|
isNested: [false]
|
|
};
|
|
|
|
return (
|
|
<div className={styles.jsonContainer}>
|
|
<div className={styles.jsonHeader}>
|
|
<span className={styles.jsonTitle}>{t('files.preview.json.invalid', 'Raw Content (Invalid JSON)')}: {fileName}</span>
|
|
<div className={styles.jsonHeaderRight}>
|
|
<button
|
|
className={styles.copyButton}
|
|
onClick={handleCopyJson}
|
|
title={t('files.preview.json.copyRaw', 'Copy content to clipboard')}
|
|
>
|
|
📋 {t('common.copy', 'Copy')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{renderTable(rawData)}
|
|
</div>
|
|
);
|
|
}
|
|
}
|