mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
CSV viewer plugin enhancements (#33)
* init * render table * add packages * filtering and sorting * init pagination * partially working min-max * fix broken style * styles fixes * init * sticky header * resizable cols * scrollable tbody instead * remove unused deps * add file size limit * style fixes * scrollable x-axis * border fixes * more style fixes * handle csv without headers * fix headers rendering * revert changes to model * revert changes to code plugin * ellipsis mixin * fix height calc problem * minor fix * minor fix * minor fix * minor fix * minor fix * reset model and code plugin changes * more fixes * fix wrong theme import path
This commit is contained in:
parent
17a85734c8
commit
1fafc53605
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<unknown>
|
||||
}
|
||||
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<any> = (row, columnId, value, addMeta) => {
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed
|
||||
}
|
||||
|
||||
const fuzzySort: SortingFn<any> = (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<any>();
|
||||
|
||||
const CSVRenderer: FC<Props> = (props: Props) => {
|
||||
const csvCacheRef = useRef(new Map<string, string>());
|
||||
const rowRef = useRef<(HTMLTableRowElement | null)[]>([]);
|
||||
const headerRef = useRef<HTMLTableRowElement | null>(null);
|
||||
const [state, setState] = useState<State>({
|
||||
content: null,
|
||||
message: null,
|
||||
isPreviewerAvailable: false,
|
||||
showReadonly: true,
|
||||
totalHeight: 0,
|
||||
});
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const [globalFilter, setGlobalFilter] = React.useState('')
|
||||
const [globalFilter, setGlobalFilter] = useState('')
|
||||
const [isFileTooLarge, setIsFileTooLarge] = useState<boolean>(false);
|
||||
|
||||
const filePath = props.lineState["prompt:file"];
|
||||
const { screenId, lineId } = props.context;
|
||||
@ -121,9 +86,29 @@ const CSVRenderer: FC<Props> = (props: Props) => {
|
||||
// Parse the CSV data
|
||||
const parsedData = useMemo<CSVRow[]>(() => {
|
||||
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: Props) => {
|
||||
})
|
||||
) as CSVRow;
|
||||
});
|
||||
}, [state.content]);
|
||||
}, [state.content]);
|
||||
|
||||
// Column Definitions
|
||||
const columns = useMemo(() => {
|
||||
@ -158,12 +143,30 @@ const CSVRenderer: FC<Props> = (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 = () => (
|
||||
<div style={{ position: "absolute", bottom: "-3px", left: "14px" }}>
|
||||
@ -183,66 +186,51 @@ const CSVRenderer: FC<Props> = (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 (
|
||||
<div className="csv-renderer" style={{ fontSize: GlobalModel.termFontSize.get() }}>
|
||||
<div className="load-error-text">The file size exceeds 10MB and cannot be displayed.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (content == null) return <div className="csv-renderer" style={{ height: props.savedHeight }} />;
|
||||
|
||||
if (exitcode === 1)
|
||||
return (
|
||||
<div
|
||||
className="csv-renderer"
|
||||
style={{
|
||||
fontSize: GlobalModel.termFontSize.get(),
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="csv-renderer">
|
||||
<div className="global-search-render">
|
||||
<DebouncedInput
|
||||
value={globalFilter ?? ''}
|
||||
onChange={value => setGlobalFilter(String(value))}
|
||||
className="global-search"
|
||||
placeholder="Search all columns..."
|
||||
/>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => (
|
||||
<th key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: (
|
||||
<>
|
||||
{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()
|
||||
? 'cursor-pointer select-none'
|
||||
? 'inner cursor-pointer select-none'
|
||||
: '',
|
||||
onClick: header.column.getToggleSortingHandler(),
|
||||
}}
|
||||
@ -251,36 +239,29 @@ const CSVRenderer: FC<Props> = (props: Props) => {
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{{
|
||||
asc: ' 🔼',
|
||||
desc: ' 🔽',
|
||||
}[header.column.getIsSorted() as string] ?? null}
|
||||
{
|
||||
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>
|
||||
{header.column.getCanFilter() ? (
|
||||
<div>
|
||||
<Filter column={header.column} table={table} />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map(row => (
|
||||
<tr key={row.id}>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<td key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
<tbody style={{"height": `${state.totalHeight}px`}}>
|
||||
{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>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Pagination table={table} />
|
||||
{message && getMessage()}
|
||||
</div>
|
||||
);
|
||||
|
@ -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<any, unknown>;
|
||||
table: Table<any>;
|
||||
}> = ({ 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' ? (
|
||||
<div className="min-max-renderer">
|
||||
<DebouncedInput
|
||||
type="number"
|
||||
min={Number(column.getFacetedMinMaxValues()?.[0] ?? '')}
|
||||
max={Number(column.getFacetedMinMaxValues()?.[1] ?? '')}
|
||||
value={(columnFilterValue as [number, number])?.[0] ?? ''}
|
||||
onChange={value =>
|
||||
column.setFilterValue((old: [number, number]) => [value, old?.[1]])
|
||||
}
|
||||
placeholder={`Min ${
|
||||
column.getFacetedMinMaxValues()?.[0]
|
||||
? `(${column.getFacetedMinMaxValues()?.[0]})`
|
||||
: ''
|
||||
}`}
|
||||
className="w-24 border shadow rounded"
|
||||
/>
|
||||
<DebouncedInput
|
||||
type="number"
|
||||
min={Number(column.getFacetedMinMaxValues()?.[0] ?? '')}
|
||||
max={Number(column.getFacetedMinMaxValues()?.[1] ?? '')}
|
||||
value={(columnFilterValue as [number, number])?.[1] ?? ''}
|
||||
onChange={value =>
|
||||
column.setFilterValue((old: [number, number]) => [old?.[0], value])
|
||||
}
|
||||
placeholder={`Max ${
|
||||
column.getFacetedMinMaxValues()?.[1]
|
||||
? `(${column.getFacetedMinMaxValues()?.[1]})`
|
||||
: ''
|
||||
}`}
|
||||
className="w-24 border shadow rounded"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="search-renderer">
|
||||
<datalist id={column.id + 'list'}>
|
||||
{sortedUniqueValues.slice(0, 5000).map((value: any) => (
|
||||
<option value={value} key={value} />
|
||||
))}
|
||||
</datalist>
|
||||
<DebouncedInput
|
||||
type="text"
|
||||
value={(columnFilterValue ?? '') as string}
|
||||
onChange={value => column.setFilterValue(value)}
|
||||
placeholder={`Search... (${column.getFacetedUniqueValues().size})`}
|
||||
list={column.id + 'list'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Filter
|
1
src/plugins/csv/img/sort-down-solid.svg
Normal file
1
src/plugins/csv/img/sort-down-solid.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M182.6 470.6c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8H288c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128z"/></svg>
|
After Width: | Height: | Size: 403 B |
1
src/plugins/csv/img/sort-solid.svg
Normal file
1
src/plugins/csv/img/sort-solid.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M137.4 41.4c12.5-12.5 32.8-12.5 45.3 0l128 128c9.2 9.2 11.9 22.9 6.9 34.9s-16.6 19.8-29.6 19.8H32c-12.9 0-24.6-7.8-29.6-19.8s-2.2-25.7 6.9-34.9l128-128zm0 429.3l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8H288c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128c-12.5 12.5-32.8 12.5-45.3 0z"/></svg>
|
After Width: | Height: | Size: 551 B |
1
src/plugins/csv/img/sort-up-solid.svg
Normal file
1
src/plugins/csv/img/sort-up-solid.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M182.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8H288c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"/></svg>
|
After Width: | Height: | Size: 402 B |
@ -1,76 +0,0 @@
|
||||
import React, { FC } from 'react';
|
||||
import {
|
||||
Table,
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
type PaginationProps = {
|
||||
table: Table<any>;
|
||||
};
|
||||
|
||||
const Pagination: FC<PaginationProps> = ({ table }) => {
|
||||
return (
|
||||
<div className="pagination-renderer flex items-center gap-2">
|
||||
<button
|
||||
className="border rounded p-1"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
{'<<'}
|
||||
</button>
|
||||
<button
|
||||
className="border rounded p-1"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
{'<'}
|
||||
</button>
|
||||
<button
|
||||
className="border rounded p-1"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
{'>'}
|
||||
</button>
|
||||
<button
|
||||
className="border rounded p-1"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
{'>>'}
|
||||
</button>
|
||||
<span className="flex items-center gap-1 page-label">
|
||||
<div>Page</div>
|
||||
<strong>
|
||||
{table.getState().pagination.pageIndex + 1} of{' '}
|
||||
{table.getPageCount()}
|
||||
</strong>
|
||||
</span>
|
||||
<span className="flex items-center gap-1 go-to-page-label">
|
||||
| Go to page:
|
||||
<input
|
||||
type="number"
|
||||
defaultValue={table.getState().pagination.pageIndex + 1}
|
||||
onChange={e => {
|
||||
const page = e.target.value ? Number(e.target.value) - 1 : 0;
|
||||
table.setPageIndex(page);
|
||||
}}
|
||||
className="border p-1 rounded w-16"
|
||||
/>
|
||||
</span>
|
||||
<select
|
||||
value={table.getState().pagination.pageSize}
|
||||
onChange={e => {
|
||||
table.setPageSize(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{[10, 20, 30, 40, 50].map(pageSize => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
Show {pageSize}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
Loading…
Reference in New Issue
Block a user