From 6ae263f9faca9a3730a034ff2484eb3666f27868 Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Wed, 25 Oct 2023 09:34:37 +0800 Subject: [PATCH] csvviewer rendering flash (#44) * more fixes * add delay and slight opacity animation * cleanup --- .gitignore | 2 +- src/plugins/csv/csv.less | 32 +++++--- src/plugins/csv/csv.tsx | 168 ++++++++++++++++++++++----------------- 3 files changed, 115 insertions(+), 87 deletions(-) diff --git a/.gitignore b/.gitignore index 2f84708f..d87ce222 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,4 @@ webshare/dist-dev/ temp.sql .idea/ test/ - +.vscode/ diff --git a/src/plugins/csv/csv.less b/src/plugins/csv/csv.less index 800d85e0..d51b2322 100644 --- a/src/plugins/csv/csv.less +++ b/src/plugins/csv/csv.less @@ -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; @@ -14,11 +16,11 @@ .cursor-pointer { cursor: pointer; } - + .select-none { user-select: none; } - + .global-search-render { margin-bottom: 10px; } @@ -29,17 +31,17 @@ } table { - border-collapse: collapse; - overflow-x: auto; + border-collapse: collapse; + overflow-x: auto; border: 1px solid @scrollbar-thumb; thead { - position:relative; + position: relative; display: block; width: 100%; overflow-y: scroll; - tr { + tr { border-bottom: 1px solid @scrollbar-thumb; th { @@ -54,7 +56,7 @@ position: relative; .inner { - text-align:left; + text-align: left; padding-right: 15px; position: relative; .ellipsis(); @@ -70,23 +72,23 @@ } } } - + tbody { - display: block; - position:relative; + display: block; + 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; @@ -94,4 +96,8 @@ } } } -} \ No newline at end of file +} + +.csv-renderer.loaded { + opacity: 1; /* When loaded class is added, set the opacity to 1, making it visible */ +} diff --git a/src/plugins/csv/csv.tsx b/src/plugins/csv/csv.tsx index 43597489..69f6380b 100644 --- a/src/plugins/csv/csv.tsx +++ b/src/plugins/csv/csv.tsx @@ -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 = (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(); @@ -66,17 +66,19 @@ const CSVRenderer: FC = (props: Props) => { const rowRef = useRef<(HTMLTableRowElement | null)[]>([]); const headerRef = useRef(null); const probeRef = useRef(null); + const tbodyRef = useRef(null); const [state, setState] = useState({ content: null, showReadonly: true, + tbodyHeight: maxHeight, }); - const [globalFilter, setGlobalFilter] = useState('') + const [globalFilter, setGlobalFilter] = useState(""); const [isFileTooLarge, setIsFileTooLarge] = useState(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(() => { @@ -84,7 +86,7 @@ const CSVRenderer: FC = (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) => { // 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; @@ -103,11 +105,11 @@ const CSVRenderer: FC = (props: Props) => { return newRow; }); } - - 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) => { }); }, [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,20 +141,43 @@ const CSVRenderer: FC = (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; } - + data.text().then((content: string) => { setState((prevState) => ({ ...prevState, content })); csvCacheRef.current.set(cacheKey, content); }); } - }, []); + }, []); + 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) => { filterFns: { fuzzy: fuzzyFilter, }, - state: { + state: { globalFilter, }, globalFilterFn: fuzzyFilter, @@ -181,56 +204,56 @@ const CSVRenderer: FC = (props: Props) => { ); } - if (content == null) return
; - return ( -
+
- + + + + +
dummy data
dummy data
- {table.getHeaderGroups().map(headerGroup => ( + {table.getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map(header => ( - ))} ))} - + {table.getRowModel().rows.map((row, index) => ( - rowRef.current[index] = el}> - {row.getVisibleCells().map(cell => ( - + (rowRef.current[index] = el)}> + {row.getVisibleCells().map((cell) => ( + ))} ))} @@ -238,7 +261,6 @@ const CSVRenderer: FC = (props: Props) => {
- {header.isPlaceholder - ? null - : ( -
- {flexRender( - header.column.columnDef.header, - header.getContext() - )} - { - header.column.getIsSorted() === 'asc' ? Ascending : - header.column.getIsSorted() === 'desc' ? Descending : null - } -
- )} + {headerGroup.headers.map((header) => ( +
+ {header.isPlaceholder ? null : ( +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {header.column.getIsSorted() === "asc" ? ( + Ascending + ) : header.column.getIsSorted() === "desc" ? ( + Descending + ) : null} +
+ )}
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
{flexRender(cell.column.columnDef.cell, cell.getContext())}
); -} +}; export { CSVRenderer }; -