mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-23 16:58:27 +01:00
211 lines
8.0 KiB
TypeScript
211 lines
8.0 KiB
TypeScript
// Copyright 2024, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
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 { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
|
|
import "./csvview.less";
|
|
|
|
const MAX_DATA_SIZE = 10 * 1024 * 1024; // 10MB in bytes
|
|
|
|
type CSVRow = {
|
|
[key: string]: string | number;
|
|
};
|
|
|
|
interface CSVViewProps {
|
|
parentRef: React.MutableRefObject<HTMLDivElement>;
|
|
content: string;
|
|
filename: string;
|
|
readonly: boolean;
|
|
}
|
|
|
|
interface State {
|
|
content: string | null;
|
|
showReadonly: boolean;
|
|
tbodyHeight: number;
|
|
}
|
|
|
|
const columnHelper = createColumnHelper<any>();
|
|
|
|
// TODO remove parentRef dependency -- use own height
|
|
const CSVView = ({ parentRef, filename, content }: CSVViewProps) => {
|
|
const csvCacheRef = useRef(new Map<string, string>());
|
|
const rowRef = useRef<(HTMLTableRowElement | null)[]>([]);
|
|
const headerRef = useRef<HTMLTableRowElement | null>(null);
|
|
const probeRef = useRef<HTMLTableRowElement | null>(null);
|
|
const tbodyRef = useRef<HTMLTableSectionElement | null>(null);
|
|
|
|
const [state, setState] = useState<State>({
|
|
content,
|
|
showReadonly: true,
|
|
tbodyHeight: 0,
|
|
});
|
|
|
|
const [tableLoaded, setTableLoaded] = useState(false);
|
|
|
|
const { listeners } = useTableNav();
|
|
const domRect = useDimensionsWithExistingRef(parentRef, 30);
|
|
const parentHeight = domRect?.height ?? 0;
|
|
|
|
const cacheKey = `${filename}`;
|
|
csvCacheRef.current.set(cacheKey, content);
|
|
|
|
// Parse the CSV data
|
|
const parsedData = useMemo<CSVRow[]>(() => {
|
|
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 (
|
|
<div className={clsx("csv-view", { show: tableLoaded })} style={{ height: "auto" }}>
|
|
<table className="probe">
|
|
<tbody>
|
|
<tr ref={probeRef}>
|
|
<td>dummy data</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<table {...listeners}>
|
|
<thead>
|
|
{table.getHeaderGroups().map((headerGroup, index) => (
|
|
<tr key={headerGroup.id} ref={headerRef} id={headerGroup.id} tabIndex={index}>
|
|
{headerGroup.headers.map((header, index) => (
|
|
<th
|
|
key={header.id}
|
|
colSpan={header.colSpan}
|
|
id={header.id}
|
|
tabIndex={index}
|
|
style={{ width: header.getSize() }}
|
|
>
|
|
{header.isPlaceholder ? null : (
|
|
<div
|
|
{...{
|
|
className: header.column.getCanSort()
|
|
? "inner cursor-pointer select-none"
|
|
: "",
|
|
onClick: header.column.getToggleSortingHandler(),
|
|
}}
|
|
>
|
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
{header.column.getIsSorted() === "asc" ? (
|
|
<i className="sort-icon fa-sharp fa-solid fa-sort-up"></i>
|
|
) : header.column.getIsSorted() === "desc" ? (
|
|
<i className="sort-icon fa-sharp fa-solid fa-sort-down"></i>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</thead>
|
|
<tbody style={{ height: `${state.tbodyHeight}px` }} ref={tbodyRef}>
|
|
{table.getRowModel().rows.map((row, index) => (
|
|
<tr key={row.id} ref={(el) => (rowRef.current[index] = el)} id={row.id} tabIndex={index}>
|
|
{row.getVisibleCells().map((cell) => (
|
|
<td key={cell.id} id={cell.id} tabIndex={index}>
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export { CSVView };
|