diff --git a/src/plugins/csv/csv.less b/src/plugins/csv/csv.less index 8786c4e45..b77a5c46a 100644 --- a/src/plugins/csv/csv.less +++ b/src/plugins/csv/csv.less @@ -1,126 +1,92 @@ @import "../../app/common/themes/themes.less"; .csv-renderer { + .ellipsis() { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + overflow-x: auto; + overflow-y: hidden; + .cursor-pointer { cursor: pointer; } - + .select-none { user-select: none; } - - /* Flexbox utilities */ - .flex { - display: flex; - } - - .items-center { - align-items: center; - } - - .gap-2 { - gap: 8px; - } - - /* Button styles */ - button.border { - border: 1px solid #ccc; - } - - button.rounded { - border-radius: 4px; - } - - button.p-1 { - padding: 4px; - } - - /* Disabling styles for buttons */ - button[disabled] { - opacity: 0.6; - cursor: not-allowed; - } - - /* Input styles */ - input.border, - select.border { - border: 1px solid #ccc; - } - - input.rounded, - select.rounded { - border-radius: 4px; - } - - input.p-1, - select.p-1 { - padding: 4px; - } - - input.w-16 { - width: 64px; - } - - /* Text styles */ - strong { - font-weight: bold; - } - + .global-search-render { margin-bottom: 10px; } table { - border: 1px solid rgb(146, 146, 146); - margin-bottom: 10px; + border-collapse: collapse; + overflow-x: auto; + border: 1px solid @scrollbar-thumb; - tbody { - border-bottom: 1px solid rgb(146, 146, 146); - } + thead { + position:relative; + display: block; + width: 100%; + overflow-y: scroll; - th { - color: white; - border-bottom: 1px solid rgb(146, 146, 146); - border-right: 1px solid rgb(146, 146, 146); - padding: 2px 10px 4px 10px; + tr { + border-bottom: 1px solid @scrollbar-thumb; - .min-max-renderer { - min-width: 200px; - display: flex; + th { + color:@term-white; + border: 1px solid @scrollbar-thumb; + border-bottom: none; + padding: 2px 10px 4px 10px; + flex-basis:100%; + flex-grow:2; + display: block; + text-align:left; + position: relative; - .search-renderer { - flex: 1; + .inner { + text-align:left; + padding-right: 15px; + position: relative; + .ellipsis(); - input { - width: 99%; + .sort-icon { + filter: invert(100%); + position: absolute; + right: 0px; + top: 2px; + width: 9px; + } } } - - .search-renderer:first-child { - margin-right: 3px; - } } } + + tbody { + display: block; + position:relative; + overflow-y: scroll; + overscroll-behavior: contain; + } tr { - td { - border-right: 1px solid rgb(146, 146, 146); - padding: 2px 10px; - } - td:last-child { - border-right: none; - } - } - } + width: 100%; + display:flex; - .pagination-renderer { - .page-label { - div { - padding-right: 6px; - } - strong { - color: white; + td { + border-right: 1px solid @scrollbar-thumb; + border-left: 1px solid @scrollbar-thumb; + padding: 2px 10px; + flex-basis:100%; + flex-grow:2; + display: block; + text-align:left; + .ellipsis(); } } } -} +} \ No newline at end of file diff --git a/src/plugins/csv/csv.tsx b/src/plugins/csv/csv.tsx index 1655607c9..f52573514 100644 --- a/src/plugins/csv/csv.tsx +++ b/src/plugins/csv/csv.tsx @@ -6,36 +6,20 @@ import { createColumnHelper, flexRender, useReactTable, - ColumnFiltersState, getCoreRowModel, getFilteredRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFacetedMinMaxValues, - getPaginationRowModel, - sortingFns, getSortedRowModel, FilterFn, } from '@tanstack/react-table' import { - RankingInfo, rankItem, - compareItems, } from '@tanstack/match-sorter-utils' -import Filter from "./filter"; -import DebouncedInput from "./search"; -import Pagination from "./pagination"; +import SortUpIcon from './img/sort-up-solid.svg'; +import SortDownIcon from './img/sort-down-solid.svg'; import "./csv.less"; -declare module '@tanstack/table-core' { - interface FilterFns { - fuzzy: FilterFn - } - interface FilterMeta { - itemRank: RankingInfo - } -} +const MAX_DATA_SIZE = 10 * 1024 * 1024 // 10MB in bytes type CSVRow = { [key: string]: string | number; @@ -52,27 +36,6 @@ const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { // Return if the item should be filtered in/out return itemRank.passed - } - - const fuzzySort: SortingFn = (rowA, rowB, columnId) => { - let dir = 0 - - // Only sort by rank if the column has ranking information - if (rowA.columnFiltersMeta[columnId]) { - dir = compareItems( - rowA.columnFiltersMeta[columnId]?.itemRank!, - rowB.columnFiltersMeta[columnId]?.itemRank! - ) - } - - // Provide an alphanumeric fallback for when the item ranks are equal - return dir === 0 ? sortingFns.alphanumeric(rowA, rowB, columnId) : dir -} - - -interface DataColumn { - Header: string; - accessor: string; } interface Props { @@ -97,22 +60,24 @@ interface State { message: { status: string; text: string } | null; isPreviewerAvailable: boolean; showReadonly: boolean; + totalHeight: number; } const columnHelper = createColumnHelper(); const CSVRenderer: FC = (props: Props) => { const csvCacheRef = useRef(new Map()); + const rowRef = useRef<(HTMLTableRowElement | null)[]>([]); + const headerRef = useRef(null); const [state, setState] = useState({ content: null, message: null, isPreviewerAvailable: false, showReadonly: true, + totalHeight: 0, }); - const [columnFilters, setColumnFilters] = React.useState( - [] - ) - const [globalFilter, setGlobalFilter] = React.useState('') + const [globalFilter, setGlobalFilter] = useState('') + const [isFileTooLarge, setIsFileTooLarge] = useState(false); const filePath = props.lineState["prompt:file"]; const { screenId, lineId } = props.context; @@ -121,9 +86,29 @@ const CSVRenderer: FC = (props: Props) => { // Parse the CSV data const parsedData = useMemo(() => { if (!state.content) return []; - - const results = Papa.parse(state.content, { header: true }); - + + // 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]) => { @@ -137,7 +122,7 @@ const CSVRenderer: FC = (props: Props) => { }) ) as CSVRow; }); - }, [state.content]); + }, [state.content]); // Column Definitions const columns = useMemo(() => { @@ -158,12 +143,30 @@ const CSVRenderer: FC = (props: Props) => { if (content) { setState((prevState) => ({ ...prevState, content })); } else { + // Check if the file size exceeds 10MB + if (props.data.size > MAX_DATA_SIZE) { // 10MB in bytes + setIsFileTooLarge(true); + return; + } + props.data.text().then((content: string) => { setState((prevState) => ({ ...prevState, content })); csvCacheRef.current.set(cacheKey, content); }); } - }, []); + }, []); + + // Effect to compute height after rendering + useEffect(() => { + if (headerRef.current && rowRef.current && rowRef.current[0]) { + const rowHeight = rowRef.current[0]?.offsetHeight ?? 0; // Using optional chaining + const totalHeight = rowHeight * parsedData.length; + const th = Math.min(totalHeight, props.opts.maxSize.height); + + setState((prevState) => ({ ...prevState, totalHeight: th })); + } + }, [parsedData, props.opts]); + const getMessage = () => (
@@ -183,66 +186,51 @@ const CSVRenderer: FC = (props: Props) => { const { content, message } = state; const table = useReactTable({ + manualPagination: true, data: parsedData, columns, filterFns: { fuzzy: fuzzyFilter, }, state: { - columnFilters, globalFilter, }, - onColumnFiltersChange: setColumnFilters, - onGlobalFilterChange: setGlobalFilter, globalFilterFn: fuzzyFilter, + onGlobalFilterChange: setGlobalFilter, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - getFacetedMinMaxValues: getFacetedMinMaxValues(), }); + if (isFileTooLarge) { + return ( +
+
The file size exceeds 10MB and cannot be displayed.
+
+ ); + } + if (content == null) return
; - if (exitcode === 1) - return ( -
- {content} -
- ); - return (
-
- setGlobalFilter(String(value))} - className="global-search" - placeholder="Search all columns..." - /> -
- {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map(header => ( - + {headerGroup.headers.map(header => ( + - ))} - - ))} + )} + + ))} + + ))} - - {table.getRowModel().rows.map(row => ( - - {row.getVisibleCells().map(cell => ( - - ))} + + {table.getRowModel().rows.map((row, index) => ( + rowRef.current[index] = el}> + {row.getVisibleCells().map(cell => ( + + ))} ))}
- {header.isPlaceholder - ? null - : ( - <> + {table.getHeaderGroups().map(headerGroup => ( +
+ {header.isPlaceholder + ? null + : (
= (props: Props) => { header.column.columnDef.header, header.getContext() )} - {{ - asc: ' 🔼', - desc: ' 🔽', - }[header.column.getIsSorted() as string] ?? null} + { + header.column.getIsSorted() === 'asc' ? Ascending : + header.column.getIsSorted() === 'desc' ? Descending : null + }
- {header.column.getCanFilter() ? ( -
- -
- ) : null} - - )} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
- {message && getMessage()}
); diff --git a/src/plugins/csv/filter.tsx b/src/plugins/csv/filter.tsx deleted file mode 100644 index 7bb3d66a7..000000000 --- a/src/plugins/csv/filter.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { FC, useMemo } from "react"; -import DebouncedInput from "./search"; -import { - Column, - Table, -} from '@tanstack/react-table'; -import Split from "react-split-it"; - -const Filter: FC<{ - column: Column; - table: Table; -}> = ({ column, table }) => { - const firstValue = table - .getPreFilteredRowModel() - .flatRows[0]?.getValue(column.id); - - const columnFilterValue = column.getFilterValue(); - - const sortedUniqueValues = useMemo( - () => - typeof firstValue === 'number' - ? [] - : Array.from(column.getFacetedUniqueValues().keys()).sort(), - [column.getFacetedUniqueValues()] - ); - - return typeof firstValue === 'number' ? ( -
- - column.setFilterValue((old: [number, number]) => [value, old?.[1]]) - } - placeholder={`Min ${ - column.getFacetedMinMaxValues()?.[0] - ? `(${column.getFacetedMinMaxValues()?.[0]})` - : '' - }`} - className="w-24 border shadow rounded" - /> - - column.setFilterValue((old: [number, number]) => [old?.[0], value]) - } - placeholder={`Max ${ - column.getFacetedMinMaxValues()?.[1] - ? `(${column.getFacetedMinMaxValues()?.[1]})` - : '' - }`} - className="w-24 border shadow rounded" - /> -
- ) : ( -
- - {sortedUniqueValues.slice(0, 5000).map((value: any) => ( - - column.setFilterValue(value)} - placeholder={`Search... (${column.getFacetedUniqueValues().size})`} - list={column.id + 'list'} - /> -
- ); -}; - -export default Filter \ No newline at end of file diff --git a/src/plugins/csv/img/sort-down-solid.svg b/src/plugins/csv/img/sort-down-solid.svg new file mode 100644 index 000000000..6fffc0638 --- /dev/null +++ b/src/plugins/csv/img/sort-down-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugins/csv/img/sort-solid.svg b/src/plugins/csv/img/sort-solid.svg new file mode 100644 index 000000000..b8e1b4aaf --- /dev/null +++ b/src/plugins/csv/img/sort-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugins/csv/img/sort-up-solid.svg b/src/plugins/csv/img/sort-up-solid.svg new file mode 100644 index 000000000..af0a520e1 --- /dev/null +++ b/src/plugins/csv/img/sort-up-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugins/csv/pagination.tsx b/src/plugins/csv/pagination.tsx deleted file mode 100644 index a01c9ecb4..000000000 --- a/src/plugins/csv/pagination.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { FC } from 'react'; -import { - Table, -} from '@tanstack/react-table'; - -type PaginationProps = { - table: Table; -}; - -const Pagination: FC = ({ table }) => { - return ( -
- - - - - -
Page
- - {table.getState().pagination.pageIndex + 1} of{' '} - {table.getPageCount()} - -
- - | Go to page: - { - const page = e.target.value ? Number(e.target.value) - 1 : 0; - table.setPageIndex(page); - }} - className="border p-1 rounded w-16" - /> - - -
- ); -}; - -export default Pagination;