mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 00:39:13 +00:00

* feat(releases): action status show client and filter * feat(releases): add better tooltip
329 lines
11 KiB
TypeScript
329 lines
11 KiB
TypeScript
import * as React from "react";
|
|
import { useQuery } from "react-query";
|
|
import {
|
|
useTable,
|
|
useSortBy,
|
|
usePagination,
|
|
useFilters,
|
|
Column
|
|
} from "react-table";
|
|
import {
|
|
ChevronDoubleLeftIcon,
|
|
ChevronLeftIcon,
|
|
ChevronRightIcon,
|
|
ChevronDoubleRightIcon
|
|
} from "@heroicons/react/solid";
|
|
|
|
import { APIClient } from "../../api/APIClient";
|
|
import { EmptyListState } from "../../components/emptystates";
|
|
|
|
import * as Icons from "../../components/Icons";
|
|
import * as DataTable from "../../components/data-table";
|
|
|
|
import {
|
|
IndexerSelectColumnFilter,
|
|
PushStatusSelectColumnFilter, SearchColumnFilter
|
|
} from "./Filters";
|
|
|
|
type TableState = {
|
|
queryPageIndex: number;
|
|
queryPageSize: number;
|
|
totalCount: number;
|
|
queryFilters: ReleaseFilter[];
|
|
};
|
|
|
|
const initialState: TableState = {
|
|
queryPageIndex: 0,
|
|
queryPageSize: 10,
|
|
totalCount: 0,
|
|
queryFilters: []
|
|
};
|
|
|
|
enum ActionType {
|
|
PAGE_CHANGED = "PAGE_CHANGED",
|
|
PAGE_SIZE_CHANGED = "PAGE_SIZE_CHANGED",
|
|
TOTAL_COUNT_CHANGED = "TOTAL_COUNT_CHANGED",
|
|
FILTER_CHANGED = "FILTER_CHANGED"
|
|
}
|
|
|
|
type Actions =
|
|
| { type: ActionType.FILTER_CHANGED; payload: ReleaseFilter[]; }
|
|
| { type: ActionType.PAGE_CHANGED; payload: number; }
|
|
| { type: ActionType.PAGE_SIZE_CHANGED; payload: number; }
|
|
| { type: ActionType.TOTAL_COUNT_CHANGED; payload: number; };
|
|
|
|
const TableReducer = (state: TableState, action: Actions): TableState => {
|
|
switch (action.type) {
|
|
case ActionType.PAGE_CHANGED:
|
|
return { ...state, queryPageIndex: action.payload };
|
|
case ActionType.PAGE_SIZE_CHANGED:
|
|
return { ...state, queryPageSize: action.payload };
|
|
case ActionType.FILTER_CHANGED:
|
|
return { ...state, queryFilters: action.payload };
|
|
case ActionType.TOTAL_COUNT_CHANGED:
|
|
return { ...state, totalCount: action.payload };
|
|
default:
|
|
throw new Error(`Unhandled action type: ${action}`);
|
|
}
|
|
};
|
|
|
|
export const ReleaseTable = () => {
|
|
const columns = React.useMemo(() => [
|
|
{
|
|
Header: "Age",
|
|
accessor: "timestamp",
|
|
Cell: DataTable.AgeCell
|
|
},
|
|
{
|
|
Header: "Release",
|
|
accessor: "torrent_name",
|
|
Cell: DataTable.TitleCell,
|
|
Filter: SearchColumnFilter
|
|
},
|
|
{
|
|
Header: "Actions",
|
|
accessor: "action_status",
|
|
Cell: DataTable.ReleaseStatusCell,
|
|
Filter: PushStatusSelectColumnFilter
|
|
},
|
|
{
|
|
Header: "Indexer",
|
|
accessor: "indexer",
|
|
Cell: DataTable.TitleCell,
|
|
Filter: IndexerSelectColumnFilter,
|
|
filter: "equal"
|
|
}
|
|
] as Column<Release>[], []);
|
|
|
|
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
|
|
React.useReducer(TableReducer, initialState);
|
|
|
|
const { isLoading, error, data, isSuccess } = useQuery(
|
|
["releases", queryPageIndex, queryPageSize, queryFilters],
|
|
() => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
|
|
{
|
|
keepPreviousData: true,
|
|
staleTime: 5000
|
|
}
|
|
);
|
|
|
|
// Use the state and functions returned from useTable to build your UI
|
|
const {
|
|
getTableProps,
|
|
getTableBodyProps,
|
|
headerGroups,
|
|
prepareRow,
|
|
page, // Instead of using 'rows', we'll use page,
|
|
// which has only the rows for the active page
|
|
|
|
// The rest of these things are super handy, too ;)
|
|
canPreviousPage,
|
|
canNextPage,
|
|
pageOptions,
|
|
pageCount,
|
|
gotoPage,
|
|
nextPage,
|
|
previousPage,
|
|
setPageSize,
|
|
state: { pageIndex, pageSize, filters }
|
|
} = useTable(
|
|
{
|
|
columns,
|
|
data: data && isSuccess ? data.data : [],
|
|
initialState: {
|
|
pageIndex: queryPageIndex,
|
|
pageSize: queryPageSize,
|
|
filters: []
|
|
},
|
|
manualPagination: true,
|
|
manualFilters: true,
|
|
manualSortBy: true,
|
|
pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : 0,
|
|
autoResetSortBy: false,
|
|
autoResetExpanded: false,
|
|
autoResetPage: false
|
|
},
|
|
useFilters,
|
|
useSortBy,
|
|
usePagination
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
dispatch({ type: ActionType.PAGE_CHANGED, payload: pageIndex });
|
|
}, [pageIndex]);
|
|
|
|
React.useEffect(() => {
|
|
dispatch({ type: ActionType.PAGE_SIZE_CHANGED, payload: pageSize });
|
|
gotoPage(0);
|
|
}, [pageSize, gotoPage]);
|
|
|
|
React.useEffect(() => {
|
|
if (data?.count) {
|
|
dispatch({
|
|
type: ActionType.TOTAL_COUNT_CHANGED,
|
|
payload: data.count
|
|
});
|
|
}
|
|
}, [data?.count]);
|
|
|
|
React.useEffect(() => {
|
|
dispatch({ type: ActionType.FILTER_CHANGED, payload: filters });
|
|
}, [filters]);
|
|
|
|
if (error)
|
|
return <p>Error</p>;
|
|
|
|
if (isLoading)
|
|
return <p>Loading...</p>;
|
|
|
|
if (!data)
|
|
return <EmptyListState text="No recent activity" />;
|
|
|
|
// Render the UI for your table
|
|
return (
|
|
<div className="flex flex-col">
|
|
<div className="flex mb-6 flex-col sm:flex-row">
|
|
{headerGroups.map((headerGroup) =>
|
|
headerGroup.headers.map((column) => (
|
|
column.Filter ? (
|
|
<React.Fragment key={column.id}>{column.render("Filter")}</React.Fragment>
|
|
) : null
|
|
))
|
|
)}
|
|
</div>
|
|
<div className="bg-white shadow-lg dark:bg-gray-800 rounded-lg">
|
|
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
<thead className="bg-gray-50 dark:bg-gray-800">
|
|
{headerGroups.map((headerGroup) => {
|
|
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps();
|
|
return (
|
|
<tr key={rowKey} {...rowRest}>
|
|
{headerGroup.headers.map((column) => {
|
|
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps());
|
|
return (
|
|
// Add the sorting props to control sorting. For this example
|
|
// we can add them into the header props
|
|
<th
|
|
key={`${rowKey}-${columnKey}`}
|
|
scope="col"
|
|
className="first:pl-5 pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group"
|
|
{...columnRest}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<>{column.render("Header")}</>
|
|
{/* Add a sort direction indicator */}
|
|
<span>
|
|
{column.isSorted ? (
|
|
column.isSortedDesc ? (
|
|
<Icons.SortDownIcon className="w-4 h-4 text-gray-400" />
|
|
) : (
|
|
<Icons.SortUpIcon className="w-4 h-4 text-gray-400" />
|
|
)
|
|
) : (
|
|
<Icons.SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100" />
|
|
)}
|
|
</span>
|
|
</div>
|
|
</th>
|
|
);
|
|
})}
|
|
</tr>
|
|
);
|
|
})}
|
|
</thead>
|
|
<tbody
|
|
{...getTableBodyProps()}
|
|
className="divide-y divide-gray-200 dark:divide-gray-700"
|
|
>
|
|
{page.map((row) => {
|
|
prepareRow(row);
|
|
|
|
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
|
return (
|
|
<tr key={bodyRowKey} {...bodyRowRest}>
|
|
{row.cells.map((cell) => {
|
|
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
|
return (
|
|
<td
|
|
key={cellRowKey}
|
|
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
|
role="cell"
|
|
{...cellRowRest}
|
|
>
|
|
<>{cell.render("Cell")}</>
|
|
</td>
|
|
);
|
|
})}
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
|
|
{/* Pagination */}
|
|
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700">
|
|
<div className="flex justify-between flex-1 sm:hidden">
|
|
<DataTable.Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</DataTable.Button>
|
|
<DataTable.Button onClick={() => nextPage()} disabled={!canNextPage}>Next</DataTable.Button>
|
|
</div>
|
|
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
|
<div className="flex items-baseline gap-x-2">
|
|
<span className="text-sm text-gray-700">
|
|
Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
|
|
</span>
|
|
<label>
|
|
<span className="sr-only bg-gray-700">Items Per Page</span>
|
|
<select
|
|
className="block w-full border-gray-300 rounded-md shadow-sm cursor-pointer dark:bg-gray-800 dark:border-gray-800 dark:text-gray-600 dark:hover:text-gray-500 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
|
value={pageSize}
|
|
onChange={e => {
|
|
setPageSize(Number(e.target.value));
|
|
}}
|
|
>
|
|
{[5, 10, 20, 50].map(pageSize => (
|
|
<option key={pageSize} value={pageSize}>
|
|
Show {pageSize}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
</div>
|
|
<div>
|
|
<nav className="relative z-0 inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
|
<DataTable.PageButton
|
|
className="rounded-l-md"
|
|
onClick={() => gotoPage(0)}
|
|
disabled={!canPreviousPage}
|
|
>
|
|
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">First</span>
|
|
<ChevronDoubleLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
|
</DataTable.PageButton>
|
|
<DataTable.PageButton
|
|
onClick={() => previousPage()}
|
|
disabled={!canPreviousPage}
|
|
>
|
|
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Previous</span>
|
|
<ChevronLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
|
</DataTable.PageButton>
|
|
<DataTable.PageButton
|
|
onClick={() => nextPage()}
|
|
disabled={!canNextPage}>
|
|
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Next</span>
|
|
<ChevronRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
|
</DataTable.PageButton>
|
|
<DataTable.PageButton
|
|
className="rounded-r-md"
|
|
onClick={() => gotoPage(pageCount - 1)}
|
|
disabled={!canNextPage}
|
|
>
|
|
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Last</span>
|
|
<ChevronDoubleRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
|
</DataTable.PageButton>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|