refactor(web): replace pkg react-query with tanstack/react-query (#868)

* refactor: move to tanstack/react-query and fix cache

* refactor(releases): move to tanstack/react-query

* refactor(logs): move to tanstack/react-query

* refactor(base): move to tanstack/react-query

* refactor(base): move to tanstack/react-query

* refactor(dashboard): move to tanstack/react-query

* refactor(auth): move to tanstack/react-query

* refactor(filters): move to tanstack/react-query

* refactor(settings): move to tanstack/react-query

* chore(pkg): add tanstack/react-query

* refactor(filters): move to tanstack/react-query

* refactor: move to tanstack/react-query

* refactor: invalidate queries

* chore(pkg): remove old react-query

* chore: change imports to root prefixes

* build: remove needs web from test

* set enableReinitialize to true to fix formik caching issues

* fix all property for apiKeys const

* fix toast when enabling/disabling feed

---------

Co-authored-by: martylukyy <35452459+martylukyy@users.noreply.github.com>
This commit is contained in:
ze0s 2023-04-27 21:26:27 +02:00 committed by GitHub
parent 0be92bef65
commit 6e5385a490
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1101 additions and 1117 deletions

View file

@ -1,5 +1,5 @@
import { QueryClient, QueryClientProvider, useQueryErrorResetBoundary } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
import { QueryClient, QueryClientProvider, useQueryErrorResetBoundary } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ErrorBoundary } from "react-error-boundary";
import { toast, Toaster } from "react-hot-toast";

View file

@ -34,6 +34,8 @@ export async function HttpClient<T>(
// Show an error toast to notify the user what occurred
return Promise.reject(new Error("Unauthorized"));
} else if (response.status === 404) {
return Promise.reject(new Error("Not found"));
}
return Promise.reject(new Error(await response.text()));
@ -50,7 +52,7 @@ export async function HttpClient<T>(
const appClient = {
Get: <T>(endpoint: string) => HttpClient<T>(endpoint, "GET"),
Post: <T = void>(endpoint: string, data: PostBody = undefined) => HttpClient<T>(endpoint, "POST", { body: data }),
Put: (endpoint: string, data: PostBody) => HttpClient<void>(endpoint, "PUT", { body: data }),
Put: <T = void>(endpoint: string, data: PostBody) => HttpClient<T>(endpoint, "PUT", { body: data }),
Patch: (endpoint: string, data: PostBody = undefined) => HttpClient<void>(endpoint, "PATCH", { body: data }),
Delete: (endpoint: string) => HttpClient<void>(endpoint, "DELETE")
};
@ -113,7 +115,7 @@ export const APIClient = {
},
getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`),
create: (filter: Filter) => appClient.Post<Filter>("api/filters", filter),
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter),
update: (filter: Filter) => appClient.Put<Filter>(`api/filters/${filter.id}`, filter),
duplicate: (id: number) => appClient.Get<Filter>(`api/filters/${id}/duplicate`),
toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }),
delete: (id: number) => appClient.Delete(`api/filters/${id}`)

View file

@ -1,5 +1,5 @@
import { Link } from "react-router-dom";
import logo from "@/logo.png";
import logo from "@app/logo.png";
export const NotFound = () => {
return (

View file

@ -1,5 +1,5 @@
import React from "react";
import { classNames } from "../../utils";
import { classNames } from "@utils";
interface ButtonProps {
className?: string;

View file

@ -3,7 +3,7 @@ import { formatDistanceToNowStrict } from "date-fns";
import { CheckIcon } from "@heroicons/react/24/solid";
import { ClockIcon, ExclamationCircleIcon, NoSymbolIcon } from "@heroicons/react/24/outline";
import { classNames, simplifyDate } from "../../utils";
import { classNames, simplifyDate } from "@utils";
import { Tooltip } from "../tooltips/Tooltip";
interface CellProps {

View file

@ -1,5 +1,5 @@
import { FC } from "react";
import { SettingsContext } from "../utils/Context";
import { SettingsContext } from "@utils/Context";
interface DebugProps {
values: unknown;

View file

@ -1,4 +1,4 @@
import { useToggle } from "../../hooks/hooks";
import { useToggle } from "@hooks/hooks";
import { CheckIcon, DocumentDuplicateIcon, EyeIcon, EyeSlashIcon } from "@heroicons/react/24/outline";
import { useState } from "react";

View file

@ -1,6 +1,6 @@
import { Field, FieldProps } from "formik";
import { classNames } from "../../utils";
import { CustomTooltip } from "../tooltips/CustomTooltip";
import { classNames } from "@utils";
import { CustomTooltip } from "@components/tooltips/CustomTooltip";
interface ErrorFieldProps {
name: string;

View file

@ -1,8 +1,8 @@
import { Field, FieldProps, useFormikContext } from "formik";
import { classNames } from "../../utils";
import { classNames } from "@utils";
import { EyeIcon, EyeSlashIcon, CheckCircleIcon, XCircleIcon } from "@heroicons/react/24/solid";
import { useToggle } from "../../hooks/hooks";
import { CustomTooltip } from "../tooltips/CustomTooltip";
import { useToggle } from "@hooks/hooks";
import { CustomTooltip } from "@components/tooltips/CustomTooltip";
import { useEffect } from "react";
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;

View file

@ -1,13 +1,13 @@
import type { FieldProps, FieldValidator } from "formik";
import { Field } from "formik";
import { classNames } from "../../utils";
import { useToggle } from "../../hooks/hooks";
import { classNames } from "@utils";
import { useToggle } from "@hooks/hooks";
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
import { Switch } from "@headlessui/react";
import { ErrorField, RequiredField } from "./common";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { SelectFieldProps } from "./select";
import { CustomTooltip } from "../tooltips/CustomTooltip";
import { CustomTooltip } from "@components/tooltips/CustomTooltip";
interface TextFieldWideProps {
name: string;

View file

@ -1,6 +1,6 @@
import { Field, useFormikContext } from "formik";
import { RadioGroup } from "@headlessui/react";
import { classNames } from "../../utils";
import { classNames } from "@utils";
export interface radioFieldsetOption {
label: string;

View file

@ -4,9 +4,9 @@ import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/solid";
import { MultiSelect as RMSC } from "react-multi-select-component";
import { classNames, COL_WIDTHS } from "../../utils";
import { SettingsContext } from "../../utils/Context";
import { CustomTooltip } from "../tooltips/CustomTooltip";
import { classNames, COL_WIDTHS } from "@utils";
import { SettingsContext } from "@utils/Context";
import { CustomTooltip } from "@components/tooltips/CustomTooltip";
export interface MultiSelectOption {
value: string | number;
@ -265,8 +265,8 @@ export const Select = ({
}: SelectFieldProps) => {
return (
<div
className={classNames(
columns ? `col-span-${columns}` : "col-span-6"
className={classNames(
columns ? `col-span-${columns}` : "col-span-6"
)}
>
<Field name={name} type="select">

View file

@ -1,7 +1,7 @@
import type { FieldProps } from "formik";
import { Field } from "formik";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { OptionBasicTyped } from "../../domain/constants";
import { OptionBasicTyped } from "@domain/constants";
import CreatableSelect from "react-select/creatable";
import { CustomTooltip } from "../tooltips/CustomTooltip";

View file

@ -2,7 +2,8 @@ import React from "react";
import type { FieldInputProps, FieldMetaProps, FieldProps, FormikProps, FormikValues } from "formik";
import { Field } from "formik";
import { Switch as HeadlessSwitch } from "@headlessui/react";
import { classNames } from "../../utils";
import { classNames } from "@utils";
import { CustomTooltip } from "../tooltips/CustomTooltip";
type SwitchProps<V = unknown> = {
@ -82,7 +83,7 @@ const SwitchGroup = ({
}: SwitchGroupProps) => (
<HeadlessSwitch.Group as="ol" className="py-4 flex items-center justify-between">
{label && <div className="flex flex-col">
<HeadlessSwitch.Label as={heading ? "h2" : "p"} className={classNames("flex float-left cursor-default mb-2 text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide", heading ? "text-lg" : "text-sm")}
<HeadlessSwitch.Label as={heading ? "h2" : "span"} className={classNames("flex float-left cursor-default mb-2 text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide", heading ? "text-lg" : "text-sm")}
passive>
<div className="flex">
{label}

View file

@ -1,7 +1,7 @@
import React, { FC, forwardRef, ReactNode } from "react";
import { DeepMap, FieldError, Path, RegisterOptions, UseFormRegister } from "react-hook-form";
import { classNames, get } from "../../utils";
import { useToggle } from "../../hooks/hooks";
import { classNames, get } from "@utils";
import { useToggle } from "@hooks/hooks";
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
import { ErrorMessage } from "@hookform/error-message";
import type { FieldValues } from "react-hook-form";

View file

@ -1,7 +1,7 @@
import { FC } from "react";
import { CheckCircleIcon, ExclamationCircleIcon, ExclamationTriangleIcon, XMarkIcon } from "@heroicons/react/24/solid";
import { toast, Toast as Tooast } from "react-hot-toast";
import { classNames } from "../../utils";
import { classNames } from "@utils";
type Props = {
type: "error" | "success" | "warning"
@ -26,7 +26,7 @@ const Toast: FC<Props> = ({ type, body, t }) => (
{type === "error" && "Error"}
{type === "warning" && "Warning"}
</p>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</p>
<span className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</span>
</div>
<div className="ml-4 flex-shrink-0 flex">
<button

View file

@ -3,10 +3,11 @@ import { XMarkIcon } from "@heroicons/react/24/solid";
import { Dialog, Transition } from "@headlessui/react";
import { Form, Formik } from "formik";
import type { FormikValues } from "formik";
import DEBUG from "../debug";
import { useToggle } from "../../hooks/hooks";
import { useToggle } from "@hooks/hooks";
import { DeleteModal } from "../modals";
import { classNames } from "../../utils";
import { classNames } from "@utils";
interface SlideOverProps<DataType> {
title: string;

View file

@ -1,7 +1,7 @@
import * as React from "react";
import type { ReactNode } from "react";
import { usePopperTooltip } from "react-popper-tooltip";
import { classNames } from "../../utils";
import { classNames } from "@utils";
interface TooltipProps {
label: ReactNode;

View file

@ -1,4 +1,4 @@
import { MultiSelectOption } from "@/components/inputs/select";
import { MultiSelectOption } from "@components/inputs/select";
export const resolutions = [
"2160p",

View file

@ -1,13 +1,13 @@
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Login } from "../screens/auth/login";
import { Onboarding } from "../screens/auth/onboarding";
import Base from "../screens/Base";
import { Dashboard } from "../screens/dashboard";
import { FilterDetails, Filters } from "../screens/filters";
import { Logs } from "../screens/Logs";
import { Releases } from "../screens/releases";
import Settings from "../screens/Settings";
import { Login } from "@screens/auth/login";
import { Onboarding } from "@screens/auth/onboarding";
import Base from "@screens/Base";
import { Dashboard } from "@screens/dashboard";
import { FilterDetails, Filters } from "@screens/filters";
import { Logs } from "@screens/Logs";
import { Releases } from "@screens/releases";
import Settings from "@screens/Settings";
import {
APISettings,
ApplicationSettings,
@ -18,11 +18,11 @@ import {
LogSettings,
NotificationSettings,
ReleaseSettings
} from "../screens/settings/index";
import { RegexPlayground } from "../screens/settings/RegexPlayground";
import { NotFound } from "@/components/alerts/NotFound";
} from "@screens/settings/index";
import { RegexPlayground } from "@screens/settings/RegexPlayground";
import { NotFound } from "@components/alerts/NotFound";
import { baseUrl } from "../utils";
import { baseUrl } from "@utils";
export const LocalRouter = ({ isLoggedIn }: { isLoggedIn: boolean }) => (
<BrowserRouter basename={baseUrl()}>

View file

@ -1,16 +1,17 @@
import { Fragment } from "react";
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast";
import { XMarkIcon } from "@heroicons/react/24/solid";
import { Dialog, Transition } from "@headlessui/react";
import type { FieldProps } from "formik";
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
import { APIClient } from "../../api/APIClient";
import DEBUG from "../../components/debug";
import Toast from "../../components/notifications/Toast";
import { useNavigate } from "react-router-dom";
import { APIClient } from "@api/APIClient";
import DEBUG from "@components/debug";
import Toast from "@components/notifications/Toast";
import { filterKeys } from "@screens/filters/list";
interface filterAddFormProps {
isOpen: boolean;
toggle: () => void;
@ -19,22 +20,22 @@ interface filterAddFormProps {
function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
const queryClient = useQueryClient();
const navigate = useNavigate();
const mutation = useMutation(
(filter: Filter) => APIClient.filters.create(filter),
{
onSuccess: (filter) => {
queryClient.invalidateQueries("filters");
toast.custom((t) => <Toast type="success" body={`Filter ${filter.name} was added`} t={t} />);
const mutation = useMutation({
mutationFn: (filter: Filter) => APIClient.filters.create(filter),
onSuccess: (filter) => {
queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
toggle();
if (filter.id) {
navigate(filter.id.toString());
}
toast.custom((t) => <Toast type="success" body={`Filter ${filter.name} was added`} t={t} />);
toggle();
if (filter.id) {
navigate(filter.id.toString());
}
}
);
});
const handleSubmit = (data: unknown) => mutation.mutate(data as Filter);
const validate = (values: FormikValues) => {
const errors = {} as FormikErrors<FormikValues>;
if (!values.name) {

View file

@ -1,14 +1,15 @@
import { Fragment } from "react";
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast";
import { XMarkIcon } from "@heroicons/react/24/solid";
import { Dialog, Transition } from "@headlessui/react";
import type { FieldProps } from "formik";
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
import { APIClient } from "../../api/APIClient";
import DEBUG from "../../components/debug";
import Toast from "../../components/notifications/Toast";
import { APIClient } from "@api/APIClient";
import DEBUG from "@components/debug";
import Toast from "@components/notifications/Toast";
import { apiKeys } from "@screens/settings/Api";
interface apiKeyAddFormProps {
isOpen: boolean;
@ -18,17 +19,16 @@ interface apiKeyAddFormProps {
function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) {
const queryClient = useQueryClient();
const mutation = useMutation(
(apikey: APIKey) => APIClient.apikeys.create(apikey),
{
onSuccess: (_, key) => {
queryClient.invalidateQueries("apikeys");
toast.custom((t) => <Toast type="success" body={`API key ${key.name} was added`} t={t}/>);
const mutation = useMutation({
mutationFn: (apikey: APIKey) => APIClient.apikeys.create(apikey),
onSuccess: (_, key) => {
queryClient.invalidateQueries({ queryKey: apiKeys.lists() });
toast.custom((t) => <Toast type="success" body={`API key ${key.name} was added`} t={t}/>);
toggle();
}
toggle();
}
);
});
const handleSubmit = (data: unknown) => mutation.mutate(data as APIKey);
const validate = (values: FormikValues) => {
@ -56,7 +56,6 @@ function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) {
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
<Formik
initialValues={{
name: "",
@ -114,10 +113,7 @@ function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) {
type="text"
className="block w-full shadow-sm dark:bg-gray-800 border-gray-300 dark:border-gray-700 sm:text-sm dark:text-white focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500 rounded-md"
/>
{meta.touched && meta.error &&
<span className="block mt-2 text-red-500">{meta.error}</span>}
{meta.touched && meta.error && <span className="block mt-2 text-red-500">{meta.error}</span>}
</div>
)}
</Field>

View file

@ -1,26 +1,26 @@
import React, { Fragment, useRef, useState } from "react";
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/solid";
import { classNames, sleep } from "../../utils";
import { Form, Formik, useFormikContext } from "formik";
import DEBUG from "../../components/debug";
import { APIClient } from "../../api/APIClient";
import { DownloadClientTypeOptions, DownloadRuleConditionOptions } from "../../domain/constants";
import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { useToggle } from "../../hooks/hooks";
import { DeleteModal } from "../../components/modals";
import { classNames, sleep } from "@utils";
import DEBUG from "@components/debug";
import { APIClient } from "@api/APIClient";
import { DownloadClientTypeOptions, DownloadRuleConditionOptions } from "@domain/constants";
import Toast from "@components/notifications/Toast";
import { useToggle } from "@hooks/hooks";
import { DeleteModal } from "@components/modals";
import {
NumberFieldWide,
PasswordFieldWide,
RadioFieldsetWide,
SwitchGroupWide,
TextFieldWide
} from "../../components/inputs";
import DownloadClient from "../../screens/settings/DownloadClient";
import { SelectFieldWide } from "../../components/inputs/input_wide";
} from "@components/inputs";
import DownloadClient, { clientKeys } from "@screens/settings/DownloadClient";
import { SelectFieldWide } from "@components/inputs/input_wide";
interface InitialValuesSettings {
basic?: {
@ -517,59 +517,51 @@ export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
const queryClient = useQueryClient();
const mutation = useMutation(
(client: DownloadClient) => APIClient.download_clients.create(client),
{
onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]);
toast.custom((t) => <Toast type="success" body="Client was added" t={t}/>);
const addMutation = useMutation({
mutationFn: (client: DownloadClient) => APIClient.download_clients.create(client),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: clientKeys.lists() });
toast.custom((t) => <Toast type="success" body="Client was added" t={t}/>);
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="Client could not be added" t={t}/>);
}
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="Client could not be added" t={t}/>);
}
);
});
const testClientMutation = useMutation(
(client: DownloadClient) => APIClient.download_clients.test(client),
{
onMutate: () => {
setIsTesting(true);
setIsErrorTest(false);
setIsSuccessfulTest(false);
},
onSuccess: () => {
sleep(1000)
.then(() => {
setIsTesting(false);
setIsSuccessfulTest(true);
})
.then(() => {
sleep(2500).then(() => {
setIsSuccessfulTest(false);
});
const onSubmit = (data: unknown) => addMutation.mutate(data as DownloadClient);
const testClientMutation = useMutation({
mutationFn: (client: DownloadClient) => APIClient.download_clients.test(client),
onMutate: () => {
setIsTesting(true);
setIsErrorTest(false);
setIsSuccessfulTest(false);
},
onSuccess: () => {
sleep(1000)
.then(() => {
setIsTesting(false);
setIsSuccessfulTest(true);
})
.then(() => {
sleep(2500).then(() => {
setIsSuccessfulTest(false);
});
},
onError: () => {
console.log("not added");
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
}
},
onError: () => {
console.log("not added");
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
}
);
});
const onSubmit = (data: unknown) => {
mutation.mutate(data as DownloadClient);
};
const testClient = (data: unknown) => {
testClientMutation.mutate(data as DownloadClient);
};
const testClient = (data: unknown) => testClientMutation.mutate(data as DownloadClient);
const initialValues: InitialValues = {
name: "",
@ -692,74 +684,67 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP
const [isErrorTest, setIsErrorTest] = useState(false);
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const queryClient = useQueryClient();
const mutation = useMutation(
(client: DownloadClient) => APIClient.download_clients.update(client),
{
onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]);
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t}/>);
toggle();
}
}
);
const deleteMutation = useMutation(
(clientID: number) => APIClient.download_clients.delete(clientID),
{
onSuccess: () => {
queryClient.invalidateQueries();
toast.custom((t) => <Toast type="success" body={`${client.name} was deleted.`} t={t}/>);
toggleDeleteModal();
}
}
);
const testClientMutation = useMutation(
(client: DownloadClient) => APIClient.download_clients.test(client),
{
onMutate: () => {
setIsTesting(true);
setIsErrorTest(false);
setIsSuccessfulTest(false);
},
onSuccess: () => {
sleep(1000)
.then(() => {
setIsTesting(false);
setIsSuccessfulTest(true);
})
.then(() => {
sleep(2500).then(() => {
setIsSuccessfulTest(false);
});
});
},
onError: () => {
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
}
}
);
const onSubmit = (data: unknown) => {
mutation.mutate(data as DownloadClient);
};
const cancelButtonRef = useRef(null);
const cancelModalButtonRef = useRef(null);
const deleteAction = () => {
deleteMutation.mutate(client.id);
};
const queryClient = useQueryClient();
const testClient = (data: unknown) => {
testClientMutation.mutate(data as DownloadClient);
};
const mutation = useMutation({
mutationFn: (client: DownloadClient) => APIClient.download_clients.update(client),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: clientKeys.lists() });
queryClient.invalidateQueries({ queryKey: clientKeys.detail(client.id) });
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t}/>);
toggle();
}
});
const onSubmit = (data: unknown) => mutation.mutate(data as DownloadClient);
const deleteMutation = useMutation({
mutationFn: (clientID: number) => APIClient.download_clients.delete(clientID),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: clientKeys.lists() });
queryClient.invalidateQueries({ queryKey: clientKeys.detail(client.id) });
toast.custom((t) => <Toast type="success" body={`${client.name} was deleted.`} t={t}/>);
toggleDeleteModal();
}
});
const deleteAction = () => deleteMutation.mutate(client.id);
const testClientMutation = useMutation({
mutationFn: (client: DownloadClient) => APIClient.download_clients.test(client),
onMutate: () => {
setIsTesting(true);
setIsErrorTest(false);
setIsSuccessfulTest(false);
},
onSuccess: () => {
sleep(1000)
.then(() => {
setIsTesting(false);
setIsSuccessfulTest(true);
})
.then(() => {
sleep(2500).then(() => {
setIsSuccessfulTest(false);
});
});
},
onError: () => {
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
}
});
const testClient = (data: unknown) => testClientMutation.mutate(data as DownloadClient);
const initialValues = {
id: client.id,

View file

@ -1,16 +1,18 @@
import { useMutation, useQueryClient } from "react-query";
import { APIClient } from "../../api/APIClient";
import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { SlideOver } from "../../components/panels";
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs";
import { SelectFieldBasic } from "../../components/inputs/select_wide";
import { componentMapType } from "./DownloadClientForms";
import { sleep } from "../../utils";
import { useState } from "react";
import { ImplementationBadges } from "../../screens/settings/Indexer";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast";
import { useFormikContext } from "formik";
import { FeedDownloadTypeOptions } from "../../domain/constants";
import { APIClient } from "@api/APIClient";
import Toast from "@components/notifications/Toast";
import { SlideOver } from "@components/panels";
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
import { SelectFieldBasic } from "@components/inputs/select_wide";
import { componentMapType } from "./DownloadClientForms";
import { sleep } from "@utils";
import { ImplementationBadges } from "@screens/settings/Indexer";
import { FeedDownloadTypeOptions } from "@domain/constants";
import { feedKeys } from "@screens/settings/Feed";
interface UpdateProps {
isOpen: boolean;
@ -40,68 +42,58 @@ export function FeedUpdateForm({ isOpen, toggle, feed }: UpdateProps) {
const queryClient = useQueryClient();
const mutation = useMutation(
(feed: Feed) => APIClient.feeds.update(feed),
{
onSuccess: () => {
queryClient.invalidateQueries(["feeds"]);
toast.custom((t) => <Toast type="success" body={`${feed.name} was updated successfully`} t={t} />);
toggle();
}
const mutation = useMutation({
mutationFn: (feed: Feed) => APIClient.feeds.update(feed),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: feedKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${feed.name} was updated successfully`} t={t} />);
toggle();
}
);
});
const onSubmit = (formData: unknown) => {
mutation.mutate(formData as Feed);
};
const onSubmit = (formData: unknown) => mutation.mutate(formData as Feed);
const deleteMutation = useMutation(
(feedID: number) => APIClient.feeds.delete(feedID),
{
onSuccess: () => {
queryClient.invalidateQueries(["feeds"]);
toast.custom((t) => <Toast type="success" body={`${feed.name} was deleted.`} t={t} />);
}
const deleteMutation = useMutation({
mutationFn: (feedID: number) => APIClient.feeds.delete(feedID),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: feedKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${feed.name} was deleted.`} t={t} />);
}
);
});
const deleteAction = () => {
deleteMutation.mutate(feed.id);
};
const deleteAction = () => deleteMutation.mutate(feed.id);
const testFeedMutation = useMutation(
(feed: Feed) => APIClient.feeds.test(feed),
{
onMutate: () => {
setIsTesting(true);
setIsErrorTest(false);
setIsSuccessfulTest(false);
},
onSuccess: () => {
sleep(1000)
.then(() => {
setIsTesting(false);
setIsSuccessfulTest(true);
})
.then(() => {
sleep(2500).then(() => {
setIsSuccessfulTest(false);
});
const testFeedMutation = useMutation({
mutationFn: (feed: Feed) => APIClient.feeds.test(feed),
onMutate: () => {
setIsTesting(true);
setIsErrorTest(false);
setIsSuccessfulTest(false);
},
onSuccess: () => {
sleep(1000)
.then(() => {
setIsTesting(false);
setIsSuccessfulTest(true);
})
.then(() => {
sleep(2500).then(() => {
setIsSuccessfulTest(false);
});
},
onError: () => {
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
}
},
onError: () => {
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
}
);
});
const testFeed = (data: unknown) => {
testFeedMutation.mutate(data as Feed);
};
const testFeed = (data: unknown) => testFeedMutation.mutate(data as Feed);
const initialValues: InitialValues = {
id: feed.id,

View file

@ -1,20 +1,23 @@
import React, { Fragment, useState } from "react";
import { toast } from "react-hot-toast";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import type { FieldProps } from "formik";
import { Field, Form, Formik, FormikValues } from "formik";
import { XMarkIcon } from "@heroicons/react/24/solid";
import { Dialog, Transition } from "@headlessui/react";
import { classNames, sleep } from "../../utils";
import DEBUG from "../../components/debug";
import { APIClient } from "../../api/APIClient";
import { PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs";
import { SlideOver } from "../../components/panels";
import Toast from "../../components/notifications/Toast";
import { SelectFieldBasic, SelectFieldCreatable } from "../../components/inputs/select_wide";
import { CustomTooltip } from "../../components/tooltips/CustomTooltip";
import { FeedDownloadTypeOptions } from "../../domain/constants";
import { classNames, sleep } from "@utils";
import DEBUG from "@components/debug";
import { APIClient } from "@api/APIClient";
import { PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
import { SlideOver } from "@components/panels";
import Toast from "@components/notifications/Toast";
import { SelectFieldBasic, SelectFieldCreatable } from "@components/inputs/select_wide";
import { CustomTooltip } from "@components/tooltips/CustomTooltip";
import { FeedDownloadTypeOptions } from "@domain/constants";
import { feedKeys } from "@screens/settings/Feed";
import { indexerKeys } from "@screens/settings/Indexer";
const Input = (props: InputProps) => (
<components.Input
@ -244,35 +247,37 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
const [indexer, setIndexer] = useState<IndexerDefinition>({} as IndexerDefinition);
const queryClient = useQueryClient();
const { data } = useQuery(
"indexerDefinition",
() => APIClient.indexers.getSchema(),
{
enabled: isOpen,
refetchOnWindowFocus: false
const { data } = useQuery({
queryKey: ["indexerDefinition"],
queryFn: APIClient.indexers.getSchema,
enabled: isOpen,
refetchOnWindowFocus: false
});
const mutation = useMutation({
mutationFn: (indexer: Indexer) => APIClient.indexers.create(indexer),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: indexerKeys.lists() });
toast.custom((t) => <Toast type="success" body="Indexer was added" t={t} />);
sleep(1500);
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="Indexer could not be added" t={t} />);
}
);
});
const mutation = useMutation(
(indexer: Indexer) => APIClient.indexers.create(indexer), {
onSuccess: () => {
queryClient.invalidateQueries(["indexer"]);
toast.custom((t) => <Toast type="success" body="Indexer was added" t={t} />);
sleep(1500);
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="Indexer could not be added" t={t} />);
}
});
const ircMutation = useMutation({
mutationFn: (network: IrcNetworkCreate) => APIClient.irc.createNetwork(network)
});
const ircMutation = useMutation(
(network: IrcNetworkCreate) => APIClient.irc.createNetwork(network)
);
const feedMutation = useMutation(
(feed: FeedCreate) => APIClient.feeds.create(feed)
);
const feedMutation = useMutation({
mutationFn: (feed: FeedCreate) => APIClient.feeds.create(feed),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: feedKeys.lists() });
}
});
const onSubmit = (formData: FormikValues) => {
const ind = data && data.find(i => i.identifier === formData.identifier);
@ -587,39 +592,37 @@ function TestApiButton({ values, show }: TestApiButtonProps) {
return null;
}
const testApiMutation = useMutation(
(req: IndexerTestApiReq) => APIClient.indexers.testApi(req),
{
onMutate: () => {
setIsTesting(true);
setIsErrorTest(false);
setIsSuccessfulTest(false);
},
onSuccess: () => {
toast.custom((t) => <Toast type="success" body="API test successful!" t={t} />);
const testApiMutation = useMutation({
mutationFn: (req: IndexerTestApiReq) => APIClient.indexers.testApi(req),
onMutate: () => {
setIsTesting(true);
setIsErrorTest(false);
setIsSuccessfulTest(false);
},
onSuccess: () => {
toast.custom((t) => <Toast type="success" body="API test successful!" t={t} />);
sleep(1000)
.then(() => {
setIsTesting(false);
setIsSuccessfulTest(true);
})
.then(() => {
sleep(2500).then(() => {
setIsSuccessfulTest(false);
});
sleep(1000)
.then(() => {
setIsTesting(false);
setIsSuccessfulTest(true);
})
.then(() => {
sleep(2500).then(() => {
setIsSuccessfulTest(false);
});
},
onError: (error: Error) => {
toast.custom((t) => <Toast type="error" body={error.message} t={t} />);
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
}
},
onError: (error: Error) => {
toast.custom((t) => <Toast type="error" body={error.message} t={t} />);
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
}
);
});
const testApi = () => {
const req: IndexerTestApiReq = {
@ -706,9 +709,11 @@ interface UpdateProps {
export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
const queryClient = useQueryClient();
const mutation = useMutation((indexer: Indexer) => APIClient.indexers.update(indexer), {
const mutation = useMutation({
mutationFn: (indexer: Indexer) => APIClient.indexers.update(indexer),
onSuccess: () => {
queryClient.invalidateQueries(["indexer"]);
queryClient.invalidateQueries({ queryKey: indexerKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${indexer.name} was updated successfully`} t={t} />);
sleep(1500);
@ -716,23 +721,23 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
}
});
const deleteMutation = useMutation((id: number) => APIClient.indexers.delete(id), {
onSuccess: () => {
queryClient.invalidateQueries(["indexer"]);
toast.custom((t) => <Toast type="success" body={`${indexer.name} was deleted.`} t={t} />);
toggle();
}
});
const onSubmit = (data: unknown) => {
// TODO clear data depending on type
mutation.mutate(data as Indexer);
};
const deleteAction = () => {
deleteMutation.mutate(indexer.id ?? 0);
};
const deleteMutation = useMutation({
mutationFn: (id: number) => APIClient.indexers.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: indexerKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${indexer.name} was deleted.`} t={t} />);
toggle();
}
});
const deleteAction = () => deleteMutation.mutate(indexer.id ?? 0);
const renderSettingFields = (settings: IndexerSetting[]) => {
if (settings === undefined) {

View file

@ -1,13 +1,18 @@
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast";
import { XMarkIcon } from "@heroicons/react/24/solid";
import type { FieldProps } from "formik";
import { Field, FieldArray, FormikErrors, FormikValues } from "formik";
import { APIClient } from "../../api/APIClient";
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, SwitchGroupWideRed, TextFieldWide } from "../../components/inputs";
import { SlideOver } from "../../components/panels";
import Toast from "../../components/notifications/Toast";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { Dialog } from "@headlessui/react";
import { IrcAuthMechanismTypeOptions, OptionBasicTyped } from "@domain/constants";
import { ircKeys } from "@screens/settings/Irc";
import { APIClient } from "@api/APIClient";
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, SwitchGroupWideRed, TextFieldWide } from "@components/inputs";
import { SlideOver } from "@components/panels";
import Toast from "@components/notifications/Toast";
interface ChannelsFieldArrayProps {
channels: IrcChannel[];
@ -96,43 +101,21 @@ interface AddFormProps {
export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
const queryClient = useQueryClient();
const mutation = useMutation(
(network: IrcNetwork) => APIClient.irc.createNetwork(network),
{
onSuccess: () => {
queryClient.invalidateQueries(["networks"]);
toast.custom((t) => <Toast type="success" body="IRC Network added. Please allow up to 30 seconds for the network to come online." t={t} />);
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />);
}
const mutation = useMutation({
mutationFn: (network: IrcNetwork) => APIClient.irc.createNetwork(network),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
toast.custom((t) => <Toast type="success" body="IRC Network added. Please allow up to 30 seconds for the network to come online." t={t} />);
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />);
}
);
});
const onSubmit = (data: unknown) => {
mutation.mutate(data as IrcNetwork);
};
const validate = (values: FormikValues) => {
const errors = {} as FormikErrors<FormikValues>;
if (!values.name)
errors.name = "Required";
if (!values.port)
errors.port = "Required";
if (!values.server)
errors.server = "Required";
if (!values.nick)
errors.nick = "Required";
// if (!values.auth || !values.auth.account)
// errors.auth = { account: "Required" };
return errors;
};
const onSubmit = (data: unknown) => mutation.mutate(data as IrcNetwork);
const initialValues: IrcNetworkAddFormValues = {
name: "",
@ -157,7 +140,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
toggle={toggle}
onSubmit={onSubmit}
initialValues={initialValues}
validate={validate}
validate={validateNetwork}
>
{(values) => (
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
@ -214,6 +197,28 @@ export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
);
}
const validateNetwork = (values: FormikValues) => {
const errors = {} as FormikErrors<FormikValues>;
if (!values.name) {
errors.name = "Required";
}
if (!values.server) {
errors.server = "Required";
}
if (!values.port) {
errors.port = "Required";
}
if (!values.nick) {
errors.nick = "Required";
}
return errors;
};
interface IrcNetworkUpdateFormValues {
id: number;
name: string;
@ -241,53 +246,31 @@ export function IrcNetworkUpdateForm({
}: IrcNetworkUpdateFormProps) {
const queryClient = useQueryClient();
const mutation = useMutation((network: IrcNetwork) => APIClient.irc.updateNetwork(network), {
const updateMutation = useMutation({
mutationFn: (network: IrcNetwork) => APIClient.irc.updateNetwork(network),
onSuccess: () => {
queryClient.invalidateQueries(["networks"]);
queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />);
toggle();
}
});
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
const onSubmit = (data: unknown) => updateMutation.mutate(data as IrcNetwork);
const deleteMutation = useMutation({
mutationFn: (id: number) => APIClient.irc.deleteNetwork(id),
onSuccess: () => {
queryClient.invalidateQueries(["networks"]);
queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />);
toggle();
}
});
const onSubmit = (data: unknown) => {
console.log("submit: ", data);
mutation.mutate(data as IrcNetwork);
};
const validate = (values: FormikValues) => {
const errors = {} as FormikErrors<FormikValues>;
if (!values.name) {
errors.name = "Required";
}
if (!values.server) {
errors.server = "Required";
}
if (!values.port) {
errors.port = "Required";
}
if (!values.nick) {
errors.nick = "Required";
}
return errors;
};
const deleteAction = () => {
deleteMutation.mutate(network.id);
};
const deleteAction = () => deleteMutation.mutate(network.id);
const initialValues: IrcNetworkUpdateFormValues = {
id: network.id,
@ -312,7 +295,7 @@ export function IrcNetworkUpdateForm({
onSubmit={onSubmit}
deleteAction={deleteAction}
initialValues={initialValues}
validate={validate}
validate={validateNetwork}
>
{(values) => (
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
@ -459,10 +442,6 @@ function SelectField<T>({ name, label, options }: SelectFieldProps<T>) {
);
}
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { IrcAuthMechanismTypeOptions, OptionBasicTyped } from "../../domain/constants";
import { Dialog } from "@headlessui/react";
const Input = (props: InputProps) => {
return (
<components.Input

View file

@ -4,15 +4,17 @@ import type { FieldProps } from "formik";
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
import { XMarkIcon } from "@heroicons/react/24/solid";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs";
import DEBUG from "../../components/debug";
import { EventOptions, NotificationTypeOptions, SelectOption } from "../../domain/constants";
import { useMutation, useQueryClient } from "react-query";
import { APIClient } from "../../api/APIClient";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { SlideOver } from "../../components/panels";
import { PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
import DEBUG from "@components/debug";
import { EventOptions, NotificationTypeOptions, SelectOption } from "@domain/constants";
import { APIClient } from "@api/APIClient";
import Toast from "@components/notifications/Toast";
import { SlideOver } from "@components/panels";
import { componentMapType } from "./DownloadClientForms";
import { notificationKeys } from "@screens/settings/Notifications";
const Input = (props: InputProps) => {
return (
@ -137,36 +139,29 @@ interface AddProps {
export function NotificationAddForm({ isOpen, toggle }: AddProps) {
const queryClient = useQueryClient();
const mutation = useMutation(
(notification: Notification) => APIClient.notifications.create(notification),
{
onSuccess: () => {
queryClient.invalidateQueries(["notifications"]);
toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />);
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="Notification could not be added" t={t} />);
}
const createMutation = useMutation({
mutationFn: (notification: Notification) => APIClient.notifications.create(notification),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: notificationKeys.lists() });
toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />);
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="Notification could not be added" t={t} />);
}
);
});
const onSubmit = (formData: unknown) => {
mutation.mutate(formData as Notification);
};
const onSubmit = (formData: unknown) => createMutation.mutate(formData as Notification);
const testMutation = useMutation(
(n: Notification) => APIClient.notifications.test(n),
{
onError: (err) => {
console.error(err);
}
const testMutation = useMutation({
mutationFn: (n: Notification) => APIClient.notifications.test(n),
onError: (err) => {
console.error(err);
}
);
});
const testNotification = (data: unknown) => {
testMutation.mutate(data as Notification);
};
const testNotification = (data: unknown) => testMutation.mutate(data as Notification);
const validate = (values: NotificationAddFormValues) => {
const errors = {} as FormikErrors<FormikValues>;
@ -410,47 +405,37 @@ interface InitialValues {
export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateProps) {
const queryClient = useQueryClient();
const mutation = useMutation(
(notification: Notification) => APIClient.notifications.update(notification),
{
onSuccess: () => {
queryClient.invalidateQueries(["notifications"]);
toast.custom((t) => <Toast type="success" body={`${notification.name} was updated successfully`} t={t}/>);
toggle();
}
const mutation = useMutation({
mutationFn: (notification: Notification) => APIClient.notifications.update(notification),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: notificationKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${notification.name} was updated successfully`} t={t}/>);
toggle();
}
);
});
const deleteMutation = useMutation(
(notificationID: number) => APIClient.notifications.delete(notificationID),
{
onSuccess: () => {
queryClient.invalidateQueries(["notifications"]);
toast.custom((t) => <Toast type="success" body={`${notification.name} was deleted.`} t={t}/>);
}
const onSubmit = (formData: unknown) => mutation.mutate(formData as Notification);
const deleteMutation = useMutation({
mutationFn: (notificationID: number) => APIClient.notifications.delete(notificationID),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: notificationKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${notification.name} was deleted.`} t={t}/>);
}
);
});
const onSubmit = (formData: unknown) => {
mutation.mutate(formData as Notification);
};
const deleteAction = () => deleteMutation.mutate(notification.id);
const deleteAction = () => {
deleteMutation.mutate(notification.id);
};
const testMutation = useMutation(
(n: Notification) => APIClient.notifications.test(n),
{
onError: (err) => {
console.error(err);
}
const testMutation = useMutation({
mutationFn: (n: Notification) => APIClient.notifications.test(n),
onError: (err) => {
console.error(err);
}
);
});
const testNotification = (data: unknown) => {
testMutation.mutate(data as Notification);
};
const testNotification = (data: unknown) => testMutation.mutate(data as Notification);
const initialValues: InitialValues = {
id: notification.id,

View file

@ -1,26 +1,22 @@
import { Fragment } from "react";
import React, { Fragment } from "react";
import { Link, NavLink, Outlet } from "react-router-dom";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import { ArrowTopRightOnSquareIcon, UserIcon } from "@heroicons/react/24/solid";
import { Bars3Icon, XMarkIcon, MegaphoneIcon } from "@heroicons/react/24/outline";
import { AuthContext } from "../utils/Context";
import logo from "../logo.png";
import { useQuery } from "react-query";
import { APIClient } from "../api/APIClient";
import { useMutation, useQuery } from "@tanstack/react-query";
import toast from "react-hot-toast";
import Toast from "@/components/notifications/Toast";
import { AuthContext } from "@utils/Context";
import logo from "@app/logo.png";
import { APIClient } from "@api/APIClient";
import Toast from "@components/notifications/Toast";
import { classNames } from "@utils";
interface NavItem {
name: string;
path: string;
}
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(" ");
}
const nav: Array<NavItem> = [
{ name: "Dashboard", path: "/" },
{ name: "Filters", path: "/filters" },
@ -32,24 +28,27 @@ const nav: Array<NavItem> = [
export default function Base() {
const authContext = AuthContext.useValue();
const { data } = useQuery(
["updates"],
() => APIClient.updates.getLatestRelease(),
{
retry: false,
refetchOnWindowFocus: false,
onError: err => console.log(err)
}
);
const { data } = useQuery({
queryKey: ["updates"],
queryFn: () => APIClient.updates.getLatestRelease(),
retry: false,
refetchOnWindowFocus: false,
onError: err => console.log(err)
});
const LogOutUser = () => {
APIClient.auth.logout()
.then(() => {
AuthContext.reset();
toast.custom((t) => (
<Toast type="success" body="You have been logged out. Goodbye!" t={t} />
));
});
const logoutMutation = useMutation( {
mutationFn: APIClient.auth.logout,
onSuccess: () => {
AuthContext.reset();
toast.custom((t) => (
<Toast type="success" body="You have been logged out. Goodbye!" t={t} />
));
}
});
const logoutAction = () => {
logoutMutation.mutate();
};
return (
@ -172,7 +171,7 @@ export default function Base() {
<Menu.Item>
{({ active }) => (
<button
onClick={LogOutUser}
onClick={logoutAction}
className={classNames(
active
? "bg-gray-100 dark:bg-gray-600"
@ -242,7 +241,7 @@ export default function Base() {
</NavLink>
))}
<button
onClick={LogOutUser}
onClick={logoutAction}
className="w-full shadow-sm border bg-gray-100 border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium text-left"
>
Logout

View file

@ -2,20 +2,20 @@ import { Fragment, useEffect, useRef, useState } from "react";
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
import format from "date-fns/format";
import { DebounceInput } from "react-debounce-input";
import { APIClient } from "../api/APIClient";
import { Checkbox } from "../components/Checkbox";
import { classNames, simplifyDate } from "../utils";
import { SettingsContext } from "../utils/Context";
import { EmptySimple } from "../components/emptystates";
import {
Cog6ToothIcon,
DocumentArrowDownIcon
} from "@heroicons/react/24/outline";
import { useQuery } from "react-query";
import { useQuery } from "@tanstack/react-query";
import { Menu, Transition } from "@headlessui/react";
import { baseUrl } from "../utils";
import { RingResizeSpinner } from "@/components/Icons";
import { APIClient } from "@api/APIClient";
import { Checkbox } from "@components/Checkbox";
import { classNames, simplifyDate } from "@utils";
import { SettingsContext } from "@utils/Context";
import { EmptySimple } from "@components/emptystates";
import { baseUrl } from "@utils";
import { RingResizeSpinner } from "@components/Icons";
type LogEvent = {
time: string;
@ -83,7 +83,6 @@ export const Logs = () => {
</div>
</header>
<div className="max-w-screen-xl mx-auto pb-12 px-2 sm:px-4 lg:px-8">
<div className="flex justify-center py-4">
<ExclamationTriangleIcon
@ -158,15 +157,13 @@ export const Logs = () => {
};
export const LogFiles = () => {
const { isLoading, data } = useQuery(
["log-files"],
() => APIClient.logs.files(),
{
retry: false,
refetchOnWindowFocus: false,
onError: err => console.log(err)
}
);
const { isLoading, data } = useQuery({
queryKey: ["log-files"],
queryFn: () => APIClient.logs.files(),
retry: false,
refetchOnWindowFocus: false,
onError: err => console.log(err)
});
return (
<div>
@ -224,10 +221,8 @@ const LogFilesItem = ({ file }: LogFilesItemProps) => {
URL.revokeObjectURL(url);
setIsDownloading(false);
};
return (
<li className="text-gray-500 dark:text-gray-400">
<div className="grid grid-cols-12 items-center py-2">
<div className="col-span-4 sm:col-span-5 px-2 py-0 truncate hidden sm:block sm:text-sm text-md font-medium text-gray-900 dark:text-gray-200">

View file

@ -10,7 +10,7 @@ import {
Square3Stack3DIcon
} from "@heroicons/react/24/outline";
import { classNames } from "../utils";
import { classNames } from "@utils";
interface NavTabType {
name: string;

View file

@ -1,14 +1,15 @@
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useMutation } from "react-query";
import logo from "../../logo.png";
import { APIClient } from "../../api/APIClient";
import { AuthContext } from "../../utils/Context";
import { PasswordInput, TextInput } from "../../components/inputs/text";
import { Tooltip } from "react-tooltip";
import Toast from "@/components/notifications/Toast";
import { useMutation } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { Tooltip } from "react-tooltip";
import logo from "@app/logo.png";
import { APIClient } from "@api/APIClient";
import { AuthContext } from "@utils/Context";
import { PasswordInput, TextInput } from "@components/inputs/text";
import Toast from "@components/notifications/Toast";
type LoginFormFields = {
username: string;
@ -37,23 +38,21 @@ export const Login = () => {
.catch(() => { /*don't log to console PAHLLEEEASSSE*/ });
}, []);
const loginMutation = useMutation(
(data: LoginFormFields) => APIClient.auth.login(data.username, data.password),
{
onSuccess: (_, variables: LoginFormFields) => {
setAuthContext({
username: variables.username,
isLoggedIn: true
});
navigate("/");
},
onError: () => {
toast.custom((t) => (
<Toast type="error" body="Wrong password or username!" t={t} />
));
}
const loginMutation = useMutation({
mutationFn: (data: LoginFormFields) => APIClient.auth.login(data.username, data.password),
onSuccess: (_, variables: LoginFormFields) => {
setAuthContext({
username: variables.username,
isLoggedIn: true
});
navigate("/");
},
onError: () => {
toast.custom((t) => (
<Toast type="error" body="Wrong password or username!" t={t} />
));
}
);
});
const onSubmit = (data: LoginFormFields) => loginMutation.mutate(data);

View file

@ -1,10 +1,10 @@
import { Form, Formik } from "formik";
import { useMutation } from "react-query";
import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { APIClient } from "../../api/APIClient";
import { TextField, PasswordField } from "../../components/inputs";
import logo from "../../logo.png";
import { APIClient } from "@api/APIClient";
import { TextField, PasswordField } from "@components/inputs";
import logo from "@app/logo.png";
interface InputValues {
username: string;
@ -33,10 +33,10 @@ export const Onboarding = () => {
const navigate = useNavigate();
const mutation = useMutation(
(data: InputValues) => APIClient.auth.onboard(data.username, data.password1),
{ onSuccess: () => navigate("/") }
);
const mutation = useMutation({
mutationFn: (data: InputValues) => APIClient.auth.onboard(data.username, data.password1),
onSuccess: () => navigate("/")
});
return (
<div className="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8">

View file

@ -1,5 +1,5 @@
import * as React from "react";
import { useQuery } from "react-query";
import { useQuery } from "@tanstack/react-query";
import {
useTable,
useFilters,
@ -8,11 +8,10 @@ import {
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";
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
@ -74,8 +73,9 @@ function Table({ columns, data }: TableProps) {
usePagination
);
if (!page.length)
if (!page.length) {
return <EmptyListState text="No recent activity" />;
}
// Render the UI for your table
return (
@ -178,24 +178,25 @@ export const ActivityTable = () => {
}
], []);
const { isLoading, data } = useQuery(
"dash_recent_releases",
() => APIClient.release.findRecent(),
{ refetchOnWindowFocus: false }
);
const { isLoading, data } = useQuery({
queryKey: ["dash_recent_releases"],
queryFn: APIClient.release.findRecent,
refetchOnWindowFocus: false
});
if (isLoading)
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">
&nbsp;
&nbsp;
</h3>
<div className="animate-pulse text-black dark:text-white">
<EmptyListState text="Loading..." />
<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">

View file

@ -1,6 +1,6 @@
import { useQuery } from "react-query";
import { APIClient } from "../../api/APIClient";
import { classNames } from "../../utils";
import { useQuery } from "@tanstack/react-query";
import { APIClient } from "@api/APIClient";
import { classNames } from "@utils";
interface StatsItemProps {
name: string;
@ -28,17 +28,18 @@ const StatsItem = ({ name, placeholder, value }: StatsItemProps) => (
);
export const Stats = () => {
const { isLoading, data } = useQuery(
"dash_release_stats",
() => APIClient.release.stats(),
{ refetchOnWindowFocus: false }
);
const { isLoading, data } = useQuery({
queryKey: ["dash_release_stats"],
queryFn: APIClient.release.stats,
refetchOnWindowFocus: false
});
return (
<div>
<h1 className="text-3xl font-bold text-black dark:text-white">
Stats
</h1>
<dl className={classNames("grid grid-cols-1 gap-5 mt-5 sm:grid-cols-2 lg:grid-cols-3", isLoading ? "animate-pulse" : "")}>
<StatsItem name="Filtered Releases" value={data?.filtered_count ?? 0} />
{/* <StatsItem name="Filter Rejected Releases" stat={data?.filter_rejected_count} /> */}

View file

@ -1,21 +1,21 @@
import { AlertWarning } from "../../components/alerts";
import { DownloadClientSelect, NumberField, Select, SwitchGroup, TextField } from "../../components/inputs";
import { ActionContentLayoutOptions, ActionRtorrentRenameOptions, ActionTypeNameMap, ActionTypeOptions } from "../../domain/constants";
import { AlertWarning } from "@components/alerts";
import { DownloadClientSelect, NumberField, Select, SwitchGroup, TextField } from "@components/inputs";
import { ActionContentLayoutOptions, ActionRtorrentRenameOptions, ActionTypeNameMap, ActionTypeOptions } from "@domain/constants";
import React, { Fragment, useRef, useEffect, useState } from "react";
import { useQuery } from "react-query";
import { APIClient } from "../../api/APIClient";
import { useQuery } from "@tanstack/react-query";
import { APIClient } from "@api/APIClient";
import { Field, FieldArray, FieldProps, FormikValues } from "formik";
import { EmptyListState } from "../../components/emptystates";
import { useToggle } from "../../hooks/hooks";
import { classNames } from "../../utils";
import { EmptyListState } from "@components/emptystates";
import { useToggle } from "@hooks/hooks";
import { classNames } from "@utils";
import { Dialog, Switch as SwitchBasic, Transition } from "@headlessui/react";
import { ChevronRightIcon } from "@heroicons/react/24/solid";
import { DeleteModal } from "../../components/modals";
import { DeleteModal } from "@components/modals";
import { CollapsableSection } from "./details";
import { CustomTooltip } from "../../components/tooltips/CustomTooltip";
import { CustomTooltip } from "@components/tooltips/CustomTooltip";
import { Link } from "react-router-dom";
import { useFormikContext } from "formik";
import { TextArea } from "../../components/inputs/input";
import { TextArea } from "@components/inputs/input";
interface FilterActionsProps {
filter: Filter;

View file

@ -1,5 +1,5 @@
import React, { useRef } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { NavLink, Route, Routes, useLocation, useNavigate, useParams } from "react-router-dom";
import { toast } from "react-hot-toast";
import { Form, Formik, FormikValues, useFormikContext } from "formik";
@ -20,10 +20,10 @@ import {
SOURCES_MUSIC_OPTIONS,
SOURCES_OPTIONS,
tagsMatchLogicOptions
} from "../../domain/constants";
import { APIClient } from "../../api/APIClient";
import { useToggle } from "../../hooks/hooks";
import { classNames } from "../../utils";
} from "@app/domain/constants";
import { APIClient } from "@api/APIClient";
import { useToggle } from "@hooks/hooks";
import { classNames } from "@utils";
import {
CheckboxField,
@ -34,13 +34,14 @@ import {
SwitchGroup,
TextField,
RegexField
} from "../../components/inputs";
import DEBUG from "../../components/debug";
import Toast from "../../components/notifications/Toast";
import { DeleteModal } from "../../components/modals";
import { TitleSubtitle } from "../../components/headings";
import { TextArea } from "../../components/inputs/input";
} from "@components/inputs";
import DEBUG from "@components/debug";
import Toast from "@components/notifications/Toast";
import { DeleteModal } from "@components/modals";
import { TitleSubtitle } from "@components/headings";
import { TextArea } from "@components/inputs/input";
import { FilterActions } from "./action";
import { filterKeys } from "./list";
interface tabType {
name: string;
@ -144,37 +145,49 @@ export default function FilterDetails() {
const navigate = useNavigate();
const { filterId } = useParams<{ filterId: string }>();
const { isLoading, data: filter } = useQuery(
["filters", filterId],
() => APIClient.filters.getByID(parseInt(filterId ?? "0")),
{
retry: false,
refetchOnWindowFocus: false,
onError: () => navigate("./")
}
);
if (filterId === "0" || undefined) {
navigate("/filters");
}
const updateMutation = useMutation(
(filter: Filter) => APIClient.filters.update(filter),
{
onSuccess: (_, currentFilter) => {
toast.custom((t) => (
<Toast type="success" body={`${currentFilter.name} was updated successfully`} t={t} />
));
queryClient.refetchQueries(["filters"]);
// queryClient.invalidateQueries(["filters", currentFilter.id]);
}
}
);
const id = parseInt(filterId!);
const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), {
const { isLoading, data: filter } = useQuery({
queryKey: filterKeys.detail(id),
queryFn: ({ queryKey }) => APIClient.filters.getByID(queryKey[2]),
refetchOnWindowFocus: false,
onError: () => {
navigate("/filters");
}
});
const updateMutation = useMutation({
mutationFn: (filter: Filter) => APIClient.filters.update(filter),
onSuccess: (newFilter, variables) => {
queryClient.setQueryData(filterKeys.detail(variables.id), newFilter);
queryClient.setQueryData<Filter[]>(filterKeys.lists(), (previous) => {
if (previous) {
return previous.map((filter: Filter) => (filter.id === variables.id ? newFilter : filter));
}
});
toast.custom((t) => (
<Toast type="success" body={`${newFilter.name} was updated successfully`} t={t}/>
));
}
});
const deleteMutation = useMutation({
mutationFn: (id: number) => APIClient.filters.delete(id),
onSuccess: () => {
// Invalidate filters just in case, most likely not necessary but can't hurt.
queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
queryClient.invalidateQueries({ queryKey: filterKeys.detail(id) });
toast.custom((t) => (
<Toast type="success" body={`${filter?.name} was deleted`} t={t} />
));
// Invalidate filters just in case, most likely not necessary but can't hurt.
queryClient.invalidateQueries(["filters"]);
// redirect
navigate("/filters");
@ -234,7 +247,7 @@ export default function FilterDetails() {
initialValues={{
id: filter.id,
name: filter.name,
enabled: filter.enabled || false,
enabled: filter.enabled,
min_size: filter.min_size,
max_size: filter.max_size,
delay: filter.delay,
@ -298,6 +311,7 @@ export default function FilterDetails() {
external_webhook_expect_status: filter.external_webhook_expect_status || 0
} as Filter}
onSubmit={handleSubmit}
enableReinitialize={true}
>
{({ values, dirty, resetForm }) => (
<Form>
@ -322,11 +336,11 @@ export default function FilterDetails() {
}
export function General(){
const { isLoading, data: indexers } = useQuery(
["filters", "indexer_list"],
() => APIClient.indexers.getOptions(),
{ refetchOnWindowFocus: false }
);
const { isLoading, data: indexers } = useQuery({
queryKey: ["filters", "indexer_list"],
queryFn: APIClient.indexers.getOptions,
refetchOnWindowFocus: false
});
const opts = indexers && indexers.length > 0 ? indexers.map(v => ({
label: v.name,

View file

@ -2,11 +2,10 @@ import { Dispatch, FC, Fragment, MouseEventHandler, useReducer, useRef, useState
import { Link } from "react-router-dom";
import { toast } from "react-hot-toast";
import { Listbox, Menu, Switch, Transition } from "@headlessui/react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { FormikValues } from "formik";
import { useCallback } from "react";
import { Tooltip } from "react-tooltip";
import { FilterListContext, FilterListState } from "../../utils/Context";
import {
ArrowsRightLeftIcon,
CheckIcon,
@ -18,15 +17,25 @@ import {
ChatBubbleBottomCenterTextIcon,
TrashIcon
} from "@heroicons/react/24/outline";
import { classNames } from "../../utils";
import { FilterAddForm } from "../../forms";
import { useToggle } from "../../hooks/hooks";
import { APIClient } from "../../api/APIClient";
import Toast from "../../components/notifications/Toast";
import { EmptyListState } from "../../components/emptystates";
import { DeleteModal } from "../../components/modals";
import { ArrowDownTrayIcon } from "@heroicons/react/24/solid";
import { FilterListContext, FilterListState } from "@utils/Context";
import { classNames } from "@utils";
import { FilterAddForm } from "@forms";
import { useToggle } from "@hooks/hooks";
import { APIClient } from "@api/APIClient";
import Toast from "@components/notifications/Toast";
import { EmptyListState } from "@components/emptystates";
import { DeleteModal } from "@components/modals";
export const filterKeys = {
all: ["filters"] as const,
lists: () => [...filterKeys.all, "list"] as const,
list: (indexers: string[], sortOrder: string) => [...filterKeys.lists(), { indexers, sortOrder }] as const,
details: () => [...filterKeys.all, "detail"] as const,
detail: (id: number) => [...filterKeys.details(), id] as const
};
enum ActionType {
INDEXER_FILTER_CHANGE = "INDEXER_FILTER_CHANGE",
INDEXER_FILTER_RESET = "INDEXER_FILTER_RESET",
@ -63,11 +72,7 @@ const FilterListReducer = (state: FilterListState, action: Actions): FilterListS
}
};
interface FilterProps {
values?: FormikValues;
}
export default function Filters({}: FilterProps){
export default function Filters() {
const queryClient = useQueryClient();
const [createFilterIsOpen, setCreateFilterIsOpen] = useState(false);
@ -115,7 +120,7 @@ export default function Filters({}: FilterProps){
await APIClient.filters.create(newFilter);
// Update the filter list
queryClient.invalidateQueries("filters");
queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
toast.custom((t) => <Toast type="success" body="Filter imported successfully." t={t} />);
setShowImportModal(false);
@ -146,7 +151,7 @@ export default function Filters({}: FilterProps){
}}
>
<PlusIcon className="h-5 w-5 mr-1" />
Add Filter
Add Filter
</button>
<Menu.Button className="relative inline-flex items-center px-2 py-2 border-l border-spacing-1 dark:border-black shadow-sm text-sm font-medium rounded-r-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500">
<ChevronDownIcon className="h-5 w-5" />
@ -172,7 +177,7 @@ export default function Filters({}: FilterProps){
} w-full text-left py-2 px-4 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500`}
onClick={() => setShowImportModal(true)}
>
Import Filter
Import Filter
</button>
)}
</Menu.Item>
@ -250,11 +255,11 @@ function FilterList({ toggleCreateFilter }: any) {
filterListState
);
const { error, data } = useQuery(
["filters", indexerFilter, sortOrder],
() => APIClient.filters.find(indexerFilter, sortOrder),
{ refetchOnWindowFocus: false }
);
const { data, error } = useQuery({
queryKey: filterKeys.list(indexerFilter, sortOrder),
queryFn: ({ queryKey }) => APIClient.filters.find(queryKey[2].indexers, queryKey[2].sortOrder),
refetchOnWindowFocus: false
});
useEffect(() => {
FilterListContext.set({ indexerFilter, sortOrder, status });
@ -284,9 +289,13 @@ function FilterList({ toggleCreateFilter }: any) {
{data && data.length > 0 ? (
<ol className="min-w-full">
{filtered.filtered.map((filter: Filter, idx) => (
<FilterListItem filter={filter} values={filter} key={filter.id} idx={idx} />
))}
{filtered.filtered.length > 0
? filtered.filtered.map((filter: Filter, idx) => (
<FilterListItem filter={filter} values={filter} key={filter.id} idx={idx} />
))
: <EmptyListState text={`No ${status} filters`} />
}
</ol>
) : (
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />
@ -444,28 +453,25 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
const queryClient = useQueryClient();
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const deleteMutation = useMutation(
(id: number) => APIClient.filters.delete(id),
{
onSuccess: () => {
queryClient.invalidateQueries(["filters"]);
queryClient.invalidateQueries(["filters", filter.id]);
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} was deleted`} t={t} />);
}
const deleteMutation = useMutation({
mutationFn: (id: number) => APIClient.filters.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
queryClient.invalidateQueries({ queryKey: filterKeys.detail(filter.id) });
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} was deleted`} t={t} />);
}
);
});
const duplicateMutation = useMutation(
(id: number) => APIClient.filters.duplicate(id),
{
onSuccess: () => {
queryClient.invalidateQueries(["filters"]);
const duplicateMutation = useMutation({
mutationFn: (id: number) => APIClient.filters.duplicate(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} duplicated`} t={t} />);
}
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} duplicated`} t={t} />);
}
);
});
return (
<Menu as="div">
@ -634,26 +640,22 @@ interface FilterListItemProps {
}
function FilterListItem({ filter, values, idx }: FilterListItemProps) {
const [enabled, setEnabled] = useState(filter.enabled);
const queryClient = useQueryClient();
const updateMutation = useMutation(
(status: boolean) => APIClient.filters.toggleEnable(filter.id, status),
{
onSuccess: () => {
toast.custom((t) => <Toast type="success" body={`${filter.name} was ${enabled ? "disabled" : "enabled"} successfully`} t={t} />);
const updateMutation = useMutation({
mutationFn: (status: boolean) => APIClient.filters.toggleEnable(filter.id, status),
onSuccess: () => {
toast.custom((t) => <Toast type="success" body={`${filter.name} was ${!filter.enabled ? "disabled" : "enabled"} successfully`} t={t} />);
// We need to invalidate both keys here.
// The filters key is used on the /filters page,
// while the ["filter", filter.id] key is used on the details page.
queryClient.invalidateQueries(["filters"]);
queryClient.invalidateQueries(["filters", filter?.id]);
}
// We need to invalidate both keys here.
// The filters key is used on the /filters page,
// while the ["filter", filter.id] key is used on the details page.
queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
queryClient.invalidateQueries({ queryKey: filterKeys.detail(filter.id) });
}
);
});
const toggleActive = (status: boolean) => {
setEnabled(status);
updateMutation.mutate(status);
};
@ -671,10 +673,10 @@ function FilterListItem({ filter, values, idx }: FilterListItemProps) {
className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100"
>
<Switch
checked={enabled}
checked={filter.enabled}
onChange={toggleActive}
className={classNames(
enabled ? "bg-blue-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-700",
filter.enabled ? "bg-blue-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-700",
"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"
)}
>
@ -682,7 +684,7 @@ function FilterListItem({ filter, values, idx }: FilterListItemProps) {
<span
aria-hidden="true"
className={classNames(
enabled ? "translate-x-5" : "translate-x-0",
filter.enabled ? "translate-x-5" : "translate-x-0",
"inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200"
)}
/>
@ -730,7 +732,7 @@ function FilterListItem({ filter, values, idx }: FilterListItemProps) {
/>
</span>
<span className="text-sm text-gray-800 dark:text-gray-500">
<Tooltip style={{ width: "350px", fontSize: "12px", textTransform: "none", fontWeight: "normal", borderRadius: "0.375rem", backgroundColor: "#34343A", color: "#fff", opacity: "1", whiteSpace: "pre-wrap", overflow: "hidden", textOverflow: "ellipsis" }} delayShow={100} delayHide={150} data-html={true} place="right" anchorId={`tooltip-actions-${filter.id}`} html="<p>You need to setup an action in the filter otherwise you will not get any snatches.</p>" />
<Tooltip style={{ width: "350px", fontSize: "12px", textTransform: "none", fontWeight: "normal", borderRadius: "0.375rem", backgroundColor: "#34343A", color: "#fff", opacity: "1", whiteSpace: "pre-wrap", overflow: "hidden", textOverflow: "ellipsis" }} delayShow={100} delayHide={150} data-html={true} place="right" data-tooltip-id={`tooltip-actions-${filter.id}`} html="<p>You need to setup an action in the filter otherwise you will not get any snatches.</p>" />
</span>
</>
)}
@ -848,14 +850,12 @@ const ListboxFilter = ({
// a unique option from a list
const IndexerSelectFilter = ({ dispatch }: any) => {
const { data, isSuccess } = useQuery(
"indexers_options",
() => APIClient.indexers.getOptions(),
{
keepPreviousData: true,
staleTime: Infinity
}
);
const { data, isSuccess } = useQuery({
queryKey: ["filters","indexers_options"],
queryFn: () => APIClient.indexers.getOptions(),
keepPreviousData: true,
staleTime: Infinity
});
const setFilter = (value: string) => {
if (value == undefined || value == "") {

View file

@ -1,11 +1,11 @@
import * as React from "react";
import { useQuery } from "react-query";
import { useQuery } from "@tanstack/react-query";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/solid";
import { APIClient } from "../../api/APIClient";
import { classNames } from "../../utils";
import { PushStatusOptions } from "../../domain/constants";
import { APIClient } from "@api/APIClient";
import { classNames } from "@utils";
import { PushStatusOptions } from "@domain/constants";
import { FilterProps } from "react-table";
import { DebounceInput } from "react-debounce-input";
@ -62,14 +62,12 @@ const ListboxFilter = ({
export const IndexerSelectColumnFilter = ({
column: { filterValue, setFilter, id }
}: FilterProps<object>) => {
const { data, isSuccess } = useQuery(
"indexer_options",
() => APIClient.release.indexerOptions(),
{
keepPreviousData: true,
staleTime: Infinity
}
);
const { data, isSuccess } = useQuery({
queryKey: ["indexer_options"],
queryFn: () => APIClient.release.indexerOptions(),
keepPreviousData: true,
staleTime: Infinity
});
// Render a multi-select box
return (

View file

@ -1,5 +1,5 @@
import * as React from "react";
import { useQuery } from "react-query";
import { useQuery } from "@tanstack/react-query";
import { CellProps, Column, useFilters, usePagination, useSortBy, useTable } from "react-table";
import {
ChevronDoubleLeftIcon,
@ -8,16 +8,25 @@ import {
ChevronRightIcon
} from "@heroicons/react/24/solid";
import { APIClient } from "../../api/APIClient";
import { EmptyListState } from "../../components/emptystates";
import { APIClient } from "@api/APIClient";
import { EmptyListState } from "@components/emptystates";
import * as Icons from "../../components/Icons";
import * as DataTable from "../../components/data-table";
import * as Icons from "@components/Icons";
import * as DataTable from "@components/data-table";
import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./Filters";
import { classNames } from "../../utils";
import { classNames } from "@utils";
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
import { Tooltip } from "../../components/tooltips/Tooltip";
import { Tooltip } from "@components/tooltips/Tooltip";
export const releaseKeys = {
all: ["releases"] as const,
lists: () => [...releaseKeys.all, "list"] as const,
list: (pageIndex: number, pageSize: number, filters: ReleaseFilter[]) => [...releaseKeys.lists(), { pageIndex, pageSize, filters }] as const,
details: () => [...releaseKeys.all, "detail"] as const,
detail: (id: number) => [...releaseKeys.details(), id] as const
};
type TableState = {
queryPageIndex: number;
@ -120,14 +129,12 @@ export const ReleaseTable = () => {
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
React.useReducer(TableReducer, initialState);
const { isLoading, error, data, isSuccess } = useQuery(
["releases", queryPageIndex, queryPageSize, queryFilters],
() => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
{
keepPreviousData: true,
staleTime: 5000
}
);
const { isLoading, error, data, isSuccess } = useQuery({
queryKey: releaseKeys.list(queryPageIndex, queryPageSize, queryFilters),
queryFn: () => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
keepPreviousData: true,
staleTime: 5000
});
// Use the state and functions returned from useTable to build your UI
const {
@ -192,10 +199,11 @@ export const ReleaseTable = () => {
dispatch({ type: ActionType.FILTER_CHANGED, payload: filters });
}, [filters]);
if (error)
if (error) {
return <p>Error</p>;
}
if (isLoading)
if (isLoading) {
return (
<div className="flex flex-col animate-pulse">
<div className="flex mb-6 flex-col sm:flex-row">
@ -211,9 +219,7 @@ export const ReleaseTable = () => {
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th
scope="col"
className="first:pl-5 pl-3 pr-3 py-3 first:rounded-tl-md last:rounded-tr-md text-xs font-medium tracking-wider text-left text-gray-500 uppercase group"
@ -224,28 +230,30 @@ export const ReleaseTable = () => {
</span>
</div>
</th>
</tr>
</thead>
<tbody className=" divide-gray-200 dark:divide-gray-700">
<tr className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<tr
className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
</tr>
<tr className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<tr
className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
</tr>
<tr className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<tr
className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap">&nbsp;</td>
</tr>
<tr className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<tr
className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
@ -255,27 +263,32 @@ export const ReleaseTable = () => {
<p className="text-black dark:text-white">Loading release table...</p>
</td>
</tr>
<tr className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<tr
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
</tr>
<tr className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<tr
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
</tr>
<tr className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<tr
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
</tr>
<tr className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<tr
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
</tr>
<tr className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<tr
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
@ -292,7 +305,8 @@ export const ReleaseTable = () => {
<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 dark:text-gray-500">
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>
<label>
<span className="sr-only bg-gray-700">Items Per Page</span>
@ -305,7 +319,7 @@ export const ReleaseTable = () => {
>
{[5, 10, 20, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
Show {pageSize}
</option>
))}
</select>
@ -319,20 +333,20 @@ export const ReleaseTable = () => {
disabled={!canPreviousPage}
>
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">First</span>
<ChevronDoubleLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
<ChevronDoubleLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true"/>
</DataTable.PageButton>
<DataTable.PageButton
onClick={() => previousPage()}
disabled={!canPreviousPage}
>
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Previous</span>
<ChevronLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
<ChevronLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true"/>
</DataTable.PageButton>
<DataTable.PageButton
onClick={() => nextPage()}
disabled={!canNextPage}>
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Next</span>
<ChevronRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
<ChevronRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true"/>
</DataTable.PageButton>
<DataTable.PageButton
className="rounded-r-md"
@ -340,7 +354,7 @@ export const ReleaseTable = () => {
disabled={!canNextPage}
>
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Last</span>
<ChevronDoubleRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
<ChevronDoubleRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true"/>
</DataTable.PageButton>
</nav>
</div>
@ -349,9 +363,11 @@ export const ReleaseTable = () => {
</div>
</div>
);
}
if (!data)
if (!data) {
return <EmptyListState text="No recent activity" />;
}
// Render the UI for your table
return (

View file

@ -1,20 +1,31 @@
import { useRef } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { KeyField } from "../../components/fields/text";
import { DeleteModal } from "../../components/modals";
import APIKeyAddForm from "../../forms/settings/APIKeyAddForm";
import Toast from "../../components/notifications/Toast";
import { APIClient } from "../../api/APIClient";
import { useToggle } from "../../hooks/hooks";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast";
import { classNames } from "../../utils";
import { TrashIcon } from "@heroicons/react/24/outline";
import { EmptySimple } from "../../components/emptystates";
import { KeyField } from "@components/fields/text";
import { DeleteModal } from "@components/modals";
import APIKeyAddForm from "@forms/settings/APIKeyAddForm";
import Toast from "@components/notifications/Toast";
import { APIClient } from "@api/APIClient";
import { useToggle } from "@hooks/hooks";
import { classNames } from "@utils";
import { EmptySimple } from "@components/emptystates";
export const apiKeys = {
all: ["api_keys"] as const,
lists: () => [...apiKeys.all, "list"] as const,
details: () => [...apiKeys.all, "detail"] as const,
// detail: (id: number) => [...apiKeys.details(), id] as const
detail: (id: string) => [...apiKeys.details(), id] as const
};
function APISettings() {
const [addFormIsOpen, toggleAddForm] = useToggle(false);
const { data } = useQuery(["apikeys"], () => APIClient.apikeys.getAll(), {
const { data } = useQuery({
queryKey: apiKeys.lists(),
queryFn: APIClient.apikeys.getAll,
retry: false,
refetchOnWindowFocus: false,
onError: (err) => console.log(err)
@ -57,7 +68,7 @@ function APISettings() {
</div>
</li>
{data && data.map((k) => <APIListItem key={k.key} apikey={k} />)}
{data && data.map((k, idx) => <APIListItem key={idx} apikey={k} />)}
</ol>
</section>
) : (
@ -83,23 +94,21 @@ function APIListItem({ apikey }: ApiKeyItemProps) {
const queryClient = useQueryClient();
const deleteMutation = useMutation(
(key: string) => APIClient.apikeys.delete(key),
{
onSuccess: () => {
queryClient.invalidateQueries(["apikeys"]);
queryClient.invalidateQueries(["apikeys", apikey.key]);
const deleteMutation = useMutation({
mutationFn: (key: string) => APIClient.apikeys.delete(key),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiKeys.lists() });
queryClient.invalidateQueries({ queryKey: apiKeys.detail(apikey.key) });
toast.custom((t) => (
<Toast
type="success"
body={`API key ${apikey?.name} was deleted`}
t={t}
/>
));
}
toast.custom((t) => (
<Toast
type="success"
body={`API key ${apikey?.name} was deleted`}
t={t}
/>
));
}
);
});
return (
<li className="text-gray-500 dark:text-gray-400">

View file

@ -1,10 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "react-query";
import { APIClient } from "../../api/APIClient";
import { Checkbox } from "../../components/Checkbox";
import { SettingsContext } from "../../utils/Context";
import { GithubRelease } from "../../types/Update";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { APIClient } from "@api/APIClient";
import { Checkbox } from "@components/Checkbox";
import { SettingsContext } from "@utils/Context";
import { GithubRelease } from "@app/types/Update";
import Toast from "@components/notifications/Toast";
interface RowItemProps {
label: string;
@ -47,8 +48,9 @@ const RowItemNumber = ({ label, value, title, unit }: RowItemNumberProps) => {
};
const RowItemVersion = ({ label, value, title, newUpdate }: RowItemProps) => {
if (!value)
if (!value) {
return null;
}
return (
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
@ -68,49 +70,41 @@ const RowItemVersion = ({ label, value, title, newUpdate }: RowItemProps) => {
function ApplicationSettings() {
const [settings, setSettings] = SettingsContext.use();
const { isLoading, data } = useQuery(
["config"],
() => APIClient.config.get(),
{
retry: false,
refetchOnWindowFocus: false,
onError: err => console.log(err)
}
);
const { isLoading, data } = useQuery({
queryKey: ["config"],
queryFn: APIClient.config.get,
retry: false,
refetchOnWindowFocus: false,
onError: err => console.log(err)
});
const { data: updateData } = useQuery(
["updates"],
() => APIClient.updates.getLatestRelease(),
{
retry: false,
refetchOnWindowFocus: false,
onError: err => console.log(err)
}
);
const { data: updateData } = useQuery({
queryKey: ["updates"],
queryFn: APIClient.updates.getLatestRelease,
retry: false,
refetchOnWindowFocus: false,
onError: err => console.log(err)
});
const queryClient = useQueryClient();
const checkUpdateMutation = useMutation(
() => APIClient.updates.check(),
{
onSuccess: () => {
queryClient.invalidateQueries(["updates"]);
}
const checkUpdateMutation = useMutation({
mutationFn: APIClient.updates.check,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["updates"] });
}
);
});
const toggleCheckUpdateMutation = useMutation(
(value: boolean) => APIClient.config.update({ check_for_updates: value }),
{
onSuccess: () => {
toast.custom((t) => <Toast type="success" body={"Config successfully updated!"} t={t}/>);
const toggleCheckUpdateMutation = useMutation({
mutationFn: (value: boolean) => APIClient.config.update({ check_for_updates: value }),
onSuccess: () => {
toast.custom((t) => <Toast type="success" body={"Config successfully updated!"} t={t}/>);
queryClient.invalidateQueries(["config"]);
queryClient.invalidateQueries({ queryKey: ["config"] });
checkUpdateMutation.mutate();
}
checkUpdateMutation.mutate();
}
);
});
return (
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">

View file

@ -1,14 +1,23 @@
import { useToggle } from "../../hooks/hooks";
import { Switch } from "@headlessui/react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { classNames } from "../../utils";
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
import { EmptySimple } from "../../components/emptystates";
import { APIClient } from "../../api/APIClient";
import { DownloadClientTypeNameMap } from "../../domain/constants";
import toast from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { useState, useMemo } from "react";
import { Switch } from "@headlessui/react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { useToggle } from "@hooks/hooks";
import { classNames } from "@utils";
import { DownloadClientAddForm, DownloadClientUpdateForm } from "@forms";
import { EmptySimple } from "@components/emptystates";
import { APIClient } from "@api/APIClient";
import { DownloadClientTypeNameMap } from "@domain/constants";
import Toast from "@components/notifications/Toast";
export const clientKeys = {
all: ["download_clients"] as const,
lists: () => [...clientKeys.all, "list"] as const,
// list: (indexers: string[], sortOrder: string) => [...clientKeys.lists(), { indexers, sortOrder }] as const,
details: () => [...clientKeys.all, "detail"] as const,
detail: (id: number) => [...clientKeys.details(), id] as const
};
interface DLSettingsItemProps {
client: DownloadClient;
@ -61,7 +70,6 @@ function useSort(items: ListItemProps["clients"][], config?: SortConfig) {
}
setSortConfig({ key, direction });
};
const getSortIndicator = (key: keyof ListItemProps["clients"]) => {
if (!sortConfig || sortConfig.key !== key) {
@ -79,15 +87,14 @@ function DownloadClientSettingsListItem({ client }: DLSettingsItemProps) {
const queryClient = useQueryClient();
const mutation = useMutation(
(client: DownloadClient) => APIClient.download_clients.update(client),
{
onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]);
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t}/>);
}
const mutation = useMutation({
mutationFn: (client: DownloadClient) => APIClient.download_clients.update(client),
onSuccess: () => {
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t}/>);
queryClient.invalidateQueries({ queryKey: clientKeys.lists() });
}
);
});
const onToggleMutation = (newState: boolean) => {
mutation.mutate({
@ -139,11 +146,11 @@ function DownloadClientSettingsListItem({ client }: DLSettingsItemProps) {
function DownloadClientSettings() {
const [addClientIsOpen, toggleAddClient] = useToggle(false);
const { error, data } = useQuery(
"downloadClients",
() => APIClient.download_clients.getAll(),
{ refetchOnWindowFocus: false }
);
const { error, data } = useQuery({
queryKey: clientKeys.lists(),
queryFn: APIClient.download_clients.getAll,
refetchOnWindowFocus: false
});
const sortedClients = useSort(data || []);
@ -151,10 +158,8 @@ function DownloadClientSettings() {
return <p>Failed to fetch download clients</p>;
}
return (
<div className="lg:col-span-9">
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} />
<div className="py-6 px-2 lg:pb-8">
@ -177,8 +182,8 @@ function DownloadClientSettings() {
</div>
<div className="flex flex-col mt-6 px-4">
{sortedClients.items.length > 0 ?
<section className="light:bg-white dark:bg-gray-800 light:shadow sm:rounded-sm">
{sortedClients.items.length > 0
? <section className="light:bg-white dark:bg-gray-800 light:shadow sm:rounded-sm">
<ol className="min-w-full relative">
<li className="grid grid-cols-12 border-b border-gray-200 dark:border-gray-700">
<div className="flex col-span-2 sm:col-span-1 px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
@ -213,7 +218,6 @@ function DownloadClientSettings() {
</div>
</div>
</div>
);
}

View file

@ -1,13 +1,7 @@
import { useToggle } from "../../hooks/hooks";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { APIClient } from "../../api/APIClient";
import { Menu, Switch, Transition } from "@headlessui/react";
import { baseUrl, classNames, IsEmptyDate, simplifyDate } from "../../utils";
import { Fragment, useRef, useState, useMemo } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Menu, Switch, Transition } from "@headlessui/react";
import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { DeleteModal } from "../../components/modals";
import {
ArrowsRightLeftIcon,
DocumentTextIcon,
@ -15,10 +9,24 @@ import {
PencilSquareIcon,
TrashIcon
} from "@heroicons/react/24/outline";
import { FeedUpdateForm } from "../../forms/settings/FeedForms";
import { EmptySimple } from "../../components/emptystates";
import { APIClient } from "@api/APIClient";
import { useToggle } from "@hooks/hooks";
import { baseUrl, classNames, IsEmptyDate, simplifyDate } from "@utils";
import Toast from "@components/notifications/Toast";
import { DeleteModal } from "@components/modals";
import { FeedUpdateForm } from "@forms/settings/FeedForms";
import { EmptySimple } from "@components/emptystates";
import { ImplementationBadges } from "./Indexer";
export const feedKeys = {
all: ["feeds"] as const,
lists: () => [...feedKeys.all, "list"] as const,
// list: (indexers: string[], sortOrder: string) => [...feedKeys.lists(), { indexers, sortOrder }] as const,
details: () => [...feedKeys.all, "detail"] as const,
detail: (id: number) => [...feedKeys.details(), id] as const
};
interface SortConfig {
key: keyof ListItemProps["feed"] | "enabled";
direction: "ascending" | "descending";
@ -27,8 +35,6 @@ interface SortConfig {
function useSort(items: ListItemProps["feed"][], config?: SortConfig) {
const [sortConfig, setSortConfig] = useState(config);
const sortedItems = useMemo(() => {
if (!sortConfig) {
return items;
@ -76,11 +82,11 @@ function useSort(items: ListItemProps["feed"][], config?: SortConfig) {
}
function FeedSettings() {
const { data } = useQuery(
"feeds",
() => APIClient.feeds.find(),
{ refetchOnWindowFocus: false }
);
const { data } = useQuery({
queryKey: feedKeys.lists(),
queryFn: APIClient.feeds.find,
refetchOnWindowFocus: false
});
const sortedFeeds = useSort(data || []);
@ -142,19 +148,15 @@ function ListItem({ feed }: ListItemProps) {
const [enabled, setEnabled] = useState(feed.enabled);
const queryClient = useQueryClient();
const updateMutation = useMutation(
(status: boolean) => APIClient.feeds.toggleEnable(feed.id, status),
{
onSuccess: () => {
toast.custom((t) => <Toast type="success"
body={`${feed.name} was ${enabled ? "disabled" : "enabled"} successfully`}
t={t}/>);
const updateMutation = useMutation({
mutationFn: (status: boolean) => APIClient.feeds.toggleEnable(feed.id, status),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: feedKeys.lists() });
queryClient.invalidateQueries({ queryKey: feedKeys.detail(feed.id) });
queryClient.invalidateQueries(["feeds"]);
queryClient.invalidateQueries(["feeds", feed?.id]);
}
toast.custom((t) => <Toast type="success" body={`${feed.name} was ${!enabled ? "disabled" : "enabled"} successfully`} t={t}/>);
}
);
});
const toggleActive = (status: boolean) => {
setEnabled(status);
@ -227,17 +229,15 @@ const FeedItemDropdown = ({
const queryClient = useQueryClient();
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const deleteMutation = useMutation(
(id: number) => APIClient.feeds.delete(id),
{
onSuccess: () => {
queryClient.invalidateQueries(["feeds"]);
queryClient.invalidateQueries(["feeds", feed.id]);
const deleteMutation = useMutation({
mutationFn: (id: number) => APIClient.feeds.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: feedKeys.lists() });
queryClient.invalidateQueries({ queryKey: feedKeys.detail(feed.id) });
toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} was deleted`} t={t}/>);
}
toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} was deleted`} t={t}/>);
}
);
});
return (
<Menu as="div">

View file

@ -1,12 +1,21 @@
import { useToggle } from "../../hooks/hooks";
import { useQuery } from "react-query";
import { IndexerAddForm, IndexerUpdateForm } from "../../forms";
import { Switch } from "@headlessui/react";
import { classNames } from "../../utils";
import { EmptySimple } from "../../components/emptystates";
import { APIClient } from "../../api/APIClient";
import { componentMapType } from "../../forms/settings/DownloadClientForms";
import { useState, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { Switch } from "@headlessui/react";
import { IndexerAddForm, IndexerUpdateForm } from "@forms";
import { useToggle } from "@hooks/hooks";
import { classNames } from "@utils";
import { EmptySimple } from "@components/emptystates";
import { APIClient } from "@api/APIClient";
import { componentMapType } from "@forms/settings/DownloadClientForms";
export const indexerKeys = {
all: ["indexers"] as const,
lists: () => [...indexerKeys.all, "list"] as const,
// list: (indexers: string[], sortOrder: string) => [...indexerKeys.lists(), { indexers, sortOrder }] as const,
details: () => [...indexerKeys.all, "detail"] as const,
detail: (id: number) => [...indexerKeys.details(), id] as const
};
interface SortConfig {
key: keyof ListItemProps["indexer"] | "enabled";
@ -149,16 +158,17 @@ const ListItem = ({ indexer }: ListItemProps) => {
function IndexerSettings() {
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false);
const { error, data } = useQuery(
"indexer",
() => APIClient.indexers.getAll(),
{ refetchOnWindowFocus: false }
);
const { error, data } = useQuery({
queryKey: indexerKeys.lists(),
queryFn: APIClient.indexers.getAll,
refetchOnWindowFocus: false
});
const sortedIndexers = useSort(data || []);
if (error)
if (error) {
return (<p>An error has occurred</p>);
}
return (
<div className="lg:col-span-9">

View file

@ -1,16 +1,8 @@
import { useMutation, useQuery, useQueryClient } from "react-query";
import { classNames, IsEmptyDate, simplifyDate } from "../../utils";
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
import { useToggle } from "../../hooks/hooks";
import { APIClient } from "../../api/APIClient";
import { EmptySimple } from "../../components/emptystates";
import { Fragment, useRef, useState, useMemo } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/24/solid";
import { Menu, Switch, Transition } from "@headlessui/react";
import { Fragment, useRef } from "react";
import { DeleteModal } from "../../components/modals";
import { useState, useMemo } from "react";
import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import {
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
@ -20,6 +12,22 @@ import {
TrashIcon
} from "@heroicons/react/24/outline";
import { classNames, IsEmptyDate, simplifyDate } from "@utils";
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "@forms";
import { useToggle } from "@hooks/hooks";
import { APIClient } from "@api/APIClient";
import { EmptySimple } from "@components/emptystates";
import { DeleteModal } from "@components/modals";
import Toast from "@components/notifications/Toast";
export const ircKeys = {
all: ["irc_networks"] as const,
lists: () => [...ircKeys.all, "list"] as const,
// list: (indexers: string[], sortOrder: string) => [...ircKeys.lists(), { indexers, sortOrder }] as const,
details: () => [...ircKeys.all, "detail"] as const,
detail: (id: number) => [...ircKeys.details(), id] as const
};
interface SortConfig {
key: keyof ListItemProps["network"] | "enabled";
direction: "ascending" | "descending";
@ -79,10 +87,11 @@ const IrcSettings = () => {
const [expandNetworks, toggleExpand] = useToggle(false);
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false);
const { data } = useQuery("networks", () => APIClient.irc.getNetworks(), {
const { data } = useQuery({
queryKey: ircKeys.lists(),
queryFn: APIClient.irc.getNetworks,
refetchOnWindowFocus: false,
// Refetch every 3 seconds
refetchInterval: 3000
refetchInterval: 3000 // Refetch every 3 seconds
});
const sortedNetworks = useSort(data || []);
@ -204,18 +213,17 @@ const ListItem = ({ idx, network, expanded }: ListItemProps) => {
const queryClient = useQueryClient();
const mutation = useMutation(
(network: IrcNetwork) => APIClient.irc.updateNetwork(network),
{
onSuccess: () => {
queryClient.invalidateQueries(["networks"]);
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t}/>);
}
const updateMutation = useMutation({
mutationFn: (network: IrcNetwork) => APIClient.irc.updateNetwork(network),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t}/>);
}
);
});
const onToggleMutation = (newState: boolean) => {
mutation.mutate({
updateMutation.mutate({
...network,
enabled: newState
});
@ -399,37 +407,30 @@ const ListItemDropdown = ({
const queryClient = useQueryClient();
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const deleteMutation = useMutation(
(id: number) => APIClient.irc.deleteNetwork(id),
{
onSuccess: () => {
queryClient.invalidateQueries(["networks"]);
queryClient.invalidateQueries(["networks", network.id]);
toast.custom((t) => <Toast type="success" body={`Network ${network.name} was deleted`} t={t}/>);
const deleteMutation = useMutation({
mutationFn: (id: number) => APIClient.irc.deleteNetwork(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
queryClient.invalidateQueries({ queryKey: ircKeys.detail(network.id) });
toggleDeleteModal();
}
toast.custom((t) => <Toast type="success" body={`Network ${network.name} was deleted`} t={t}/>);
toggleDeleteModal();
}
);
});
const restartMutation = useMutation(
(id: number) => APIClient.irc.restartNetwork(id),
{
onSuccess: () => {
toast.custom((t) => <Toast type="success"
body={`${network.name} was successfully restarted`}
t={t}/>);
const restartMutation = useMutation({
mutationFn: (id: number) => APIClient.irc.restartNetwork(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
queryClient.invalidateQueries({ queryKey: ircKeys.detail(network.id) });
queryClient.invalidateQueries(["networks"]);
queryClient.invalidateQueries(["networks", network.id]);
}
toast.custom((t) => <Toast type="success" body={`${network.name} was successfully restarted`} t={t}/>);
}
);
});
const restart = (id: number) => {
restartMutation.mutate(id);
};
const restart = (id: number) => restartMutation.mutate(id);
return (
<Menu as="div">

View file

@ -1,10 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "react-query";
import { APIClient } from "../../api/APIClient";
import { GithubRelease } from "../../types/Update";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { LogLevelOptions, SelectOption } from "../../domain/constants";
import { APIClient } from "@api/APIClient";
import { GithubRelease } from "@app/types/Update";
import Toast from "@components/notifications/Toast";
import { LogLevelOptions, SelectOption } from "@domain/constants";
import { LogFiles } from "../Logs";
interface RowItemProps {
@ -121,28 +122,24 @@ const RowItemSelect = ({ id, title, label, value, options, onChange }: any) => {
};
function LogSettings() {
const { isLoading, data } = useQuery(
["config"],
() => APIClient.config.get(),
{
retry: false,
refetchOnWindowFocus: false,
onError: err => console.log(err)
}
);
const { isLoading, data } = useQuery({
queryKey: ["config"],
queryFn: APIClient.config.get,
retry: false,
refetchOnWindowFocus: false,
onError: err => console.log(err)
});
const queryClient = useQueryClient();
const setLogLevelUpdateMutation = useMutation(
(value: string) => APIClient.config.update({ log_level: value }),
{
onSuccess: () => {
toast.custom((t) => <Toast type="success" body={"Config successfully updated!"} t={t}/>);
const setLogLevelUpdateMutation = useMutation({
mutationFn: (value: string) => APIClient.config.update({ log_level: value }),
onSuccess: () => {
toast.custom((t) => <Toast type="success" body={"Config successfully updated!"} t={t}/>);
queryClient.invalidateQueries(["config"]);
}
queryClient.invalidateQueries({ queryKey: ["config"] });
}
);
});
return (
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">

View file

@ -1,19 +1,27 @@
import { useQuery } from "react-query";
import { APIClient } from "../../api/APIClient";
import { EmptySimple } from "../../components/emptystates";
import { useToggle } from "../../hooks/hooks";
import { NotificationAddForm, NotificationUpdateForm } from "../../forms/settings/NotificationForms";
import { useQuery } from "@tanstack/react-query";
import { Switch } from "@headlessui/react";
import { classNames } from "../../utils";
import { componentMapType } from "../../forms/settings/DownloadClientForms";
import { APIClient } from "@api/APIClient";
import { EmptySimple } from "@components/emptystates";
import { useToggle } from "@hooks/hooks";
import { NotificationAddForm, NotificationUpdateForm } from "@forms/settings/NotificationForms";
import { classNames } from "@utils";
import { componentMapType } from "@forms/settings/DownloadClientForms";
export const notificationKeys = {
all: ["notifications"] as const,
lists: () => [...notificationKeys.all, "list"] as const,
details: () => [...notificationKeys.all, "detail"] as const,
detail: (id: number) => [...notificationKeys.details(), id] as const
};
function NotificationSettings() {
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false);
const { data } = useQuery(
"notifications",
() => APIClient.notifications.getAll(),
{ refetchOnWindowFocus: false }
const { data } = useQuery({
queryKey: notificationKeys.lists(),
queryFn: APIClient.notifications.getAll,
refetchOnWindowFocus: false }
);
return (

View file

@ -1,29 +1,30 @@
import { useRef } from "react";
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast";
import { APIClient } from "../../api/APIClient";
import Toast from "../../components/notifications/Toast";
import { useToggle } from "../../hooks/hooks";
import { DeleteModal } from "../../components/modals";
import { APIClient } from "@api/APIClient";
import Toast from "@components/notifications/Toast";
import { useToggle } from "@hooks/hooks";
import { DeleteModal } from "@components/modals";
import { releaseKeys } from "@screens/releases/ReleaseTable";
function ReleaseSettings() {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const queryClient = useQueryClient();
const deleteMutation = useMutation(() => APIClient.release.delete(), {
const deleteMutation = useMutation({
mutationFn: APIClient.release.delete,
onSuccess: () => {
toast.custom((t) => (
<Toast type="success" body={"All releases were deleted"} t={t}/>
));
// Invalidate filters just in case, most likely not necessary but can't hurt.
queryClient.invalidateQueries("releases");
queryClient.invalidateQueries({ queryKey: releaseKeys.lists() });
}
});
const deleteAction = () => {
deleteMutation.mutate();
};
const deleteAction = () => deleteMutation.mutate();
const cancelModalButtonRef = useRef(null);

View file

@ -1,6 +1,5 @@
import { newRidgeState } from "react-ridge-state";
export const InitializeGlobalContext = () => {
const auth_ctx = localStorage.getItem("auth");
if (auth_ctx)