diff --git a/web/package.json b/web/package.json index 05accc3..2c25566 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 27366e0..2368081 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -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 diff --git a/web/src/api/APIClient.ts b/web/src/api/APIClient.ts index bca32d9..e4e2d12 100644 --- a/web/src/api/APIClient.ts +++ b/web/src/api/APIClient.ts @@ -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 | null; type Primitive = string | number | boolean | symbol | undefined; @@ -406,7 +407,7 @@ export const APIClient = { release: { find: (query?: string) => appClient.Get(`api/release${query}`), findRecent: () => appClient.Get("api/release/recent"), - findQuery: (offset?: number, limit?: number, filters?: ReleaseFilter[]) => { + findQuery: (offset?: number, limit?: number, filters?: ColumnFilter[]) => { const params: Record = { 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); + } } }); diff --git a/web/src/api/queries.ts b/web/src/api/queries.ts index f24bfbc..0cf21c6 100644 --- a/web/src/api/queries.ts +++ b/web/src/api/queries.ts @@ -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 diff --git a/web/src/api/query_keys.ts b/web/src/api/query_keys.ts index d091106..a7bd766 100644 --- a/web/src/api/query_keys.ts +++ b/web/src/api/query_keys.ts @@ -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 diff --git a/web/src/components/data-table/Cells.tsx b/web/src/components/data-table/Cells.tsx index 5e8a552..12f4225 100644 --- a/web/src/components/data-table/Cells.tsx +++ b/web/src/components/data-table/Cells.tsx @@ -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) => ( +export const NameCell = (props: CellContext) => (
) => ( >
- {String(props.cell.value)} + {String(props.cell.getValue())}
Category: {props.row.original.category} @@ -47,7 +47,7 @@ export const NameCell = (props: CellProps) => (
); -export const LinksCell = (props: CellProps) => { +export const LinksCell = (props: CellContext) => { return (
@@ -104,13 +104,13 @@ export const LinksCell = (props: CellProps) => { ); }; -export const AgeCell = ({value}: CellProps) => ( -
- {formatDistanceToNowStrict(new Date(value), {addSuffix: false})} +export const AgeCell = ({cell}: CellContext) => ( +
+ {formatDistanceToNowStrict(new Date(cell.getValue() as string), {addSuffix: false})}
); -export const IndexerCell = (props: CellProps) => ( +export const IndexerCell = (props: CellContext) => (
) => (
); -export const TitleCell = ({value}: CellProps) => ( +export const TitleCell = ({cell}: CellContext) => (
) => ( > - {value} + {cell.getValue()}
@@ -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) => (
- {value.map((v, idx) => ( + {row.original.action_status.map((v, idx) => (
) { - // Calculate the options for filtering - // using the preFilteredRows - const options = React.useMemo(() => { - const options = new Set(); - preFilteredRows.forEach((row: { values: { [x: string]: string } }) => { - options.add(row.values[id]); - }); - return [...options.values()]; - }, [id, preFilteredRows]); - - // Render a multi-select box - return ( - - ); -} - interface TableProps { - columns: Column[]; + columns: ColumnDef[]; 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 ( -
+
@@ -91,74 +42,49 @@ function Table({ columns, data }: TableProps) { ) } - // Render the UI for your table return (
- +
- {headerGroups.map((headerGroup) => { - const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps(); - return ( - - {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 - - ); - })} - - ); - })} + {tableInstance.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ) + )} + + ))} - - {page.map((row) => { - prepareRow(row); - const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps(); - return ( - - {row.cells.map((cell) => { - const { key: cellRowKey, ...cellRowRest } = cell.getCellProps(); - return ( - - ); - })} - - ); - })} + + + {tableInstance.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))}
-
- <>{column.render("Header")} - {/* Add a sort direction indicator */} - - {column.isSorted ? ( - column.isSortedDesc ? ( - - ) : ( - - ) - ) : ( - - )} - -
-
+
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+
- <>{cell.render("Cell")} -
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
@@ -167,30 +93,28 @@ function Table({ columns, data }: TableProps) { } export const ActivityTable = () => { - const columns = React.useMemo(() => [ + const columns = React.useMemo[]>(() => [ { - 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 - +
diff --git a/web/src/screens/releases/ReleaseFilters.tsx b/web/src/screens/releases/ReleaseFilters.tsx index 700ba57..310d683 100644 --- a/web/src/screens/releases/ReleaseFilters.tsx +++ b/web/src/screens/releases/ReleaseFilters.tsx @@ -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 = ({ ); -// a unique option from a list -export const IndexerSelectColumnFilter = ({ - column: { filterValue, setFilter, id } -}: FilterProps) => { +export const IndexerSelectColumnFilter = ({ column }: { column: Column }) => { 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 ( column.setFilterValue(newValue || undefined)} > {isSuccess && data && data?.map((indexer, idx) => ( @@ -95,9 +91,9 @@ interface FilterOptionProps { const FilterOption = ({ label, value }: FilterOptionProps) => ( 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) => ( ); -export const PushStatusSelectColumnFilter = ({ - column: { filterValue, setFilter, id }, - initialFilterValue -}: FilterProps) => { - 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 }) => { + // 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 ( -
+
{ + column.setFilterValue(value || undefined); + }} > {PushStatusOptions.map((status, idx) => ( @@ -147,17 +144,16 @@ export const PushStatusSelectColumnFilter = ({ ); }; -export const SearchColumnFilter = ({ - column: { filterValue, setFilter, id } -}: FilterProps) => { +export const SearchColumnFilter = ({ column }: { column: Column }) => { return ( -
+
{ - 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" diff --git a/web/src/screens/releases/ReleaseTable.tsx b/web/src/screens/releases/ReleaseTable.tsx index 16e04fb..1ab8862 100644 --- a/web/src/screens/releases/ReleaseTable.tsx +++ b/web/src/screens/releases/ReleaseTable.tsx @@ -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 { + 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 = () => ( -
+
@@ -91,50 +59,87 @@ const EmptyReleaseList = () => ( ); -export const ReleaseTable = () => { - const search = ReleasesRoute.useSearch() +function Filter({ column }: { column: Column }) { + 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[], []); + switch (filterVariant) { + case "search": + return - if (search.action_status != "") { - initialState.queryFilters = [{id: "action_status", value: search.action_status! }] + case "indexerSelect": + return + + case "actionPushStatus": + return + + 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([]) + + const columns = React.useMemo[]>(() => [ + { + 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({ + pageIndex: 0, + pageSize: 10, + }) + + const { + isLoading, + error, + data, + } = useQuery(ReleasesListQueryOptions(pagination.pageIndex * pagination.pageSize, pagination.pageSize, columnFilters)); const [modifiedData, setModifiedData] = useState([]); 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

Error

; @@ -242,17 +215,19 @@ export const ReleaseTable = () => { return (
- { headerGroups.map((headerGroup) => headerGroup.headers.map((column) => ( - column.Filter ? ( - { column.render("Filter") } + {tableInstance.getHeaderGroups().map((headerGroup) => + headerGroup.headers.map((header) => ( + header.column.getCanFilter() ? ( + ) : null )) - ) } + )}
-
-
-
-
+
+
+
+
@@ -261,14 +236,13 @@ export const ReleaseTable = () => { ) } - // Render the UI for your table return (
- {headerGroups.map((headerGroup) => - headerGroup.headers.map((column) => ( - column.Filter ? ( - {column.render("Filter")} + {tableInstance.getHeaderGroups().map((headerGroup) => + headerGroup.headers.map((header) => ( + header.column.getCanFilter() ? ( + ) : null )) )} @@ -277,156 +251,151 @@ export const ReleaseTable = () => { {displayData.length === 0 ? : ( -
-
- - {headerGroups.map((headerGroup) => { - const {key: rowKey, ...rowRest} = headerGroup.getHeaderGroupProps(); - return ( - - {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 - - ); - })} +
+
-
- <>{column.render("Header")} - {/* Add a sort direction indicator */} - - {column.isSorted ? ( - column.isSortedDesc ? ( - - ) : ( - - ) - ) : ( - - )} - -
-
+ + {tableInstance.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} - ); - })} - - - {page.map((row) => { - prepareRow(row); + ))} + - const {key: bodyRowKey, ...bodyRowRest} = row.getRowProps(); - return ( - - {row.cells.map((cell) => { - const {key: cellRowKey, ...cellRowRest} = cell.getCellProps(); - return ( - - ); - })} + + {tableInstance.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} - ); - })} - -
+
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + {/*<>{header.render("Header")}*/} + {/*/!* Add a sort direction indicator *!/*/} + {/**/} + {/* {header.isSorted ? (*/} + {/* header.isSortedDesc ? (*/} + {/* */} + {/* ) : (*/} + {/* */} + {/* )*/} + {/* ) : (*/} + {/* */} + {/* )}*/} + {/**/} +
+
- <>{cell.render("Cell")} -
+ <> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + +
- {/* Pagination */} -
-
- previousPage()} disabled={!canPreviousPage}>Previous - nextPage()} disabled={!canNextPage}>Next -
-
-
- - Page {pageIndex + 1} of {pageOptions.length} - - + ))} + + + + {/* Pagination */} +
+
+ tableInstance.previousPage()} disabled={!tableInstance.getCanPreviousPage()}>Previous + tableInstance.nextPage()} disabled={!tableInstance.getCanNextPage()}>Next
-
- +
+
+ + Page {tableInstance.getState().pagination.pageIndex + 1} of {tableInstance.getPageCount()} + + +
+
+ +
+ +
+ +
-
- -
-
- )} + )}
);