mirror of
https://github.com/idanoo/autobrr
synced 2025-07-26 10:19:13 +00:00
feat(web): move from react-router to @tanstack/router (#1338)
* fix(auth): invalid cookie handling and wrongful basic auth invalidation * fix(auth): fix test to reflect new HTTP status code * fix(auth/web): do not throw on error * fix(http): replace http codes in middleware to prevent basic auth invalidation fix typo in comment * fix test * fix(web): api client handle 403 * refactor(http): auth_test use testify.assert * refactor(http): set session opts after valid login * refactor(http): send more client headers * fix(http): test * refactor(web): move router to tanstack/router * refactor(web): use route loaders and suspense * refactor(web): useSuspense for settings * refactor(web): invalidate cookie in middleware * fix: loclfile * fix: load filter/id * fix(web): login, onboard, types, imports * fix(web): filter load * fix(web): build errors * fix(web): ts-expect-error * fix(tests): filter_test.go * fix(filters): tests * refactor: remove duplicate spinner components refactor: ReleaseTable.tsx loading animation refactor: remove dedicated `pendingComponent` for `settingsRoute` * fix: refactor missed SectionLoader to RingResizeSpinner * fix: substitute divides with borders to account for unloaded elements * fix(api): action status URL param * revert: action status URL param add comment * fix(routing): notfound handling and split files * fix(filters): notfound get params * fix(queries): colon * fix(queries): comments ts-ignore * fix(queries): extract queryKeys * fix(queries): remove err * fix(routes): move zob schema inline * fix(auth): middleware and redirect to login * fix(auth): failing test * fix(logs): invalidate correct key * fix(logs): invalidate correct key * fix(logs): invalidate correct key * fix: JSX element stealing focus from searchbar * reimplement empty release table state text * fix(context): use deep-copy * fix(releases): empty state and filter input warnings * fix(releases): empty states * fix(auth): onboarding * fix(cache): invalidate queries --------- Co-authored-by: ze0s <43699394+zze0s@users.noreply.github.com>
This commit is contained in:
parent
cc9656cd41
commit
1a23b69bcf
64 changed files with 2543 additions and 2091 deletions
|
@ -4,15 +4,15 @@
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { useQuery, keepPreviousData } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
import { APIClient } from "@api/APIClient";
|
||||
import { classNames } from "@utils";
|
||||
import { PushStatusOptions } from "@domain/constants";
|
||||
import { FilterProps } from "react-table";
|
||||
import { DebounceInput } from "react-debounce-input";
|
||||
import { ReleasesIndexersQueryOptions } from "@api/queries";
|
||||
|
||||
interface ListboxFilterProps {
|
||||
id: string;
|
||||
|
@ -54,7 +54,7 @@ const ListboxFilter = ({
|
|||
<Listbox.Options
|
||||
className="absolute z-10 w-full mt-1 overflow-auto text-base bg-white dark:bg-gray-800 rounded-md shadow-lg max-h-60 border border-opacity-5 border-black dark:border-gray-700 dark:border-opacity-40 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<FilterOption label="All" />
|
||||
<FilterOption label="All" value="" />
|
||||
{children}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
|
@ -67,12 +67,7 @@ const ListboxFilter = ({
|
|||
export const IndexerSelectColumnFilter = ({
|
||||
column: { filterValue, setFilter, id }
|
||||
}: FilterProps<object>) => {
|
||||
const { data, isSuccess } = useQuery({
|
||||
queryKey: ["indexer_options"],
|
||||
queryFn: () => APIClient.release.indexerOptions(),
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: Infinity
|
||||
});
|
||||
const { data, isSuccess } = useQuery(ReleasesIndexersQueryOptions());
|
||||
|
||||
// Render a multi-select box
|
||||
return (
|
||||
|
@ -80,10 +75,10 @@ export const IndexerSelectColumnFilter = ({
|
|||
id={id}
|
||||
key={id}
|
||||
label={filterValue ?? "Indexer"}
|
||||
currentValue={filterValue}
|
||||
currentValue={filterValue ?? ""}
|
||||
onChange={setFilter}
|
||||
>
|
||||
{isSuccess && data?.map((indexer, idx) => (
|
||||
{isSuccess && data && data?.map((indexer, idx) => (
|
||||
<FilterOption key={idx} label={indexer} value={indexer} />
|
||||
))}
|
||||
</ListboxFilter>
|
||||
|
@ -138,7 +133,7 @@ export const PushStatusSelectColumnFilter = ({
|
|||
<ListboxFilter
|
||||
id={id}
|
||||
label={label ?? "Push status"}
|
||||
currentValue={filterValue}
|
||||
currentValue={filterValue ?? ""}
|
||||
onChange={setFilter}
|
||||
>
|
||||
{PushStatusOptions.map((status, idx) => (
|
|
@ -4,33 +4,28 @@
|
|||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { Column, useFilters, usePagination, useSortBy, useTable } from "react-table";
|
||||
import {
|
||||
ChevronDoubleLeftIcon,
|
||||
ChevronDoubleRightIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon
|
||||
ChevronRightIcon,
|
||||
EyeIcon,
|
||||
EyeSlashIcon
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
import { ReleasesIndexRoute } from "@app/routes";
|
||||
import { ReleasesListQueryOptions } from "@api/queries";
|
||||
import { RandomLinuxIsos } from "@utils";
|
||||
import { APIClient } from "@api/APIClient";
|
||||
import { EmptyListState } from "@components/emptystates";
|
||||
|
||||
import * as Icons from "@components/Icons";
|
||||
import { RingResizeSpinner } from "@components/Icons";
|
||||
import * as DataTable from "@components/data-table";
|
||||
|
||||
import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./Filters";
|
||||
|
||||
export const releaseKeys = {
|
||||
all: ["releases"] as const,
|
||||
lists: () => [...releaseKeys.all, "list"] as const,
|
||||
list: (pageIndex: number, pageSize: number, filters: ReleaseFilter[]) => [...releaseKeys.lists(), { pageIndex, pageSize, filters }] as const,
|
||||
details: () => [...releaseKeys.all, "detail"] as const,
|
||||
detail: (id: number) => [...releaseKeys.details(), id] as const
|
||||
};
|
||||
import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./ReleaseFilters";
|
||||
import { EmptyListState } from "@components/emptystates";
|
||||
|
||||
type TableState = {
|
||||
queryPageIndex: number;
|
||||
|
@ -79,10 +74,28 @@ const TableReducer = (state: TableState, action: Actions): TableState => {
|
|||
}
|
||||
};
|
||||
|
||||
const EmptyReleaseList = () => (
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-table rounded-md overflow-auto">
|
||||
<table className="min-w-full rounded-md divide-y divide-gray-200 dark:divide-gray-750">
|
||||
<thead className="bg-gray-100 dark:bg-gray-850 border-b border-gray-200 dark:border-gray-750">
|
||||
<tr>
|
||||
<th>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="h-10"/>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<div className="flex items-center justify-center py-52">
|
||||
<EmptyListState text="No results"/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ReleaseTable = () => {
|
||||
const location = useLocation();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const filterTypeFromUrl = queryParams.get("filter");
|
||||
const search = ReleasesIndexRoute.useSearch()
|
||||
|
||||
const columns = React.useMemo(() => [
|
||||
{
|
||||
Header: "Age",
|
||||
|
@ -116,14 +129,14 @@ export const ReleaseTable = () => {
|
|||
}
|
||||
] as Column<Release>[], []);
|
||||
|
||||
if (search.action_status != "") {
|
||||
initialState.queryFilters = [{id: "action_status", value: search.action_status! }]
|
||||
}
|
||||
|
||||
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
|
||||
React.useReducer(TableReducer, initialState);
|
||||
|
||||
const { isLoading, error, data, isSuccess } = useQuery({
|
||||
queryKey: releaseKeys.list(queryPageIndex, queryPageSize, queryFilters),
|
||||
queryFn: () => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
|
||||
staleTime: 5000
|
||||
});
|
||||
const { isLoading, error, data, isSuccess } = useQuery(ReleasesListQueryOptions(queryPageIndex * queryPageSize, queryPageSize, queryFilters));
|
||||
|
||||
const [modifiedData, setModifiedData] = useState<Release[]>([]);
|
||||
const [showLinuxIsos, setShowLinuxIsos] = useState(false);
|
||||
|
@ -207,10 +220,10 @@ export const ReleaseTable = () => {
|
|||
}, [filters]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (filterTypeFromUrl != null) {
|
||||
dispatch({ type: ActionType.FILTER_CHANGED, payload: [{ id: "action_status", value: filterTypeFromUrl! }] });
|
||||
if (search.action_status != null) {
|
||||
dispatch({ type: ActionType.FILTER_CHANGED, payload: [{ id: "action_status", value: search.action_status! }] });
|
||||
}
|
||||
}, [filterTypeFromUrl]);
|
||||
}, [search.action_status]);
|
||||
|
||||
if (error) {
|
||||
return <p>Error</p>;
|
||||
|
@ -218,167 +231,33 @@ export const ReleaseTable = () => {
|
|||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col animate-pulse">
|
||||
<div>
|
||||
<div className="flex mb-6 flex-col sm:flex-row">
|
||||
{headerGroups.map((headerGroup) =>
|
||||
headerGroup.headers.map((column) => (
|
||||
{ headerGroups.map((headerGroup) => headerGroup.headers.map((column) => (
|
||||
column.Filter ? (
|
||||
<React.Fragment key={column.id}>{column.render("Filter")}</React.Fragment>
|
||||
<React.Fragment key={ column.id }>{ column.render("Filter") }</React.Fragment>
|
||||
) : null
|
||||
))
|
||||
)}
|
||||
) }
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-lg rounded-md overflow-auto">
|
||||
<table {...getTableProps()} className="min-w-full rounded-md divide-y divide-gray-200 dark:divide-gray-750">
|
||||
<thead className="bg-gray-100 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="first:pl-5 pl-3 pr-3 py-3 first:rounded-tl-md last:rounded-tr-md text-xs font-medium tracking-wider text-left text-gray-500 uppercase group"
|
||||
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Add a sort direction indicator */}
|
||||
<span className="h-4">
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-lg rounded-md mt-4">
|
||||
<table className="min-w-full rounded-md divide-y divide-gray-200 dark:divide-gray-750">
|
||||
<thead className="bg-gray-100 dark:bg-gray-850 border-b border-gray-200 dark:border-gray-750">
|
||||
<tr>
|
||||
<th>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="h-10"/>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-150 dark:divide-gray-700">
|
||||
<tr
|
||||
className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
</tr>
|
||||
<tr
|
||||
className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
</tr>
|
||||
<tr
|
||||
className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap"> </td>
|
||||
</tr>
|
||||
<tr
|
||||
className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
</tr>
|
||||
<tr className="justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300">
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap text-center">
|
||||
<p className="text-black dark:text-white">Loading release table...</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap"> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
</tr>
|
||||
<tr
|
||||
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
</tr>
|
||||
<tr
|
||||
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
</tr>
|
||||
<tr
|
||||
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
</tr>
|
||||
<tr
|
||||
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </td>
|
||||
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap "> </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 dark:text-gray-500">
|
||||
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="py-1 pl-2 pr-8 text-sm block w-full border-gray-300 rounded-md shadow-sm cursor-pointer dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100 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="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 className="flex items-center justify-center py-64">
|
||||
<RingResizeSpinner className="text-blue-500 size-24"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <EmptyListState text="No recent activity" />;
|
||||
)
|
||||
}
|
||||
|
||||
// Render the UI for your table
|
||||
|
@ -394,18 +273,21 @@ export const ReleaseTable = () => {
|
|||
)}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-table rounded-md overflow-auto">
|
||||
<table {...getTableProps()} className="min-w-full rounded-md divide-y divide-gray-200 dark:divide-gray-750">
|
||||
<thead className="bg-gray-100 dark:bg-gray-850">
|
||||
{displayData.length === 0
|
||||
? <EmptyReleaseList/>
|
||||
: (
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-table rounded-md overflow-auto">
|
||||
<table {...getTableProps()} className="min-w-full rounded-md divide-y divide-gray-200 dark:divide-gray-750">
|
||||
<thead className="bg-gray-100 dark:bg-gray-850">
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps();
|
||||
const {key: rowKey, ...rowRest} = headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<tr key={rowKey} {...rowRest}>
|
||||
{headerGroup.headers.map((column) => {
|
||||
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps());
|
||||
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
|
||||
// Add the sorting props to control sorting. For this example
|
||||
// we can add them into the header props
|
||||
<th
|
||||
key={`${rowKey}-${columnKey}`}
|
||||
scope="col"
|
||||
|
@ -418,12 +300,12 @@ export const ReleaseTable = () => {
|
|||
<span>
|
||||
{column.isSorted ? (
|
||||
column.isSortedDesc ? (
|
||||
<Icons.SortDownIcon className="w-4 h-4 text-gray-400" />
|
||||
<Icons.SortDownIcon className="w-4 h-4 text-gray-400"/>
|
||||
) : (
|
||||
<Icons.SortUpIcon 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" />
|
||||
<Icons.SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100"/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -433,19 +315,19 @@ export const ReleaseTable = () => {
|
|||
</tr>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
{...getTableBodyProps()}
|
||||
className="divide-y divide-gray-150 dark:divide-gray-750"
|
||||
>
|
||||
</thead>
|
||||
<tbody
|
||||
{...getTableBodyProps()}
|
||||
className="divide-y divide-gray-150 dark:divide-gray-750"
|
||||
>
|
||||
{page.map((row) => {
|
||||
prepareRow(row);
|
||||
|
||||
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
||||
const {key: bodyRowKey, ...bodyRowRest} = row.getRowProps();
|
||||
return (
|
||||
<tr key={bodyRowKey} {...bodyRowRest}>
|
||||
{row.cells.map((cell) => {
|
||||
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
||||
const {key: cellRowKey, ...cellRowRest} = cell.getCellProps();
|
||||
return (
|
||||
<td
|
||||
key={cellRowKey}
|
||||
|
@ -460,88 +342,90 @@ export const ReleaseTable = () => {
|
|||
</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">
|
||||
</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 dark:text-gray-500">
|
||||
Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
|
||||
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="py-1 pl-2 pr-8 text-sm block w-full border-gray-300 rounded-md shadow-sm cursor-pointer transition-colors dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:text-gray-200 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}>
|
||||
{pageSize} entries
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<nav className="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">First</span>
|
||||
<ChevronDoubleLeftIcon className="w-4 h-4" aria-hidden="true" />
|
||||
</DataTable.PageButton>
|
||||
<DataTable.PageButton
|
||||
className="pl-1 pr-2"
|
||||
onClick={() => previousPage()}
|
||||
disabled={!canPreviousPage}
|
||||
>
|
||||
<ChevronLeftIcon className="w-4 h-4 mr-1" aria-hidden="true" />
|
||||
<span>Prev</span>
|
||||
</DataTable.PageButton>
|
||||
<DataTable.PageButton
|
||||
className="pl-2 pr-1"
|
||||
onClick={() => nextPage()}
|
||||
disabled={!canNextPage}>
|
||||
<span>Next</span>
|
||||
<ChevronRightIcon className="w-4 h-4 ml-1" aria-hidden="true" />
|
||||
</DataTable.PageButton>
|
||||
<DataTable.PageButton
|
||||
className="rounded-r-md"
|
||||
onClick={() => gotoPage(pageCount - 1)}
|
||||
disabled={!canNextPage}
|
||||
>
|
||||
<ChevronDoubleRightIcon className="w-4 h-4" aria-hidden="true" />
|
||||
<span className="sr-only">Last</span>
|
||||
</DataTable.PageButton>
|
||||
</nav>
|
||||
<label>
|
||||
<span className="sr-only bg-gray-700">Items Per Page</span>
|
||||
<select
|
||||
className="py-1 pl-2 pr-8 text-sm block w-full border-gray-300 rounded-md shadow-sm cursor-pointer transition-colors dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:text-gray-200 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}>
|
||||
{pageSize} entries
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<nav className="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">First</span>
|
||||
<ChevronDoubleLeftIcon className="w-4 h-4" aria-hidden="true"/>
|
||||
</DataTable.PageButton>
|
||||
<DataTable.PageButton
|
||||
className="pl-1 pr-2"
|
||||
onClick={() => previousPage()}
|
||||
disabled={!canPreviousPage}
|
||||
>
|
||||
<ChevronLeftIcon className="w-4 h-4 mr-1" aria-hidden="true"/>
|
||||
<span>Prev</span>
|
||||
</DataTable.PageButton>
|
||||
<DataTable.PageButton
|
||||
className="pl-2 pr-1"
|
||||
onClick={() => nextPage()}
|
||||
disabled={!canNextPage}>
|
||||
<span>Next</span>
|
||||
<ChevronRightIcon className="w-4 h-4 ml-1" aria-hidden="true"/>
|
||||
</DataTable.PageButton>
|
||||
<DataTable.PageButton
|
||||
className="rounded-r-md"
|
||||
onClick={() => gotoPage(pageCount - 1)}
|
||||
disabled={!canNextPage}
|
||||
>
|
||||
<ChevronDoubleRightIcon className="w-4 h-4" aria-hidden="true"/>
|
||||
<span className="sr-only">Last</span>
|
||||
</DataTable.PageButton>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -bottom-11 right-0 p-2">
|
||||
<button
|
||||
onClick={toggleReleaseNames}
|
||||
className="p-2 absolute bottom-0 right-0 bg-gray-750 text-white rounded-full opacity-10 hover:opacity-100 transition-opacity duration-300"
|
||||
aria-label="Toggle view"
|
||||
title="Go incognito"
|
||||
>
|
||||
{showLinuxIsos ? (
|
||||
<EyeIcon className="h-4 w-4"/>
|
||||
) : (
|
||||
<EyeSlashIcon className="h-4 w-4"/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -bottom-11 right-0 p-2">
|
||||
<button
|
||||
onClick={toggleReleaseNames}
|
||||
className="p-2 absolute bottom-0 right-0 bg-gray-750 text-white rounded-full opacity-10 hover:opacity-100 transition-opacity duration-300"
|
||||
aria-label="Toggle view"
|
||||
title="Go incognito"
|
||||
>
|
||||
{showLinuxIsos ? (
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<EyeSlashIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue