Mark Duppenthaler
update with css
d588824
import React, { useState, useEffect, useMemo } from 'react';
import { useTable, useSortBy, useFilters, usePagination } from 'react-table';
import api from '../api';
// Default filter UI for columns
function DefaultColumnFilter({
column: { filterValue, preFilteredRows, setFilter },
}) {
const count = preFilteredRows.length;
return (
<input
value={filterValue || ''}
onChange={e => {
setFilter(e.target.value || undefined);
}}
placeholder={`Search ${count} records...`}
className="px-2 py-1 text-sm border rounded focus:outline-none focus:ring-2 focus:ring-primary/50 w-full"
/>
);
}
// Dummy data for when API fails
const dummyData = [
{
Model: 'WatermarkA',
'Success Rate': '95%',
'Attack Resistance': 'High',
'Visual Quality': '9.2',
'Compute Requirements': 'Medium'
},
{
Model: 'WatermarkB',
'Success Rate': '92%',
'Attack Resistance': 'Medium',
'Visual Quality': '9.5',
'Compute Requirements': 'Low'
},
{
Model: 'WatermarkC',
'Success Rate': '98%',
'Attack Resistance': 'Very High',
'Visual Quality': '8.8',
'Compute Requirements': 'High'
}
];
// Dummy columns for when API fails
const dummyColumns = [
{ Header: 'Model', accessor: 'Model' },
{ Header: 'Success Rate', accessor: 'Success Rate' },
{ Header: 'Attack Resistance', accessor: 'Attack Resistance' },
{ Header: 'Visual Quality', accessor: 'Visual Quality' },
{ Header: 'Compute Requirements', accessor: 'Compute Requirements' }
];
function Leaderboard({ benchmark }) {
const [data, setData] = useState([]);
const [columns, setColumns] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
// Fetch leaderboard data and columns when the benchmark changes
async function fetchData() {
try {
setLoading(true);
// Fetch available columns first
const columnsResponse = await api.getColumns(benchmark);
if (!columnsResponse.success) {
throw new Error(columnsResponse.error || 'Failed to fetch columns');
}
// Format columns for react-table
const tableColumns = columnsResponse.columns.map(column => ({
Header: column,
accessor: column,
Filter: DefaultColumnFilter,
}));
setColumns(tableColumns);
// Fetch leaderboard data
const dataResponse = await api.getLeaderboard(benchmark);
if (!dataResponse.success) {
throw new Error(dataResponse.error || 'Failed to fetch leaderboard data');
}
setData(dataResponse.data);
setError(null);
} catch (err) {
console.error('Leaderboard error:', err);
setError(err.message || 'An error occurred');
// Set dummy data so UI doesn't show error
setData(dummyData);
setColumns(dummyColumns.map(col => ({
...col,
Filter: DefaultColumnFilter
})));
} finally {
setLoading(false);
}
}
fetchData();
}, [benchmark]);
// Filter data based on search query
const filteredData = useMemo(() => {
if (!searchQuery) return data;
return data.filter(row => {
return Object.values(row).some(value =>
String(value).toLowerCase().includes(searchQuery.toLowerCase())
);
});
}, [data, searchQuery]);
// Set up react-table
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
prepareRow,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data: filteredData,
initialState: { pageIndex: 0, pageSize: 10 },
},
useFilters,
useSortBy,
usePagination
);
if (loading) {
return (
<div className="flex justify-center items-center p-8">
<div className="animate-pulse text-primary font-medium">Loading leaderboard data...</div>
</div>
);
}
return (
<div className="leaderboard">
<h2 className="text-2xl font-display font-medium text-secondary mb-6">
{benchmark.charAt(0).toUpperCase() + benchmark.slice(1)} Watermark Benchmark
</h2>
<div className="mb-4">
<input
type="text"
placeholder="Search across all columns..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div className="overflow-x-auto rounded-md shadow">
<table {...getTableProps()} className="data-table w-full">
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th
{...column.getHeaderProps(column.getSortByToggleProps())}
className="bg-gray-100 text-left px-4 py-3 font-semibold text-gray-700"
>
<div className="flex items-center space-x-1">
<span>{column.render('Header')}</span>
<span>
{column.isSorted
? column.isSortedDesc
? ' ↓'
: ' ↑'
: ''}
</span>
</div>
<div className="mt-2">{column.canFilter ? column.render('Filter') : null}</div>
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map(row => {
prepareRow(row);
return (
<tr {...row.getRowProps()} className="hover:bg-gray-50">
{row.cells.map(cell => (
<td
{...cell.getCellProps()}
className="border-t px-4 py-3"
>
{cell.render('Cell')}
</td>
))}
</tr>
);
})}
</tbody>
</table>
</div>
<div className="mt-4 flex flex-wrap items-center justify-between">
<div className="flex space-x-2 mb-2 sm:mb-0">
<button
onClick={() => gotoPage(0)}
disabled={!canPreviousPage}
className="px-3 py-1 rounded border bg-white disabled:opacity-50"
>
{'<<'}
</button>
<button
onClick={() => previousPage()}
disabled={!canPreviousPage}
className="px-3 py-1 rounded border bg-white disabled:opacity-50"
>
{'<'}
</button>
<button
onClick={() => nextPage()}
disabled={!canNextPage}
className="px-3 py-1 rounded border bg-white disabled:opacity-50"
>
{'>'}
</button>
<button
onClick={() => gotoPage(pageCount - 1)}
disabled={!canNextPage}
className="px-3 py-1 rounded border bg-white disabled:opacity-50"
>
{'>>'}
</button>
</div>
<span className="text-sm">
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>
</span>
<div className="flex items-center space-x-2">
<span className="text-sm">Go to page:</span>
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
gotoPage(page);
}}
className="w-16 px-2 py-1 border rounded"
/>
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value));
}}
className="px-2 py-1 border rounded"
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</div>
</div>
);
}
export default Leaderboard;