feat: dark mode (#32)

This commit is contained in:
Ludvig Lundgren 2021-09-26 16:52:37 +02:00 committed by GitHub
parent 974ca95d80
commit 66048c5533
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1736 additions and 1992 deletions

View file

@ -1,20 +1,17 @@
import React, {Fragment, useEffect} from "react";
import {useMutation} from "react-query";
import {Filter} from "../../domain/interfaces";
import {queryClient} from "../../App";
import {XIcon} from "@heroicons/react/solid";
import {Dialog, Transition} from "@headlessui/react";
import {Field, Form} from "react-final-form";
import { Fragment, useEffect } from "react";
import { useMutation } from "react-query";
import { Filter } from "../../domain/interfaces";
import { queryClient } from "../../App";
import { XIcon } from "@heroicons/react/solid";
import { Dialog, Transition } from "@headlessui/react";
import { Field, Form } from "react-final-form";
import DEBUG from "../../components/debug";
import APIClient from "../../api/APIClient";
import { toast } from 'react-hot-toast'
import Toast from '../../components/notifications/Toast';
const required = (value: any) => (value ? undefined : 'Required')
function FilterAddForm({isOpen, toggle}: any) {
function FilterAddForm({ isOpen, toggle }: any) {
const mutation = useMutation((filter: Filter) => APIClient.filters.create(filter), {
onSuccess: () => {
queryClient.invalidateQueries('filter');
@ -32,11 +29,21 @@ function FilterAddForm({isOpen, toggle}: any) {
mutation.mutate(data)
}
const validate = (values: any) => {
const errors = {} as any;
if (!values.name) {
errors.name = "Required";
}
return errors;
}
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0"/>
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
@ -48,7 +55,7 @@ function FilterAddForm({isOpen, toggle}: any) {
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl">
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
<Form
initialValues={{
@ -59,38 +66,34 @@ function FilterAddForm({isOpen, toggle}: any) {
sources: [],
containers: []
}}
// validate={validate}
validate={validate}
onSubmit={onSubmit}
>
{({handleSubmit, values}) => {
{({ handleSubmit, values }) => {
return (
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll" onSubmit={handleSubmit}>
<form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll" onSubmit={handleSubmit}>
<div className="flex-1">
{/* Header */}
<div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title
className="text-lg font-medium text-gray-900">Create
filter</Dialog.Title>
<p className="text-sm text-gray-500">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Create filter</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Add new filter.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
className="light:bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true"/>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
</div>
{/* Divider container */}
<div
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<div
@ -98,21 +101,21 @@ function FilterAddForm({isOpen, toggle}: any) {
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
>
Name
</label>
</div>
<Field name="name" validate={required}>
{({input, meta}) => (
<Field name="name">
{({ input, meta }) => (
<div className="sm:col-span-2">
<input
type="text"
{...input}
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md"
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-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 rounded-md"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
<span className="block mt-2 text-red-500">{meta.error}</span>}
</div>
)}
</Field>
@ -122,24 +125,24 @@ function FilterAddForm({isOpen, toggle}: any) {
</div>
<div
className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
<div className="space-x-3 flex justify-end">
<button
type="button"
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
className="bg-white dark:bg-gray-800 py-2 px-4 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
>
Create
</button>
</div>
</div>
<DEBUG values={values}/>
<DEBUG values={values} />
</form>
)
}}

View file

@ -2,10 +2,6 @@ export { default as FilterAddForm } from "./filters/FilterAddForm";
export { default as FilterActionAddForm } from "./filters/FilterActionAddForm";
export { default as FilterActionUpdateForm } from "./filters/FilterActionUpdateForm";
export { default as DownloadClientAddForm } from "./settings/downloadclient/DownloadClientAddForm";
export { default as DownloadClientUpdateForm } from "./settings/downloadclient/DownloadClientUpdateForm";
export { default as IndexerAddForm } from "./settings/IndexerAddForm";
export { default as IndexerUpdateForm } from "./settings/IndexerUpdateForm";
export { default as IrcNetworkAddForm } from "./settings/IrcNetworkAddForm";
export { DownloadClientAddForm, DownloadClientUpdateForm } from "./settings/DownloadClientForms";
export { IndexerAddForm, IndexerUpdateForm } from "./settings/IndexerForms";
export { IrcNetworkAddForm, IrcNetworkUpdateForm } from "./settings/IrcForms";

View file

@ -0,0 +1,617 @@
import { Fragment, useRef, useState } from "react";
import { useMutation } from "react-query";
import {
DOWNLOAD_CLIENT_TYPES,
DownloadClient,
} from "../../domain/interfaces";
import { Dialog, Transition } from "@headlessui/react";
import { XIcon } from "@heroicons/react/solid";
import { classNames } from "../../styles/utils";
import { Form, useField } from "react-final-form";
import DEBUG from "../../components/debug";
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
import { queryClient } from "../../App";
import APIClient from "../../api/APIClient";
import { sleep } from "../../utils/utils";
import { DownloadClientTypeOptions } from "../../domain/constants";
import { NumberFieldWide, PasswordFieldWide, RadioFieldsetWide } from "../../components/inputs/wide";
import { toast } from 'react-hot-toast'
import Toast from '../../components/notifications/Toast';
import { useToggle } from "../../hooks/hooks";
import { DeleteModal } from "../../components/modals";
function FormFieldsDefault() {
return (
<Fragment>
<TextFieldWide name="host" label="Host" help="Url domain.ltd/client" />
<NumberFieldWide name="port" label="Port" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="ssl" label="SSL" />
</div>
<TextFieldWide name="username" label="Username" />
<PasswordFieldWide name="password" label="Password" />
</Fragment>
);
}
function FormFieldsArr() {
const { input } = useField("settings.basic.auth");
return (
<Fragment>
<TextFieldWide name="host" label="Host" help="Full url like http(s)://domain.ltd/" />
<PasswordFieldWide name="settings.apikey" label="API key" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.basic.auth" label="Basic auth" />
</div>
{input.value === true && (
<Fragment>
<TextFieldWide name="settings.basic.username" label="Username" />
<PasswordFieldWide name="settings.basic.password" label="Password" />
</Fragment>
)}
</Fragment>
);
}
export const componentMap: any = {
DELUGE_V1: <FormFieldsDefault />,
DELUGE_V2: <FormFieldsDefault />,
QBITTORRENT: <FormFieldsDefault />,
RADARR: <FormFieldsArr />,
SONARR: <FormFieldsArr />,
LIDARR: <FormFieldsArr />,
};
function FormFieldsRulesBasic() {
const { input: enabled } = useField("settings.rules.enabled");
return (
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
<div className="px-6 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Rules</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Manage max downloads.
</p>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
</div>
{enabled.value === true && (
<Fragment>
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
</Fragment>
)}
</div>
);
}
function FormFieldsRules() {
const { input } = useField("settings.rules.ignore_slow_torrents");
const { input: enabled } = useField("settings.rules.enabled");
return (
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
<div className="px-6 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Rules</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Manage max downloads etc.
</p>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
</div>
{enabled.value === true && (
<Fragment>
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents" />
</div>
{input.value === true && (
<Fragment>
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold" placeholder="in KB/s" help="If download speed is below this when max active downloads is hit, download anyways. KB/s" />
</Fragment>
)}
</Fragment>
)}
</div>
);
}
export const rulesComponentMap: any = {
DELUGE_V1: <FormFieldsRulesBasic />,
DELUGE_V2: <FormFieldsRulesBasic />,
QBITTORRENT: <FormFieldsRules />,
};
export function DownloadClientAddForm({ isOpen, toggle }: any) {
const [isTesting, setIsTesting] = useState(false);
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
const [isErrorTest, setIsErrorTest] = useState(false);
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} />)
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);
});
});
},
onError: (error) => {
console.log('not added')
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
},
}
);
const onSubmit = (data: any) => {
mutation.mutate(data);
};
const testClient = (data: any) => {
testClientMutation.mutate(data);
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog
as="div"
static
className="fixed inset-0 overflow-hidden"
open={isOpen}
onClose={toggle}
>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
<Form
initialValues={{
name: "",
type: DOWNLOAD_CLIENT_TYPES.qBittorrent,
enabled: true,
host: "",
port: 10000,
ssl: false,
username: "",
password: "",
}}
onSubmit={onSubmit}
>
{({ handleSubmit, values }) => {
return (
<form
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}
>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
Add client
</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Add download client.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white dark:bg-gray-800 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon
className="h-6 w-6"
aria-hidden="true"
/>
</button>
</div>
</div>
</div>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<TextFieldWide name="name" label="Name" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<RadioFieldsetWide
name="type"
legend="Type"
options={DownloadClientTypeOptions}
/>
<div>{componentMap[values.type]}</div>
</div>
</div>
{rulesComponentMap[values.type]}
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
<div className="space-x-3 flex justify-end">
<button
type="button"
className={classNames(
isSuccessfulTest
? "text-green-500 border-green-500 bg-green-50"
: isErrorTest
? "text-red-500 border-red-500 bg-red-50"
: "border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-400 bg-white dark:bg-gray-700 hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
isTesting ? "cursor-not-allowed" : "",
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
)}
disabled={isTesting}
onClick={() => testClient(values)}
>
{isTesting ? (
<svg
className="animate-spin h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : isSuccessfulTest ? (
"OK!"
) : isErrorTest ? (
"ERROR"
) : (
"Test"
)}
</button>
<button
type="button"
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
>
Create
</button>
</div>
</div>
<DEBUG values={values} />
</form>
);
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}
export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
const [isTesting, setIsTesting] = useState(false);
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
const [isErrorTest, setIsErrorTest] = useState(false);
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
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: (error) => {
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
},
}
);
const onSubmit = (data: any) => {
mutation.mutate(data);
};
const cancelButtonRef = useRef(null);
const cancelModalButtonRef = useRef(null);
const deleteAction = () => {
deleteMutation.mutate(client.id);
};
const testClient = (data: any) => {
testClientMutation.mutate(data);
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog
as="div"
static
className="fixed inset-0 overflow-hidden"
open={isOpen}
onClose={toggle}
initialFocus={cancelButtonRef}
>
<DeleteModal
isOpen={deleteModalIsOpen}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={deleteAction}
title="Remove download client"
text="Are you sure you want to remove this download client? This action cannot be undone."
/>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
<Form
initialValues={{
id: client.id,
name: client.name,
type: client.type,
enabled: client.enabled,
host: client.host,
port: client.port,
ssl: client.ssl,
username: client.username,
password: client.password,
settings: client.settings,
}}
onSubmit={onSubmit}
>
{({ handleSubmit, values }) => {
return (
<form
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}
>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
Edit client
</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Edit download client settings.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon
className="h-6 w-6"
aria-hidden="true"
/>
</button>
</div>
</div>
</div>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<TextFieldWide name="name" label="Name" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<RadioFieldsetWide
name="type"
legend="Type"
options={DownloadClientTypeOptions}
/>
<div>{componentMap[values.type]}</div>
</div>
</div>
{rulesComponentMap[values.type]}
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
<div className="space-x-3 flex justify-between">
<button
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal}
>
Remove
</button>
<div className="flex">
<button
type="button"
className={classNames(
isSuccessfulTest
? "text-green-500 border-green-500 bg-green-50"
: isErrorTest
? "text-red-500 border-red-500 bg-red-50"
: "border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-400 bg-white dark:bg-gray-700 hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
isTesting ? "cursor-not-allowed" : "",
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
)}
disabled={isTesting}
onClick={() => testClient(values)}
>
{isTesting ? (
<svg
className="animate-spin h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : isSuccessfulTest ? (
"OK!"
) : isErrorTest ? (
"ERROR"
) : (
"Test"
)}
</button>
<button
type="button"
className="mr-4 bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
>
Create
</button>
</div>
</div>
</div>
<DEBUG values={values} />
</form>
);
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}

View file

@ -13,12 +13,14 @@ import APIClient from "../../api/APIClient";
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
import { toast } from 'react-hot-toast'
import Toast from '../../components/notifications/Toast';
interface props {
import { SlideOver } from "../../components/panels";
interface AddProps {
isOpen: boolean;
toggle: any;
}
function IndexerAddForm({ isOpen, toggle }: props) {
export function IndexerAddForm({ isOpen, toggle }: AddProps) {
const { data } = useQuery<IndexerSchema[], Error>('indexerSchema', APIClient.indexers.getSchema,
{
enabled: isOpen,
@ -40,7 +42,7 @@ function IndexerAddForm({ isOpen, toggle }: props) {
const ircMutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
onSuccess: (data) => {
console.log("irc mutation: ", data);
// console.log("irc mutation: ", data);
// queryClient.invalidateQueries(['indexer']);
// sleep(1500)
@ -97,13 +99,14 @@ function IndexerAddForm({ isOpen, toggle }: props) {
switch (f.type) {
case "text":
return (
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue=""/>
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue="" />
)
case "secret":
return (
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue="" />
)
}
return null
})}
<div hidden={true}>
<TextFieldWide name={`name`} label="Name" defaultValue={ind?.name} />
@ -121,10 +124,10 @@ function IndexerAddForm({ isOpen, toggle }: props) {
return (
<Fragment>
{ind && ind.irc && ind.irc.settings && (
<div className="border-t border-gray-200 py-5">
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
<div className="px-6 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900">IRC</Dialog.Title>
<p className="text-sm text-gray-500">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">IRC</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-200">
Networks, channels and invite commands are configured automatically.
</p>
</div>
@ -135,6 +138,7 @@ function IndexerAddForm({ isOpen, toggle }: props) {
case "secret":
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} />
}
return null
})}
<div hidden={true}>
@ -165,7 +169,7 @@ function IndexerAddForm({ isOpen, toggle }: props) {
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl">
<div className="w-screen max-w-2xl dark:border-gray-700 border-l">
<Form
initialValues={{
enabled: true,
@ -176,23 +180,23 @@ function IndexerAddForm({ isOpen, toggle }: props) {
>
{({ handleSubmit, values }) => {
return (
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
<form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title
className="text-lg font-medium text-gray-900">Add
className="text-lg font-medium text-gray-900 dark:text-white">Add
indexer</Dialog.Title>
<p className="text-sm text-gray-500">
<p className="text-sm text-gray-500 dark:text-gray-200">
Add indexer.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
@ -203,14 +207,14 @@ function IndexerAddForm({ isOpen, toggle }: props) {
</div>
<div
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
className="py-6 space-y-6 py-0 space-y-0 divide-y divide-gray-200 dark:divide-gray-700">
<div
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div>
<label
htmlFor="identifier"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
>
Indexer
</label>
@ -249,18 +253,18 @@ function IndexerAddForm({ isOpen, toggle }: props) {
</div>
<div
className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
<div className="space-x-3 flex justify-end">
<button
type="button"
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
>
Save
</button>
@ -282,4 +286,120 @@ function IndexerAddForm({ isOpen, toggle }: props) {
)
}
export default IndexerAddForm;
interface UpdateProps {
isOpen: boolean;
toggle: any;
indexer: Indexer;
}
export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
const mutation = useMutation((indexer: Indexer) => APIClient.indexers.update(indexer), {
onSuccess: () => {
queryClient.invalidateQueries(['indexer']);
toast.custom((t) => <Toast type="success" body={`${indexer.name} was updated successfully`} t={t} />)
sleep(1500)
toggle()
}
})
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} />)
}
})
const onSubmit = (data: any) => {
// TODO clear data depending on type
mutation.mutate(data)
};
const deleteAction = () => {
deleteMutation.mutate(indexer.id)
}
const renderSettingFields = (settings: any[]) => {
if (settings !== []) {
return (
<div key="opt">
{settings && settings.map((f: any, idx: number) => {
switch (f.type) {
case "text":
return (
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
)
case "secret":
return (
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
)
}
return null
})}
</div>
)
}
}
let initialValues = {
id: indexer.id,
name: indexer.name,
enabled: indexer.enabled,
identifier: indexer.identifier,
settings: indexer.settings.reduce((o: any, obj: any) => ({ ...o, [obj.name]: obj.value }), {}),
}
return (
<SlideOver
type="UPDATE"
title="Indexer"
isOpen={isOpen}
toggle={toggle}
deleteAction={deleteAction}
onSubmit={onSubmit}
initialValues={initialValues}
>
{({ values }: any) => (
<>
<div
className="py-6 space-y-6 sm:py-0 sm:space-y-0 divide-y divide-gray-200 dark:divide-gray-700">
<div
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
>
Name
</label>
</div>
<Field name="name">
{({ input, meta }) => (
<div className="sm:col-span-2">
<input
type="text"
{...input}
className="block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 dark:border-gray-700 rounded-md"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
<SwitchGroup name="enabled" label="Enabled" />
</div>
{renderSettingFields(indexer.settings)}
</div>
</>
)}
</SlideOver>
)
}

View file

@ -1,288 +0,0 @@
import { Fragment, useRef } from "react";
import { useMutation } from "react-query";
import { Indexer } from "../../domain/interfaces";
import { sleep } from "../../utils/utils";
import { ExclamationIcon, XIcon } from "@heroicons/react/solid";
import { Dialog, Transition } from "@headlessui/react";
import { Field, Form } from "react-final-form";
import DEBUG from "../../components/debug";
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
import { useToggle } from "../../hooks/hooks";
import APIClient from "../../api/APIClient";
import { queryClient } from "../../App";
import { PasswordFieldWide } from "../../components/inputs/wide";
import { toast } from 'react-hot-toast'
import Toast from '../../components/notifications/Toast';
interface props {
isOpen: boolean;
toggle: any;
indexer: Indexer;
}
function IndexerUpdateForm({ isOpen, toggle, indexer }: props) {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
const mutation = useMutation((indexer: Indexer) => APIClient.indexers.update(indexer), {
onSuccess: () => {
queryClient.invalidateQueries(['indexer']);
toast.custom((t) => <Toast type="success" body={`${indexer.name} was updated successfully`} t={t}/>)
sleep(1500)
toggle()
}
})
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}/>)
}
})
const cancelModalButtonRef = useRef(null)
const onSubmit = (data: any) => {
// TODO clear data depending on type
mutation.mutate(data)
};
const deleteAction = () => {
deleteMutation.mutate(indexer.id)
}
const renderSettingFields = (settings: any[]) => {
if (settings !== []) {
return (
<div key="opt">
{settings && settings.map((f: any, idx: number) => {
switch (f.type) {
case "text":
return (
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
)
case "secret":
return (
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
)
}
})}
</div>
)
}
}
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
<Transition.Root show={deleteModalIsOpen} as={Fragment}>
<Dialog
as="div"
static
className="fixed z-10 inset-0 overflow-y-auto"
initialFocus={cancelModalButtonRef}
open={deleteModalIsOpen}
onClose={toggleDeleteModal}
>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900">
Remove indexer
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to remove this indexer?
This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
onClick={deleteAction}
>
Remove
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={toggleDeleteModal}
ref={cancelModalButtonRef}
>
Cancel
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl">
<Form
initialValues={{
id: indexer.id,
name: indexer.name,
enabled: indexer.enabled,
identifier: indexer.identifier,
settings: indexer.settings.reduce((o: any, obj: any) => ({ ...o, [obj.name]: obj.value }), {}),
}}
onSubmit={onSubmit}
>
{({ handleSubmit, values }) => {
return (
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title
className="text-lg font-medium text-gray-900">Update
indexer</Dialog.Title>
<p className="text-sm text-gray-500">
Update indexer.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
</div>
<div
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<div
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Name
</label>
</div>
<Field name="name">
{({ input, meta }) => (
<div className="sm:col-span-2">
<input
type="text"
{...input}
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled" />
</div>
{renderSettingFields(indexer.settings)}
</div>
</div>
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
<div className="space-x-3 flex justify-between">
<button
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal}
>
Remove
</button>
<div>
<button
type="button"
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="ml-4 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Save
</button>
</div>
</div>
</div>
<DEBUG values={values} />
</form>
)
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default IndexerUpdateForm;

View file

@ -0,0 +1,347 @@
import { useMutation } from "react-query";
import { Network } from "../../domain/interfaces";
import { XIcon } from "@heroicons/react/solid";
import { Field } from "react-final-form";
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
import { queryClient } from "../../App";
import arrayMutators from "final-form-arrays";
import { FieldArray } from "react-final-form-arrays";
import APIClient from "../../api/APIClient";
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
import { toast } from 'react-hot-toast';
import Toast from '../../components/notifications/Toast';
import { SlideOver } from "../../components/panels";
export function IrcNetworkAddForm({ isOpen, toggle }: any) {
const mutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
onSuccess: (data) => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body="IRC Network added" t={t} />)
toggle()
},
onError: () => {
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />)
},
})
const onSubmit = (data: any) => {
// easy way to split textarea lines into array of strings for each newline.
// parse on the field didn't really work.
let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g, "\n").split("\n") : [];
data.connect_commands = cmds
console.log("formated", data)
mutation.mutate(data)
};
const validate = (values: any) => {
const errors = {
nickserv: {
account: null,
}
} as any;
if (!values.name) {
errors.name = "Required";
}
if (!values.port) {
errors.port = "Required";
}
if (!values.server) {
errors.server = "Required";
}
if (!values.nickserv?.account) {
errors.nickserv.account = "Required";
}
return errors;
}
const initialValues = {
name: "",
enabled: true,
server: "",
tls: false,
pass: "",
nickserv: {
account: ""
}
}
const mutators = {
...arrayMutators
}
return (
<SlideOver
type="CREATE"
title="Network"
isOpen={isOpen}
toggle={toggle}
onSubmit={onSubmit}
initialValues={initialValues}
mutators={mutators}
validate={validate}
>
{() => (
<>
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<div>
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="tls" label="TLS" />
</div>
<PasswordFieldWide name="pass" label="Password" help="Network password" />
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<PasswordFieldWide name="invite_command" label="Invite command" />
</div>
</div>
<div className="p-6">
<FieldArray name="channels">
{({ fields }) => (
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
{fields && (fields.length as any) > 0 ? (
fields.map((name, index) => (
<div key={name} className="flex justify-between">
<div className="flex">
<Field
name={`${name}.name`}
component="input"
type="text"
placeholder="#Channel"
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
/>
<Field
name={`${name}.password`}
component="input"
type="text"
placeholder="Password"
className="dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
/>
</div>
<button
type="button"
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={() => fields.remove(index)}
>
<span className="sr-only">Remove</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
))
) : (
<span className="text-center text-sm text-grey-darker dark:text-white">
No channels!
</span>
)}
<button
type="button"
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 rounded self-center text-center"
onClick={() => fields.push({ name: "", password: "" })}
>
Add Channel
</button>
</div>
)}
</FieldArray>
</div>
</>
)}
</SlideOver>
)
}
export function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) {
const mutation = useMutation((network: Network) => APIClient.irc.updateNetwork(network), {
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />)
toggle()
}
})
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />)
toggle()
}
})
const onSubmit = (data: any) => {
console.log(data)
// easy way to split textarea lines into array of strings for each newline.
// parse on the field didn't really work.
// TODO fix connect_commands on network update
// let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
// data.connect_commands = cmds
// console.log("formatted", data)
mutation.mutate(data)
};
const validate = (values: any) => {
const errors = {} as any;
if (!values.name) {
errors.name = "Required";
}
if (!values.server) {
errors.server = "Required";
}
if (!values.port) {
errors.port = "Required";
}
if (!values.nickserv?.account) {
errors.nickserv.account = "Required";
}
return errors;
}
const deleteAction = () => {
deleteMutation.mutate(network.id)
}
const initialValues = {
id: network.id,
name: network.name,
enabled: network.enabled,
server: network.server,
port: network.port,
tls: network.tls,
nickserv: network.nickserv,
pass: network.pass,
invite_command: network.invite_command,
// connect_commands: network.connect_commands,
channels: network.channels
}
const mutators = {
...arrayMutators
}
return (
<SlideOver
type="UPDATE"
title="Network"
isOpen={isOpen}
toggle={toggle}
onSubmit={onSubmit}
deleteAction={deleteAction}
initialValues={initialValues}
mutators={mutators}
validate={validate}
>
{() => (
<>
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<div>
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="tls" label="TLS" />
</div>
<PasswordFieldWide name="pass" label="Password" help="Network password" />
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<PasswordFieldWide name="invite_command" label="Invite command" />
</div>
</div>
<div className="p-6">
<FieldArray name="channels">
{({ fields }) => (
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
{fields && (fields.length as any) > 0 ? (
fields.map((name, index) => (
<div key={name} className="flex justify-between">
<div className="flex">
<Field
name={`${name}.name`}
component="input"
type="text"
placeholder="#Channel"
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
/>
<Field
name={`${name}.password`}
component="input"
type="text"
placeholder="Password"
className="dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
/>
</div>
<button
type="button"
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={() => fields.remove(index)}
>
<span className="sr-only">Remove</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
))
) : (
<span className="text-center text-sm text-grey-darker dark:text-white">
No channels!
</span>
)}
<button
type="button"
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500 rounded self-center text-center"
onClick={() => fields.push({ name: "", password: "" })}
>
Add Channel
</button>
</div>
)}
</FieldArray>
</div>
</>
)}
</SlideOver>
)
}

View file

@ -1,254 +0,0 @@
import {Fragment} from "react";
import {useMutation} from "react-query";
import {Network} from "../../domain/interfaces";
import {Dialog, Transition} from "@headlessui/react";
import {XIcon} from "@heroicons/react/solid";
import {Field, Form} from "react-final-form";
import DEBUG from "../../components/debug";
import {SwitchGroup, TextFieldWide} from "../../components/inputs";
import {queryClient} from "../../App";
import arrayMutators from "final-form-arrays";
import { FieldArray } from "react-final-form-arrays";
import {classNames} from "../../styles/utils";
import APIClient from "../../api/APIClient";
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
import { toast } from 'react-hot-toast';
import Toast from '../../components/notifications/Toast';
type FormValues = {
name: string
server: string
nickserv: {
account: string
}
port: number
}
function IrcNetworkAddForm({isOpen, toggle}: any) {
const mutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
onSuccess: (data) => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body="IRC Network added" t={t} />)
toggle()
},
onError: () => {
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t}/>)
},
})
const onSubmit = (data: any) => {
// easy way to split textarea lines into array of strings for each newline.
// parse on the field didn't really work.
let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
data.connect_commands = cmds
console.log("formated", data)
mutation.mutate(data)
};
const validate = (values: any) => {
const errors = {
nickserv: {
account: null,
}
} as any;
if (!values.name) {
errors.name = "Required";
}
if (!values.port) {
errors.port = "Required";
}
if (!values.server) {
errors.server = "Required";
}
if(!values.nickserv?.account) {
errors.nickserv.account = "Required";
}
return errors;
}
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" static className="fixed inset-0 overflow-hidden transition-all" open={isOpen} onClose={toggle}>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0"/>
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl">
<Form
initialValues={{
name: "",
enabled: true,
server: "",
tls: false,
pass: "",
nickserv: {
account: ""
}
}}
mutators={{
...arrayMutators
}}
validate={validate}
onSubmit={onSubmit}
>
{({handleSubmit, values, pristine, invalid}) => {
return (
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title
className="text-lg font-medium text-gray-900">Add
network</Dialog.Title>
<p className="text-sm text-gray-500">
Add irc network.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true"/>
</button>
</div>
</div>
</div>
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<div
className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled"/>
</div>
<div>
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="tls" label="TLS"/>
</div>
<PasswordFieldWide name="pass" label="Password" help="Network password" />
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<PasswordFieldWide name="invite_command" label="Invite command" />
</div>
</div>
<div className="p-6">
<FieldArray name="channels">
{({ fields }) => (
<div className="flex flex-col border-2 border-dashed p-4">
{fields && (fields.length as any) > 0 ? (
fields.map((name, index) => (
<div key={name} className="flex justify-between">
<div className="flex">
<Field
name={`${name}.name`}
component="input"
type="text"
placeholder="#Channel"
className="mr-4 focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 block w-full shadow-sm sm:text-sm rounded-md"
/>
<Field
name={`${name}.password`}
component="input"
type="text"
placeholder="Password"
className="focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 block w-full shadow-sm sm:text-sm rounded-md"
/>
</div>
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={() => fields.remove(index)}
>
<span className="sr-only">Remove</span>
<XIcon className="h-6 w-6" aria-hidden="true"/>
</button>
</div>
))
) : (
<span className="text-center text-sm text-grey-darker">
No channels!
</span>
)}
<button
type="button"
className="border my-4 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded self-center text-center"
onClick={() => fields.push({ name: "", password: "" })}
>
Add Channel
</button>
</div>
)}
</FieldArray>
</div>
</div>
<div
className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
<div className="space-x-3 flex justify-end">
<button
type="button"
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
disabled={pristine || invalid}
className={classNames(pristine || invalid ? "bg-indigo-300" : "bg-indigo-600 hover:bg-indigo-700","inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500")}
>
Create
</button>
</div>
</div>
<DEBUG values={values}/>
</form>
)
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default IrcNetworkAddForm;

View file

@ -1,297 +0,0 @@
import { Fragment, useEffect, useRef } from "react";
import { useMutation } from "react-query";
import { Network } from "../../domain/interfaces";
import { Dialog, Transition } from "@headlessui/react";
import { XIcon } from "@heroicons/react/solid";
import { Field, Form } from "react-final-form";
import DEBUG from "../../components/debug";
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
import { queryClient } from "../../App";
import arrayMutators from "final-form-arrays";
import { FieldArray } from "react-final-form-arrays";
import { classNames } from "../../styles/utils";
import { useToggle } from "../../hooks/hooks";
import { DeleteModal } from "../../components/modals";
import APIClient from "../../api/APIClient";
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
import { toast } from 'react-hot-toast';
import Toast from '../../components/notifications/Toast';
function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
const mutation = useMutation((network: Network) => APIClient.irc.updateNetwork(network), {
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />)
toggle()
}
})
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />)
toggle()
}
})
useEffect(() => {
console.log("render add network form")
}, []);
const onSubmit = (data: any) => {
console.log(data)
// easy way to split textarea lines into array of strings for each newline.
// parse on the field didn't really work.
// TODO fix connect_commands on network update
// let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
// data.connect_commands = cmds
// console.log("formatted", data)
mutation.mutate(data)
};
const validate = (values: any) => {
const errors = {} as any;
if (!values.name) {
errors.name = "Required";
}
if (!values.server) {
errors.server = "Required";
}
if (!values.port) {
errors.port = "Required";
}
if(!values.nickserv?.account) {
errors.nickserv.account = "Required";
}
return errors;
}
const cancelModalButtonRef = useRef(null)
const deleteAction = () => {
deleteMutation.mutate(network.id)
}
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
<DeleteModal
isOpen={deleteModalIsOpen}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={deleteAction}
title="Remove network"
text="Are you sure you want to remove this network and channels? This action cannot be undone."
/>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl">
<Form
initialValues={{
id: network.id,
name: network.name,
enabled: network.enabled,
server: network.server,
port: network.port,
tls: network.tls,
nickserv: network.nickserv,
pass: network.pass,
invite_command: network.invite_command,
// connect_commands: network.connect_commands,
channels: network.channels
}}
mutators={{
...arrayMutators
}}
validate={validate}
onSubmit={onSubmit}
>
{({ handleSubmit, values, pristine, invalid }) => {
return (
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}>
<div className="flex-1">
{/* Header */}
<div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title
className="text-lg font-medium text-gray-900">Update network</Dialog.Title>
<p className="text-sm text-gray-500">
Update irc network.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
</div>
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<div
className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<div>
<div className="px-6 space-y-1 mt-6">
<Dialog.Title className="text-lg font-medium text-gray-900">Connection</Dialog.Title>
{/* <p className="text-sm text-gray-500">
Networks, channels and invite commands are configured automatically.
</p> */}
</div>
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
<NumberFieldWide name="port" label="Port" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="tls" label="TLS" />
</div>
<PasswordFieldWide name="pass" label="Password" help="Network password" />
<div className="px-6 space-y-1 border-t pt-6">
<Dialog.Title className="text-lg font-medium text-gray-900">Account</Dialog.Title>
{/* <p className="text-sm text-gray-500">
Networks, channels and invite commands are configured automatically.
</p> */}
</div>
<TextFieldWide name="nickserv.account" label="NickServ Account" required={true} />
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<PasswordFieldWide name="invite_command" label="Invite command" />
</div>
</div>
<div className="p-6">
<FieldArray name="channels">
{({ fields }) => (
<div className="flex flex-col border-2 border-dashed p-4">
{fields && (fields.length as any) > 0 ? (
fields.map((name, index) => (
<div key={name} className="flex justify-between">
<div className="flex">
<Field
name={`${name}.name`}
component="input"
type="text"
placeholder="#Channel"
className="focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 block w-full shadow-sm sm:text-sm rounded-md"
/>
<Field
name={`${name}.password`}
component="input"
type="text"
placeholder="Password"
className="focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 block w-full shadow-sm sm:text-sm rounded-md"
/>
</div>
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={() => fields.remove(index)}
>
<span className="sr-only">Remove</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
))
) : (
<span className="text-center text-sm text-grey-darker">
No channels!
</span>
)}
<button
type="button"
className="border my-4 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded self-center text-center"
onClick={() => fields.push({ name: "", password: "" })}
>
Add Channel
</button>
</div>
)}
</FieldArray>
</div>
</div>
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
<div className="space-x-3 flex justify-between">
<button
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal}
>
Remove
</button>
<div>
<button
type="button"
className="mr-4 bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
// disabled={pristine || invalid}
className={classNames(pristine || invalid ? "bg-indigo-300" : "bg-indigo-600 hover:bg-indigo-700", "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500")}
>
Save
</button>
</div>
</div>
</div>
<DEBUG values={values} />
</form>
)
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default IrcNetworkUpdateForm;

View file

@ -1,245 +0,0 @@
import { Fragment, useState } from "react";
import { useMutation } from "react-query";
import {
DOWNLOAD_CLIENT_TYPES,
DownloadClient,
} from "../../../domain/interfaces";
import { Dialog, Transition } from "@headlessui/react";
import { XIcon } from "@heroicons/react/solid";
import { classNames } from "../../../styles/utils";
import { Form } from "react-final-form";
import DEBUG from "../../../components/debug";
import { SwitchGroup, TextFieldWide } from "../../../components/inputs";
import { queryClient } from "../../../App";
import APIClient from "../../../api/APIClient";
import { sleep } from "../../../utils/utils";
import { DownloadClientTypeOptions } from "../../../domain/constants";
import { RadioFieldsetWide } from "../../../components/inputs/wide";
import { componentMap, rulesComponentMap } from "./shared";
import { toast } from 'react-hot-toast'
import Toast from '../../../components/notifications/Toast';
function DownloadClientAddForm({ isOpen, toggle }: any) {
const [isTesting, setIsTesting] = useState(false);
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
const [isErrorTest, setIsErrorTest] = useState(false);
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} />)
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);
});
});
},
onError: (error) => {
console.log('not added')
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
},
}
);
const onSubmit = (data: any) => {
mutation.mutate(data);
};
const testClient = (data: any) => {
testClientMutation.mutate(data);
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog
as="div"
static
className="fixed inset-0 overflow-hidden"
open={isOpen}
onClose={toggle}
>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl">
<Form
initialValues={{
name: "",
type: DOWNLOAD_CLIENT_TYPES.qBittorrent,
enabled: true,
host: "",
port: 10000,
ssl: false,
username: "",
password: "",
}}
onSubmit={onSubmit}
>
{({ handleSubmit, values }) => {
return (
<form
className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}
>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900">
Add client
</Dialog.Title>
<p className="text-sm text-gray-500">
Add download client.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon
className="h-6 w-6"
aria-hidden="true"
/>
</button>
</div>
</div>
</div>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<TextFieldWide name="name" label="Name" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<RadioFieldsetWide
name="type"
legend="Type"
options={DownloadClientTypeOptions}
/>
<div>{componentMap[values.type]}</div>
</div>
</div>
{rulesComponentMap[values.type]}
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
<div className="space-x-3 flex justify-end">
<button
type="button"
className={classNames(
isSuccessfulTest
? "text-green-500 border-green-500 bg-green-50"
: isErrorTest
? "text-red-500 border-red-500 bg-red-50"
: "border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
isTesting ? "cursor-not-allowed" : "",
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150"
)}
disabled={isTesting}
onClick={() => testClient(values)}
>
{isTesting ? (
<svg
className="animate-spin h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : isSuccessfulTest ? (
"OK!"
) : isErrorTest ? (
"ERROR"
) : (
"Test"
)}
</button>
<button
type="button"
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Create
</button>
</div>
</div>
<DEBUG values={values} />
</form>
);
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}
export default DownloadClientAddForm;

View file

@ -1,279 +0,0 @@
import { Fragment, useRef, useState } from "react";
import { useToggle } from "../../../hooks/hooks";
import { useMutation } from "react-query";
import { DownloadClient } from "../../../domain/interfaces";
import { queryClient } from "../../../App";
import { Dialog, Transition } from "@headlessui/react";
import { XIcon } from "@heroicons/react/solid";
import { classNames } from "../../../styles/utils";
import { Form } from "react-final-form";
import DEBUG from "../../../components/debug";
import { SwitchGroup, TextFieldWide } from "../../../components/inputs";
import { DownloadClientTypeOptions } from "../../../domain/constants";
import APIClient from "../../../api/APIClient";
import { sleep } from "../../../utils/utils";
import { componentMap, rulesComponentMap } from "./shared";
import { RadioFieldsetWide } from "../../../components/inputs/wide";
import { DeleteModal } from "../../../components/modals";
import { toast } from 'react-hot-toast'
import Toast from '../../../components/notifications/Toast';
function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
const [isTesting, setIsTesting] = useState(false);
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
const [isErrorTest, setIsErrorTest] = useState(false);
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
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: (error) => {
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
},
}
);
const onSubmit = (data: any) => {
mutation.mutate(data);
};
const cancelButtonRef = useRef(null);
const cancelModalButtonRef = useRef(null);
const deleteAction = () => {
deleteMutation.mutate(client.id);
};
const testClient = (data: any) => {
testClientMutation.mutate(data);
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog
as="div"
static
className="fixed inset-0 overflow-hidden"
open={isOpen}
onClose={toggle}
initialFocus={cancelButtonRef}
>
<DeleteModal
isOpen={deleteModalIsOpen}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={deleteAction}
title="Remove download client"
text="Are you sure you want to remove this download client? This action cannot be undone."
/>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl">
<Form
initialValues={{
id: client.id,
name: client.name,
type: client.type,
enabled: client.enabled,
host: client.host,
port: client.port,
ssl: client.ssl,
username: client.username,
password: client.password,
settings: client.settings,
}}
onSubmit={onSubmit}
>
{({ handleSubmit, values }) => {
return (
<form
className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}
>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900">
Edit client
</Dialog.Title>
<p className="text-sm text-gray-500">
Edit download client settings.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon
className="h-6 w-6"
aria-hidden="true"
/>
</button>
</div>
</div>
</div>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<TextFieldWide name="name" label="Name" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<RadioFieldsetWide
name="type"
legend="Type"
options={DownloadClientTypeOptions}
/>
<div>{componentMap[values.type]}</div>
</div>
</div>
{rulesComponentMap[values.type]}
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
<div className="space-x-3 flex justify-between">
<button
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal}
>
Remove
</button>
<div className="flex">
<button
type="button"
className={classNames(
isSuccessfulTest
? "text-green-500 border-green-500 bg-green-50"
: isErrorTest
? "text-red-500 border-red-500 bg-red-50"
: "border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
isTesting ? "cursor-not-allowed" : "",
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150"
)}
disabled={isTesting}
onClick={() => testClient(values)}
>
{isTesting ? (
<svg
className="animate-spin h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : isSuccessfulTest ? (
"OK!"
) : isErrorTest ? (
"ERROR"
) : (
"Test"
)}
</button>
<button
type="button"
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="ml-4 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Save
</button>
</div>
</div>
</div>
<DEBUG values={values} />
</form>
);
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}
export default DownloadClientUpdateForm;

View file

@ -1,122 +0,0 @@
import { Fragment } from "react";
import { SwitchGroup, TextFieldWide } from "../../../components/inputs";
import { NumberFieldWide, PasswordFieldWide } from "../../../components/inputs/wide";
import { useField } from "react-final-form";
import { Dialog } from "@headlessui/react";
function FormFieldsDefault() {
return (
<Fragment>
<TextFieldWide name="host" label="Host" help="Url domain.ltd/client" />
<NumberFieldWide name="port" label="Port" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="ssl" label="SSL" />
</div>
<TextFieldWide name="username" label="Username" />
<PasswordFieldWide name="password" label="Password" />
</Fragment>
);
}
function FormFieldsArr() {
const { input } = useField("settings.basic.auth");
return (
<Fragment>
<TextFieldWide name="host" label="Host" help="Full url like http(s)://domain.ltd/" />
<PasswordFieldWide name="settings.apikey" label="API key" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.basic.auth" label="Basic auth" />
</div>
{input.value === true && (
<Fragment>
<TextFieldWide name="settings.basic.username" label="Username" />
<PasswordFieldWide name="settings.basic.password" label="Password" />
</Fragment>
)}
</Fragment>
);
}
export const componentMap: any = {
DELUGE_V1: <FormFieldsDefault />,
DELUGE_V2: <FormFieldsDefault />,
QBITTORRENT: <FormFieldsDefault />,
RADARR: <FormFieldsArr />,
SONARR: <FormFieldsArr />,
LIDARR: <FormFieldsArr />,
};
function FormFieldsRulesBasic() {
const { input: enabled } = useField("settings.rules.enabled");
return (
<div className="border-t border-gray-200 py-5">
<div className="px-6 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900">Rules</Dialog.Title>
<p className="text-sm text-gray-500">
Manage max downloads.
</p>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
</div>
{enabled.value === true && (
<Fragment>
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
</Fragment>
)}
</div>
);
}
function FormFieldsRules() {
const { input } = useField("settings.rules.ignore_slow_torrents");
const { input: enabled } = useField("settings.rules.enabled");
return (
<div className="border-t border-gray-200 py-5">
<div className="px-6 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900">Rules</Dialog.Title>
<p className="text-sm text-gray-500">
Manage max downloads etc.
</p>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
</div>
{enabled.value === true && (
<Fragment>
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents" />
</div>
{input.value === true && (
<Fragment>
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold" placeholder="in KB/s" help="If download speed is below this when max active downloads is hit, download anyways. KB/s" />
</Fragment>
)}
</Fragment>
)}
</div>
);
}
export const rulesComponentMap: any = {
DELUGE_V1: <FormFieldsRulesBasic />,
DELUGE_V2: <FormFieldsRulesBasic />,
QBITTORRENT: <FormFieldsRules />,
};