frontend_nyla/src/components/FilePreview/renderers/JsonRenderer.tsx

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>
);
}
}