feat(web): migrate react-table to v8 (#1866)

* feat(web): migrate react-table to v8

* chore(web): cleanup

* chore(web): fix types

* chore(web): ignore unused

* chore(web): fix types ActivityTable.tsx

* chore(web): remove console log
This commit is contained in:
ze0s 2024-12-08 16:50:01 +01:00 committed by GitHub
parent 172fa651af
commit ec85d53d8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 440 additions and 540 deletions

View file

@ -41,10 +41,10 @@
"@tanstack/react-query": "^5.61.4", "@tanstack/react-query": "^5.61.4",
"@tanstack/react-query-devtools": "^5.29.2", "@tanstack/react-query-devtools": "^5.29.2",
"@tanstack/react-router": "^1.82.12", "@tanstack/react-router": "^1.82.12",
"@tanstack/react-table": "^8.20.5",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@types/react-table": "^7.7.20",
"@typescript-eslint/eslint-plugin": "^8.16.0", "@typescript-eslint/eslint-plugin": "^8.16.0",
"@typescript-eslint/parser": "^8.16.0", "@typescript-eslint/parser": "^8.16.0",
"@vitejs/plugin-react-swc": "^3.7.2", "@vitejs/plugin-react-swc": "^3.7.2",
@ -63,7 +63,6 @@
"react-popper-tooltip": "^4.4.2", "react-popper-tooltip": "^4.4.2",
"react-ridge-state": "4.2.9", "react-ridge-state": "4.2.9",
"react-select": "^5.8.3", "react-select": "^5.8.3",
"react-table": "^7.8.0",
"react-textarea-autosize": "^8.5.5", "react-textarea-autosize": "^8.5.5",
"stacktracey": "^2.1.8", "stacktracey": "^2.1.8",
"tailwind-lerp-colors": "1.2.6", "tailwind-lerp-colors": "1.2.6",
@ -79,7 +78,6 @@
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@types/react-table": "^7.7.20",
"@typescript-eslint/eslint-plugin": "^8.16.0", "@typescript-eslint/eslint-plugin": "^8.16.0",
"@typescript-eslint/parser": "^8.16.0", "@typescript-eslint/parser": "^8.16.0",
"@vitejs/plugin-react-swc": "^3.7.2", "@vitejs/plugin-react-swc": "^3.7.2",

44
web/pnpm-lock.yaml generated
View file

@ -37,6 +37,9 @@ importers:
'@tanstack/react-router': '@tanstack/react-router':
specifier: ^1.82.12 specifier: ^1.82.12
version: 1.82.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 1.82.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tanstack/react-table':
specifier: ^8.20.5
version: 8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@types/node': '@types/node':
specifier: ^22.10.0 specifier: ^22.10.0
version: 22.10.0 version: 22.10.0
@ -46,9 +49,6 @@ importers:
'@types/react-dom': '@types/react-dom':
specifier: ^18.3.1 specifier: ^18.3.1
version: 18.3.1 version: 18.3.1
'@types/react-table':
specifier: ^7.7.20
version: 7.7.20
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: ^8.16.0 specifier: ^8.16.0
version: 8.16.0(@typescript-eslint/parser@8.16.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2) version: 8.16.0(@typescript-eslint/parser@8.16.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)
@ -103,9 +103,6 @@ importers:
react-select: react-select:
specifier: ^5.8.3 specifier: ^5.8.3
version: 5.8.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 5.8.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-table:
specifier: ^7.8.0
version: 7.8.0(react@18.3.1)
react-textarea-autosize: react-textarea-autosize:
specifier: ^8.5.5 specifier: ^8.5.5
version: 8.5.5(@types/react@18.3.12)(react@18.3.1) version: 8.5.5(@types/react@18.3.12)(react@18.3.1)
@ -1285,6 +1282,13 @@ packages:
react: ^18.3.1 react: ^18.3.1
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/react-table@8.20.5':
resolution: {integrity: sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==}
engines: {node: '>=12'}
peerDependencies:
react: ^18.3.1
react-dom: '>=16.8'
'@tanstack/react-virtual@3.10.9': '@tanstack/react-virtual@3.10.9':
resolution: {integrity: sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g==} resolution: {integrity: sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g==}
peerDependencies: peerDependencies:
@ -1302,6 +1306,10 @@ packages:
'@tanstack/store@0.6.0': '@tanstack/store@0.6.0':
resolution: {integrity: sha512-+m2OBglsjXcLmmKOX6/9v8BDOCtyxhMmZLsRUDswOOSdIIR9mvv6i0XNKsmTh3AlYU8c1mRcodC8/Vyf+69VlQ==} resolution: {integrity: sha512-+m2OBglsjXcLmmKOX6/9v8BDOCtyxhMmZLsRUDswOOSdIIR9mvv6i0XNKsmTh3AlYU8c1mRcodC8/Vyf+69VlQ==}
'@tanstack/table-core@8.20.5':
resolution: {integrity: sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==}
engines: {node: '>=12'}
'@tanstack/virtual-core@3.10.9': '@tanstack/virtual-core@3.10.9':
resolution: {integrity: sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw==} resolution: {integrity: sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw==}
@ -1344,9 +1352,6 @@ packages:
'@types/react-dom@18.3.1': '@types/react-dom@18.3.1':
resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==}
'@types/react-table@7.7.20':
resolution: {integrity: sha512-ahMp4pmjVlnExxNwxyaDrFgmKxSbPwU23sGQw2gJK4EhCvnvmib2s/O/+y1dfV57dXOwpr2plfyBol+vEHbi2w==}
'@types/react-transition-group@4.4.11': '@types/react-transition-group@4.4.11':
resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==} resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==}
@ -2834,11 +2839,6 @@ packages:
react: ^18.3.1 react: ^18.3.1
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
react-table@7.8.0:
resolution: {integrity: sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==}
peerDependencies:
react: ^18.3.1
react-textarea-autosize@8.5.5: react-textarea-autosize@8.5.5:
resolution: {integrity: sha512-CVA94zmfp8m4bSHtWwmANaBR8EPsKy2aZ7KwqhoS4Ftib87F9Kvi7XQhOixypPLMc6kVYgOXvKFuuzZDpHGRPg==} resolution: {integrity: sha512-CVA94zmfp8m4bSHtWwmANaBR8EPsKy2aZ7KwqhoS4Ftib87F9Kvi7XQhOixypPLMc6kVYgOXvKFuuzZDpHGRPg==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -4709,6 +4709,12 @@ snapshots:
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
use-sync-external-store: 1.2.2(react@18.3.1) use-sync-external-store: 1.2.2(react@18.3.1)
'@tanstack/react-table@8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tanstack/table-core': 8.20.5
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@tanstack/react-virtual@3.10.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@tanstack/react-virtual@3.10.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@tanstack/virtual-core': 3.10.9 '@tanstack/virtual-core': 3.10.9
@ -4727,6 +4733,8 @@ snapshots:
'@tanstack/store@0.6.0': {} '@tanstack/store@0.6.0': {}
'@tanstack/table-core@8.20.5': {}
'@tanstack/virtual-core@3.10.9': {} '@tanstack/virtual-core@3.10.9': {}
'@tsconfig/node10@1.0.9': {} '@tsconfig/node10@1.0.9': {}
@ -4764,10 +4772,6 @@ snapshots:
dependencies: dependencies:
'@types/react': 18.3.12 '@types/react': 18.3.12
'@types/react-table@7.7.20':
dependencies:
'@types/react': 18.3.12
'@types/react-transition-group@4.4.11': '@types/react-transition-group@4.4.11':
dependencies: dependencies:
'@types/react': 18.3.12 '@types/react': 18.3.12
@ -6406,10 +6410,6 @@ snapshots:
- '@types/react' - '@types/react'
- supports-color - supports-color
react-table@7.8.0(react@18.3.1):
dependencies:
react: 18.3.1
react-textarea-autosize@8.5.5(@types/react@18.3.12)(react@18.3.1): react-textarea-autosize@8.5.5(@types/react@18.3.12)(react@18.3.1):
dependencies: dependencies:
'@babel/runtime': 7.26.0 '@babel/runtime': 7.26.0

View file

@ -6,6 +6,7 @@
import { baseUrl, sseBaseUrl } from "@utils"; import { baseUrl, sseBaseUrl } from "@utils";
import { GithubRelease } from "@app/types/Update"; import { GithubRelease } from "@app/types/Update";
import { AuthContext } from "@utils/Context"; import { AuthContext } from "@utils/Context";
import { ColumnFilter } from "@tanstack/react-table";
type RequestBody = BodyInit | object | Record<string, unknown> | null; type RequestBody = BodyInit | object | Record<string, unknown> | null;
type Primitive = string | number | boolean | symbol | undefined; type Primitive = string | number | boolean | symbol | undefined;
@ -406,7 +407,7 @@ export const APIClient = {
release: { release: {
find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`), find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`),
findRecent: () => appClient.Get<ReleaseFindResponse>("api/release/recent"), findRecent: () => appClient.Get<ReleaseFindResponse>("api/release/recent"),
findQuery: (offset?: number, limit?: number, filters?: ReleaseFilter[]) => { findQuery: (offset?: number, limit?: number, filters?: ColumnFilter[]) => {
const params: Record<string, string[]> = { const params: Record<string, string[]> = {
indexer: [], indexer: [],
push_status: [], push_status: [],
@ -418,13 +419,25 @@ export const APIClient = {
return; return;
if (filter.id == "indexer.identifier") { if (filter.id == "indexer.identifier") {
params["indexer"].push(filter.value); if (typeof filter.value === "string") {
params["indexer"].push(filter.value);
}
} else if (filter.id == "indexer_identifier") {
if (typeof filter.value === "string") {
params["indexer"].push(filter.value);
}
} else if (filter.id === "action_status") { } else if (filter.id === "action_status") {
params["push_status"].push(filter.value); // push_status is the correct value here otherwise the releases table won't load when filtered by push status if (typeof filter.value === "string") {
params["push_status"].push(filter.value);
} // push_status is the correct value here otherwise the releases table won't load when filtered by push status
} else if (filter.id === "push_status") { } else if (filter.id === "push_status") {
params["push_status"].push(filter.value); if (typeof filter.value === "string") {
params["push_status"].push(filter.value);
}
} else if (filter.id == "name") { } else if (filter.id == "name") {
params["q"].push(filter.value); if (typeof filter.value === "string") {
params["q"].push(filter.value);
}
} }
}); });

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later * SPDX-License-Identifier: GPL-2.0-or-later
*/ */
import { queryOptions } from "@tanstack/react-query"; import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { APIClient } from "@api/APIClient"; import { APIClient } from "@api/APIClient";
import { import {
ApiKeys, ApiKeys,
@ -15,6 +15,7 @@ import {
ReleaseKeys, ReleaseKeys,
SettingsKeys SettingsKeys
} from "@api/query_keys"; } from "@api/query_keys";
import { ColumnFilter } from "@tanstack/react-table";
export const FiltersQueryOptions = (indexers: string[], sortOrder: string) => export const FiltersQueryOptions = (indexers: string[], sortOrder: string) =>
queryOptions({ queryOptions({
@ -104,10 +105,11 @@ export const ApikeysQueryOptions = () =>
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
export const ReleasesListQueryOptions = (offset: number, limit: number, filters: ReleaseFilter[]) => export const ReleasesListQueryOptions = (offset: number, limit: number, filters: ColumnFilter[]) =>
queryOptions({ queryOptions({
queryKey: ReleaseKeys.list(offset, limit, filters), queryKey: ReleaseKeys.list(offset, limit, filters),
queryFn: () => APIClient.release.findQuery(offset, limit, filters), queryFn: () => APIClient.release.findQuery(offset, limit, filters),
placeholderData: keepPreviousData,
staleTime: 5000, staleTime: 5000,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
refetchInterval: 15000 // refetch releases table on releases page every 15s refetchInterval: 15000 // refetch releases table on releases page every 15s

View file

@ -3,6 +3,8 @@
* SPDX-License-Identifier: GPL-2.0-or-later * SPDX-License-Identifier: GPL-2.0-or-later
*/ */
import { ColumnFilter } from "@tanstack/react-table";
export const SettingsKeys = { export const SettingsKeys = {
all: ["settings"] as const, all: ["settings"] as const,
updates: () => [...SettingsKeys.all, "updates"] as const, updates: () => [...SettingsKeys.all, "updates"] as const,
@ -21,7 +23,7 @@ export const FilterKeys = {
export const ReleaseKeys = { export const ReleaseKeys = {
all: ["releases"] as const, all: ["releases"] as const,
lists: () => [...ReleaseKeys.all, "list"] as const, lists: () => [...ReleaseKeys.all, "list"] as const,
list: (pageIndex: number, pageSize: number, filters: ReleaseFilter[]) => [...ReleaseKeys.lists(), { list: (pageIndex: number, pageSize: number, filters: ColumnFilter[]) => [...ReleaseKeys.lists(), {
pageIndex, pageIndex,
pageSize, pageSize,
filters filters

View file

@ -7,7 +7,7 @@ import * as React from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { formatDistanceToNowStrict } from "date-fns"; import { formatDistanceToNowStrict } from "date-fns";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { CellProps } from "react-table"; import { CellContext } from "@tanstack/react-table";
import { ArrowPathIcon, CheckIcon } from "@heroicons/react/24/solid"; import { ArrowPathIcon, CheckIcon } from "@heroicons/react/24/solid";
import { import {
ClockIcon, ClockIcon,
@ -25,7 +25,7 @@ import Toast from "@components/notifications/Toast";
import { RingResizeSpinner } from "@components/Icons"; import { RingResizeSpinner } from "@components/Icons";
import { Tooltip } from "@components/tooltips/Tooltip"; import { Tooltip } from "@components/tooltips/Tooltip";
export const NameCell = (props: CellProps<Release>) => ( export const NameCell = (props: CellContext<Release, unknown>) => (
<div <div
className={classNames( className={classNames(
"flex justify-between items-center py-2 text-sm font-medium box-content text-gray-900 dark:text-gray-300", "flex justify-between items-center py-2 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
@ -34,7 +34,7 @@ export const NameCell = (props: CellProps<Release>) => (
> >
<div className="flex flex-col truncate"> <div className="flex flex-col truncate">
<span className="truncate"> <span className="truncate">
{String(props.cell.value)} {String(props.cell.getValue())}
</span> </span>
<div className="text-xs truncate"> <div className="text-xs truncate">
<span className="text-xs text-gray-500 dark:text-gray-400">Category:</span> {props.row.original.category} <span className="text-xs text-gray-500 dark:text-gray-400">Category:</span> {props.row.original.category}
@ -47,7 +47,7 @@ export const NameCell = (props: CellProps<Release>) => (
</div> </div>
); );
export const LinksCell = (props: CellProps<Release>) => { export const LinksCell = (props: CellContext<Release, unknown>) => {
return ( return (
<div className="flex space-x-2 text-blue-400 dark:text-blue-500"> <div className="flex space-x-2 text-blue-400 dark:text-blue-500">
<div> <div>
@ -104,13 +104,13 @@ export const LinksCell = (props: CellProps<Release>) => {
); );
}; };
export const AgeCell = ({value}: CellProps<Release>) => ( export const AgeCell = ({cell}: CellContext<Release, unknown>) => (
<div className="text-sm text-gray-500" title={simplifyDate(value)}> <div className="text-sm text-gray-500" title={simplifyDate(cell.getValue() as string)}>
{formatDistanceToNowStrict(new Date(value), {addSuffix: false})} {formatDistanceToNowStrict(new Date(cell.getValue() as string), {addSuffix: false})}
</div> </div>
); );
export const IndexerCell = (props: CellProps<Release>) => ( export const IndexerCell = (props: CellContext<Release, unknown>) => (
<div <div
className={classNames( className={classNames(
"py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300", "py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
@ -129,7 +129,7 @@ export const IndexerCell = (props: CellProps<Release>) => (
</div> </div>
); );
export const TitleCell = ({value}: CellProps<Release>) => ( export const TitleCell = ({cell}: CellContext<Release, string>) => (
<div <div
className={classNames( className={classNames(
"py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300", "py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
@ -138,11 +138,11 @@ export const TitleCell = ({value}: CellProps<Release>) => (
> >
<Tooltip <Tooltip
requiresClick requiresClick
label={value} label={cell.getValue()}
maxWidth="max-w-[90vw]" maxWidth="max-w-[90vw]"
> >
<span className="whitespace-pre-wrap break-words"> <span className="whitespace-pre-wrap break-words">
{value} {cell.getValue()}
</span> </span>
</Tooltip> </Tooltip>
</div> </div>
@ -188,10 +188,6 @@ const RetryActionButton = ({ status }: RetryActionButtonProps) => {
); );
}; };
interface ReleaseStatusCellProps {
value: ReleaseActionStatus[];
}
interface StatusCellMapEntry { interface StatusCellMapEntry {
colors: string; colors: string;
icon: React.ReactElement; icon: React.ReactElement;
@ -295,9 +291,9 @@ const CellLine = ({ title, children }: { title: string; children?: string; }) =>
); );
}; };
export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => ( export const ReleaseStatusCell = ({ row }: CellContext<Release, unknown>) => (
<div className="flex text-sm font-medium text-gray-900 dark:text-gray-300"> <div className="flex text-sm font-medium text-gray-900 dark:text-gray-300">
{value.map((v, idx) => ( {row.original.action_status.map((v, idx) => (
<div <div
key={idx} key={idx}
className={classNames( className={classNames(

View file

@ -6,84 +6,35 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useSuspenseQuery } from "@tanstack/react-query"; import { useSuspenseQuery } from "@tanstack/react-query";
import { import {
useTable, useReactTable,
useFilters, getCoreRowModel,
useGlobalFilter, flexRender,
useSortBy, ColumnDef
usePagination, FilterProps, Column } from "@tanstack/react-table";
} from "react-table";
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid"; import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
import { EmptyListState } from "@components/emptystates"; import { EmptyListState } from "@components/emptystates";
import * as Icons from "@components/Icons";
import * as DataTable from "@components/data-table"; import * as DataTable from "@components/data-table";
import { RandomLinuxIsos } from "@utils"; import { RandomLinuxIsos } from "@utils";
import { ReleasesLatestQueryOptions } from "@api/queries"; import { ReleasesLatestQueryOptions } from "@api/queries";
import { IndexerCell } from "@components/data-table"; import { IndexerCell } from "@components/data-table";
// This is a custom filter UI for selecting
// a unique option from a list
function SelectColumnFilter({
column: { filterValue, setFilter, preFilteredRows, id, render }
}: FilterProps<object>) {
// Calculate the options for filtering
// using the preFilteredRows
const options = React.useMemo(() => {
const options = new Set<string>();
preFilteredRows.forEach((row: { values: { [x: string]: string } }) => {
options.add(row.values[id]);
});
return [...options.values()];
}, [id, preFilteredRows]);
// Render a multi-select box
return (
<label className="flex items-baseline gap-x-2">
<span className="text-gray-700"><>{render("Header")}:</></span>
<select
className="border-gray-300 rounded-md shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
name={id}
id={id}
value={filterValue}
onChange={e => {
setFilter(e.target.value || undefined);
}}
>
<option value="">All</option>
{options.map((option, i) => (
<option key={i} value={option}>
{option}
</option>
))}
</select>
</label>
);
}
interface TableProps { interface TableProps {
columns: Column[]; columns: ColumnDef<Release>[];
data: Release[]; data: Release[];
} }
function Table({ columns, data }: TableProps) { function Table({ columns, data }: TableProps) {
// Use the state and functions returned from useTable to build your UI const tableInstance = useReactTable({
const { columns,
getTableProps, data,
getTableBodyProps, getCoreRowModel: getCoreRowModel(),
headerGroups, })
prepareRow,
page // Instead of using 'rows', we'll use page,
} = useTable(
{ columns, data },
useFilters,
useGlobalFilter,
useSortBy,
usePagination
);
if (data.length === 0) { if (data.length === 0) {
return ( return (
<div className="mt-4 mb-2 bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-table rounded-md overflow-auto"> <div
className="mt-4 mb-2 bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-table rounded-md overflow-auto">
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<EmptyListState text="No recent activity"/> <EmptyListState text="No recent activity"/>
</div> </div>
@ -91,74 +42,49 @@ function Table({ columns, data }: TableProps) {
) )
} }
// Render the UI for your table
return ( return (
<div className="inline-block min-w-full mt-4 mb-2 align-middle"> <div className="inline-block min-w-full mt-4 mb-2 align-middle">
<div className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-table rounded-md overflow-auto"> <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"> <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"> <thead className="bg-gray-100 dark:bg-gray-850">
{headerGroups.map((headerGroup) => { {tableInstance.getHeaderGroups().map((headerGroup) => (
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps(); <tr key={headerGroup.id}>
return ( {headerGroup.headers.map((header) => (
<tr key={rowKey} {...rowRest}> <th
{headerGroup.headers.map((column) => { key={header.id}
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps()); scope="col"
return ( className="first:pl-5 first:rounded-tl-md last:rounded-tr-md pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left uppercase group text-gray-600 dark:text-gray-400 transition hover:bg-gray-200 dark:hover:bg-gray-775"
// Add the sorting props to control sorting. For this example colSpan={header.colSpan}
// we can add them into the header props >
<th <div className="flex items-center justify-between">
key={`${rowKey}-${columnKey}`} {header.isPlaceholder
scope="col" ? null
className="first:pl-5 first:rounded-tl-md last:rounded-tr-md pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left uppercase group text-gray-600 dark:text-gray-400 transition hover:bg-gray-200 dark:hover:bg-gray-775" : flexRender(
{...columnRest} header.column.columnDef.header,
> header.getContext()
<div className="flex items-center justify-between"> )}
<>{column.render("Header")}</> </div>
{/* Add a sort direction indicator */} </th>
<span> )
{column.isSorted ? ( )}
column.isSortedDesc ? ( </tr>
<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> </thead>
<tbody
{...getTableBodyProps()} <tbody className="divide-y divide-gray-150 dark:divide-gray-750">
className="divide-y divide-gray-150 dark:divide-gray-750" {tableInstance.getRowModel().rows.map((row) => (
> <tr key={row.id}>
{page.map((row) => { {row.getVisibleCells().map((cell) => (
prepareRow(row); <td
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps(); key={cell.id}
return ( className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
<tr key={bodyRowKey} {...bodyRowRest}> role="cell"
{row.cells.map((cell) => { >
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps(); {flexRender(cell.column.columnDef.cell, cell.getContext())}
return ( </td>
<td ))}
key={cellRowKey} </tr>
className="first:pl-5 pl-3 pr-3 whitespace-nowrap" ))}
role="cell"
{...cellRowRest}
>
<>{cell.render("Cell")}</>
</td>
);
})}
</tr>
);
})}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -167,30 +93,28 @@ function Table({ columns, data }: TableProps) {
} }
export const ActivityTable = () => { export const ActivityTable = () => {
const columns = React.useMemo(() => [ const columns = React.useMemo<ColumnDef<Release, unknown>[]>(() => [
{ {
Header: "Age", header: "Age",
accessor: "timestamp", accessorKey: "timestamp",
Cell: DataTable.AgeCell cell: DataTable.AgeCell
}, },
{ {
Header: "Release", header: "Release",
accessor: "name", accessorKey: "name",
Cell: DataTable.TitleCell cell: DataTable.TitleCell,
}, },
{ {
Header: "Actions", header: "Actions",
accessor: "action_status", accessorKey: "action_status",
Cell: DataTable.ReleaseStatusCell cell: DataTable.ReleaseStatusCell
}, },
{ {
Header: "Indexer", header: "Indexer",
accessor: "indexer.identifier", accessorKey: "indexer.identifier",
Cell: IndexerCell, cell: IndexerCell,
Filter: SelectColumnFilter,
filter: "includes"
} }
] as Column[], []); ], []);
const { isLoading, data } = useSuspenseQuery(ReleasesLatestQueryOptions()); const { isLoading, data } = useSuspenseQuery(ReleasesLatestQueryOptions());
@ -236,7 +160,7 @@ export const ActivityTable = () => {
Recent activity Recent activity
</h3> </h3>
<Table columns={columns} data={displayData} /> <Table columns={columns} data={displayData}/>
<button <button
onClick={toggleReleaseNames} onClick={toggleReleaseNames}
@ -245,9 +169,9 @@ export const ActivityTable = () => {
title="Go incognito" title="Go incognito"
> >
{showLinuxIsos ? ( {showLinuxIsos ? (
<EyeIcon className="h-4 w-4" /> <EyeIcon className="h-4 w-4"/>
) : ( ) : (
<EyeSlashIcon className="h-4 w-4" /> <EyeSlashIcon className="h-4 w-4"/>
)} )}
</button> </button>
</div> </div>

View file

@ -5,13 +5,13 @@
import * as React from "react"; import * as React from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Column } from "@tanstack/react-table";
import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from "@headlessui/react"; import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from "@headlessui/react";
import { DebounceInput } from "react-debounce-input";
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/solid"; import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/solid";
import { classNames } from "@utils"; import { classNames } from "@utils";
import { PushStatusOptions } from "@domain/constants"; import { PushStatusOptions } from "@domain/constants";
import { FilterProps } from "react-table";
import { DebounceInput } from "react-debounce-input";
import { ReleasesIndexersQueryOptions } from "@api/queries"; import { ReleasesIndexersQueryOptions } from "@api/queries";
interface ListboxFilterProps { interface ListboxFilterProps {
@ -63,23 +63,19 @@ const ListboxFilter = ({
</div> </div>
); );
// a unique option from a list export const IndexerSelectColumnFilter = ({ column }: { column: Column<Release, unknown> }) => {
export const IndexerSelectColumnFilter = ({
column: { filterValue, setFilter, id }
}: FilterProps<object>) => {
const { data, isSuccess } = useQuery(ReleasesIndexersQueryOptions()); const { data, isSuccess } = useQuery(ReleasesIndexersQueryOptions());
// Assign indexer name based on the filterValue (indexer.identifier) // Assign indexer name based on the filterValue (indexer.identifier)
const currentIndexerName = data?.find(indexer => indexer.identifier === filterValue)?.name ?? "Indexer"; const currentIndexerName = data?.find(indexer => indexer.identifier === column.getFilterValue())?.name ?? "Indexer";
// Render a multi-select box
return ( return (
<ListboxFilter <ListboxFilter
id={id} id={column.id}
key={id} key={column.id}
label={currentIndexerName} label={currentIndexerName}
currentValue={filterValue ?? ""} currentValue={column.getFilterValue() as string || ""}
onChange={setFilter} onChange={newValue => column.setFilterValue(newValue || undefined)}
> >
{isSuccess && data && data?.map((indexer, idx) => ( {isSuccess && data && data?.map((indexer, idx) => (
<FilterOption key={idx} label={indexer.name} value={indexer.identifier} /> <FilterOption key={idx} label={indexer.name} value={indexer.identifier} />
@ -95,9 +91,9 @@ interface FilterOptionProps {
const FilterOption = ({ label, value }: FilterOptionProps) => ( const FilterOption = ({ label, value }: FilterOptionProps) => (
<ListboxOption <ListboxOption
className={({ active }) => classNames( className={({ focus }) => classNames(
"cursor-pointer select-none relative py-2 pl-10 pr-4", "cursor-pointer select-none relative py-2 pl-10 pr-4",
active ? "text-black dark:text-gray-200 bg-gray-100 dark:bg-gray-900" : "text-gray-700 dark:text-gray-400" focus ? "text-black dark:text-gray-200 bg-gray-100 dark:bg-gray-900" : "text-gray-700 dark:text-gray-400"
)} )}
value={value} value={value}
> >
@ -121,23 +117,24 @@ const FilterOption = ({ label, value }: FilterOptionProps) => (
</ListboxOption> </ListboxOption>
); );
export const PushStatusSelectColumnFilter = ({ export const PushStatusSelectColumnFilter = ({ column }: { column: Column<Release, unknown> }) => {
column: { filterValue, setFilter, id }, // React.useEffect(() => {
initialFilterValue // if (initialFilterValue) {
}: FilterProps<object>) => { // setFilter(initialFilterValue);
React.useEffect(() => { // }
if (initialFilterValue) { // }, [initialFilterValue, setFilter]);
setFilter(initialFilterValue);
} const label = column.getFilterValue() ? PushStatusOptions.find((o) => o.value === column.getFilterValue() && o.value)?.label : "Push status";
}, [initialFilterValue, setFilter]);
const label = filterValue ? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label : "Push status";
return ( return (
<div className="mr-3" key={id}> <div className="mr-3" key={column.id}>
<ListboxFilter <ListboxFilter
id={id} id={column.id}
label={label ?? "Push status"} label={label ?? "Push status"}
currentValue={filterValue ?? ""} currentValue={column.getFilterValue() as string ?? ""}
onChange={setFilter} onChange={value => {
column.setFilterValue(value || undefined);
}}
> >
{PushStatusOptions.map((status, idx) => ( {PushStatusOptions.map((status, idx) => (
<FilterOption key={idx} value={status.value} label={status.label} /> <FilterOption key={idx} value={status.value} label={status.label} />
@ -147,17 +144,16 @@ export const PushStatusSelectColumnFilter = ({
); );
}; };
export const SearchColumnFilter = ({ export const SearchColumnFilter = ({ column }: { column: Column<Release, unknown> }) => {
column: { filterValue, setFilter, id }
}: FilterProps<object>) => {
return ( return (
<div className="flex-1 mr-3 mt-1" key={id}> <div className="flex-1 mr-3 mt-1" key={column.id}>
<DebounceInput <DebounceInput
minLength={2} minLength={2}
value={filterValue || undefined} value={column.getFilterValue() as string || undefined}
debounceTimeout={500} debounceTimeout={500}
onChange={e => { onChange={e => {
setFilter(e.target.value || undefined); // Set undefined to remove the filter entirely // Set undefined to remove the filter entirely
column.setFilterValue(e.target.value || undefined)
}} }}
id="filter" id="filter"
type="text" type="text"

View file

@ -6,7 +6,15 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Column, useFilters, usePagination, useSortBy, useTable } from "react-table"; import {
useReactTable,
getCoreRowModel,
flexRender,
ColumnDef,
Column,
RowData,
PaginationState,
} from "@tanstack/react-table";
import { import {
ChevronDoubleLeftIcon, ChevronDoubleLeftIcon,
ChevronDoubleRightIcon, ChevronDoubleRightIcon,
@ -16,64 +24,24 @@ import {
EyeSlashIcon EyeSlashIcon
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import { ReleasesRoute } from "@app/routes";
import { ReleasesListQueryOptions } from "@api/queries"; import { ReleasesListQueryOptions } from "@api/queries";
import { RandomLinuxIsos } from "@utils"; import { RandomLinuxIsos } from "@utils";
import { RingResizeSpinner, SortDownIcon, SortIcon, SortUpIcon } from "@components/Icons"; import { RingResizeSpinner } from "@components/Icons";
import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./ReleaseFilters"; import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./ReleaseFilters";
import { EmptyListState } from "@components/emptystates"; import { EmptyListState } from "@components/emptystates";
import { TableButton, TablePageButton } from "@components/data-table/Buttons.tsx"; import { TableButton, TablePageButton, AgeCell, IndexerCell, LinksCell, NameCell, ReleaseStatusCell } from "@components/data-table";
import { AgeCell, IndexerCell, LinksCell, NameCell, ReleaseStatusCell } from "@components/data-table";
type TableState = { declare module '@tanstack/react-table' {
queryPageIndex: number; //allows us to define custom properties for our columns
queryPageSize: number; // @eslint-ignore
totalCount: number; interface ColumnMeta<TData extends RowData, TValue> {
queryFilters: ReleaseFilter[]; filterVariant?: 'text' | 'range' | 'select' | 'search' | 'actionPushStatus' | 'indexerSelect';
}; }
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}`);
}
}
};
const EmptyReleaseList = () => ( const EmptyReleaseList = () => (
<div className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-table rounded-md overflow-auto"> <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"> <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"> <thead className="bg-gray-100 dark:bg-gray-850 border-b border-gray-200 dark:border-gray-750">
<tr> <tr>
@ -91,50 +59,87 @@ const EmptyReleaseList = () => (
</div> </div>
); );
export const ReleaseTable = () => { function Filter({ column }: { column: Column<Release, unknown> }) {
const search = ReleasesRoute.useSearch() const { filterVariant } = column.columnDef.meta ?? {}
const columns = React.useMemo(() => [ switch (filterVariant) {
{ case "search":
Header: "Age", return <SearchColumnFilter column={column}/>
accessor: "timestamp",
Cell: AgeCell
},
{
Header: "Release",
accessor: "name",
Cell: NameCell,
Filter: SearchColumnFilter
},
{
Header: "Links",
accessor: (row) => ({ download_url: row.download_url, info_url: row.info_url }),
id: "links",
Cell: LinksCell
},
{
Header: "Actions",
accessor: "action_status",
Cell: ReleaseStatusCell,
Filter: PushStatusSelectColumnFilter
},
{
Header: "Indexer",
accessor: "indexer.identifier",
Cell: IndexerCell,
Filter: IndexerSelectColumnFilter,
filter: "equal"
}
] as Column<Release>[], []);
if (search.action_status != "") { case "indexerSelect":
initialState.queryFilters = [{id: "action_status", value: search.action_status! }] return <IndexerSelectColumnFilter column={column}/>
case "actionPushStatus":
return <PushStatusSelectColumnFilter column={column}/>
default:
return null;
} }
}
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] = interface ColumnFilter {
React.useReducer(TableReducer, initialState); id: string
value: unknown
}
const { isLoading, error, data, isSuccess } = useQuery(ReleasesListQueryOptions(queryPageIndex * queryPageSize, queryPageSize, queryFilters)); type ColumnFiltersState = ColumnFilter[];
export const ReleaseTable = () => {
// const search = ReleasesRoute.useSearch()
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const columns = React.useMemo<ColumnDef<Release, unknown>[]>(() => [
{
header: "Age",
accessorKey: "timestamp",
cell: AgeCell
},
{
header: "Release",
accessorKey: "name",
cell: NameCell,
meta: {
filterVariant: 'search',
},
},
{
header: "Links",
accessorFn: (row) => ({ download_url: row.download_url, info_url: row.info_url }),
id: "links",
cell: LinksCell
},
{
header: "Actions",
accessorKey: "action_status",
cell: ReleaseStatusCell,
meta: {
filterVariant: 'actionPushStatus',
},
},
{
header: "Indexer",
accessorKey: "indexer.identifier",
cell: IndexerCell,
meta: {
filterVariant: 'indexerSelect',
},
}
], []);
// if (search.action_status != "") {
// setColumnFilters(prevState => [...prevState, { id: "action_status", value: search.action_status }]);
// }
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const {
isLoading,
error,
data,
} = useQuery(ReleasesListQueryOptions(pagination.pageIndex * pagination.pageSize, pagination.pageSize, columnFilters));
const [modifiedData, setModifiedData] = useState<Release[]>([]); const [modifiedData, setModifiedData] = useState<Release[]>([]);
const [showLinuxIsos, setShowLinuxIsos] = useState(false); const [showLinuxIsos, setShowLinuxIsos] = useState(false);
@ -163,76 +168,44 @@ export const ReleaseTable = () => {
} }
}; };
const displayData = showLinuxIsos ? modifiedData : (data?.data ?? []); const defaultData = React.useMemo(() => [], [])
const displayData = showLinuxIsos ? modifiedData : (data?.data ?? defaultData);
const { const tableInstance = useReactTable({
getTableProps, columns,
getTableBodyProps, data: displayData,
headerGroups, getCoreRowModel: getCoreRowModel(),
prepareRow, manualFiltering: true,
page, // Instead of using 'rows', we'll use page, manualPagination: true,
// which has only the rows for the active page manualSorting: true,
rowCount: data?.count,
// The rest of these things are super handy, too ;) state: {
canPreviousPage, columnFilters,
canNextPage, pagination,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize, filters }
} = useTable(
{
columns,
data: displayData, // Use displayData here
initialState: {
pageIndex: queryPageIndex,
pageSize: queryPageSize,
filters: queryFilters,
},
manualPagination: true,
manualFilters: true,
manualSortBy: true,
pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : 0,
autoResetSortBy: false,
autoResetExpanded: false,
autoResetPage: false
}, },
useFilters, initialState: {
useSortBy, pagination
usePagination },
); onPaginationChange: setPagination,
onColumnFiltersChange: setColumnFilters,
});
React.useEffect(() => { // Manage your own state
dispatch({ type: ActionType.PAGE_CHANGED, payload: pageIndex }); // const [state, setState] = React.useState(tableInstance.initialState)
}, [pageIndex]);
React.useEffect(() => { // Override the state managers for the table to your own
dispatch({ type: ActionType.PAGE_SIZE_CHANGED, payload: pageSize }); // tableInstance.setOptions(prev => ({
gotoPage(0); // ...prev,
}, [pageSize, gotoPage]); // state,
// onStateChange: setState,
React.useEffect(() => { // // These are just table options, so if things
if (data?.count) { // // need to change based on your state, you can
dispatch({ // // derive them here
type: ActionType.TOTAL_COUNT_CHANGED, //
payload: data.count // // Just for fun, let's debug everything if the pageIndex
}); // // is greater than 2
} // // debugTable: state.pagination.pageIndex > 2,
}, [data?.count]); // }))
React.useEffect(() => {
dispatch({ type: ActionType.FILTER_CHANGED, payload: filters });
gotoPage(0);
}, [filters]);
React.useEffect(() => {
if (search.action_status != null) {
dispatch({ type: ActionType.FILTER_CHANGED, payload: [{ id: "action_status", value: search.action_status! }] });
}
}, [search.action_status]);
if (error) { if (error) {
return <p>Error</p>; return <p>Error</p>;
@ -242,17 +215,19 @@ export const ReleaseTable = () => {
return ( return (
<div> <div>
<div className="flex mb-6 flex-col sm:flex-row"> <div className="flex mb-6 flex-col sm:flex-row">
{ headerGroups.map((headerGroup) => headerGroup.headers.map((column) => ( {tableInstance.getHeaderGroups().map((headerGroup) =>
column.Filter ? ( headerGroup.headers.map((header) => (
<React.Fragment key={ column.id }>{ column.render("Filter") }</React.Fragment> header.column.getCanFilter() ? (
<Filter key={header.column.id} column={header.column}/>
) : null ) : null
)) ))
) } )}
</div> </div>
<div className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-lg rounded-md mt-4"> <div
<div className="bg-gray-100 dark:bg-gray-850 border-b border-gray-200 dark:border-gray-750"> className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-lg rounded-md mt-4">
<div className="flex h-10"/> <div className="bg-gray-100 dark:bg-gray-850 border-b border-gray-200 dark:border-gray-750">
</div> <div className="flex h-10"/>
</div>
<div className="flex items-center justify-center py-64"> <div className="flex items-center justify-center py-64">
<RingResizeSpinner className="text-blue-500 size-24"/> <RingResizeSpinner className="text-blue-500 size-24"/>
</div> </div>
@ -261,14 +236,13 @@ export const ReleaseTable = () => {
) )
} }
// Render the UI for your table
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex mb-6 flex-col sm:flex-row"> <div className="flex mb-6 flex-col sm:flex-row">
{headerGroups.map((headerGroup) => {tableInstance.getHeaderGroups().map((headerGroup) =>
headerGroup.headers.map((column) => ( headerGroup.headers.map((header) => (
column.Filter ? ( header.column.getCanFilter() ? (
<React.Fragment key={column.id}>{column.render("Filter")}</React.Fragment> <Filter key={header.column.id} column={header.column}/>
) : null ) : null
)) ))
)} )}
@ -277,156 +251,151 @@ export const ReleaseTable = () => {
{displayData.length === 0 {displayData.length === 0
? <EmptyReleaseList/> ? <EmptyReleaseList/>
: ( : (
<div className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-table rounded-md overflow-auto"> <div
<table {...getTableProps()} className="min-w-full rounded-md divide-y divide-gray-200 dark:divide-gray-750"> className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-table rounded-md overflow-auto">
<thead className="bg-gray-100 dark:bg-gray-850"> <table className="min-w-full rounded-md divide-y divide-gray-200 dark:divide-gray-750">
{headerGroups.map((headerGroup) => { <thead className="bg-gray-100 dark:bg-gray-850">
const {key: rowKey, ...rowRest} = headerGroup.getHeaderGroupProps(); {tableInstance.getHeaderGroups().map((headerGroup) => (
return ( <tr key={headerGroup.id}>
<tr key={rowKey} {...rowRest}> {headerGroup.headers.map((header) => (
{headerGroup.headers.map((column) => { <th
const {key: columnKey, ...columnRest} = column.getHeaderProps(column.getSortByToggleProps()); key={header.id}
return ( scope="col"
// Add the sorting props to control sorting. For this example colSpan={header.colSpan}
// we can add them into the header props className="first:pl-5 first:rounded-tl-md last:rounded-tr-md pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left uppercase group text-gray-600 dark:text-gray-400 transition hover:bg-gray-200 dark:hover:bg-gray-775"
<th >
key={`${rowKey}-${columnKey}`} <div className="flex items-center justify-between">
scope="col" {header.isPlaceholder
className="first:pl-5 first:rounded-tl-md last:rounded-tr-md pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left uppercase group text-gray-600 dark:text-gray-400 transition hover:bg-gray-200 dark:hover:bg-gray-775" ? null
{...columnRest} : flexRender(
> header.column.columnDef.header,
<div className="flex items-center justify-between"> header.getContext()
<>{column.render("Header")}</> )}
{/* Add a sort direction indicator */} {/*<>{header.render("Header")}</>*/}
<span> {/*/!* Add a sort direction indicator *!/*/}
{column.isSorted ? ( {/*<span>*/}
column.isSortedDesc ? ( {/* {header.isSorted ? (*/}
<SortDownIcon className="w-4 h-4 text-gray-400"/> {/* header.isSortedDesc ? (*/}
) : ( {/* <SortDownIcon className="w-4 h-4 text-gray-400"/>*/}
<SortUpIcon className="w-4 h-4 text-gray-400"/> {/* ) : (*/}
) {/* <SortUpIcon className="w-4 h-4 text-gray-400"/>*/}
) : ( {/* )*/}
<SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100"/> {/* ) : (*/}
)} {/* <SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100"/>*/}
</span> {/* )}*/}
</div> {/*</span>*/}
</th> </div>
); </th>
})} ))}
</tr> </tr>
); ))}
})} </thead>
</thead>
<tbody
{...getTableBodyProps()}
className="divide-y divide-gray-150 dark:divide-gray-750"
>
{page.map((row) => {
prepareRow(row);
const {key: bodyRowKey, ...bodyRowRest} = row.getRowProps(); <tbody className="divide-y divide-gray-150 dark:divide-gray-750">
return ( {tableInstance.getRowModel().rows.map((row) => (
<tr key={bodyRowKey} {...bodyRowRest}> <tr key={row.id}>
{row.cells.map((cell) => { {row.getVisibleCells().map((cell) => (
const {key: cellRowKey, ...cellRowRest} = cell.getCellProps(); <td
return ( key={cell.id}
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
key={cellRowKey} role="cell"
className="first:pl-5 pl-3 pr-3 whitespace-nowrap" >
role="cell" <>
{...cellRowRest} {flexRender(
> cell.column.columnDef.cell,
<>{cell.render("Cell")}</> cell.getContext()
</td> )}
); </>
})} </td>
))}
</tr> </tr>
); ))}
})} </tbody>
</tbody> </table>
</table>
{/* Pagination */} {/* Pagination */}
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700"> <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"> <div className="flex justify-between flex-1 sm:hidden">
<TableButton onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</TableButton> <TableButton onClick={() => tableInstance.previousPage()} disabled={!tableInstance.getCanPreviousPage()}>Previous</TableButton>
<TableButton onClick={() => nextPage()} disabled={!canNextPage}>Next</TableButton> <TableButton onClick={() => tableInstance.nextPage()} disabled={!tableInstance.getCanNextPage()}>Next</TableButton>
</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 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>
<div> <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<nav className="inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination"> <div className="flex items-baseline gap-x-2">
<TablePageButton <span className="text-sm text-gray-700 dark:text-gray-500">
className="rounded-l-md" Page <span className="font-medium">{tableInstance.getState().pagination.pageIndex + 1}</span> of <span
onClick={() => gotoPage(0)} className="font-medium">{tableInstance.getPageCount()}</span>
disabled={!canPreviousPage} </span>
> <label>
<span className="sr-only">First</span> <span className="sr-only bg-gray-700">Items Per Page</span>
<ChevronDoubleLeftIcon className="w-4 h-4" aria-hidden="true"/> <select
</TablePageButton> 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"
<TablePageButton value={tableInstance.getState().pagination.pageSize}
className="pl-1 pr-2" onChange={e => {
onClick={() => previousPage()} tableInstance.setPageSize(Number(e.target.value));
disabled={!canPreviousPage} }}
> >
<ChevronLeftIcon className="w-4 h-4 mr-1" aria-hidden="true"/> {[5, 10, 20, 50].map(pageSize => (
<span>Prev</span> <option key={pageSize} value={pageSize}>
</TablePageButton> {pageSize} entries
<TablePageButton </option>
className="pl-2 pr-1" ))}
onClick={() => nextPage()} </select>
disabled={!canNextPage}> </label>
<span>Next</span> </div>
<ChevronRightIcon className="w-4 h-4 ml-1" aria-hidden="true"/> <div>
</TablePageButton> <nav className="inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
<TablePageButton <TablePageButton
className="rounded-r-md" className="rounded-l-md"
onClick={() => gotoPage(pageCount - 1)} onClick={() => tableInstance.firstPage()}
disabled={!canNextPage} disabled={!tableInstance.getCanPreviousPage()}
> >
<ChevronDoubleRightIcon className="w-4 h-4" aria-hidden="true"/> <span className="sr-only">First</span>
<span className="sr-only">Last</span> <ChevronDoubleLeftIcon className="w-4 h-4" aria-hidden="true"/>
</TablePageButton> </TablePageButton>
</nav> <TablePageButton
className="pl-1 pr-2"
onClick={() => tableInstance.previousPage()}
disabled={!tableInstance.getCanPreviousPage()}
>
<ChevronLeftIcon className="w-4 h-4 mr-1" aria-hidden="true"/>
<span>Prev</span>
</TablePageButton>
<TablePageButton
className="pl-2 pr-1"
onClick={() => tableInstance.nextPage()}
disabled={!tableInstance.getCanNextPage()}>
<span>Next</span>
<ChevronRightIcon className="w-4 h-4 ml-1" aria-hidden="true"/>
</TablePageButton>
<TablePageButton
className="rounded-r-md"
onClick={() => tableInstance.lastPage()}
disabled={!tableInstance.getCanNextPage()}
>
<ChevronDoubleRightIcon className="w-4 h-4" aria-hidden="true"/>
<span className="sr-only">Last</span>
</TablePageButton>
</nav>
</div>
</div> </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>
<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>
</div> </div>
); );