mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +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": "^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
44
web/pnpm-lock.yaml
generated
|
@ -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
|
||||||
|
|
|
@ -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,14 +419,26 @@ export const APIClient = {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (filter.id == "indexer.identifier") {
|
if (filter.id == "indexer.identifier") {
|
||||||
|
if (typeof filter.value === "string") {
|
||||||
params["indexer"].push(filter.value);
|
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") {
|
||||||
} else if (filter.id === "push_status") {
|
|
||||||
params["push_status"].push(filter.value);
|
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") {
|
||||||
|
if (typeof filter.value === "string") {
|
||||||
|
params["push_status"].push(filter.value);
|
||||||
|
}
|
||||||
} else if (filter.id == "name") {
|
} else if (filter.id == "name") {
|
||||||
|
if (typeof filter.value === "string") {
|
||||||
params["q"].push(filter.value);
|
params["q"].push(filter.value);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return appClient.Get<ReleaseFindResponse>("api/release", {
|
return appClient.Get<ReleaseFindResponse>("api/release", {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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}>
|
|
||||||
{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
|
<th
|
||||||
key={`${rowKey}-${columnKey}`}
|
key={header.id}
|
||||||
scope="col"
|
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"
|
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}
|
colSpan={header.colSpan}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<>{column.render("Header")}</>
|
{header.isPlaceholder
|
||||||
{/* Add a sort direction indicator */}
|
? null
|
||||||
<span>
|
: flexRender(
|
||||||
{column.isSorted ? (
|
header.column.columnDef.header,
|
||||||
column.isSortedDesc ? (
|
header.getContext()
|
||||||
<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>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
);
|
)
|
||||||
})}
|
)}
|
||||||
</tr>
|
</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);
|
|
||||||
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
|
||||||
return (
|
|
||||||
<tr key={bodyRowKey} {...bodyRowRest}>
|
|
||||||
{row.cells.map((cell) => {
|
|
||||||
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
|
||||||
return (
|
|
||||||
<td
|
<td
|
||||||
key={cellRowKey}
|
key={cell.id}
|
||||||
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
||||||
role="cell"
|
role="cell"
|
||||||
{...cellRowRest}
|
|
||||||
>
|
>
|
||||||
<>{cell.render("Cell")}</>
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</td>
|
</td>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</tr>
|
</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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function Filter({ column }: { column: Column<Release, unknown> }) {
|
||||||
|
const { filterVariant } = column.columnDef.meta ?? {}
|
||||||
|
|
||||||
|
switch (filterVariant) {
|
||||||
|
case "search":
|
||||||
|
return <SearchColumnFilter column={column}/>
|
||||||
|
|
||||||
|
case "indexerSelect":
|
||||||
|
return <IndexerSelectColumnFilter column={column}/>
|
||||||
|
|
||||||
|
case "actionPushStatus":
|
||||||
|
return <PushStatusSelectColumnFilter column={column}/>
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColumnFilter {
|
||||||
|
id: string
|
||||||
|
value: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColumnFiltersState = ColumnFilter[];
|
||||||
|
|
||||||
export const ReleaseTable = () => {
|
export const ReleaseTable = () => {
|
||||||
const search = ReleasesRoute.useSearch()
|
// const search = ReleasesRoute.useSearch()
|
||||||
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||||
|
|
||||||
const columns = React.useMemo(() => [
|
const columns = React.useMemo<ColumnDef<Release, unknown>[]>(() => [
|
||||||
{
|
{
|
||||||
Header: "Age",
|
header: "Age",
|
||||||
accessor: "timestamp",
|
accessorKey: "timestamp",
|
||||||
Cell: AgeCell
|
cell: AgeCell
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Release",
|
header: "Release",
|
||||||
accessor: "name",
|
accessorKey: "name",
|
||||||
Cell: NameCell,
|
cell: NameCell,
|
||||||
Filter: SearchColumnFilter
|
meta: {
|
||||||
|
filterVariant: 'search',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Links",
|
header: "Links",
|
||||||
accessor: (row) => ({ download_url: row.download_url, info_url: row.info_url }),
|
accessorFn: (row) => ({ download_url: row.download_url, info_url: row.info_url }),
|
||||||
id: "links",
|
id: "links",
|
||||||
Cell: LinksCell
|
cell: LinksCell
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Actions",
|
header: "Actions",
|
||||||
accessor: "action_status",
|
accessorKey: "action_status",
|
||||||
Cell: ReleaseStatusCell,
|
cell: ReleaseStatusCell,
|
||||||
Filter: PushStatusSelectColumnFilter
|
meta: {
|
||||||
|
filterVariant: 'actionPushStatus',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Indexer",
|
header: "Indexer",
|
||||||
accessor: "indexer.identifier",
|
accessorKey: "indexer.identifier",
|
||||||
Cell: IndexerCell,
|
cell: IndexerCell,
|
||||||
Filter: IndexerSelectColumnFilter,
|
meta: {
|
||||||
filter: "equal"
|
filterVariant: 'indexerSelect',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
] as Column<Release>[], []);
|
], []);
|
||||||
|
|
||||||
if (search.action_status != "") {
|
// if (search.action_status != "") {
|
||||||
initialState.queryFilters = [{id: "action_status", value: search.action_status! }]
|
// setColumnFilters(prevState => [...prevState, { id: "action_status", value: search.action_status }]);
|
||||||
}
|
// }
|
||||||
|
|
||||||
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
|
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||||
React.useReducer(TableReducer, initialState);
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
})
|
||||||
|
|
||||||
const { isLoading, error, data, isSuccess } = useQuery(ReleasesListQueryOptions(queryPageIndex * queryPageSize, queryPageSize, queryFilters));
|
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,
|
|
||||||
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,
|
columns,
|
||||||
data: displayData, // Use displayData here
|
data: displayData,
|
||||||
initialState: {
|
getCoreRowModel: getCoreRowModel(),
|
||||||
pageIndex: queryPageIndex,
|
manualFiltering: true,
|
||||||
pageSize: queryPageSize,
|
|
||||||
filters: queryFilters,
|
|
||||||
},
|
|
||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
manualFilters: true,
|
manualSorting: true,
|
||||||
manualSortBy: true,
|
rowCount: data?.count,
|
||||||
pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : 0,
|
state: {
|
||||||
autoResetSortBy: false,
|
columnFilters,
|
||||||
autoResetExpanded: false,
|
pagination,
|
||||||
autoResetPage: false
|
|
||||||
},
|
},
|
||||||
useFilters,
|
initialState: {
|
||||||
useSortBy,
|
pagination
|
||||||
usePagination
|
},
|
||||||
);
|
onPaginationChange: setPagination,
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
React.useEffect(() => {
|
|
||||||
dispatch({ type: ActionType.PAGE_CHANGED, payload: pageIndex });
|
|
||||||
}, [pageIndex]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
dispatch({ type: ActionType.PAGE_SIZE_CHANGED, payload: pageSize });
|
|
||||||
gotoPage(0);
|
|
||||||
}, [pageSize, gotoPage]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (data?.count) {
|
|
||||||
dispatch({
|
|
||||||
type: ActionType.TOTAL_COUNT_CHANGED,
|
|
||||||
payload: data.count
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}, [data?.count]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
// Manage your own state
|
||||||
dispatch({ type: ActionType.FILTER_CHANGED, payload: filters });
|
// const [state, setState] = React.useState(tableInstance.initialState)
|
||||||
gotoPage(0);
|
|
||||||
}, [filters]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
// Override the state managers for the table to your own
|
||||||
if (search.action_status != null) {
|
// tableInstance.setOptions(prev => ({
|
||||||
dispatch({ type: ActionType.FILTER_CHANGED, payload: [{ id: "action_status", value: search.action_status! }] });
|
// ...prev,
|
||||||
}
|
// state,
|
||||||
}, [search.action_status]);
|
// 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) {
|
if (error) {
|
||||||
return <p>Error</p>;
|
return <p>Error</p>;
|
||||||
|
@ -242,14 +215,16 @@ 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
|
||||||
|
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="bg-gray-100 dark:bg-gray-850 border-b border-gray-200 dark:border-gray-750">
|
||||||
<div className="flex h-10"/>
|
<div className="flex h-10"/>
|
||||||
</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,93 +251,87 @@ 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">
|
||||||
|
<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}>
|
|
||||||
{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
|
<th
|
||||||
key={`${rowKey}-${columnKey}`}
|
key={header.id}
|
||||||
scope="col"
|
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"
|
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">
|
<div className="flex items-center justify-between">
|
||||||
<>{column.render("Header")}</>
|
{header.isPlaceholder
|
||||||
{/* Add a sort direction indicator */}
|
? null
|
||||||
<span>
|
: flexRender(
|
||||||
{column.isSorted ? (
|
header.column.columnDef.header,
|
||||||
column.isSortedDesc ? (
|
header.getContext()
|
||||||
<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>
|
{/*<>{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>
|
</div>
|
||||||
</th>
|
</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();
|
|
||||||
return (
|
|
||||||
<td
|
<td
|
||||||
key={cellRowKey}
|
key={cell.id}
|
||||||
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
||||||
role="cell"
|
role="cell"
|
||||||
{...cellRowRest}
|
|
||||||
>
|
>
|
||||||
<>{cell.render("Cell")}</>
|
<>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.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>
|
||||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||||
<div className="flex items-baseline gap-x-2">
|
<div className="flex items-baseline gap-x-2">
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-500">
|
<span className="text-sm text-gray-700 dark:text-gray-500">
|
||||||
Page <span className="font-medium">{pageIndex + 1}</span> of <span
|
Page <span className="font-medium">{tableInstance.getState().pagination.pageIndex + 1}</span> of <span
|
||||||
className="font-medium">{pageOptions.length}</span>
|
className="font-medium">{tableInstance.getPageCount()}</span>
|
||||||
</span>
|
</span>
|
||||||
<label>
|
<label>
|
||||||
<span className="sr-only bg-gray-700">Items Per Page</span>
|
<span className="sr-only bg-gray-700">Items Per Page</span>
|
||||||
<select
|
<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"
|
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}
|
value={tableInstance.getState().pagination.pageSize}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setPageSize(Number(e.target.value));
|
tableInstance.setPageSize(Number(e.target.value));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[5, 10, 20, 50].map(pageSize => (
|
{[5, 10, 20, 50].map(pageSize => (
|
||||||
|
@ -378,31 +346,31 @@ export const ReleaseTable = () => {
|
||||||
<nav className="inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
<nav className="inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||||
<TablePageButton
|
<TablePageButton
|
||||||
className="rounded-l-md"
|
className="rounded-l-md"
|
||||||
onClick={() => gotoPage(0)}
|
onClick={() => tableInstance.firstPage()}
|
||||||
disabled={!canPreviousPage}
|
disabled={!tableInstance.getCanPreviousPage()}
|
||||||
>
|
>
|
||||||
<span className="sr-only">First</span>
|
<span className="sr-only">First</span>
|
||||||
<ChevronDoubleLeftIcon className="w-4 h-4" aria-hidden="true"/>
|
<ChevronDoubleLeftIcon className="w-4 h-4" aria-hidden="true"/>
|
||||||
</TablePageButton>
|
</TablePageButton>
|
||||||
<TablePageButton
|
<TablePageButton
|
||||||
className="pl-1 pr-2"
|
className="pl-1 pr-2"
|
||||||
onClick={() => previousPage()}
|
onClick={() => tableInstance.previousPage()}
|
||||||
disabled={!canPreviousPage}
|
disabled={!tableInstance.getCanPreviousPage()}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon className="w-4 h-4 mr-1" aria-hidden="true"/>
|
<ChevronLeftIcon className="w-4 h-4 mr-1" aria-hidden="true"/>
|
||||||
<span>Prev</span>
|
<span>Prev</span>
|
||||||
</TablePageButton>
|
</TablePageButton>
|
||||||
<TablePageButton
|
<TablePageButton
|
||||||
className="pl-2 pr-1"
|
className="pl-2 pr-1"
|
||||||
onClick={() => nextPage()}
|
onClick={() => tableInstance.nextPage()}
|
||||||
disabled={!canNextPage}>
|
disabled={!tableInstance.getCanNextPage()}>
|
||||||
<span>Next</span>
|
<span>Next</span>
|
||||||
<ChevronRightIcon className="w-4 h-4 ml-1" aria-hidden="true"/>
|
<ChevronRightIcon className="w-4 h-4 ml-1" aria-hidden="true"/>
|
||||||
</TablePageButton>
|
</TablePageButton>
|
||||||
<TablePageButton
|
<TablePageButton
|
||||||
className="rounded-r-md"
|
className="rounded-r-md"
|
||||||
onClick={() => gotoPage(pageCount - 1)}
|
onClick={() => tableInstance.lastPage()}
|
||||||
disabled={!canNextPage}
|
disabled={!tableInstance.getCanNextPage()}
|
||||||
>
|
>
|
||||||
<ChevronDoubleRightIcon className="w-4 h-4" aria-hidden="true"/>
|
<ChevronDoubleRightIcon className="w-4 h-4" aria-hidden="true"/>
|
||||||
<span className="sr-only">Last</span>
|
<span className="sr-only">Last</span>
|
||||||
|
@ -411,6 +379,7 @@ export const ReleaseTable = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute -bottom-11 right-0 p-2">
|
<div className="absolute -bottom-11 right-0 p-2">
|
||||||
<button
|
<button
|
||||||
onClick={toggleReleaseNames}
|
onClick={toggleReleaseNames}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue