mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
csvviewer rendering flash (#44)
* more fixes * add delay and slight opacity animation * cleanup
This commit is contained in:
parent
ce033c616f
commit
6ae263f9fa
2
.gitignore
vendored
2
.gitignore
vendored
@ -19,4 +19,4 @@ webshare/dist-dev/
|
||||
temp.sql
|
||||
.idea/
|
||||
test/
|
||||
|
||||
.vscode/
|
||||
|
@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.csv-renderer.loaded {
|
||||
opacity: 1; /* When loaded class is added, set the opacity to 1, making it visible */
|
||||
}
|
||||
|
@ -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;
|
||||
@ -103,11 +105,11 @@ const CSVRenderer: FC<Props> = (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: 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: 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: 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 };
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user