File size: 4,661 Bytes
8fc8501 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | import { useMemo, useState } from "react";
import { X, Table as TableIcon, FileText } from "lucide-react";
import type { EmbedDataFile } from "../editor/embeds/embed-data-store";
import { formatBytes } from "../utils/data-files";
interface DataFileViewerProps {
file: EmbedDataFile;
onClose: () => void;
}
const MAX_PREVIEW_ROWS = 100;
function splitCsvLine(line: string, delim: string): string[] {
const out: string[] = [];
let cur = "";
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const c = line[i];
if (inQuotes) {
if (c === '"') {
if (line[i + 1] === '"') {
cur += '"';
i++;
} else {
inQuotes = false;
}
} else {
cur += c;
}
} else if (c === '"') {
inQuotes = true;
} else if (c === delim) {
out.push(cur);
cur = "";
} else {
cur += c;
}
}
out.push(cur);
return out;
}
function parseTable(content: string, delim: string, maxRows: number): {
columns: string[];
rows: string[][];
total: number;
} {
const lines = content.split(/\r\n|\n|\r/).filter((l) => l.length > 0);
if (lines.length === 0) return { columns: [], rows: [], total: 0 };
const columns = splitCsvLine(lines[0], delim);
const rows = lines
.slice(1, 1 + maxRows)
.map((l) => splitCsvLine(l, delim));
return { columns, rows, total: Math.max(0, lines.length - 1) };
}
function prettyJson(content: string): string {
try {
return JSON.stringify(JSON.parse(content), null, 2);
} catch {
return content;
}
}
export function DataFileViewer({ file, onClose }: DataFileViewerProps) {
const { ext } = file.meta;
const isTabular = ext === "csv" || ext === "tsv";
const [mode, setMode] = useState<"table" | "raw">(isTabular ? "table" : "raw");
const parsed = useMemo(() => {
if (!isTabular) return null;
return parseTable(file.content, ext === "tsv" ? "\t" : ",", MAX_PREVIEW_ROWS);
}, [file.content, ext, isTabular]);
const raw = useMemo(() => {
if (ext === "json") return prettyJson(file.content);
return file.content;
}, [file.content, ext]);
return (
<div className="es-data-viewer">
<div className="es-data-viewer__header">
<div className="es-data-viewer__title">
<strong>{file.meta.name}</strong>
<span className="es-data-viewer__meta">
{ext.toUpperCase()} - {formatBytes(file.meta.size)}
{file.meta.rowCount !== undefined ? ` - ${file.meta.rowCount} rows` : ""}
</span>
</div>
<div className="es-data-viewer__actions">
{isTabular && (
<div className="es-data-viewer__tabs">
<button
type="button"
className={`es-preview__tab ${mode === "table" ? "es-preview__tab--active" : ""}`}
onClick={() => setMode("table")}
>
<TableIcon size={12} />
Table
</button>
<button
type="button"
className={`es-preview__tab ${mode === "raw" ? "es-preview__tab--active" : ""}`}
onClick={() => setMode("raw")}
>
<FileText size={12} />
Raw
</button>
</div>
)}
<button
type="button"
className="embed-btn"
onClick={onClose}
aria-label="Close file viewer"
>
<X size={14} />
</button>
</div>
</div>
<div className="es-data-viewer__body">
{isTabular && mode === "table" && parsed ? (
<div className="es-data-table-wrap">
<table className="es-data-table">
<thead>
<tr>
{parsed.columns.map((col, i) => (
<th key={i}>{col}</th>
))}
</tr>
</thead>
<tbody>
{parsed.rows.map((row, ri) => (
<tr key={ri}>
{parsed.columns.map((_, ci) => (
<td key={ci}>{row[ci] ?? ""}</td>
))}
</tr>
))}
</tbody>
</table>
{parsed.total > parsed.rows.length && (
<div className="es-data-table__footer">
Showing first {parsed.rows.length} of {parsed.total} rows
</div>
)}
</div>
) : (
<pre className="es-data-viewer__raw">
<code>{raw}</code>
</pre>
)}
</div>
</div>
);
}
|