refactor: web api client and cleanup (#128)

refactor: refactored APIClient.ts with a new fetch wrapper and changed it into an explicit-import.

chore: modified package.json not to start browser on "npm run start"

chore: cleaned up code, deleted 2mo+ useless old comments.

fix: fixed parameter collision in screens/filters/details.tsx

fix: override react-select's Select component style to make it consistent. addresses #116

Co-authored-by: anonymous <anonymous>
This commit is contained in:
stacksmash76 2022-02-10 17:47:05 +01:00 committed by GitHub
parent 6d68a5c3b7
commit b60e5f61c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 381 additions and 793 deletions

View file

@ -23,7 +23,7 @@
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "BROWSER=none react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",

View file

@ -1,72 +1,55 @@
import {baseUrl, sseBaseUrl} from "../utils"; import {baseUrl, sseBaseUrl} from "../utils";
function baseClient(endpoint: string, method: string, { body, ...customConfig}: any = {}) { interface ConfigType {
const baseURL = baseUrl() body?: BodyInit | Record<string, unknown> | null;
headers?: Record<string, string>;
}
const headers = {'content-type': 'application/json'} export async function HttpClient<T>(
endpoint: string,
method: string,
{ body, ...customConfig }: ConfigType = {}
): Promise<T> {
const config = { const config = {
method: method, method: method,
...customConfig, body: body ? JSON.stringify(body) : null,
headers: { headers: {
...headers, "Content-Type": "application/json"
...customConfig.headers,
}, },
} // NOTE: customConfig can override the above defined settings
...customConfig
} as RequestInit;
if (body) { return window.fetch(`${baseUrl()}${endpoint}`, config)
config.body = JSON.stringify(body)
}
return window.fetch(`${baseURL}${endpoint}`, config)
.then(async response => { .then(async response => {
if (response.status === 401) { if ([401, 403, 404].includes(response.status))
// unauthorized return Promise.reject(new Error(response.statusText));
// window.location.assign(window.location)
return Promise.reject(new Error(response.statusText)) if ([201, 204].includes(response.status))
} return Promise.resolve(response);
if (response.status === 403) {
// window.location.assign("/login")
return Promise.reject(new Error(response.statusText))
// return
}
if (response.status === 404) {
return Promise.reject(new Error(response.statusText))
}
if (response.status === 201) {
return ""
}
if (response.status === 204) {
return ""
}
if (response.ok) { if (response.ok) {
return await response.json() return await response.json();
} else { } else {
const errorMessage = await response.text() const errorMessage = await response.text();
return Promise.reject(new Error(errorMessage));
return Promise.reject(new Error(errorMessage))
} }
}) });
} }
const appClient = { const appClient = {
Get: (endpoint: string) => baseClient(endpoint, "GET"), Get: <T>(endpoint: string) => HttpClient<T>(endpoint, "GET"),
Post: (endpoint: string, data: any) => baseClient(endpoint, "POST", { body: data }), Post: (endpoint: string, data: any) => HttpClient<void>(endpoint, "POST", { body: data }),
Put: (endpoint: string, data: any) => baseClient(endpoint, "PUT", { body: data }), Put: (endpoint: string, data: any) => HttpClient<void>(endpoint, "PUT", { body: data }),
Patch: (endpoint: string, data: any) => baseClient(endpoint, "PATCH", { body: data }), Patch: (endpoint: string, data: any) => HttpClient<void>(endpoint, "PATCH", { body: data }),
Delete: (endpoint: string) => baseClient(endpoint, "DELETE"), Delete: (endpoint: string) => HttpClient<void>(endpoint, "DELETE")
} }
const APIClient = { export const APIClient = {
auth: { auth: {
login: (username: string, password: string) => appClient.Post("api/auth/login", { username: username, password: password }), login: (username: string, password: string) => appClient.Post("api/auth/login", { username: username, password: password }),
logout: () => appClient.Post(`api/auth/logout`, null), logout: () => appClient.Post("api/auth/logout", null),
test: () => appClient.Get(`api/auth/test`), test: () => appClient.Get<void>("api/auth/test"),
}, },
actions: { actions: {
create: (action: Action) => appClient.Post("api/actions", action), create: (action: Action) => appClient.Post("api/actions", action),
@ -75,34 +58,37 @@ const APIClient = {
toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null), toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null),
}, },
config: { config: {
get: () => appClient.Get("api/config") get: () => appClient.Get<Config>("api/config")
}, },
download_clients: { download_clients: {
getAll: () => appClient.Get("api/download_clients"), getAll: () => appClient.Get<DownloadClient[]>("api/download_clients"),
create: (dc: DownloadClient) => appClient.Post(`api/download_clients`, dc), create: (dc: DownloadClient) => appClient.Post("api/download_clients", dc),
update: (dc: DownloadClient) => appClient.Put(`api/download_clients`, dc), update: (dc: DownloadClient) => appClient.Put("api/download_clients", dc),
delete: (id: number) => appClient.Delete(`api/download_clients/${id}`), delete: (id: number) => appClient.Delete(`api/download_clients/${id}`),
test: (dc: DownloadClient) => appClient.Post(`api/download_clients/test`, dc), test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc),
}, },
filters: { filters: {
getAll: () => appClient.Get("api/filters"), getAll: () => appClient.Get<Filter[]>("api/filters"),
getByID: (id: number) => appClient.Get(`api/filters/${id}`), getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`),
create: (filter: Filter) => appClient.Post(`api/filters`, filter), create: (filter: Filter) => appClient.Post("api/filters", filter),
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter), update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter),
toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }), toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }),
delete: (id: number) => appClient.Delete(`api/filters/${id}`), delete: (id: number) => appClient.Delete(`api/filters/${id}`),
}, },
indexers: { indexers: {
getOptions: () => appClient.Get("api/indexer/options"), // returns indexer options for all currently present/enabled indexers
getAll: () => appClient.Get("api/indexer"), getOptions: () => appClient.Get<Indexer[]>("api/indexer/options"),
getSchema: () => appClient.Get("api/indexer/schema"), // returns indexer definitions for all currently present/enabled indexers
create: (indexer: Indexer) => appClient.Post(`api/indexer`, indexer), getAll: () => appClient.Get<IndexerDefinition[]>("api/indexer"),
update: (indexer: Indexer) => appClient.Put(`api/indexer`, indexer), // returns all possible indexer definitions
getSchema: () => appClient.Get<IndexerDefinition[]>("api/indexer/schema"),
create: (indexer: Indexer) => appClient.Post("api/indexer", indexer),
update: (indexer: Indexer) => appClient.Put("api/indexer", indexer),
delete: (id: number) => appClient.Delete(`api/indexer/${id}`), delete: (id: number) => appClient.Delete(`api/indexer/${id}`),
}, },
irc: { irc: {
getNetworks: () => appClient.Get("api/irc"), getNetworks: () => appClient.Get<IrcNetwork[]>("api/irc"),
createNetwork: (network: Network) => appClient.Post(`api/irc`, network), createNetwork: (network: Network) => appClient.Post("api/irc", network),
updateNetwork: (network: Network) => appClient.Put(`api/irc/network/${network.id}`, network), updateNetwork: (network: Network) => appClient.Put(`api/irc/network/${network.id}`, network),
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`), deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`),
}, },
@ -110,9 +96,7 @@ const APIClient = {
logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true }) logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true })
}, },
release: { release: {
find: (query?: string) => appClient.Get(`api/release${query}`), find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`),
stats: () => appClient.Get(`api/release/stats`) stats: () => appClient.Get<ReleaseStats>("api/release/stats")
} }
} };
export default APIClient;

View file

@ -7,7 +7,7 @@ import { Field, Form, Formik } from "formik";
import type { FieldProps } from "formik"; import type { FieldProps } from "formik";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
import APIClient from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import DEBUG from "../../components/debug"; import DEBUG from "../../components/debug";
import Toast from '../../components/notifications/Toast'; import Toast from '../../components/notifications/Toast';

View file

@ -6,7 +6,7 @@ import { sleep, classNames } from "../../utils";
import { Form, Formik, useFormikContext } from "formik"; import { Form, Formik, useFormikContext } from "formik";
import DEBUG from "../../components/debug"; import DEBUG from "../../components/debug";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
import APIClient from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { DownloadClientTypeOptions } from "../../domain/constants"; import { DownloadClientTypeOptions } from "../../domain/constants";
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'

View file

@ -11,7 +11,7 @@ import { Dialog, Transition } from "@headlessui/react";
import { sleep } from "../../utils"; import { sleep } from "../../utils";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
import DEBUG from "../../components/debug"; import DEBUG from "../../components/debug";
import APIClient from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { import {
TextFieldWide, TextFieldWide,
PasswordFieldWide, PasswordFieldWide,
@ -48,13 +48,22 @@ const Menu = (props: any) => {
); );
} }
const Option = (props: any) => {
return (
<components.Option
{...props}
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
/>
);
}
interface AddProps { interface AddProps {
isOpen: boolean; isOpen: boolean;
toggle: any; toggle: any;
} }
export function IndexerAddForm({ isOpen, toggle }: AddProps) { export function IndexerAddForm({ isOpen, toggle }: AddProps) {
const { data } = useQuery<IndexerDefinition[], Error>('indexerDefinition', APIClient.indexers.getSchema, const { data } = useQuery('indexerDefinition', APIClient.indexers.getSchema,
{ {
enabled: isOpen, enabled: isOpen,
refetchOnWindowFocus: false refetchOnWindowFocus: false
@ -237,8 +246,14 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
<Select {...field} <Select {...field}
isClearable={true} isClearable={true}
isSearchable={true} isSearchable={true}
components={{ Input, Control, Menu }} components={{ Input, Control, Menu, Option }}
placeholder="Choose an indexer" placeholder="Choose an indexer"
styles={{
singleValue: (base) => ({
...base,
color: "unset"
})
}}
value={field?.value && field.value.value} value={field?.value && field.value.value}
onChange={(option: any) => { onChange={(option: any) => {
setFieldValue("name", option?.label ?? "") setFieldValue("name", option?.label ?? "")

View file

@ -5,7 +5,7 @@ import { Field, FieldArray } from "formik";
import type { FieldProps } from "formik"; import type { FieldProps } from "formik";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
import APIClient from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { import {
TextFieldWide, TextFieldWide,

View file

@ -9,7 +9,7 @@ import {
usePagination usePagination
} from "react-table"; } from "react-table";
import APIClient from "../api/APIClient"; import { APIClient } from "../api/APIClient";
import { EmptyListState } from "../components/emptystates"; import { EmptyListState } from "../components/emptystates";
import { ReleaseStatusCell } from "./Releases"; import { ReleaseStatusCell } from "./Releases";
@ -18,7 +18,7 @@ export function Dashboard() {
<main className="py-10 -mt-48"> <main className="py-10 -mt-48">
<div className="px-4 pb-8 mx-auto max-w-7xl sm:px-6 lg:px-8"> <div className="px-4 pb-8 mx-auto max-w-7xl sm:px-6 lg:px-8">
<Stats /> <Stats />
<DataTablee /> <DataTable />
</div> </div>
</main> </main>
) )
@ -40,15 +40,14 @@ const StatsItem = ({ name, stat }: any) => (
) )
function Stats() { function Stats() {
const { isLoading, data } = useQuery<ReleaseStats, Error>('dash_release_staats', () => APIClient.release.stats(), const { isLoading, data } = useQuery(
{ 'dash_release_stats',
refetchOnWindowFocus: false () => APIClient.release.stats(),
} { refetchOnWindowFocus: false }
) );
if (isLoading) { if (isLoading)
return null return null;
}
return ( return (
<div> <div>
@ -64,255 +63,6 @@ function Stats() {
) )
} }
/* function RecentActivity() {
let data: any[] = [
{
id: 1,
status: "FILTERED",
created_at: "2021-10-16 20:25:26",
indexer: "tl",
title: "That movie 2019 1080p x264-GROUP",
},
{
id: 2,
status: "PUSH_APPROVED",
created_at: "2021-10-15 16:16:23",
indexer: "tl",
title: "That great movie 2009 1080p x264-1GROUP",
},
{
id: 3,
status: "FILTER_REJECTED",
created_at: "2021-10-15 10:16:23",
indexer: "tl",
title: "Movie 1 2002 720p x264-1GROUP",
},
{
id: 4,
status: "PUSH_APPROVED",
created_at: "2021-10-14 16:16:23",
indexer: "tl",
title: "That bad movie 2019 2160p x265-1GROUP",
},
{
id: 5,
status: "PUSH_REJECTED",
created_at: "2021-10-13 16:16:23",
indexer: "tl",
title: "That really bad movie 20010 1080p x264-GROUP2",
},
]
return (
<div className="flex flex-col mt-12">
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-600">Recent activity</h3>
<div className="mt-3 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 sm:px-6 lg:px-8">
<div className="overflow-hidden light:shadow light:border-b light:border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="light:bg-gray-50 dark:bg-gray-800">
<tr>
<th
scope="col"
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-gray-400"
>
Age
</th>
<th
scope="col"
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-gray-400"
>
Release
</th>
<th
scope="col"
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-gray-400"
>
Status
</th>
<th
scope="col"
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-gray-400"
>
Indexer
</th>
</tr>
</thead>
<tbody className="bg-gray-800 divide-y divide-gray-200 light:bg-white dark:divide-gray-700">
{data && data.length > 0 ?
data.map((release: any, idx) => (
<ListItem key={idx} idx={idx} release={release} />
))
: <span>No recent activity</span>}
</tbody>
</table>
<nav
className="flex items-center justify-between px-4 py-3 bg-white border-t border-gray-200 dark:bg-gray-800 dark:border-gray-700 sm:px-6"
aria-label="Pagination"
>
<div className="hidden sm:block">
<p className="text-sm text-gray-700 dark:text-gray-500">
Showing <span className="font-medium">1</span> to <span className="font-medium">10</span> of{' '}
<span className="font-medium">20</span> results
</p>
</div>
<div className="flex items-center justify-between flex-1 sm:justify-end">
<p className="relative text-sm text-gray-700 dark:text-gray-500">
Show <span className="font-medium">10</span>
</p>
<Menu as="div" className="relative text-left">
<Menu.Button className="flex items-center text-sm font-medium text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600">
<span>Show</span>
<ChevronDownIcon className="w-5 h-5 ml-1 text-gray-500" aria-hidden="true" />
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-30 w-40 mt-2 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{[5, 10, 25, 50].map((child) => (
<Menu.Item key={child}>
{({ active }) => (
<a
// href={child.href}
className={classNames(
active ? 'bg-gray-100' : '',
'block px-4 py-2 text-sm text-gray-700'
)}
>
{child}
</a>
)}
</Menu.Item>
))}
</div>
</Menu.Items>
</Transition>
</Menu>
<a
href="#"
// className="px-4 py-2 mr-4 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm dark:bg-gray-700 dark:border-gray-600 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
className="relative inline-flex items-center px-4 py-2 ml-5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md dark:border-gray-600 dark:text-gray-400 dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600"
>
Previous
</a>
<a
href="#"
// className="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
className="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md dark:border-gray-600 dark:text-gray-400 dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600"
>
Next
</a>
</div>
</nav>
</div>
</div>
</div>
</div>
)
} */
/* const ListItem = ({ idx, release }: any) => {
const formatDate = formatDistanceToNowStrict(
new Date(release.created_at),
{ addSuffix: true }
)
return (
<tr key={release.id} className={idx % 2 === 0 ? 'light:bg-white' : 'light:bg-gray-50'}>
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400" title={release.created_at}>{formatDate}</td>
<td className="px-6 py-4 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-gray-300">{release.title}</td>
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap dark:text-gray-300">{statusMap[release.status]}</td>
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap dark:text-gray-300">{release.indexer}</td>
</tr>
)
} */
/*
const getData = () => {
const data: any[] = [
{
id: 1,
status: "FILTERED",
created_at: "2021-10-16 20:25:26",
indexer: "tl",
title: "That movie 2019 1080p x264-GROUP",
},
{
id: 2,
status: "PUSH_APPROVED",
created_at: "2021-10-15 16:16:23",
indexer: "tl",
title: "That great movie 2009 1080p x264-1GROUP",
},
{
id: 3,
status: "FILTER_REJECTED",
created_at: "2021-10-15 10:16:23",
indexer: "tl",
title: "Movie 1 2002 720p x264-1GROUP",
},
{
id: 4,
status: "PUSH_APPROVED",
created_at: "2021-10-14 16:16:23",
indexer: "tl",
title: "That bad movie 2019 2160p x265-1GROUP",
},
{
id: 5,
status: "PUSH_REJECTED",
created_at: "2021-10-13 16:16:23",
indexer: "tl",
title: "That really bad movie 20010 1080p x264-GROUP2",
},
]
return [...data, ...data, ...data]
} */
// 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: any) => {
setGlobalFilter(value || undefined)
}, 200)
return (
<label className="flex items-baseline gap-x-2">
<span className="text-gray-700">Search: </span>
<input
type="text"
className="border-gray-300 rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
value={value || ""}
onChange={e => {
setValue(e.target.value);
onChange(e.target.value);
}}
placeholder={`${count} records...`}
/>
</label>
)
} */
// 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({
@ -352,26 +102,6 @@ export function SelectColumnFilter({
) )
} }
// export function StatusPill({ value }: any) {
// const status = value ? value.toLowerCase() : "unknown";
// return (
// <span
// className={
// classNames(
// "px-3 py-1 uppercase leading-wide font-bold text-xs rounded-full shadow-sm",
// status.startsWith("active") ? "bg-green-100 text-green-800" : "",
// status.startsWith("inactive") ? "bg-yellow-100 text-yellow-800" : "",
// status.startsWith("offline") ? "bg-red-100 text-red-800" : "",
// )
// }
// >
// {status}
// </span>
// );
// };
export function StatusPill({ value }: any) { export function StatusPill({ value }: any) {
const statusMap: any = { const statusMap: any = {
"FILTER_APPROVED": <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase bg-blue-100 text-blue-800 ">Approved</span>, "FILTER_APPROVED": <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase bg-blue-100 text-blue-800 ">Approved</span>,
@ -439,28 +169,13 @@ function Table({ columns, data }: any) {
useGlobalFilter, useGlobalFilter,
useSortBy, useSortBy,
usePagination, // new usePagination, // new
) );
if (!page.length)
return <EmptyListState text="No recent activity" />;
// Render the UI for your table // Render the UI for your table
return ( return (
<>
<div className="sm:flex sm:gap-x-2">
{/* <GlobalFilter
preGlobalFilteredRows={preGlobalFilteredRows}
globalFilter={state.globalFilter}
setGlobalFilter={setGlobalFilter}
/> */}
{/* {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>
{page.length > 0 ?
<div className="flex flex-col mt-4"> <div className="flex flex-col mt-4">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> <div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
@ -534,81 +249,11 @@ function Table({ columns, data }: any) {
})} })}
</tbody> </tbody>
</table> </table>
{/* Pagination */}
{/* <div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700">
<div className="flex justify-between flex-1 sm:hidden">
<Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</Button>
<Button onClick={() => nextPage()} disabled={!canNextPage}>Next</Button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div className="flex items-baseline gap-x-2">
<span className="text-sm text-gray-700">
Page <span className="font-medium">{state.pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
</span>
<label>
<span className="sr-only">Items Per Page</span>
<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"
value={state.pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
}}
>
{[5, 10, 20].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</label>
</div>
<div>
<nav className="relative z-0 inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
<PageButton
className="rounded-l-md"
onClick={() => gotoPage(0)}
disabled={!canPreviousPage}
>
<span className="sr-only">First</span>
<ChevronDoubleLeftIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
</PageButton>
<PageButton
onClick={() => previousPage()}
disabled={!canPreviousPage}
>
<span className="sr-only">Previous</span>
<ChevronLeftIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
</PageButton>
<PageButton
onClick={() => nextPage()}
disabled={!canNextPage
}>
<span className="sr-only">Next</span>
<ChevronRightIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
</PageButton>
<PageButton
className="rounded-r-md"
onClick={() => gotoPage(pageCount - 1)}
disabled={!canNextPage}
>
<span className="sr-only">Last</span>
<ChevronDoubleRightIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
</PageButton>
</nav>
</div>
</div>
</div> */}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
: <EmptyListState text="No recent activity"/>} );
</>
)
} }
function SortIcon({ className }: any) { function SortIcon({ className }: any) {
@ -629,40 +274,7 @@ function SortDownIcon({ className }: any) {
) )
} }
/* function Button({ children, className, ...rest }: any) { function DataTable() {
return (
<button
type="button"
className={
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",
className
)}
{...rest}
>
{children}
</button>
)
}
function PageButton({ children, className, ...rest }: any) {
return (
<button
type="button"
className={
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",
className
)}
{...rest}
>
{children}
</button>
)
} */
function DataTablee() {
const columns = React.useMemo(() => [ const columns = React.useMemo(() => [
{ {
Header: "Age", Header: "Age",
@ -674,11 +286,6 @@ function DataTablee() {
accessor: 'torrent_name', accessor: 'torrent_name',
Cell: ReleaseCell, Cell: ReleaseCell,
}, },
// {
// Header: "Filter Status",
// accessor: 'filter_status',
// Cell: StatusPill,
// },
{ {
Header: "Actions", Header: "Actions",
accessor: 'action_status', accessor: 'action_status',
@ -693,23 +300,22 @@ function DataTablee() {
}, },
], []) ], [])
// const data = React.useMemo(() => getData(), []) const { isLoading, data } = useQuery(
'dash_release',
() => APIClient.release.find("?limit=10"),
{ refetchOnWindowFocus: false }
);
const { isLoading, data } = useQuery<ReleaseFindResponse, Error>('dash_release', () => APIClient.release.find("?limit=10"), if (isLoading)
{ return null;
refetchOnWindowFocus: false
}
)
if (isLoading) {
return null
}
return ( return (
<div className="flex flex-col mt-12"> <div className="flex flex-col mt-12">
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-600">Recent activity</h3> <h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-600">
Recent activity
</h3>
<Table columns={columns} data={data?.data} /> <Table columns={columns} data={data?.data} />
</div> </div>
) );
} }

View file

@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import APIClient from "../api/APIClient"; import { APIClient } from "../api/APIClient";
type LogEvent = { type LogEvent = {
time: string; time: string;

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 } from "react-table"; import { useTable, useSortBy, usePagination, Column } from "react-table";
import { import {
ClockIcon, ClockIcon,
BanIcon, BanIcon,
@ -15,7 +15,7 @@ import {
CheckIcon CheckIcon
} 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";
@ -222,7 +222,7 @@ function Table() {
Filter: SelectColumnFilter, // new Filter: SelectColumnFilter, // new
filter: 'includes', filter: 'includes',
}, },
], []) ] as Column<Release>[], [])
const [{ queryPageIndex, queryPageSize, totalCount }, dispatch] = const [{ queryPageIndex, queryPageSize, totalCount }, dispatch] =
React.useReducer(reducer, initialState); React.useReducer(reducer, initialState);
@ -260,7 +260,7 @@ function Table() {
// setGlobalFilter, // setGlobalFilter,
} = useTable({ } = useTable({
columns, columns,
data: isSuccess ? data.data : [], data: data && isSuccess ? data.data : [],
initialState: { initialState: {
pageIndex: queryPageIndex, pageIndex: queryPageIndex,
pageSize: queryPageSize, pageSize: queryPageSize,

View file

@ -2,7 +2,7 @@ import { useHistory } from "react-router-dom";
import { useMutation } from "react-query"; import { useMutation } from "react-query";
import { Form, Formik } from "formik"; import { Form, Formik } from "formik";
import APIClient from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { TextField, PasswordField } from "../../components/inputs"; import { TextField, PasswordField } from "../../components/inputs";
import logo from "../../logo.png"; import logo from "../../logo.png";

View file

@ -2,7 +2,7 @@ import {useEffect} from "react";
import {useCookies} from "react-cookie"; import {useCookies} from "react-cookie";
import {useHistory} from "react-router-dom"; import {useHistory} from "react-router-dom";
import APIClient from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { AuthContext } from "../../utils/Context"; import { AuthContext } from "../../utils/Context";
function Logout() { function Logout() {

View file

@ -29,7 +29,7 @@ import {
RELEASE_TYPE_MUSIC_OPTIONS RELEASE_TYPE_MUSIC_OPTIONS
} from "../../domain/constants"; } from "../../domain/constants";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
import APIClient from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "../../hooks/hooks";
import { buildPath, classNames } from "../../utils"; import { buildPath, classNames } from "../../utils";
@ -134,7 +134,7 @@ export default function FilterDetails() {
const { url } = useRouteMatch(); const { url } = useRouteMatch();
const { filterId } = useParams<{ filterId: string }>(); const { filterId } = useParams<{ filterId: string }>();
const { isLoading, data: filter } = useQuery<Filter, Error>( const { isLoading, data: filter } = useQuery(
['filter', +filterId], ['filter', +filterId],
() => APIClient.filters.getByID(parseInt(filterId)), () => APIClient.filters.getByID(parseInt(filterId)),
{ {
@ -144,14 +144,15 @@ export default function FilterDetails() {
} }
); );
const updateMutation = useMutation((filter: Filter) => APIClient.filters.update(filter), { const updateMutation = useMutation(
onSuccess: (filter) => { (filter: Filter) => APIClient.filters.update(filter),
// queryClient.setQueryData(['filter', filter.id], data) {
toast.custom((t) => <Toast type="success" body={`${filter.name} was updated successfully`} t={t} />) onSuccess: () => {
toast.custom((t) => <Toast type="success" body={`${filter?.name} was updated successfully`} t={t} />)
queryClient.invalidateQueries(["filter", filter.id]); queryClient.invalidateQueries(["filter", filter?.id]);
} }
}) }
);
const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), { const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), {
onSuccess: () => { onSuccess: () => {
@ -315,11 +316,11 @@ export default function FilterDetails() {
} }
function General() { function General() {
const { isLoading, data: indexers } = useQuery<Indexer[], Error>(["filter", "indexer_list"], APIClient.indexers.getOptions, const { isLoading, data: indexers } = useQuery(
{ ["filter", "indexer_list"],
refetchOnWindowFocus: false APIClient.indexers.getOptions,
} { refetchOnWindowFocus: false }
) );
const opts = indexers && indexers.length > 0 ? indexers.map(v => ({ const opts = indexers && indexers.length > 0 ? indexers.map(v => ({
label: v.name, label: v.name,
@ -592,11 +593,11 @@ interface FilterActionsProps {
} }
function FilterActions({ filter, values }: FilterActionsProps) { function FilterActions({ filter, values }: FilterActionsProps) {
const { data } = useQuery<DownloadClient[], Error>(['filter', 'download_clients'], APIClient.download_clients.getAll, const { data } = useQuery(
{ ['filter', 'download_clients'],
refetchOnWindowFocus: false APIClient.download_clients.getAll,
} { refetchOnWindowFocus: false }
) );
const newAction = { const newAction = {
name: "new action", name: "new action",

View file

@ -8,17 +8,17 @@ import { queryClient } from "../../App";
import { classNames } from "../../utils"; import { classNames } from "../../utils";
import { FilterAddForm } from "../../forms"; import { FilterAddForm } from "../../forms";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "../../hooks/hooks";
import APIClient from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import Toast from "../../components/notifications/Toast"; import Toast from "../../components/notifications/Toast";
import { EmptyListState } from "../../components/emptystates"; import { EmptyListState } from "../../components/emptystates";
export default function Filters() { export default function Filters() {
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false) const [createFilterIsOpen, toggleCreateFilter] = useToggle(false)
const { isLoading, error, data } = useQuery<Filter[], Error>('filter', APIClient.filters.getAll, const { isLoading, error, data } = useQuery(
{ 'filter',
refetchOnWindowFocus: false APIClient.filters.getAll,
} { refetchOnWindowFocus: false }
); );
if (isLoading) { if (isLoading) {

View file

@ -1,6 +1,6 @@
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import APIClient from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { Checkbox } from "../../components/Checkbox"; import { Checkbox } from "../../components/Checkbox";
import { SettingsContext } from "../../utils/Context"; import { SettingsContext } from "../../utils/Context";
@ -8,7 +8,7 @@ import { SettingsContext } from "../../utils/Context";
function ApplicationSettings() { function ApplicationSettings() {
const [settings, setSettings] = SettingsContext.use(); const [settings, setSettings] = SettingsContext.use();
const { isLoading, data } = useQuery<Config, Error>( const { isLoading, data } = useQuery(
['config'], ['config'],
() => APIClient.config.get(), () => APIClient.config.get(),
{ {

View file

@ -4,7 +4,7 @@ import { useQuery } from "react-query";
import { classNames } from "../../utils"; import { classNames } from "../../utils";
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms"; import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
import { EmptySimple } from "../../components/emptystates"; import { EmptySimple } from "../../components/emptystates";
import APIClient from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { DownloadClientTypeNameMap } from "../../domain/constants"; import { DownloadClientTypeNameMap } from "../../domain/constants";
interface DLSettingsItemProps { interface DLSettingsItemProps {
@ -53,10 +53,11 @@ function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
function DownloadClientSettings() { function DownloadClientSettings() {
const [addClientIsOpen, toggleAddClient] = useToggle(false) const [addClientIsOpen, toggleAddClient] = useToggle(false)
const { error, data } = useQuery<DownloadClient[], Error>('downloadClients', APIClient.download_clients.getAll, const { error, data } = useQuery(
{ 'downloadClients',
refetchOnWindowFocus: false APIClient.download_clients.getAll,
}) { refetchOnWindowFocus: false }
);
if (error) if (error)
return (<p>An error has occurred: </p>); return (<p>An error has occurred: </p>);

View file

@ -4,7 +4,7 @@ import { IndexerAddForm, IndexerUpdateForm } from "../../forms";
import { Switch } from "@headlessui/react"; import { Switch } from "@headlessui/react";
import { classNames } from "../../utils"; import { classNames } from "../../utils";
import { EmptySimple } from "../../components/emptystates"; import { EmptySimple } from "../../components/emptystates";
import APIClient from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
const ListItem = ({ indexer }: any) => { const ListItem = ({ indexer }: any) => {
const [updateIsOpen, toggleUpdate] = useToggle(false) const [updateIsOpen, toggleUpdate] = useToggle(false)
@ -45,11 +45,11 @@ const ListItem = ({ indexer }: any) => {
function IndexerSettings() { function IndexerSettings() {
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false) const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false)
const { error, data } = useQuery<any[], Error>('indexer', APIClient.indexers.getAll, const { error, data } = useQuery(
{ 'indexer',
refetchOnWindowFocus: false APIClient.indexers.getAll,
} { refetchOnWindowFocus: false }
) );
if (error) if (error)
return (<p>An error has occurred</p>); return (<p>An error has occurred</p>);
@ -104,7 +104,7 @@ function IndexerSettings() {
</tr> </tr>
</thead> </thead>
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700"> <tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
{data && data.map((indexer: Indexer, idx: number) => ( {data && data.map((indexer: IndexerDefinition, idx: number) => (
<ListItem indexer={indexer} key={idx} /> <ListItem indexer={indexer} key={idx} />
))} ))}
</tbody> </tbody>

View file

@ -1,7 +1,7 @@
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { formatDistanceToNowStrict, formatISO9075 } from "date-fns"; import { formatDistanceToNowStrict, formatISO9075 } from "date-fns";
import APIClient from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "../../hooks/hooks";
import { EmptySimple } from "../../components/emptystates"; import { EmptySimple } from "../../components/emptystates";
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms"; import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
@ -27,11 +27,11 @@ function simplifyDate(date: string) {
function IrcSettings() { function IrcSettings() {
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false) const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false)
const { data } = useQuery<IrcNetwork[], Error>('networks', APIClient.irc.getNetworks, const { data } = useQuery(
{ 'networks',
refetchOnWindowFocus: false APIClient.irc.getNetworks,
} { refetchOnWindowFocus: false }
) );
return ( return (
<div className="divide-y divide-gray-200 lg:col-span-9"> <div className="divide-y divide-gray-200 lg:col-span-9">
@ -90,27 +90,8 @@ const LiItem = ({ idx, network }: LiItemProps) => {
<li key={idx} > <li key={idx} >
<div className="grid grid-cols-12 gap-4 items-center hover:bg-gray-50 dark:hover:bg-gray-700 py-4"> <div className="grid grid-cols-12 gap-4 items-center hover:bg-gray-50 dark:hover:bg-gray-700 py-4">
<IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} /> <IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} />
{/* <div className="col-span-1 flex items-center sm:px-6">
<Switch
checked={network.enabled}
onChange={toggleUpdate}
className={classNames(
network.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)}
>
<span className="sr-only">Enable</span>
<span
aria-hidden="true"
className={classNames(
network.enabled ? 'translate-x-5' : 'translate-x-0',
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
)}
/>
</Switch>
</div> */}
<div className="col-span-3 items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white cursor-pointer" onClick={toggleEdit}> <div className="col-span-3 items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white cursor-pointer" onClick={toggleEdit}>
<span className="relative inline-flex items-center"> <span className="relative inline-flex items-center">
{ {