mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-23 21:51:30 +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,6 +1,16 @@
|
|||||||
@import "../../app/common/themes/themes.less";
|
@import "../../app/common/themes/themes.less";
|
||||||
|
|
||||||
.csv-renderer {
|
.csv-renderer {
|
||||||
|
.ellipsis() {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -9,117 +19,73 @@
|
|||||||
user-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 {
|
.global-search-render {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border: 1px solid rgb(146, 146, 146);
|
border-collapse: collapse;
|
||||||
margin-bottom: 10px;
|
overflow-x: auto;
|
||||||
|
border: 1px solid @scrollbar-thumb;
|
||||||
|
|
||||||
tbody {
|
thead {
|
||||||
border-bottom: 1px solid rgb(146, 146, 146);
|
position:relative;
|
||||||
}
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
th {
|
tr {
|
||||||
color: white;
|
border-bottom: 1px solid @scrollbar-thumb;
|
||||||
border-bottom: 1px solid rgb(146, 146, 146);
|
|
||||||
border-right: 1px solid rgb(146, 146, 146);
|
|
||||||
padding: 2px 10px 4px 10px;
|
|
||||||
|
|
||||||
.min-max-renderer {
|
th {
|
||||||
min-width: 200px;
|
color:@term-white;
|
||||||
display: flex;
|
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 {
|
.inner {
|
||||||
flex: 1;
|
text-align:left;
|
||||||
|
padding-right: 15px;
|
||||||
|
position: relative;
|
||||||
|
.ellipsis();
|
||||||
|
|
||||||
input {
|
.sort-icon {
|
||||||
width: 99%;
|
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 {
|
tr {
|
||||||
td {
|
width: 100%;
|
||||||
border-right: 1px solid rgb(146, 146, 146);
|
display:flex;
|
||||||
padding: 2px 10px;
|
|
||||||
}
|
|
||||||
td:last-child {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-renderer {
|
td {
|
||||||
.page-label {
|
border-right: 1px solid @scrollbar-thumb;
|
||||||
div {
|
border-left: 1px solid @scrollbar-thumb;
|
||||||
padding-right: 6px;
|
padding: 2px 10px;
|
||||||
}
|
flex-basis:100%;
|
||||||
strong {
|
flex-grow:2;
|
||||||
color: white;
|
display: block;
|
||||||
|
text-align:left;
|
||||||
|
.ellipsis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,36 +6,20 @@ import {
|
|||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
flexRender,
|
flexRender,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
ColumnFiltersState,
|
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
getFilteredRowModel,
|
getFilteredRowModel,
|
||||||
getFacetedRowModel,
|
|
||||||
getFacetedUniqueValues,
|
|
||||||
getFacetedMinMaxValues,
|
|
||||||
getPaginationRowModel,
|
|
||||||
sortingFns,
|
|
||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
FilterFn,
|
FilterFn,
|
||||||
} from '@tanstack/react-table'
|
} from '@tanstack/react-table'
|
||||||
import {
|
import {
|
||||||
RankingInfo,
|
|
||||||
rankItem,
|
rankItem,
|
||||||
compareItems,
|
|
||||||
} from '@tanstack/match-sorter-utils'
|
} from '@tanstack/match-sorter-utils'
|
||||||
import Filter from "./filter";
|
import SortUpIcon from './img/sort-up-solid.svg';
|
||||||
import DebouncedInput from "./search";
|
import SortDownIcon from './img/sort-down-solid.svg';
|
||||||
import Pagination from "./pagination";
|
|
||||||
|
|
||||||
import "./csv.less";
|
import "./csv.less";
|
||||||
|
|
||||||
declare module '@tanstack/table-core' {
|
const MAX_DATA_SIZE = 10 * 1024 * 1024 // 10MB in bytes
|
||||||
interface FilterFns {
|
|
||||||
fuzzy: FilterFn<unknown>
|
|
||||||
}
|
|
||||||
interface FilterMeta {
|
|
||||||
itemRank: RankingInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type CSVRow = {
|
type CSVRow = {
|
||||||
[key: string]: string | number;
|
[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 if the item should be filtered in/out
|
||||||
return itemRank.passed
|
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 {
|
interface Props {
|
||||||
@ -97,22 +60,24 @@ interface State {
|
|||||||
message: { status: string; text: string } | null;
|
message: { status: string; text: string } | null;
|
||||||
isPreviewerAvailable: boolean;
|
isPreviewerAvailable: boolean;
|
||||||
showReadonly: boolean;
|
showReadonly: boolean;
|
||||||
|
totalHeight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<any>();
|
const columnHelper = createColumnHelper<any>();
|
||||||
|
|
||||||
const CSVRenderer: FC<Props> = (props: Props) => {
|
const CSVRenderer: FC<Props> = (props: Props) => {
|
||||||
const csvCacheRef = useRef(new Map<string, string>());
|
const csvCacheRef = useRef(new Map<string, string>());
|
||||||
|
const rowRef = useRef<(HTMLTableRowElement | null)[]>([]);
|
||||||
|
const headerRef = useRef<HTMLTableRowElement | null>(null);
|
||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
content: null,
|
content: null,
|
||||||
message: null,
|
message: null,
|
||||||
isPreviewerAvailable: false,
|
isPreviewerAvailable: false,
|
||||||
showReadonly: true,
|
showReadonly: true,
|
||||||
|
totalHeight: 0,
|
||||||
});
|
});
|
||||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
const [globalFilter, setGlobalFilter] = useState('')
|
||||||
[]
|
const [isFileTooLarge, setIsFileTooLarge] = useState<boolean>(false);
|
||||||
)
|
|
||||||
const [globalFilter, setGlobalFilter] = React.useState('')
|
|
||||||
|
|
||||||
const filePath = props.lineState["prompt:file"];
|
const filePath = props.lineState["prompt:file"];
|
||||||
const { screenId, lineId } = props.context;
|
const { screenId, lineId } = props.context;
|
||||||
@ -122,7 +87,27 @@ const CSVRenderer: FC<Props> = (props: Props) => {
|
|||||||
const parsedData = useMemo<CSVRow[]>(() => {
|
const parsedData = useMemo<CSVRow[]>(() => {
|
||||||
if (!state.content) return [];
|
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 results.data.map(row => {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
@ -158,6 +143,12 @@ const CSVRenderer: FC<Props> = (props: Props) => {
|
|||||||
if (content) {
|
if (content) {
|
||||||
setState((prevState) => ({ ...prevState, content }));
|
setState((prevState) => ({ ...prevState, content }));
|
||||||
} else {
|
} 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) => {
|
props.data.text().then((content: string) => {
|
||||||
setState((prevState) => ({ ...prevState, content }));
|
setState((prevState) => ({ ...prevState, content }));
|
||||||
csvCacheRef.current.set(cacheKey, content);
|
csvCacheRef.current.set(cacheKey, content);
|
||||||
@ -165,6 +156,18 @@ const CSVRenderer: FC<Props> = (props: Props) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 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 = () => (
|
const getMessage = () => (
|
||||||
<div style={{ position: "absolute", bottom: "-3px", left: "14px" }}>
|
<div style={{ position: "absolute", bottom: "-3px", left: "14px" }}>
|
||||||
<div
|
<div
|
||||||
@ -183,66 +186,51 @@ const CSVRenderer: FC<Props> = (props: Props) => {
|
|||||||
const { content, message } = state;
|
const { content, message } = state;
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
|
manualPagination: true,
|
||||||
data: parsedData,
|
data: parsedData,
|
||||||
columns,
|
columns,
|
||||||
filterFns: {
|
filterFns: {
|
||||||
fuzzy: fuzzyFilter,
|
fuzzy: fuzzyFilter,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
columnFilters,
|
|
||||||
globalFilter,
|
globalFilter,
|
||||||
},
|
},
|
||||||
onColumnFiltersChange: setColumnFilters,
|
|
||||||
onGlobalFilterChange: setGlobalFilter,
|
|
||||||
globalFilterFn: fuzzyFilter,
|
globalFilterFn: fuzzyFilter,
|
||||||
|
onGlobalFilterChange: setGlobalFilter,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
getSortedRowModel: getSortedRowModel(),
|
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 (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 (
|
return (
|
||||||
<div className="csv-renderer">
|
<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>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map(headerGroup => (
|
{table.getHeaderGroups().map(headerGroup => (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id} ref={headerRef}>
|
||||||
{headerGroup.headers.map(header => (
|
{headerGroup.headers.map(header => (
|
||||||
<th key={header.id}>
|
<th
|
||||||
{header.isPlaceholder
|
key={header.id}
|
||||||
? null
|
colSpan={header.colSpan}
|
||||||
: (
|
style={{ width: header.getSize() }}
|
||||||
<>
|
>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: (
|
||||||
<div
|
<div
|
||||||
{...{
|
{...{
|
||||||
className: header.column.getCanSort()
|
className: header.column.getCanSort()
|
||||||
? 'cursor-pointer select-none'
|
? 'inner cursor-pointer select-none'
|
||||||
: '',
|
: '',
|
||||||
onClick: header.column.getToggleSortingHandler(),
|
onClick: header.column.getToggleSortingHandler(),
|
||||||
}}
|
}}
|
||||||
@ -251,36 +239,29 @@ const CSVRenderer: FC<Props> = (props: Props) => {
|
|||||||
header.column.columnDef.header,
|
header.column.columnDef.header,
|
||||||
header.getContext()
|
header.getContext()
|
||||||
)}
|
)}
|
||||||
{{
|
{
|
||||||
asc: ' 🔼',
|
header.column.getIsSorted() === 'asc' ? <img src={SortUpIcon} className="sort-icon sort-up-icon" alt="Ascending" /> :
|
||||||
desc: ' 🔽',
|
header.column.getIsSorted() === 'desc' ? <img src={SortDownIcon} className="sort-icon sort-down-icon" alt="Descending" /> : null
|
||||||
}[header.column.getIsSorted() as string] ?? null}
|
}
|
||||||
</div>
|
</div>
|
||||||
{header.column.getCanFilter() ? (
|
)}
|
||||||
<div>
|
</th>
|
||||||
<Filter column={header.column} table={table} />
|
))}
|
||||||
</div>
|
</tr>
|
||||||
) : null}
|
))}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody style={{"height": `${state.totalHeight}px`}}>
|
||||||
{table.getRowModel().rows.map(row => (
|
{table.getRowModel().rows.map((row, index) => (
|
||||||
<tr key={row.id}>
|
<tr key={row.id} ref={el => rowRef.current[index] = el}>
|
||||||
{row.getVisibleCells().map(cell => (
|
{row.getVisibleCells().map(cell => (
|
||||||
<td key={cell.id}>
|
<td key={cell.id}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<Pagination table={table} />
|
|
||||||
{message && getMessage()}
|
{message && getMessage()}
|
||||||
</div>
|
</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