feat(web): releases list filtering (#136)

This commit is contained in:
Ludvig Lundgren 2022-02-19 20:00:48 +01:00 committed by GitHub
parent 279d4ff7a3
commit 246e3ddc26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 459 additions and 48 deletions

View file

@ -84,14 +84,14 @@ func (repo *ReleaseRepo) StoreReleaseActionStatus(ctx context.Context, a *domain
return nil return nil
} }
func (repo *ReleaseRepo) Find(ctx context.Context, params domain.QueryParams) ([]domain.Release, int64, int64, error) { func (repo *ReleaseRepo) Find(ctx context.Context, params domain.ReleaseQueryParams) ([]domain.Release, int64, int64, error) {
//r.db.lock.RLock() //r.db.lock.RLock()
//defer r.db.lock.RUnlock() //defer r.db.lock.RUnlock()
queryBuilder := sq. queryBuilder := sq.
Select("id", "filter_status", "rejections", "indexer", "filter", "protocol", "title", "torrent_name", "size", "timestamp", "COUNT() OVER() AS total_count"). Select("r.id", "r.filter_status", "r.rejections", "r.indexer", "r.filter", "r.protocol", "r.title", "r.torrent_name", "r.size", "r.timestamp", "COUNT() OVER() AS total_count").
From("release"). From("release r").
OrderBy("timestamp DESC") OrderBy("r.timestamp DESC")
if params.Limit > 0 { if params.Limit > 0 {
queryBuilder = queryBuilder.Limit(params.Limit) queryBuilder = queryBuilder.Limit(params.Limit)
@ -104,18 +104,22 @@ func (repo *ReleaseRepo) Find(ctx context.Context, params domain.QueryParams) ([
} }
if params.Cursor > 0 { if params.Cursor > 0 {
queryBuilder = queryBuilder.Where(sq.Lt{"id": params.Cursor}) queryBuilder = queryBuilder.Where(sq.Lt{"r.id": params.Cursor})
} }
if params.Filter != nil { if params.Filters.Indexers != nil {
filter := sq.And{} filter := sq.And{}
for k, v := range params.Filter { for _, v := range params.Filters.Indexers {
filter = append(filter, sq.Eq{k: v}) filter = append(filter, sq.Eq{"r.indexer": v})
} }
queryBuilder = queryBuilder.Where(filter) queryBuilder = queryBuilder.Where(filter)
} }
if params.Filters.PushStatus != "" {
queryBuilder = queryBuilder.InnerJoin("release_action_status ras ON r.id = ras.release_id").Where(sq.Eq{"ras.status": params.Filters.PushStatus})
}
query, args, err := queryBuilder.ToSql() query, args, err := queryBuilder.ToSql()
log.Trace().Str("database", "release.find").Msgf("query: '%v', args: '%v'", query, args) log.Trace().Str("database", "release.find").Msgf("query: '%v', args: '%v'", query, args)
@ -171,6 +175,46 @@ func (repo *ReleaseRepo) Find(ctx context.Context, params domain.QueryParams) ([
return res, nextCursor, countItems, nil return res, nextCursor, countItems, nil
} }
func (repo *ReleaseRepo) GetIndexerOptions(ctx context.Context) ([]string, error) {
//r.db.lock.RLock()
//defer r.db.lock.RUnlock()
query := `
SELECT DISTINCT indexer FROM "release"
UNION
SELECT DISTINCT identifier indexer FROM indexer;`
log.Trace().Str("database", "release.get_indexers").Msgf("query: '%v'", query)
res := make([]string, 0)
rows, err := repo.db.handler.QueryContext(ctx, query)
if err != nil {
log.Error().Stack().Err(err).Msg("error fetching indexer list")
return res, err
}
defer rows.Close()
if err := rows.Err(); err != nil {
log.Error().Stack().Err(err)
return res, err
}
for rows.Next() {
var indexer string
if err := rows.Scan(&indexer); err != nil {
log.Error().Stack().Err(err).Msg("release.find: error scanning data to struct")
return res, err
}
res = append(res, indexer)
}
return res, nil
}
func (repo *ReleaseRepo) GetActionStatusByReleaseID(ctx context.Context, releaseID int64) ([]domain.ReleaseActionStatus, error) { func (repo *ReleaseRepo) GetActionStatusByReleaseID(ctx context.Context, releaseID int64) ([]domain.ReleaseActionStatus, error) {
//r.db.lock.RLock() //r.db.lock.RLock()
//defer r.db.lock.RUnlock() //defer r.db.lock.RUnlock()

View file

@ -26,7 +26,8 @@ import (
type ReleaseRepo interface { type ReleaseRepo interface {
Store(ctx context.Context, release *Release) (*Release, error) Store(ctx context.Context, release *Release) (*Release, error)
Find(ctx context.Context, params QueryParams) (res []Release, nextCursor int64, count int64, err error) Find(ctx context.Context, params ReleaseQueryParams) (res []Release, nextCursor int64, count int64, err error)
GetIndexerOptions(ctx context.Context) ([]string, error)
GetActionStatusByReleaseID(ctx context.Context, releaseID int64) ([]ReleaseActionStatus, error) GetActionStatusByReleaseID(ctx context.Context, releaseID int64) ([]ReleaseActionStatus, error)
Stats(ctx context.Context) (*ReleaseStats, error) Stats(ctx context.Context) (*ReleaseStats, error)
StoreReleaseActionStatus(ctx context.Context, actionStatus *ReleaseActionStatus) error StoreReleaseActionStatus(ctx context.Context, actionStatus *ReleaseActionStatus) error
@ -1485,11 +1486,14 @@ const (
ReleaseImplementationIRC ReleaseImplementation = "IRC" ReleaseImplementationIRC ReleaseImplementation = "IRC"
) )
type QueryParams struct { type ReleaseQueryParams struct {
Limit uint64 Limit uint64
Offset uint64 Offset uint64
Cursor uint64 Cursor uint64
Sort map[string]string Sort map[string]string
Filter map[string]string Filters struct {
Indexers []string
PushStatus string
}
Search string Search string
} }

View file

@ -3,6 +3,7 @@ package http
import ( import (
"context" "context"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/internal/domain"
@ -10,7 +11,8 @@ import (
) )
type releaseService interface { type releaseService interface {
Find(ctx context.Context, query domain.QueryParams) (res []domain.Release, nextCursor int64, count int64, err error) Find(ctx context.Context, query domain.ReleaseQueryParams) (res []domain.Release, nextCursor int64, count int64, err error)
GetIndexerOptions(ctx context.Context) ([]string, error)
Stats(ctx context.Context) (*domain.ReleaseStats, error) Stats(ctx context.Context) (*domain.ReleaseStats, error)
} }
@ -29,6 +31,7 @@ func newReleaseHandler(encoder encoder, service releaseService) *releaseHandler
func (h releaseHandler) Routes(r chi.Router) { func (h releaseHandler) Routes(r chi.Router) {
r.Get("/", h.findReleases) r.Get("/", h.findReleases)
r.Get("/stats", h.getStats) r.Get("/stats", h.getStats)
r.Get("/indexers", h.getIndexerOptions)
} }
func (h releaseHandler) findReleases(w http.ResponseWriter, r *http.Request) { func (h releaseHandler) findReleases(w http.ResponseWriter, r *http.Request) {
@ -40,6 +43,7 @@ func (h releaseHandler) findReleases(w http.ResponseWriter, r *http.Request) {
"code": "BAD_REQUEST_PARAMS", "code": "BAD_REQUEST_PARAMS",
"message": "limit parameter is invalid", "message": "limit parameter is invalid",
}, http.StatusBadRequest) }, http.StatusBadRequest)
return
} }
if limit == 0 { if limit == 0 {
limit = 20 limit = 20
@ -52,23 +56,44 @@ func (h releaseHandler) findReleases(w http.ResponseWriter, r *http.Request) {
"code": "BAD_REQUEST_PARAMS", "code": "BAD_REQUEST_PARAMS",
"message": "offset parameter is invalid", "message": "offset parameter is invalid",
}, http.StatusBadRequest) }, http.StatusBadRequest)
return
} }
cursorP := r.URL.Query().Get("cursor") cursorP := r.URL.Query().Get("cursor")
cursor, err := strconv.Atoi(cursorP) cursor := 0
if err != nil && cursorP != "" { if cursorP != "" {
h.encoder.StatusResponse(r.Context(), w, map[string]interface{}{ cursor, err = strconv.Atoi(cursorP)
"code": "BAD_REQUEST_PARAMS", if err != nil && cursorP != "" {
"message": "cursor parameter is invalid", h.encoder.StatusResponse(r.Context(), w, map[string]interface{}{
}, http.StatusBadRequest) "code": "BAD_REQUEST_PARAMS",
"message": "cursor parameter is invalid",
}, http.StatusBadRequest)
}
return
} }
query := domain.QueryParams{ u, err := url.Parse(r.URL.String())
if err != nil {
h.encoder.StatusResponse(r.Context(), w, map[string]interface{}{
"code": "BAD_REQUEST_PARAMS",
"message": "indexer parameter is invalid",
}, http.StatusBadRequest)
return
}
vals := u.Query()
indexer := vals["indexer"]
pushStatus := r.URL.Query().Get("push_status")
query := domain.ReleaseQueryParams{
Limit: uint64(limit), Limit: uint64(limit),
Offset: uint64(offset), Offset: uint64(offset),
Cursor: uint64(cursor), Cursor: uint64(cursor),
Sort: nil, Sort: nil,
//Filter: "", Filters: struct {
Indexers []string
PushStatus string
}{Indexers: indexer, PushStatus: pushStatus},
} }
releases, nextCursor, count, err := h.service.Find(r.Context(), query) releases, nextCursor, count, err := h.service.Find(r.Context(), query)
@ -90,6 +115,16 @@ func (h releaseHandler) findReleases(w http.ResponseWriter, r *http.Request) {
h.encoder.StatusResponse(r.Context(), w, ret, http.StatusOK) h.encoder.StatusResponse(r.Context(), w, ret, http.StatusOK)
} }
func (h releaseHandler) getIndexerOptions(w http.ResponseWriter, r *http.Request) {
stats, err := h.service.GetIndexerOptions(r.Context())
if err != nil {
h.encoder.StatusNotFound(r.Context(), w)
return
}
h.encoder.StatusResponse(r.Context(), w, stats, http.StatusOK)
}
func (h releaseHandler) getStats(w http.ResponseWriter, r *http.Request) { func (h releaseHandler) getStats(w http.ResponseWriter, r *http.Request) {
stats, err := h.service.Stats(r.Context()) stats, err := h.service.Stats(r.Context())

View file

@ -10,7 +10,8 @@ import (
) )
type Service interface { type Service interface {
Find(ctx context.Context, query domain.QueryParams) (res []domain.Release, nextCursor int64, count int64, err error) Find(ctx context.Context, query domain.ReleaseQueryParams) (res []domain.Release, nextCursor int64, count int64, err error)
GetIndexerOptions(ctx context.Context) ([]string, error)
Stats(ctx context.Context) (*domain.ReleaseStats, error) Stats(ctx context.Context) (*domain.ReleaseStats, error)
Store(ctx context.Context, release *domain.Release) error Store(ctx context.Context, release *domain.Release) error
StoreReleaseActionStatus(ctx context.Context, actionStatus *domain.ReleaseActionStatus) error StoreReleaseActionStatus(ctx context.Context, actionStatus *domain.ReleaseActionStatus) error
@ -29,7 +30,7 @@ func NewService(repo domain.ReleaseRepo, actionService action.Service) Service {
} }
} }
func (s *service) Find(ctx context.Context, query domain.QueryParams) (res []domain.Release, nextCursor int64, count int64, err error) { func (s *service) Find(ctx context.Context, query domain.ReleaseQueryParams) (res []domain.Release, nextCursor int64, count int64, err error) {
res, nextCursor, count, err = s.repo.Find(ctx, query) res, nextCursor, count, err = s.repo.Find(ctx, query)
if err != nil { if err != nil {
return return
@ -38,6 +39,10 @@ func (s *service) Find(ctx context.Context, query domain.QueryParams) (res []dom
return return
} }
func (s *service) GetIndexerOptions(ctx context.Context) ([]string, error) {
return s.repo.GetIndexerOptions(ctx)
}
func (s *service) Stats(ctx context.Context) (*domain.ReleaseStats, error) { func (s *service) Stats(ctx context.Context) (*domain.ReleaseStats, error) {
stats, err := s.repo.Stats(ctx) stats, err := s.repo.Stats(ctx)
if err != nil { if err != nil {

View file

@ -97,6 +97,30 @@ export const APIClient = {
}, },
release: { release: {
find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`), find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`),
findQuery: (offset?: number, limit?: number, filters?: any[]) => {
let queryString = "?"
if (offset != 0) {
queryString += `offset=${offset}`
}
if (limit != 0) {
queryString += `&limit=${limit}`
}
if (filters && filters?.length > 0) {
filters?.map((filter) => {
if (filter.id === "indexer" && filter.value != "") {
queryString += `&indexer=${filter.value}`
}
// using action_status instead of push_status because thats the column accessor
if (filter.id === "action_status" && filter.value != "") {
queryString += `&push_status=${filter.value}`
}
})
}
return appClient.Get<ReleaseFindResponse>(`api/release${queryString}`)
},
indexerOptions: () => appClient.Get<string[]>(`api/release/indexers`),
stats: () => appClient.Get<ReleaseStats>("api/release/stats") stats: () => appClient.Get<ReleaseStats>("api/release/stats")
} }
}; };

View file

@ -210,4 +210,19 @@ export const ActionTypeNameMap = {
"RADARR": "Radarr", "RADARR": "Radarr",
"SONARR": "Sonarr", "SONARR": "Sonarr",
"LIDARR": "Lidarr", "LIDARR": "Lidarr",
}; };
export const PushStatusOptions: any[] = [
{
label: "Rejected",
value: "PUSH_REJECTED",
},
{
label: "Approved",
value: "PUSH_APPROVED"
},
{
label: "Error",
value: "PUSH_ERROR"
},
];

View file

@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { formatDistanceToNowStrict } from "date-fns"; import { formatDistanceToNowStrict } from "date-fns";
import { useTable, useSortBy, usePagination, Column } from "react-table"; import { useTable, useSortBy, usePagination, useAsyncDebounce, useFilters, Column } from "react-table";
import { import {
ClockIcon, ClockIcon,
BanIcon, BanIcon,
@ -12,13 +12,18 @@ import {
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
ChevronDoubleRightIcon, ChevronDoubleRightIcon,
CheckIcon CheckIcon,
ChevronDownIcon,
} from "@heroicons/react/solid"; } from "@heroicons/react/solid";
import { APIClient } from "../api/APIClient"; import { APIClient } from "../api/APIClient";
import { EmptyListState } from "../components/emptystates"; import { EmptyListState } from "../components/emptystates";
import { classNames, simplifyDate } from "../utils"; import { classNames, simplifyDate } from "../utils";
import { Fragment } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { PushStatusOptions } from "../domain/constants";
export function Releases() { export function Releases() {
return ( return (
<main> <main>
@ -34,6 +39,33 @@ export function Releases() {
) )
} }
// // Define a default UI for filtering
// function GlobalFilter({
// preGlobalFilteredRows,
// globalFilter,
// setGlobalFilter,
// }: any) {
// const count = preGlobalFilteredRows.length
// const [value, setValue] = React.useState(globalFilter)
// const onChange = useAsyncDebounce(value => {
// setGlobalFilter(value || undefined)
// }, 200)
// return (
// <span>
// Search:{' '}
// <input
// value={value || ""}
// onChange={e => {
// setValue(e.target.value);
// onChange(e.target.value);
// }}
// placeholder={`${count} records...`}
// />
// </span>
// )
// }
// This is a custom filter UI for selecting // This is a custom filter UI for selecting
// a unique option from a list // a unique option from a list
export function SelectColumnFilter({ export function SelectColumnFilter({
@ -46,11 +78,16 @@ export function SelectColumnFilter({
preFilteredRows.forEach((row: { values: { [x: string]: unknown } }) => { preFilteredRows.forEach((row: { values: { [x: string]: unknown } }) => {
options.add(row.values[id]) options.add(row.values[id])
}) })
return [...options.values()] return [...options.values()]
}, [id, preFilteredRows]) }, [id, preFilteredRows])
const opts = ["PUSH_REJECTED"]
// Render a multi-select box // Render a multi-select box
return ( return (
<div className="mb-6">
<label className="flex items-baseline gap-x-2"> <label className="flex items-baseline gap-x-2">
<span className="text-gray-700">{render("Header")}: </span> <span className="text-gray-700">{render("Header")}: </span>
<select <select
@ -63,13 +100,211 @@ export function SelectColumnFilter({
}} }}
> >
<option value="">All</option> <option value="">All</option>
{options.map((option, i) => ( {opts.map((option, i: number) => (
<option key={i} value={option}> <option key={i} value={option}>
{option} {option}
</option> </option>
))} ))}
</select> </select>
</label> </label>
</div>
)
}
// This is a custom filter UI for selecting
// a unique option from a list
export function IndexerSelectColumnFilter({
column: { filterValue, setFilter, id },
}: any) {
const { data, isSuccess } = useQuery(
['release_indexers'],
() => APIClient.release.indexerOptions(),
{
keepPreviousData: true,
staleTime: Infinity,
}
);
const opts = isSuccess && data?.map(i => ({ value: i, label: i})) as any[]
// Render a multi-select box
return (
<div className="mr-3">
<div className="w-48">
<Listbox
refName={id}
value={filterValue}
onChange={setFilter}
>
<div className="relative mt-1">
<Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white dark:bg-gray-800 rounded-lg shadow-md cursor-default focus:outline-none focus-visible:ring-2 focus-visible:ring-opacity-75 focus-visible:ring-white focus-visible:ring-offset-orange-300 focus-visible:ring-offset-2 focus-visible:border-indigo-500 dark:text-gray-400 sm:text-sm">
<span className="block truncate">{filterValue ? filterValue : "Indexer"}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<ChevronDownIcon
className="w-5 h-5 ml-2 -mr-1 text-gray-600 hover:text-gray-600"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute w-full py-1 mt-1 overflow-auto text-base bg-white dark:bg-gray-800 rounded-md shadow-lg max-h-60 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
<Listbox.Option
key={0}
className={({ active }) =>
`cursor-default select-none relative py-2 pl-10 pr-4 ${
active ? 'text-gray-500 dark:text-gray-200 bg-gray-300 dark:bg-gray-900' : 'text-gray-900 dark:text-gray-400'
}`
}
value={undefined}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? 'font-medium' : 'font-normal'
}`}
>
All
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500 dark:text-gray-400">
<CheckIcon className="w-5 h-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
{isSuccess && data?.map((indexer, idx) => (
<Listbox.Option
key={idx}
className={({ active }) =>
`cursor-default select-none relative py-2 pl-10 pr-4 ${
active ? 'text-gray-500 dark:text-gray-200 bg-gray-300 dark:bg-gray-900' : 'text-gray-900 dark:text-gray-400'
}`
}
value={indexer}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? 'font-medium' : 'font-normal'
}`}
>
{indexer}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500 dark:text-gray-400">
<CheckIcon className="w-5 h-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
</div>
)
}
export function PushStatusSelectColumnFilter({
column: { filterValue, setFilter, id },
}: any) {
return (
<div className="mr-3">
<div className="w-48">
<Listbox
refName={id}
value={filterValue}
onChange={setFilter}
>
<div className="relative mt-1">
<Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white dark:bg-gray-800 rounded-lg shadow-md cursor-default focus:outline-none focus-visible:ring-2 focus-visible:ring-opacity-75 focus-visible:ring-white focus-visible:ring-offset-orange-300 focus-visible:ring-offset-2 focus-visible:border-indigo-500 dark:text-gray-400 sm:text-sm">
<span className="block truncate">{filterValue ? PushStatusOptions.find((o) => o.value === filterValue && o.value)!.label : "Push status"}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<ChevronDownIcon
className="w-5 h-5 ml-2 -mr-1 text-gray-600 hover:text-gray-600"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute w-full py-1 mt-1 overflow-auto text-base bg-white dark:bg-gray-800 rounded-md shadow-lg max-h-60 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
<Listbox.Option
key={0}
className={({ active }) =>
`cursor-default select-none relative py-2 pl-10 pr-4 ${
active ? 'text-gray-500 dark:text-gray-200 bg-gray-300 dark:bg-gray-900' : 'text-gray-900 dark:text-gray-400'
}`
}
value={undefined}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? 'font-medium' : 'font-normal'
}`}
>
All
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500 dark:text-gray-400">
<CheckIcon className="w-5 h-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
{PushStatusOptions.map((status, idx) => (
<Listbox.Option
key={idx}
className={({ active }) =>
`cursor-default select-none relative py-2 pl-10 pr-4 ${
active ? 'text-gray-500 dark:text-gray-200 bg-gray-300 dark:bg-gray-900' : 'text-gray-900 dark:text-gray-400'
}`
}
value={status.value}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? 'font-medium' : 'font-normal'
}`}
>
{status.label}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500 dark:text-gray-400">
<CheckIcon className="w-5 h-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
</div>
) )
} }
@ -164,11 +399,13 @@ const initialState = {
queryPageIndex: 0, queryPageIndex: 0,
queryPageSize: 10, queryPageSize: 10,
totalCount: null, totalCount: null,
queryFilters: []
}; };
const PAGE_CHANGED = 'PAGE_CHANGED'; const PAGE_CHANGED = 'PAGE_CHANGED';
const PAGE_SIZE_CHANGED = 'PAGE_SIZE_CHANGED'; const PAGE_SIZE_CHANGED = 'PAGE_SIZE_CHANGED';
const TOTAL_COUNT_CHANGED = 'TOTAL_COUNT_CHANGED'; const TOTAL_COUNT_CHANGED = 'TOTAL_COUNT_CHANGED';
const FILTER_CHANGED = 'FILTER_CHANGED';
const reducer = (state: any, { type, payload }: any) => { const reducer = (state: any, { type, payload }: any) => {
switch (type) { switch (type) {
@ -187,6 +424,12 @@ const reducer = (state: any, { type, payload }: any) => {
...state, ...state,
totalCount: payload, totalCount: payload,
}; };
case FILTER_CHANGED:
return {
...state,
queryFilters: payload,
};
default: default:
throw new Error(`Unhandled action type: ${type}`); throw new Error(`Unhandled action type: ${type}`);
} }
@ -213,28 +456,38 @@ function Table() {
Header: "Actions", Header: "Actions",
accessor: 'action_status', accessor: 'action_status',
Cell: ReleaseStatusCell, Cell: ReleaseStatusCell,
Filter: PushStatusSelectColumnFilter, // new
}, },
{ {
Header: "Indexer", Header: "Indexer",
accessor: 'indexer', accessor: 'indexer',
Cell: IndexerCell, Cell: IndexerCell,
Filter: SelectColumnFilter, // new Filter: IndexerSelectColumnFilter, // new
filter: 'includes', filter: 'equal',
// filter: 'includes',
}, },
] as Column<Release>[], []) ] as Column<Release>[], [])
const [{ queryPageIndex, queryPageSize, totalCount }, dispatch] = const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
React.useReducer(reducer, initialState); React.useReducer(reducer, initialState);
const { isLoading, error, data, isSuccess } = useQuery( const { isLoading, error, data, isSuccess } = useQuery(
['releases', queryPageIndex, queryPageSize], ['releases', queryPageIndex, queryPageSize, queryFilters],
() => APIClient.release.find(`?offset=${queryPageIndex * queryPageSize}&limit=${queryPageSize}`), // () => APIClient.release.find(`?offset=${queryPageIndex * queryPageSize}&limit=${queryPageSize}${filterIndexer && `&indexer=${filterIndexer}`}`),
() => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
{ {
keepPreviousData: true, keepPreviousData: true,
staleTime: Infinity, staleTime: Infinity,
} }
); );
// const initialFilters = React.useMemo(() => [
// {
// id: "indexer",
// value: "",
// }
// ], [])
// Use the state and functions returned from useTable to build your UI // Use the state and functions returned from useTable to build your UI
const { const {
getTableProps, getTableProps,
@ -254,21 +507,28 @@ function Table() {
previousPage, previousPage,
setPageSize, setPageSize,
state: { pageIndex, pageSize }, state: { pageIndex, pageSize, globalFilter, filters },
// preGlobalFilteredRows, // preGlobalFilteredRows,
// setGlobalFilter, // setGlobalFilter,
// preFilteredRows,
} = useTable({ } = useTable({
columns, columns,
data: data && isSuccess ? data.data : [], data: data && isSuccess ? data.data : [],
initialState: { initialState: {
pageIndex: queryPageIndex, pageIndex: queryPageIndex,
pageSize: queryPageSize, pageSize: queryPageSize,
filters: []
// filters: initialFilters
}, },
manualPagination: true, manualPagination: true,
manualFilters: true,
manualSortBy: true, manualSortBy: true,
pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : 0, pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : 0,
autoResetSortBy: false,
autoResetExpanded: false,
autoResetPage: false
}, },
// useFilters, // useFilters! useFilters, // useFilters!
// useGlobalFilter, // useGlobalFilter,
useSortBy, useSortBy,
usePagination, // new usePagination, // new
@ -292,6 +552,11 @@ function Table() {
} }
}, [data?.count]); }, [data?.count]);
React.useEffect(() => {
dispatch({ type: FILTER_CHANGED, payload: filters });
}, [filters]);
if (error) { if (error) {
return <p>Error</p>; return <p>Error</p>;
} }
@ -305,6 +570,25 @@ function Table() {
<> <>
{isSuccess && data ? ( {isSuccess && data ? (
<div className="flex flex-col"> <div className="flex flex-col">
{/* <GlobalFilter
preGlobalFilteredRows={preGlobalFilteredRows}
globalFilter={globalFilter}
setGlobalFilter={setGlobalFilter}
preFilteredRows={preFilteredRows}
/> */}
<div className="flex mb-6">
{headerGroups.map((headerGroup: { headers: any[] }) =>
headerGroup.headers.map((column) =>
column.Filter ? (
<div className="mt-2 sm:mt-0" key={column.id}>
{column.render("Filter")}
</div>
) : null
)
)}
</div>
<div className="overflow-hidden bg-white shadow-lg dark:bg-gray-800 sm:rounded-lg"> <div className="overflow-hidden bg-white shadow-lg dark:bg-gray-800 sm:rounded-lg">
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800"> <thead className="bg-gray-50 dark:bg-gray-800">
@ -388,7 +672,7 @@ function Table() {
Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span> Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
</span> </span>
<label> <label>
<span className="sr-only">Items Per Page</span> <span className="sr-only bg-gray-700">Items Per Page</span>
<select <select
className="block w-full border-gray-300 rounded-md shadow-sm cursor-pointer dark:bg-gray-800 dark:border-gray-800 dark:text-gray-600 dark:hover:text-gray-500 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50" className="block w-full border-gray-300 rounded-md shadow-sm cursor-pointer dark:bg-gray-800 dark:border-gray-800 dark:text-gray-600 dark:hover:text-gray-500 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
value={pageSize} value={pageSize}
@ -411,29 +695,29 @@ function Table() {
onClick={() => gotoPage(0)} onClick={() => gotoPage(0)}
disabled={!canPreviousPage} disabled={!canPreviousPage}
> >
<span className="sr-only">First</span> <span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">First</span>
<ChevronDoubleLeftIcon className="w-5 h-5 text-gray-400" aria-hidden="true" /> <ChevronDoubleLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
</PageButton> </PageButton>
<PageButton <PageButton
onClick={() => previousPage()} onClick={() => previousPage()}
disabled={!canPreviousPage} disabled={!canPreviousPage}
> >
<span className="sr-only">Previous</span> <span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Previous</span>
<ChevronLeftIcon className="w-5 h-5 text-gray-400" aria-hidden="true" /> <ChevronLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
</PageButton> </PageButton>
<PageButton <PageButton
onClick={() => nextPage()} onClick={() => nextPage()}
disabled={!canNextPage}> disabled={!canNextPage}>
<span className="sr-only">Next</span> <span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Next</span>
<ChevronRightIcon className="w-5 h-5 text-gray-400" aria-hidden="true" /> <ChevronRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
</PageButton> </PageButton>
<PageButton <PageButton
className="rounded-r-md" className="rounded-r-md"
onClick={() => gotoPage(pageCount - 1)} onClick={() => gotoPage(pageCount - 1)}
disabled={!canNextPage} disabled={!canNextPage}
> >
<span className="sr-only">Last</span> <span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Last</span>
<ChevronDoubleRightIcon className="w-5 h-5 text-gray-400" aria-hidden="true" /> <ChevronDoubleRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
</PageButton> </PageButton>
</nav> </nav>
</div> </div>
@ -470,7 +754,7 @@ function Button({ children, className, ...rest }: any) {
type="button" type="button"
className={ className={
classNames( classNames(
"relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50", "relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-800 text-sm font-medium rounded-md text-gray-700 dark:text-gray-500 bg-white dark:bg-gray-800 hover:bg-gray-50",
className className
)} )}
{...rest} {...rest}
@ -486,7 +770,7 @@ function PageButton({ children, className, ...rest }: any) {
type="button" type="button"
className={ className={
classNames( classNames(
"relative inline-flex items-center px-2 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600", "relative inline-flex items-center px-2 py-2 border border-gray-300 dark:border-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600",
className className
)} )}
{...rest} {...rest}