// Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { useHeight } from "@/app/hook/useHeight"; import { useTableNav } from "@table-nav/react"; import { createColumnHelper, flexRender, getCoreRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; import { clsx } from "clsx"; import Papa from "papaparse"; import { useEffect, useMemo, useRef, useState } from "react"; import "./csvview.less"; const MAX_DATA_SIZE = 10 * 1024 * 1024; // 10MB in bytes type CSVRow = { [key: string]: string | number; }; interface CSVViewProps { parentRef: React.MutableRefObject; content: string; filename: string; readonly: boolean; } interface State { content: string | null; showReadonly: boolean; tbodyHeight: number; } const columnHelper = createColumnHelper(); // TODO remove parentRef dependency -- use own height const CSVView = ({ parentRef, filename, content }: CSVViewProps) => { const csvCacheRef = useRef(new Map()); const rowRef = useRef<(HTMLTableRowElement | null)[]>([]); const headerRef = useRef(null); const probeRef = useRef(null); const tbodyRef = useRef(null); const [state, setState] = useState({ content, showReadonly: true, tbodyHeight: 0, }); const [tableLoaded, setTableLoaded] = useState(false); const { listeners } = useTableNav(); const parentHeight = useHeight(parentRef); const cacheKey = `${filename}`; csvCacheRef.current.set(cacheKey, content); // Parse the CSV data const parsedData = useMemo(() => { if (!state.content) return []; // Trim the content and then check for headers based on the first row's content. const trimmedContent = state.content.trim(); const firstRow = trimmedContent.split("\n")[0]; // This checks if the first row starts with a letter or a quote const hasHeaders = !!firstRow.match(/^[a-zA-Z"]/); const results = Papa.parse(trimmedContent, { header: hasHeaders }); // Check for non-header CSVs if (!hasHeaders && Array.isArray(results.data) && Array.isArray(results.data[0])) { const dataArray = results.data as string[][]; // Asserting the type const headers = Array.from({ length: dataArray[0].length }, (_, i) => `Column ${i + 1}`); results.data = dataArray.map((row) => { const newRow: CSVRow = {}; row.forEach((value, index) => { newRow[headers[index]] = value; }); return newRow; }); } return results.data.map((row) => { return Object.fromEntries( Object.entries(row as CSVRow).map(([key, value]) => { if (typeof value === "string") { const numberValue = parseFloat(value); if (!isNaN(numberValue) && String(numberValue) === value) { return [key, numberValue]; } } return [key, value]; }) ) as CSVRow; }); }, [state.content]); // Column Definitions const columns = useMemo(() => { if (parsedData.length === 0) { return []; } const headers = Object.keys(parsedData[0]); return headers.map((header) => columnHelper.accessor(header, { header: () => header, cell: (info) => info.renderValue(), }) ); }, [parsedData]); useEffect(() => { if (probeRef.current && headerRef.current && parsedData.length && parentRef.current) { const rowHeight = probeRef.current.offsetHeight; const fullTBodyHeight = rowHeight * parsedData.length; const headerHeight = headerRef.current.offsetHeight; const maxHeightLessHeader = parentHeight - headerHeight; const tbodyHeight = Math.min(maxHeightLessHeader, fullTBodyHeight) - 3; // 3 for the borders setState((prevState) => ({ ...prevState, tbodyHeight })); } }, [parentHeight, parsedData]); // Makes sure rows are rendered before setting the renderer as loaded useEffect(() => { let tid: NodeJS.Timeout; if (rowRef.current.length === parsedData.length) { tid = setTimeout(() => { setTableLoaded(true); }, 50); // Delay a bit to make sure the rows are rendered } return () => clearTimeout(tid); }, [rowRef, parsedData]); const table = useReactTable({ manualPagination: true, data: parsedData, columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), }); return (
dummy data
{table.getHeaderGroups().map((headerGroup, index) => ( {headerGroup.headers.map((header, index) => ( ))} ))} {table.getRowModel().rows.map((row, index) => ( (rowRef.current[index] = el)} id={row.id} tabIndex={index}> {row.getVisibleCells().map((cell) => ( ))} ))}
{header.isPlaceholder ? null : (
{flexRender(header.column.columnDef.header, header.getContext())} {header.column.getIsSorted() === "asc" ? ( ) : header.column.getIsSorted() === "desc" ? ( ) : null}
)}
{flexRender(cell.column.columnDef.cell, cell.getContext())}
); }; export { CSVView };