mirror of
https://github.com/idanoo/autobrr
synced 2025-07-22 16:29:12 +00:00
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:
parent
172fa651af
commit
ec85d53d8f
9 changed files with 440 additions and 540 deletions
|
@ -41,10 +41,10 @@
|
|||
"@tanstack/react-query": "^5.61.4",
|
||||
"@tanstack/react-query-devtools": "^5.29.2",
|
||||
"@tanstack/react-router": "^1.82.12",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@typescript-eslint/eslint-plugin": "^8.16.0",
|
||||
"@typescript-eslint/parser": "^8.16.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||
|
@ -63,7 +63,6 @@
|
|||
"react-popper-tooltip": "^4.4.2",
|
||||
"react-ridge-state": "4.2.9",
|
||||
"react-select": "^5.8.3",
|
||||
"react-table": "^7.8.0",
|
||||
"react-textarea-autosize": "^8.5.5",
|
||||
"stacktracey": "^2.1.8",
|
||||
"tailwind-lerp-colors": "1.2.6",
|
||||
|
@ -79,7 +78,6 @@
|
|||
"@types/node": "^22.10.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@typescript-eslint/eslint-plugin": "^8.16.0",
|
||||
"@typescript-eslint/parser": "^8.16.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||
|
|
44
web/pnpm-lock.yaml
generated
44
web/pnpm-lock.yaml
generated
|
@ -37,6 +37,9 @@ importers:
|
|||
'@tanstack/react-router':
|
||||
specifier: ^1.82.12
|
||||
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':
|
||||
specifier: ^22.10.0
|
||||
version: 22.10.0
|
||||
|
@ -46,9 +49,6 @@ importers:
|
|||
'@types/react-dom':
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
'@types/react-table':
|
||||
specifier: ^7.7.20
|
||||
version: 7.7.20
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
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)
|
||||
|
@ -103,9 +103,6 @@ importers:
|
|||
react-select:
|
||||
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)
|
||||
react-table:
|
||||
specifier: ^7.8.0
|
||||
version: 7.8.0(react@18.3.1)
|
||||
react-textarea-autosize:
|
||||
specifier: ^8.5.5
|
||||
version: 8.5.5(@types/react@18.3.12)(react@18.3.1)
|
||||
|
@ -1285,6 +1282,13 @@ packages:
|
|||
react: ^18.3.1
|
||||
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':
|
||||
resolution: {integrity: sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g==}
|
||||
peerDependencies:
|
||||
|
@ -1302,6 +1306,10 @@ packages:
|
|||
'@tanstack/store@0.6.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw==}
|
||||
|
||||
|
@ -1344,9 +1352,6 @@ packages:
|
|||
'@types/react-dom@18.3.1':
|
||||
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':
|
||||
resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==}
|
||||
|
||||
|
@ -2834,11 +2839,6 @@ packages:
|
|||
react: ^18.3.1
|
||||
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:
|
||||
resolution: {integrity: sha512-CVA94zmfp8m4bSHtWwmANaBR8EPsKy2aZ7KwqhoS4Ftib87F9Kvi7XQhOixypPLMc6kVYgOXvKFuuzZDpHGRPg==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -4709,6 +4709,12 @@ snapshots:
|
|||
react-dom: 18.3.1(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)':
|
||||
dependencies:
|
||||
'@tanstack/virtual-core': 3.10.9
|
||||
|
@ -4727,6 +4733,8 @@ snapshots:
|
|||
|
||||
'@tanstack/store@0.6.0': {}
|
||||
|
||||
'@tanstack/table-core@8.20.5': {}
|
||||
|
||||
'@tanstack/virtual-core@3.10.9': {}
|
||||
|
||||
'@tsconfig/node10@1.0.9': {}
|
||||
|
@ -4764,10 +4772,6 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/react': 18.3.12
|
||||
|
||||
'@types/react-table@7.7.20':
|
||||
dependencies:
|
||||
'@types/react': 18.3.12
|
||||
|
||||
'@types/react-transition-group@4.4.11':
|
||||
dependencies:
|
||||
'@types/react': 18.3.12
|
||||
|
@ -6406,10 +6410,6 @@ snapshots:
|
|||
- '@types/react'
|
||||
- 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):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import { baseUrl, sseBaseUrl } from "@utils";
|
||||
import { GithubRelease } from "@app/types/Update";
|
||||
import { AuthContext } from "@utils/Context";
|
||||
import { ColumnFilter } from "@tanstack/react-table";
|
||||
|
||||
type RequestBody = BodyInit | object | Record<string, unknown> | null;
|
||||
type Primitive = string | number | boolean | symbol | undefined;
|
||||
|
@ -406,7 +407,7 @@ export const APIClient = {
|
|||
release: {
|
||||
find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`),
|
||||
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[]> = {
|
||||
indexer: [],
|
||||
push_status: [],
|
||||
|
@ -418,13 +419,25 @@ export const APIClient = {
|
|||
return;
|
||||
|
||||
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") {
|
||||
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") {
|
||||
params["push_status"].push(filter.value);
|
||||
if (typeof filter.value === "string") {
|
||||
params["push_status"].push(filter.value);
|
||||
}
|
||||
} else if (filter.id == "name") {
|
||||
params["q"].push(filter.value);
|
||||
if (typeof filter.value === "string") {
|
||||
params["q"].push(filter.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* 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 {
|
||||
ApiKeys,
|
||||
|
@ -15,6 +15,7 @@ import {
|
|||
ReleaseKeys,
|
||||
SettingsKeys
|
||||
} from "@api/query_keys";
|
||||
import { ColumnFilter } from "@tanstack/react-table";
|
||||
|
||||
export const FiltersQueryOptions = (indexers: string[], sortOrder: string) =>
|
||||
queryOptions({
|
||||
|
@ -104,10 +105,11 @@ export const ApikeysQueryOptions = () =>
|
|||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
export const ReleasesListQueryOptions = (offset: number, limit: number, filters: ReleaseFilter[]) =>
|
||||
export const ReleasesListQueryOptions = (offset: number, limit: number, filters: ColumnFilter[]) =>
|
||||
queryOptions({
|
||||
queryKey: ReleaseKeys.list(offset, limit, filters),
|
||||
queryFn: () => APIClient.release.findQuery(offset, limit, filters),
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 5000,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchInterval: 15000 // refetch releases table on releases page every 15s
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import { ColumnFilter } from "@tanstack/react-table";
|
||||
|
||||
export const SettingsKeys = {
|
||||
all: ["settings"] as const,
|
||||
updates: () => [...SettingsKeys.all, "updates"] as const,
|
||||
|
@ -21,7 +23,7 @@ export const FilterKeys = {
|
|||
export const ReleaseKeys = {
|
||||
all: ["releases"] 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,
|
||||
pageSize,
|
||||
filters
|
||||
|
|
|
@ -7,7 +7,7 @@ import * as React from "react";
|
|||
import { toast } from "react-hot-toast";
|
||||
import { formatDistanceToNowStrict } from "date-fns";
|
||||
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 {
|
||||
ClockIcon,
|
||||
|
@ -25,7 +25,7 @@ import Toast from "@components/notifications/Toast";
|
|||
import { RingResizeSpinner } from "@components/Icons";
|
||||
import { Tooltip } from "@components/tooltips/Tooltip";
|
||||
|
||||
export const NameCell = (props: CellProps<Release>) => (
|
||||
export const NameCell = (props: CellContext<Release, unknown>) => (
|
||||
<div
|
||||
className={classNames(
|
||||
"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">
|
||||
<span className="truncate">
|
||||
{String(props.cell.value)}
|
||||
{String(props.cell.getValue())}
|
||||
</span>
|
||||
<div className="text-xs truncate">
|
||||
<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>
|
||||
);
|
||||
|
||||
export const LinksCell = (props: CellProps<Release>) => {
|
||||
export const LinksCell = (props: CellContext<Release, unknown>) => {
|
||||
return (
|
||||
<div className="flex space-x-2 text-blue-400 dark:text-blue-500">
|
||||
<div>
|
||||
|
@ -104,13 +104,13 @@ export const LinksCell = (props: CellProps<Release>) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const AgeCell = ({value}: CellProps<Release>) => (
|
||||
<div className="text-sm text-gray-500" title={simplifyDate(value)}>
|
||||
{formatDistanceToNowStrict(new Date(value), {addSuffix: false})}
|
||||
export const AgeCell = ({cell}: CellContext<Release, unknown>) => (
|
||||
<div className="text-sm text-gray-500" title={simplifyDate(cell.getValue() as string)}>
|
||||
{formatDistanceToNowStrict(new Date(cell.getValue() as string), {addSuffix: false})}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const IndexerCell = (props: CellProps<Release>) => (
|
||||
export const IndexerCell = (props: CellContext<Release, unknown>) => (
|
||||
<div
|
||||
className={classNames(
|
||||
"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>
|
||||
);
|
||||
|
||||
export const TitleCell = ({value}: CellProps<Release>) => (
|
||||
export const TitleCell = ({cell}: CellContext<Release, string>) => (
|
||||
<div
|
||||
className={classNames(
|
||||
"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
|
||||
requiresClick
|
||||
label={value}
|
||||
label={cell.getValue()}
|
||||
maxWidth="max-w-[90vw]"
|
||||
>
|
||||
<span className="whitespace-pre-wrap break-words">
|
||||
{value}
|
||||
{cell.getValue()}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
@ -188,10 +188,6 @@ const RetryActionButton = ({ status }: RetryActionButtonProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
interface ReleaseStatusCellProps {
|
||||
value: ReleaseActionStatus[];
|
||||
}
|
||||
|
||||
interface StatusCellMapEntry {
|
||||
colors: string;
|
||||
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">
|
||||
{value.map((v, idx) => (
|
||||
{row.original.action_status.map((v, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={classNames(
|
||||
|
|
|
@ -6,84 +6,35 @@
|
|||
import React, { useState } from "react";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
useTable,
|
||||
useFilters,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination, FilterProps, Column
|
||||
} from "react-table";
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
flexRender,
|
||||
ColumnDef
|
||||
} from "@tanstack/react-table";
|
||||
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
import { EmptyListState } from "@components/emptystates";
|
||||
import * as Icons from "@components/Icons";
|
||||
import * as DataTable from "@components/data-table";
|
||||
import { RandomLinuxIsos } from "@utils";
|
||||
import { ReleasesLatestQueryOptions } from "@api/queries";
|
||||
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 {
|
||||
columns: Column[];
|
||||
columns: ColumnDef<Release>[];
|
||||
data: Release[];
|
||||
}
|
||||
|
||||
function Table({ columns, data }: TableProps) {
|
||||
// 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,
|
||||
} = useTable(
|
||||
{ columns, data },
|
||||
useFilters,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination
|
||||
);
|
||||
const tableInstance = useReactTable({
|
||||
columns,
|
||||
data,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
if (data.length === 0) {
|
||||
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">
|
||||
<EmptyListState text="No recent activity"/>
|
||||
</div>
|
||||
|
@ -91,74 +42,49 @@ function Table({ columns, data }: TableProps) {
|
|||
)
|
||||
}
|
||||
|
||||
// Render the UI for your table
|
||||
return (
|
||||
<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">
|
||||
<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">
|
||||
{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 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"
|
||||
{...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>
|
||||
);
|
||||
})}
|
||||
{tableInstance.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th
|
||||
key={header.id}
|
||||
scope="col"
|
||||
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"
|
||||
colSpan={header.colSpan}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
)
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody
|
||||
{...getTableBodyProps()}
|
||||
className="divide-y divide-gray-150 dark:divide-gray-750"
|
||||
>
|
||||
{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 className="divide-y divide-gray-150 dark:divide-gray-750">
|
||||
{tableInstance.getRowModel().rows.map((row) => (
|
||||
<tr key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
||||
role="cell"
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -167,30 +93,28 @@ function Table({ columns, data }: TableProps) {
|
|||
}
|
||||
|
||||
export const ActivityTable = () => {
|
||||
const columns = React.useMemo(() => [
|
||||
const columns = React.useMemo<ColumnDef<Release, unknown>[]>(() => [
|
||||
{
|
||||
Header: "Age",
|
||||
accessor: "timestamp",
|
||||
Cell: DataTable.AgeCell
|
||||
header: "Age",
|
||||
accessorKey: "timestamp",
|
||||
cell: DataTable.AgeCell
|
||||
},
|
||||
{
|
||||
Header: "Release",
|
||||
accessor: "name",
|
||||
Cell: DataTable.TitleCell
|
||||
header: "Release",
|
||||
accessorKey: "name",
|
||||
cell: DataTable.TitleCell,
|
||||
},
|
||||
{
|
||||
Header: "Actions",
|
||||
accessor: "action_status",
|
||||
Cell: DataTable.ReleaseStatusCell
|
||||
header: "Actions",
|
||||
accessorKey: "action_status",
|
||||
cell: DataTable.ReleaseStatusCell
|
||||
},
|
||||
{
|
||||
Header: "Indexer",
|
||||
accessor: "indexer.identifier",
|
||||
Cell: IndexerCell,
|
||||
Filter: SelectColumnFilter,
|
||||
filter: "includes"
|
||||
header: "Indexer",
|
||||
accessorKey: "indexer.identifier",
|
||||
cell: IndexerCell,
|
||||
}
|
||||
] as Column[], []);
|
||||
], []);
|
||||
|
||||
const { isLoading, data } = useSuspenseQuery(ReleasesLatestQueryOptions());
|
||||
|
||||
|
@ -236,7 +160,7 @@ export const ActivityTable = () => {
|
|||
Recent activity
|
||||
</h3>
|
||||
|
||||
<Table columns={columns} data={displayData} />
|
||||
<Table columns={columns} data={displayData}/>
|
||||
|
||||
<button
|
||||
onClick={toggleReleaseNames}
|
||||
|
@ -245,9 +169,9 @@ export const ActivityTable = () => {
|
|||
title="Go incognito"
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
|
||||
import * as React from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Column } from "@tanstack/react-table";
|
||||
import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from "@headlessui/react";
|
||||
import { DebounceInput } from "react-debounce-input";
|
||||
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
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 {
|
||||
|
@ -63,23 +63,19 @@ const ListboxFilter = ({
|
|||
</div>
|
||||
);
|
||||
|
||||
// a unique option from a list
|
||||
export const IndexerSelectColumnFilter = ({
|
||||
column: { filterValue, setFilter, id }
|
||||
}: FilterProps<object>) => {
|
||||
export const IndexerSelectColumnFilter = ({ column }: { column: Column<Release, unknown> }) => {
|
||||
const { data, isSuccess } = useQuery(ReleasesIndexersQueryOptions());
|
||||
|
||||
// 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 (
|
||||
<ListboxFilter
|
||||
id={id}
|
||||
key={id}
|
||||
id={column.id}
|
||||
key={column.id}
|
||||
label={currentIndexerName}
|
||||
currentValue={filterValue ?? ""}
|
||||
onChange={setFilter}
|
||||
currentValue={column.getFilterValue() as string || ""}
|
||||
onChange={newValue => column.setFilterValue(newValue || undefined)}
|
||||
>
|
||||
{isSuccess && data && data?.map((indexer, idx) => (
|
||||
<FilterOption key={idx} label={indexer.name} value={indexer.identifier} />
|
||||
|
@ -95,9 +91,9 @@ interface FilterOptionProps {
|
|||
|
||||
const FilterOption = ({ label, value }: FilterOptionProps) => (
|
||||
<ListboxOption
|
||||
className={({ active }) => classNames(
|
||||
className={({ focus }) => classNames(
|
||||
"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}
|
||||
>
|
||||
|
@ -121,23 +117,24 @@ const FilterOption = ({ label, value }: FilterOptionProps) => (
|
|||
</ListboxOption>
|
||||
);
|
||||
|
||||
export const PushStatusSelectColumnFilter = ({
|
||||
column: { filterValue, setFilter, id },
|
||||
initialFilterValue
|
||||
}: FilterProps<object>) => {
|
||||
React.useEffect(() => {
|
||||
if (initialFilterValue) {
|
||||
setFilter(initialFilterValue);
|
||||
}
|
||||
}, [initialFilterValue, setFilter]);
|
||||
const label = filterValue ? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label : "Push status";
|
||||
export const PushStatusSelectColumnFilter = ({ column }: { column: Column<Release, unknown> }) => {
|
||||
// React.useEffect(() => {
|
||||
// if (initialFilterValue) {
|
||||
// setFilter(initialFilterValue);
|
||||
// }
|
||||
// }, [initialFilterValue, setFilter]);
|
||||
|
||||
const label = column.getFilterValue() ? PushStatusOptions.find((o) => o.value === column.getFilterValue() && o.value)?.label : "Push status";
|
||||
|
||||
return (
|
||||
<div className="mr-3" key={id}>
|
||||
<div className="mr-3" key={column.id}>
|
||||
<ListboxFilter
|
||||
id={id}
|
||||
id={column.id}
|
||||
label={label ?? "Push status"}
|
||||
currentValue={filterValue ?? ""}
|
||||
onChange={setFilter}
|
||||
currentValue={column.getFilterValue() as string ?? ""}
|
||||
onChange={value => {
|
||||
column.setFilterValue(value || undefined);
|
||||
}}
|
||||
>
|
||||
{PushStatusOptions.map((status, idx) => (
|
||||
<FilterOption key={idx} value={status.value} label={status.label} />
|
||||
|
@ -147,17 +144,16 @@ export const PushStatusSelectColumnFilter = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const SearchColumnFilter = ({
|
||||
column: { filterValue, setFilter, id }
|
||||
}: FilterProps<object>) => {
|
||||
export const SearchColumnFilter = ({ column }: { column: Column<Release, unknown> }) => {
|
||||
return (
|
||||
<div className="flex-1 mr-3 mt-1" key={id}>
|
||||
<div className="flex-1 mr-3 mt-1" key={column.id}>
|
||||
<DebounceInput
|
||||
minLength={2}
|
||||
value={filterValue || undefined}
|
||||
value={column.getFilterValue() as string || undefined}
|
||||
debounceTimeout={500}
|
||||
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"
|
||||
type="text"
|
||||
|
|
|
@ -6,7 +6,15 @@
|
|||
import React, { useState } from "react";
|
||||
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 {
|
||||
ChevronDoubleLeftIcon,
|
||||
ChevronDoubleRightIcon,
|
||||
|
@ -16,64 +24,24 @@ import {
|
|||
EyeSlashIcon
|
||||
} from "@heroicons/react/24/solid";
|
||||
|
||||
import { ReleasesRoute } from "@app/routes";
|
||||
import { ReleasesListQueryOptions } from "@api/queries";
|
||||
import { RandomLinuxIsos } from "@utils";
|
||||
import { RingResizeSpinner, SortDownIcon, SortIcon, SortUpIcon } from "@components/Icons";
|
||||
import { RingResizeSpinner } from "@components/Icons";
|
||||
import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./ReleaseFilters";
|
||||
import { EmptyListState } from "@components/emptystates";
|
||||
import { TableButton, TablePageButton } from "@components/data-table/Buttons.tsx";
|
||||
import { AgeCell, IndexerCell, LinksCell, NameCell, ReleaseStatusCell } from "@components/data-table";
|
||||
import { TableButton, TablePageButton, AgeCell, IndexerCell, LinksCell, NameCell, ReleaseStatusCell } from "@components/data-table";
|
||||
|
||||
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"
|
||||
declare module '@tanstack/react-table' {
|
||||
//allows us to define custom properties for our columns
|
||||
// @eslint-ignore
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
filterVariant?: 'text' | 'range' | 'select' | 'search' | 'actionPushStatus' | 'indexerSelect';
|
||||
}
|
||||
}
|
||||
|
||||
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 = () => (
|
||||
<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">
|
||||
<thead className="bg-gray-100 dark:bg-gray-850 border-b border-gray-200 dark:border-gray-750">
|
||||
<tr>
|
||||
|
@ -91,50 +59,87 @@ const EmptyReleaseList = () => (
|
|||
</div>
|
||||
);
|
||||
|
||||
export const ReleaseTable = () => {
|
||||
const search = ReleasesRoute.useSearch()
|
||||
function Filter({ column }: { column: Column<Release, unknown> }) {
|
||||
const { filterVariant } = column.columnDef.meta ?? {}
|
||||
|
||||
const columns = React.useMemo(() => [
|
||||
{
|
||||
Header: "Age",
|
||||
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>[], []);
|
||||
switch (filterVariant) {
|
||||
case "search":
|
||||
return <SearchColumnFilter column={column}/>
|
||||
|
||||
if (search.action_status != "") {
|
||||
initialState.queryFilters = [{id: "action_status", value: search.action_status! }]
|
||||
case "indexerSelect":
|
||||
return <IndexerSelectColumnFilter column={column}/>
|
||||
|
||||
case "actionPushStatus":
|
||||
return <PushStatusSelectColumnFilter column={column}/>
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
|
||||
React.useReducer(TableReducer, initialState);
|
||||
interface ColumnFilter {
|
||||
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 [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 {
|
||||
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: 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
|
||||
const tableInstance = useReactTable({
|
||||
columns,
|
||||
data: displayData,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualFiltering: true,
|
||||
manualPagination: true,
|
||||
manualSorting: true,
|
||||
rowCount: data?.count,
|
||||
state: {
|
||||
columnFilters,
|
||||
pagination,
|
||||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
usePagination
|
||||
);
|
||||
initialState: {
|
||||
pagination
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: ActionType.PAGE_CHANGED, payload: pageIndex });
|
||||
}, [pageIndex]);
|
||||
// Manage your own state
|
||||
// const [state, setState] = React.useState(tableInstance.initialState)
|
||||
|
||||
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 });
|
||||
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]);
|
||||
// Override the state managers for the table to your own
|
||||
// tableInstance.setOptions(prev => ({
|
||||
// ...prev,
|
||||
// state,
|
||||
// onStateChange: setState,
|
||||
// // These are just table options, so if things
|
||||
// // need to change based on your state, you can
|
||||
// // derive them here
|
||||
//
|
||||
// // Just for fun, let's debug everything if the pageIndex
|
||||
// // is greater than 2
|
||||
// // debugTable: state.pagination.pageIndex > 2,
|
||||
// }))
|
||||
|
||||
if (error) {
|
||||
return <p>Error</p>;
|
||||
|
@ -242,17 +215,19 @@ export const ReleaseTable = () => {
|
|||
return (
|
||||
<div>
|
||||
<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>
|
||||
{tableInstance.getHeaderGroups().map((headerGroup) =>
|
||||
headerGroup.headers.map((header) => (
|
||||
header.column.getCanFilter() ? (
|
||||
<Filter key={header.column.id} column={header.column}/>
|
||||
) : null
|
||||
))
|
||||
) }
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-lg rounded-md mt-4">
|
||||
<div className="bg-gray-100 dark:bg-gray-850 border-b border-gray-200 dark:border-gray-750">
|
||||
<div className="flex h-10"/>
|
||||
</div>
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-lg rounded-md mt-4">
|
||||
<div className="bg-gray-100 dark:bg-gray-850 border-b border-gray-200 dark:border-gray-750">
|
||||
<div className="flex h-10"/>
|
||||
</div>
|
||||
<div className="flex items-center justify-center py-64">
|
||||
<RingResizeSpinner className="text-blue-500 size-24"/>
|
||||
</div>
|
||||
|
@ -261,14 +236,13 @@ export const ReleaseTable = () => {
|
|||
)
|
||||
}
|
||||
|
||||
// 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>
|
||||
{tableInstance.getHeaderGroups().map((headerGroup) =>
|
||||
headerGroup.headers.map((header) => (
|
||||
header.column.getCanFilter() ? (
|
||||
<Filter key={header.column.id} column={header.column}/>
|
||||
) : null
|
||||
))
|
||||
)}
|
||||
|
@ -277,156 +251,151 @@ export const ReleaseTable = () => {
|
|||
{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();
|
||||
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 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"
|
||||
{...columnRest}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<>{column.render("Header")}</>
|
||||
{/* Add a sort direction indicator */}
|
||||
<span>
|
||||
{column.isSorted ? (
|
||||
column.isSortedDesc ? (
|
||||
<SortDownIcon 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"/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
<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">
|
||||
{tableInstance.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th
|
||||
key={header.id}
|
||||
scope="col"
|
||||
colSpan={header.colSpan}
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{/*<>{header.render("Header")}</>*/}
|
||||
{/*/!* Add a sort direction indicator *!/*/}
|
||||
{/*<span>*/}
|
||||
{/* {header.isSorted ? (*/}
|
||||
{/* header.isSortedDesc ? (*/}
|
||||
{/* <SortDownIcon 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"/>*/}
|
||||
{/* )}*/}
|
||||
{/*</span>*/}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
{...getTableBodyProps()}
|
||||
className="divide-y divide-gray-150 dark:divide-gray-750"
|
||||
>
|
||||
{page.map((row) => {
|
||||
prepareRow(row);
|
||||
))}
|
||||
</thead>
|
||||
|
||||
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>
|
||||
);
|
||||
})}
|
||||
<tbody className="divide-y divide-gray-150 dark:divide-gray-750">
|
||||
{tableInstance.getRowModel().rows.map((row) => (
|
||||
<tr key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
||||
role="cell"
|
||||
>
|
||||
<>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</>
|
||||
</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">
|
||||
<TableButton onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</TableButton>
|
||||
<TableButton onClick={() => nextPage()} disabled={!canNextPage}>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>
|
||||
))}
|
||||
</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">
|
||||
<TableButton onClick={() => tableInstance.previousPage()} disabled={!tableInstance.getCanPreviousPage()}>Previous</TableButton>
|
||||
<TableButton onClick={() => tableInstance.nextPage()} disabled={!tableInstance.getCanNextPage()}>Next</TableButton>
|
||||
</div>
|
||||
<div>
|
||||
<nav className="inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||
<TablePageButton
|
||||
className="rounded-l-md"
|
||||
onClick={() => gotoPage(0)}
|
||||
disabled={!canPreviousPage}
|
||||
>
|
||||
<span className="sr-only">First</span>
|
||||
<ChevronDoubleLeftIcon className="w-4 h-4" aria-hidden="true"/>
|
||||
</TablePageButton>
|
||||
<TablePageButton
|
||||
className="pl-1 pr-2"
|
||||
onClick={() => previousPage()}
|
||||
disabled={!canPreviousPage}
|
||||
>
|
||||
<ChevronLeftIcon className="w-4 h-4 mr-1" aria-hidden="true"/>
|
||||
<span>Prev</span>
|
||||
</TablePageButton>
|
||||
<TablePageButton
|
||||
className="pl-2 pr-1"
|
||||
onClick={() => nextPage()}
|
||||
disabled={!canNextPage}>
|
||||
<span>Next</span>
|
||||
<ChevronRightIcon className="w-4 h-4 ml-1" aria-hidden="true"/>
|
||||
</TablePageButton>
|
||||
<TablePageButton
|
||||
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>
|
||||
</TablePageButton>
|
||||
</nav>
|
||||
<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">{tableInstance.getState().pagination.pageIndex + 1}</span> of <span
|
||||
className="font-medium">{tableInstance.getPageCount()}</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={tableInstance.getState().pagination.pageSize}
|
||||
onChange={e => {
|
||||
tableInstance.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">
|
||||
<TablePageButton
|
||||
className="rounded-l-md"
|
||||
onClick={() => tableInstance.firstPage()}
|
||||
disabled={!tableInstance.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">First</span>
|
||||
<ChevronDoubleLeftIcon className="w-4 h-4" aria-hidden="true"/>
|
||||
</TablePageButton>
|
||||
<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 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