csvviewer rendering flash (#44)

* more fixes

* add delay and slight opacity animation

* cleanup
This commit is contained in:
Red J Adaya 2023-10-25 09:34:37 +08:00 committed by GitHub
parent ce033c616f
commit 6ae263f9fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 115 additions and 87 deletions

2
.gitignore vendored
View File

@ -19,4 +19,4 @@ webshare/dist-dev/
temp.sql
.idea/
test/
.vscode/

View File

@ -1,6 +1,8 @@
@import "../../app/common/themes/themes.less";
.csv-renderer {
opacity: 0; /* Start with an opacity of 0, meaning it's invisible */
.ellipsis() {
display: block;
white-space: nowrap;
@ -34,7 +36,7 @@
border: 1px solid @scrollbar-thumb;
thead {
position:relative;
position: relative;
display: block;
width: 100%;
overflow-y: scroll;
@ -54,7 +56,7 @@
position: relative;
.inner {
text-align:left;
text-align: left;
padding-right: 15px;
position: relative;
.ellipsis();
@ -73,20 +75,20 @@
tbody {
display: block;
position:relative;
position: relative;
overflow-y: scroll;
overscroll-behavior: contain;
}
tr {
width: 100%;
display:flex;
display: flex;
td {
border-right: 1px solid @scrollbar-thumb;
border-left: 1px solid @scrollbar-thumb;
padding: 3px 10px;
flex-basis:100%;
flex-basis: 100%;
flex-grow: 2;
display: block;
text-align: left;
@ -95,3 +97,7 @@
}
}
}
.csv-renderer.loaded {
opacity: 1; /* When loaded class is added, set the opacity to 1, making it visible */
}

View File

@ -4,7 +4,7 @@
import React, { FC, useEffect, useState, useRef, useMemo } from "react";
import { RendererContext, RendererOpts, LineStateType, RendererModelContainerApi } from "../../types/types";
import { GlobalModel } from "../../model/model";
import Papa from 'papaparse';
import Papa from "papaparse";
import {
createColumnHelper,
flexRender,
@ -13,16 +13,15 @@ import {
getFilteredRowModel,
getSortedRowModel,
FilterFn,
} from '@tanstack/react-table'
import {
rankItem,
} from '@tanstack/match-sorter-utils'
import SortUpIcon from './img/sort-up-solid.svg';
import SortDownIcon from './img/sort-down-solid.svg';
} from "@tanstack/react-table";
import { rankItem } from "@tanstack/match-sorter-utils";
import SortUpIcon from "./img/sort-up-solid.svg";
import SortDownIcon from "./img/sort-down-solid.svg";
import cn from "classnames";
import "./csv.less";
const MAX_DATA_SIZE = 10 * 1024 * 1024 // 10MB in bytes
const MAX_DATA_SIZE = 10 * 1024 * 1024; // 10MB in bytes
type CSVRow = {
[key: string]: string | number;
@ -30,16 +29,16 @@ type CSVRow = {
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
// Rank the item
const itemRank = rankItem(row.getValue(columnId), value)
const itemRank = rankItem(row.getValue(columnId), value);
// Store the itemRank info
addMeta({
itemRank,
})
itemRank,
});
// Return if the item should be filtered in/out
return itemRank.passed
}
return itemRank.passed;
};
interface Props {
data: Blob;
@ -54,6 +53,7 @@ interface Props {
interface State {
content: string | null;
showReadonly: boolean;
tbodyHeight: number;
}
const columnHelper = createColumnHelper<any>();
@ -66,17 +66,19 @@ const CSVRenderer: FC<Props> = (props: Props) => {
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: null,
showReadonly: true,
tbodyHeight: maxHeight,
});
const [globalFilter, setGlobalFilter] = useState('')
const [globalFilter, setGlobalFilter] = useState("");
const [isFileTooLarge, setIsFileTooLarge] = useState<boolean>(false);
const [isRendererLoaded, setRendererLoaded] = useState(false);
const filePath = lineState["prompt:file"];
const { screenId, lineId } = context;
const cacheKey = `${screenId}-${lineId}-${filePath}`;
const rowHeight = probeRef.current?.offsetHeight as number;
// Parse the CSV data
const parsedData = useMemo<CSVRow[]>(() => {
@ -84,7 +86,7 @@ const CSVRenderer: FC<Props> = (props: Props) => {
// 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];
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"]/);
@ -93,9 +95,9 @@ const CSVRenderer: FC<Props> = (props: Props) => {
// 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 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 => {
results.data = dataArray.map((row) => {
const newRow: CSVRow = {};
row.forEach((value, index) => {
newRow[headers[index]] = value;
@ -104,10 +106,10 @@ const CSVRenderer: FC<Props> = (props: Props) => {
});
}
return results.data.map(row => {
return results.data.map((row) => {
return Object.fromEntries(
Object.entries(row as CSVRow).map(([key, value]) => {
if (typeof value === 'string') {
if (typeof value === "string") {
const numberValue = parseFloat(value);
if (!isNaN(numberValue) && String(numberValue) === value) {
return [key, numberValue];
@ -119,18 +121,16 @@ const CSVRenderer: FC<Props> = (props: Props) => {
});
}, [state.content]);
const tbodyHeight = rowHeight * parsedData.length;
// Column Definitions
const columns = useMemo(() => {
if (parsedData.length === 0) {
return [];
}
const headers = Object.keys(parsedData[0]);
return headers.map(header =>
return headers.map((header) =>
columnHelper.accessor(header, {
header: () => header,
cell: info => info.renderValue(),
cell: (info) => info.renderValue(),
})
);
}, [parsedData]);
@ -141,7 +141,8 @@ const CSVRenderer: FC<Props> = (props: Props) => {
setState((prevState) => ({ ...prevState, content }));
} else {
// Check if the file size exceeds 10MB
if (data.size > MAX_DATA_SIZE) { // 10MB in bytes
if (data.size > MAX_DATA_SIZE) {
// 10MB in bytes
setIsFileTooLarge(true);
return;
}
@ -153,8 +154,30 @@ const CSVRenderer: FC<Props> = (props: Props) => {
}
}, []);
useEffect(() => {
if (probeRef.current && headerRef.current && parsedData.length) {
const rowHeight = probeRef.current.offsetHeight;
const tbodyHeight = rowHeight * parsedData.length - rowHeight;
const headerHeight = headerRef.current.offsetHeight; // For some reason, if we subtract this from maxHeight, the table is too short
const tbodyHeightLessHeader = tbodyHeight - headerHeight;
const maxTbodyHeight = Math.min(maxHeight, tbodyHeightLessHeader);
const { content } = state;
setState((prevState) => ({ ...prevState, tbodyHeight: maxTbodyHeight }));
}
}, [probeRef, headerRef, maxHeight, parsedData]);
// Makes sure rows are rendered before setting the renderer as loaded
useEffect(() => {
let timer: any;
if (rowRef.current.length === parsedData.length) {
timer = setTimeout(() => {
setRendererLoaded(true);
}, 100); // Delay a bit to make sure the rows are rendered
}
return () => clearTimeout(timer);
}, [rowRef, parsedData]);
const table = useReactTable({
manualPagination: true,
@ -163,7 +186,7 @@ const CSVRenderer: FC<Props> = (props: Props) => {
filterFns: {
fuzzy: fuzzyFilter,
},
state: {
state: {
globalFilter,
},
globalFilterFn: fuzzyFilter,
@ -181,56 +204,56 @@ const CSVRenderer: FC<Props> = (props: Props) => {
);
}
if (content == null) return <div className="csv-renderer" style={{ height: savedHeight }} />;
return (
<div className="csv-renderer">
<div className={cn("csv-renderer", { loaded: isRendererLoaded })}>
<table className="probe">
<tbody><tr ref={probeRef}><td>dummy data</td></tr></tbody>
<tbody>
<tr ref={probeRef}>
<td>dummy data</td>
</tr>
</tbody>
</table>
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} ref={headerRef}>
{headerGroup.headers.map(header => (
<th
key={header.id}
colSpan={header.colSpan}
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' ? <img src={SortUpIcon} className="sort-icon sort-up-icon" alt="Ascending" /> :
header.column.getIsSorted() === 'desc' ? <img src={SortDownIcon} className="sort-icon sort-down-icon" alt="Descending" /> : null
}
</div>
)}
{headerGroup.headers.map((header) => (
<th key={header.id} colSpan={header.colSpan} 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" ? (
<img
src={SortUpIcon}
className="sort-icon sort-up-icon"
alt="Ascending"
/>
) : header.column.getIsSorted() === "desc" ? (
<img
src={SortDownIcon}
className="sort-icon sort-down-icon"
alt="Descending"
/>
) : null}
</div>
)}
</th>
))}
</tr>
))}
</thead>
<tbody style={{"height": `${Math.min(tbodyHeight, maxHeight)}px`}}>
<tbody style={{ height: `${state.tbodyHeight}px` }} ref={tbodyRef}>
{table.getRowModel().rows.map((row, index) => (
<tr key={row.id} ref={el => rowRef.current[index] = el}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
<tr key={row.id} ref={(el) => (rowRef.current[index] = el)}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
))}
</tr>
))}
@ -238,7 +261,6 @@ const CSVRenderer: FC<Props> = (props: Props) => {
</table>
</div>
);
}
};
export { CSVRenderer };