feat(ui): add sortable column support to DataTable and implement in flows
This commit is contained in:
1 parent
525b9ceead
commit
83da34a1ab
2 files changed
+54
-7
No files matched your search
@@ -117,11 +117,15 @@ export default function FlowsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Download",
|
header: "Download",
|
||||||
accessor: (row) => <span className="text-blue-400">{fmtBytes(row.bytes_download)}</span>
|
accessor: (row) => <span className="text-blue-400">{fmtBytes(row.bytes_download)}</span>,
|
||||||
|
sortable: true,
|
||||||
|
sortAccessor: (row) => row.bytes_download ?? 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Upload",
|
header: "Upload",
|
||||||
accessor: (row) => <span className="text-green-500">{fmtBytes(row.bytes_upload)}</span>
|
accessor: (row) => <span className="text-green-500">{fmtBytes(row.bytes_upload)}</span>,
|
||||||
|
sortable: true,
|
||||||
|
sortAccessor: (row) => row.bytes_upload ?? 0
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export interface Column<T> {
|
|||||||
header: React.ReactNode;
|
header: React.ReactNode;
|
||||||
accessor: (row: T, index: number) => React.ReactNode;
|
accessor: (row: T, index: number) => React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
sortable?: boolean;
|
||||||
|
sortAccessor?: (row: T) => number | string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CsvExportOptions<T> {
|
export interface CsvExportOptions<T> {
|
||||||
@@ -60,10 +62,39 @@ export function DataTable<T>({
|
|||||||
}: DataTableProps<T>) {
|
}: DataTableProps<T>) {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [sortConfig, setSortConfig] = useState<{ key: number; direction: 'asc' | 'desc' } | null>(null);
|
||||||
|
|
||||||
const filteredData = searchFilter
|
const handleSort = (columnIndex: number) => {
|
||||||
? data.filter(row => searchFilter(row, query.trim()))
|
let direction: 'asc' | 'desc' = 'asc';
|
||||||
: data;
|
if (sortConfig && sortConfig.key === columnIndex && sortConfig.direction === 'asc') {
|
||||||
|
direction = 'desc';
|
||||||
|
}
|
||||||
|
setSortConfig({ key: columnIndex, direction });
|
||||||
|
};
|
||||||
|
|
||||||
|
let processedData = [...data];
|
||||||
|
|
||||||
|
if (searchFilter && query.trim()) {
|
||||||
|
processedData = processedData.filter(row => searchFilter(row, query.trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortConfig !== null) {
|
||||||
|
const column = columns[sortConfig.key];
|
||||||
|
if (column && column.sortable && column.sortAccessor) {
|
||||||
|
processedData.sort((a, b) => {
|
||||||
|
const valA = column.sortAccessor!(a);
|
||||||
|
const valB = column.sortAccessor!(b);
|
||||||
|
if (valA === valB) return 0;
|
||||||
|
if (valA === null || valA === undefined) return 1;
|
||||||
|
if (valB === null || valB === undefined) return -1;
|
||||||
|
if (valA < valB) return sortConfig.direction === 'asc' ? -1 : 1;
|
||||||
|
if (valA > valB) return sortConfig.direction === 'asc' ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredData = processedData;
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(filteredData.length / pageSize));
|
const totalPages = Math.max(1, Math.ceil(filteredData.length / pageSize));
|
||||||
const startIndex = (currentPage - 1) * pageSize;
|
const startIndex = (currentPage - 1) * pageSize;
|
||||||
@@ -113,8 +144,20 @@ export function DataTable<T>({
|
|||||||
<thead className="bg-secondary/30 text-muted-foreground border-b border-border/50 relative z-20">
|
<thead className="bg-secondary/30 text-muted-foreground border-b border-border/50 relative z-20">
|
||||||
<tr>
|
<tr>
|
||||||
{columns.map((col, i) => (
|
{columns.map((col, i) => (
|
||||||
<th key={i} className={`px-4 py-3.5 h-[48px] font-medium tracking-wide whitespace-nowrap ${col.className?.includes("text-") ? "" : "text-center"} ${col.className || ""}`}>
|
<th
|
||||||
{col.header}
|
key={i}
|
||||||
|
onClick={() => col.sortable && handleSort(i)}
|
||||||
|
className={`px-4 py-3.5 h-[48px] font-medium tracking-wide whitespace-nowrap ${col.className?.includes("text-") ? "" : "text-center"} ${col.className || ""} ${col.sortable ? "cursor-pointer hover:bg-white/5 transition-colors group select-none" : ""}`}
|
||||||
|
>
|
||||||
|
<div className={`flex items-center gap-1 ${col.className?.includes("text-") && !col.className?.includes("text-center") ? "" : "justify-center"}`}>
|
||||||
|
{col.header}
|
||||||
|
{col.sortable && (
|
||||||
|
<div className="flex flex-col text-[10px] opacity-40 group-hover:opacity-100 transition-opacity">
|
||||||
|
<span className={`${sortConfig?.key === i && sortConfig.direction === 'asc' ? 'text-primary opacity-100' : ''}`}>▲</span>
|
||||||
|
<span className={`-mt-1 ${sortConfig?.key === i && sortConfig.direction === 'desc' ? 'text-primary opacity-100' : ''}`}>▼</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in new issue
Block a user