mirror of
https://github.com/idanoo/autobrr
synced 2025-07-26 10:19:13 +00:00
214 lines
6.7 KiB
TypeScript
214 lines
6.7 KiB
TypeScript
/*
|
|
* Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
|
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
|
|
import * as React from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import {
|
|
useTable,
|
|
useFilters,
|
|
useGlobalFilter,
|
|
useSortBy,
|
|
usePagination, FilterProps, Column
|
|
} from "react-table";
|
|
|
|
import { APIClient } from "@api/APIClient";
|
|
import { EmptyListState } from "@components/emptystates";
|
|
import * as Icons from "@components/Icons";
|
|
import * as DataTable from "@components/data-table";
|
|
|
|
// This is a custom filter UI for selecting
|
|
// a unique option from a list
|
|
function SelectColumnFilter({
|
|
column: { filterValue, setFilter, preFilteredRows, id, render }
|
|
}: FilterProps<object>) {
|
|
// Calculate the options for filtering
|
|
// using the preFilteredRows
|
|
const options = React.useMemo(() => {
|
|
const options = new Set<string>();
|
|
preFilteredRows.forEach((row: { values: { [x: string]: string } }) => {
|
|
options.add(row.values[id]);
|
|
});
|
|
return [...options.values()];
|
|
}, [id, preFilteredRows]);
|
|
|
|
// Render a multi-select box
|
|
return (
|
|
<label className="flex items-baseline gap-x-2">
|
|
<span className="text-gray-700"><>{render("Header")}:</></span>
|
|
<select
|
|
className="border-gray-300 rounded-md shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
|
name={id}
|
|
id={id}
|
|
value={filterValue}
|
|
onChange={e => {
|
|
setFilter(e.target.value || undefined);
|
|
}}
|
|
>
|
|
<option value="">All</option>
|
|
{options.map((option, i) => (
|
|
<option key={i} value={option}>
|
|
{option}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
);
|
|
}
|
|
|
|
interface TableProps {
|
|
columns: Column[];
|
|
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
|
|
);
|
|
|
|
if (!page.length) {
|
|
return <EmptyListState text="No recent activity" />;
|
|
}
|
|
|
|
// Render the UI for your table
|
|
return (
|
|
<div className="inline-block min-w-full mt-4 mb-2 align-middle">
|
|
<div className="bg-white shadow-lg dark:bg-gray-800 rounded-md overflow-auto">
|
|
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
<thead className="bg-gray-50 dark:bg-gray-800">
|
|
{headerGroups.map((headerGroup) => {
|
|
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps();
|
|
return (
|
|
<tr key={rowKey} {...rowRest}>
|
|
{headerGroup.headers.map((column) => {
|
|
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps());
|
|
return (
|
|
// Add the sorting props to control sorting. For this example
|
|
// we can add them into the header props
|
|
<th
|
|
key={`${rowKey}-${columnKey}`}
|
|
scope="col"
|
|
className="first:pl-5 first:rounded-tl-md last:rounded-tr-md pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group"
|
|
{...columnRest}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<>{column.render("Header")}</>
|
|
{/* Add a sort direction indicator */}
|
|
<span>
|
|
{column.isSorted ? (
|
|
column.isSortedDesc ? (
|
|
<Icons.SortDownIcon className="w-4 h-4 text-gray-400" />
|
|
) : (
|
|
<Icons.SortUpIcon className="w-4 h-4 text-gray-400" />
|
|
)
|
|
) : (
|
|
<Icons.SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100" />
|
|
)}
|
|
</span>
|
|
</div>
|
|
</th>
|
|
);
|
|
})}
|
|
</tr>
|
|
);
|
|
})}
|
|
</thead>
|
|
<tbody
|
|
{...getTableBodyProps()}
|
|
className="divide-y divide-gray-200 dark:divide-gray-700"
|
|
>
|
|
{page.map((row) => {
|
|
prepareRow(row);
|
|
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
|
return (
|
|
<tr key={bodyRowKey} {...bodyRowRest}>
|
|
{row.cells.map((cell) => {
|
|
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
|
return (
|
|
<td
|
|
key={cellRowKey}
|
|
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
|
role="cell"
|
|
{...cellRowRest}
|
|
>
|
|
<>{cell.render("Cell")}</>
|
|
</td>
|
|
);
|
|
})}
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export const ActivityTable = () => {
|
|
const columns = React.useMemo(() => [
|
|
{
|
|
Header: "Age",
|
|
accessor: "timestamp",
|
|
Cell: DataTable.AgeCell
|
|
},
|
|
{
|
|
Header: "Release",
|
|
accessor: "torrent_name",
|
|
Cell: DataTable.TitleCell
|
|
},
|
|
{
|
|
Header: "Actions",
|
|
accessor: "action_status",
|
|
Cell: DataTable.ReleaseStatusCell
|
|
},
|
|
{
|
|
Header: "Indexer",
|
|
accessor: "indexer",
|
|
Cell: DataTable.TitleCell,
|
|
Filter: SelectColumnFilter,
|
|
filter: "includes"
|
|
}
|
|
], []);
|
|
|
|
const { isLoading, data } = useQuery({
|
|
queryKey: ["dash_recent_releases"],
|
|
queryFn: APIClient.release.findRecent,
|
|
refetchOnWindowFocus: false
|
|
});
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex flex-col mt-12">
|
|
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
|
|
|
|
</h3>
|
|
<div className="animate-pulse text-black dark:text-white">
|
|
<EmptyListState text="Loading..."/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col mt-12">
|
|
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
|
|
Recent activity
|
|
</h3>
|
|
|
|
<Table columns={columns} data={data?.data ?? []} />
|
|
</div>
|
|
);
|
|
};
|