feat: add webui

This commit is contained in:
Ludvig Lundgren 2021-08-11 15:27:48 +02:00
parent a838d994a6
commit 773e57afe6
59 changed files with 19794 additions and 0 deletions

View file

@ -0,0 +1,829 @@
import React, {Fragment, useEffect } from "react";
import {useMutation} from "react-query";
import {Action, DownloadClient, Filter} from "../../domain/interfaces";
import {queryClient} from "../../index";
import {sleep} from "../../utils/utils";
import {CheckIcon, ExclamationIcon, SelectorIcon, XIcon} from "@heroicons/react/solid";
import {Dialog, Listbox, RadioGroup, Transition} from "@headlessui/react";
import {classNames} from "../../styles/utils";
import {Field, Form} from "react-final-form";
import DEBUG from "../../components/debug";
import APIClient from "../../api/APIClient";
interface radioFieldsetOption {
label: string;
description: string;
value: string;
}
const actionTypeOptions: radioFieldsetOption[] = [
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE"},
];
interface props {
filter: Filter;
isOpen: boolean;
toggle: any;
clients: DownloadClient[];
}
function FilterActionAddForm({filter, isOpen, toggle, clients}: props) {
const mutation = useMutation((action: Action) => APIClient.actions.create(action), {
onSuccess: () => {
queryClient.invalidateQueries(['filter', filter.id]);
sleep(500).then(() => toggle())
}
})
useEffect(() => {
// console.log("render add action form", clients)
}, []);
const onSubmit = (data: any) => {
// TODO clear data depending on type
mutation.mutate(data)
};
const TypeForm = (values: any) => {
switch (values.type) {
case "TEST":
return (
<div className="p-4">
<div className="rounded-md bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true"/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">Notice</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>
The test action does nothing except to show if the filter works.
</p>
</div>
</div>
</div>
</div>
</div>
)
case "WATCH_FOLDER":
return (
<div className="">
<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="watch_folder"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Watch dir
</label>
</div>
<div className="sm:col-span-2">
<Field name="watch_folder">
{({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"
placeholder="Watch directory eg. /home/user/watch_folder"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
</div>
)
case "EXEC":
return (
<div className="">
<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="exec_cmd"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Program
</label>
</div>
<div className="sm:col-span-2">
<Field name="exec_cmd">
{({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"
placeholder="Path to program eg. /bin/test"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
<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="exec_args"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Arguments
</label>
</div>
<div className="sm:col-span-2">
<Field name="exec_args">
{({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"
placeholder="Arguments eg. --test"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
</div>
)
case "QBITTORRENT":
return (
<div>
<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">
{/*// TODO change available clients to match only selected action type. eg qbittorrent or deluge*/}
<Field
name="client_id"
type="select"
render={({input}) => (
<Listbox value={input.value} onChange={input.onChange}>
{({open}) => (
<>
<Listbox.Label
className="block text-sm font-medium text-gray-700">Client</Listbox.Label>
<div className="mt-1 relative">
<Listbox.Button
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span className="block truncate">{input.value ? clients.find(c => c.id === input.value)!.name : "Choose a client"}</span>
{/*<span className="block truncate">Choose a client</span>*/}
<span
className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
static
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
{clients.filter((c) => c.type === values.type).map((client: any) => (
<Listbox.Option
key={client.id}
className={({active}) =>
classNames(
active ? 'text-white bg-indigo-600' : 'text-gray-900',
'cursor-default select-none relative py-2 pl-3 pr-9'
)
}
value={client.id}
>
{({selected, active}) => (
<>
<span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>
{client.name}
</span>
{selected ? (
<span
className={classNames(
active ? 'text-white' : 'text-indigo-600',
'absolute inset-y-0 right-0 flex items-center pr-4'
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
)} />
</div>
<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="category"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Category
</label>
</div>
<div className="sm:col-span-2">
<Field name="category">
{({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"
// placeholder="Arguments eg. --test"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
<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="tags"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Tags
</label>
</div>
<div className="sm:col-span-2">
<Field name="tags">
{({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"
placeholder="Comma separated eg. 4k,remux"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
<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="save_path"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Save path. <br/><span className="text-gray-500">if left blank and category is selected it will use category path</span>
</label>
</div>
<div className="sm:col-span-2">
<Field name="save_path">
{({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>
<div className="divide-y px-4 divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">Limit speeds</h3>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
Limit download and upload speed for torrents in this filter. In KB/s.
</p>
</div>
<div className="space-y-6 sm:space-y-5 divide-y divide-gray-200">
<div className="pt-6 sm:pt-5">
<div className="space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<div>
<label
htmlFor="limit_download_speed"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Limit download speed
</label>
</div>
<div className="sm:col-span-2">
<Field name="limit_download_speed">
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...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>
</div>
</div>
<div className="space-y-6 sm:space-y-5 divide-y divide-gray-200">
<div className="pt-6 sm:pt-5">
<div className="space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<div>
<label
htmlFor="limit_upload_speed"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Limit upload speed
</label>
</div>
<div className="sm:col-span-2">
<Field name="limit_upload_speed">
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...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>
</div>
</div>
</div>
</div>
)
case "DELUGE":
return (
<div>
{/*TODO choose client*/}
<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">
<Field
name="client_id"
type="select"
render={({input}) => (
<Listbox value={input.value} onChange={input.onChange}>
{({open}) => (
<>
<Listbox.Label
className="block text-sm font-medium text-gray-700">Client</Listbox.Label>
<div className="mt-1 relative">
<Listbox.Button
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span className="block truncate">{input.value ? clients.find(c => c.id === input.value)!.name : "Choose a client"}</span>
{/*<span className="block truncate">Choose a client</span>*/}
<span
className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
static
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
{clients.filter((c) => c.type === values.type).map((client: any) => (
<Listbox.Option
key={client.id}
className={({active}) =>
classNames(
active ? 'text-white bg-indigo-600' : 'text-gray-900',
'cursor-default select-none relative py-2 pl-3 pr-9'
)
}
value={client.id}
>
{({selected, active}) => (
<>
<span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>
{client.name}
</span>
{selected ? (
<span
className={classNames(
active ? 'text-white' : 'text-indigo-600',
'absolute inset-y-0 right-0 flex items-center pr-4'
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
)} />
</div>
<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="label"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Label
</label>
</div>
<div className="sm:col-span-2">
<Field name="label">
{({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>
<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="save_path"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Save path
</label>
</div>
<div className="sm:col-span-2">
<Field name="save_path">
{({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>
<div className="divide-y px-4 divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">Limit speeds</h3>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
Limit download and upload speed for torrents in this filter. In KB/s.
</p>
</div>
<div className="space-y-6 sm:space-y-5 divide-y divide-gray-200">
<div className="pt-6 sm:pt-5">
<div className="space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<div>
<label
htmlFor="limit_download_speed"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Limit download speed
</label>
</div>
<div className="sm:col-span-2">
<Field name="limit_download_speed">
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...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>
</div>
</div>
<div className="space-y-6 sm:space-y-5 divide-y divide-gray-200">
<div className="pt-6 sm:pt-5">
<div className="space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<div>
<label
htmlFor="limit_upload_speed"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Limit upload speed
</label>
</div>
<div className="sm:col-span-2">
<Field name="limit_upload_speed">
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...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>
</div>
</div>
</div>
</div>
)
default:
return (
<div className="p-4">
<div className="rounded-md bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true"/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">Notice</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>
The test action does nothing except to show if the filter works.
</p>
</div>
</div>
</div>
</div>
</div>
)
}
}
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: "",
enabled: false,
type: "TEST",
watch_folder: "",
exec_cmd: "",
exec_args: "",
category: "",
tags: "",
label: "",
save_path: "",
paused: false,
ignore_rules: false,
limit_upload_speed: 0,
limit_download_speed: 0,
filter_id: filter.id,
client_id: null,
}}
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">
{/* 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">Add
action</Dialog.Title>
<p className="text-sm text-gray-500">
Add filter action.
</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>
{/* Divider container */}
<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"
>
Action 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>
<fieldset>
<div
className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
<div>
<legend
className="text-sm font-medium text-gray-900">Type
</legend>
</div>
<div className="space-y-5 sm:col-span-2">
<div className="space-y-5 sm:mt-0">
<Field
name="type"
type="radio"
render={({input}) => (
<RadioGroup value={values.type} onChange={input.onChange}>
<RadioGroup.Label className="sr-only">Privacy setting</RadioGroup.Label>
<div className="bg-white rounded-md -space-y-px">
{actionTypeOptions.map((setting, settingIdx) => (
<RadioGroup.Option
key={setting.value}
value={setting.value}
className={({checked}) =>
classNames(
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
settingIdx === actionTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
checked ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200',
'relative border p-4 flex cursor-pointer focus:outline-none'
)
}
>
{({
active,
checked
}) => (
<Fragment>
<span
className={classNames(
checked ? 'bg-indigo-600 border-transparent' : 'bg-white border-gray-300',
active ? 'ring-2 ring-offset-2 ring-indigo-500' : '',
'h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center'
)}
aria-hidden="true"
>
<span className="rounded-full bg-white w-1.5 h-1.5"/>
</span>
<div
className="ml-3 flex flex-col">
<RadioGroup.Label
as="span"
className={classNames(checked ? 'text-indigo-900' : 'text-gray-900', 'block text-sm font-medium')}
>
{setting.label}
</RadioGroup.Label>
<RadioGroup.Description
as="span"
className={classNames(checked ? 'text-indigo-700' : 'text-gray-500', 'block text-sm')}
>
{setting.description}
</RadioGroup.Description>
</div>
</Fragment>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
)}
/>
</div>
</div>
</div>
</fieldset>
{TypeForm(values)}
</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"
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"
>
Save
</button>
</div>
</div>
<DEBUG values={values}/>
</form>
)
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default FilterActionAddForm;

View file

@ -0,0 +1,834 @@
import {Fragment, useEffect} from "react";
import {useMutation} from "react-query";
import {Action, DownloadClient, Filter} from "../../domain/interfaces";
import {queryClient} from "../../index";
import {sleep} from "../../utils/utils";
import {CheckIcon, ExclamationIcon, SelectorIcon, XIcon} from "@heroicons/react/solid";
import {Dialog, Listbox, RadioGroup, Transition} from "@headlessui/react";
import {classNames} from "../../styles/utils";
import {Field, Form} from "react-final-form";
import DEBUG from "../../components/debug";
import APIClient from "../../api/APIClient";
interface radioFieldsetOption {
label: string;
description: string;
value: string;
}
const actionTypeOptions: radioFieldsetOption[] = [
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE"},
];
interface props {
filter: Filter;
isOpen: boolean;
toggle: any;
clients: DownloadClient[];
action: Action;
}
function FilterActionUpdateForm({filter, isOpen, toggle, clients, action}: props) {
const mutation = useMutation((action: Action) => APIClient.actions.update(action), {
onSuccess: () => {
console.log("add action");
queryClient.invalidateQueries(['filter', filter.id]);
sleep(1500)
toggle()
}
})
useEffect(() => {
console.log("render add action form", clients)
}, [clients]);
const onSubmit = (data: any) => {
// TODO clear data depending on type
console.log(data)
mutation.mutate(data)
};
const TypeForm = (values: any) => {
switch (values.type) {
case "TEST":
return (
<div className="p-4">
<div className="rounded-md bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true"/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">Notice</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>
The test action does nothing except to show if the filter works.
</p>
</div>
</div>
</div>
</div>
</div>
)
case "WATCH_FOLDER":
return (
<div className="">
<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="watch_folder"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Watch dir
</label>
</div>
<div className="sm:col-span-2">
<Field name="watch_folder">
{({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"
placeholder="Watch directory eg. /home/user/watch_folder"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
</div>
)
case "EXEC":
return (
<div className="">
<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="exec_cmd"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Program
</label>
</div>
<div className="sm:col-span-2">
<Field name="exec_cmd">
{({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"
placeholder="Path to program eg. /bin/test"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
<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="exec_args"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Arguments
</label>
</div>
<div className="sm:col-span-2">
<Field name="exec_args">
{({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"
placeholder="Arguments eg. --test"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
</div>
)
case "QBITTORRENT":
return (
<div>
<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">
{/*// TODO change available clients to match only selected action type. eg qbittorrent or deluge*/}
<Field
name="client_id"
type="select"
render={({input}) => (
<Listbox value={input.value} onChange={input.onChange}>
{({open}) => (
<>
<Listbox.Label
className="block text-sm font-medium text-gray-700">Client</Listbox.Label>
<div className="mt-1 relative">
<Listbox.Button
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span className="block truncate">{input.value ? clients.find(c => c.id === input.value)!.name : "Choose a client"}</span>
{/*<span className="block truncate">Choose a client</span>*/}
<span
className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
static
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
{clients.filter((c) => c.type === values.type).map((client: any) => (
<Listbox.Option
key={client.id}
className={({active}) =>
classNames(
active ? 'text-white bg-indigo-600' : 'text-gray-900',
'cursor-default select-none relative py-2 pl-3 pr-9'
)
}
value={client.id}
>
{({selected, active}) => (
<>
<span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>
{client.name}
</span>
{selected ? (
<span
className={classNames(
active ? 'text-white' : 'text-indigo-600',
'absolute inset-y-0 right-0 flex items-center pr-4'
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
)} />
</div>
<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="category"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Category
</label>
</div>
<div className="sm:col-span-2">
<Field name="category">
{({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"
// placeholder="Arguments eg. --test"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
<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="tags"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Tags
</label>
</div>
<div className="sm:col-span-2">
<Field name="tags">
{({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"
placeholder="Comma separated eg. 4k,remux"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
<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="save_path"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Save path. <br/><span className="text-gray-500">if left blank and category is selected it will use category path</span>
</label>
</div>
<div className="sm:col-span-2">
<Field name="save_path">
{({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>
<div className="divide-y px-4 divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">Limit speeds</h3>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
Limit download and upload speed for torrents in this filter. In KB/s.
</p>
</div>
<div className="space-y-6 sm:space-y-5 divide-y divide-gray-200">
<div className="pt-6 sm:pt-5">
<div className="space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<div>
<label
htmlFor="limit_download_speed"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Limit download speed
</label>
</div>
<div className="sm:col-span-2">
<Field name="limit_download_speed">
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...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>
</div>
</div>
<div className="space-y-6 sm:space-y-5 divide-y divide-gray-200">
<div className="pt-6 sm:pt-5">
<div className="space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<div>
<label
htmlFor="limit_upload_speed"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Limit upload speed
</label>
</div>
<div className="sm:col-span-2">
<Field name="limit_upload_speed">
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...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>
</div>
</div>
</div>
</div>
)
case "DELUGE":
return (
<div>
{/*TODO choose client*/}
<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">
<Field
name="client_id"
type="select"
render={({input}) => (
<Listbox value={input.value} onChange={input.onChange}>
{({open}) => (
<>
<Listbox.Label
className="block text-sm font-medium text-gray-700">Client</Listbox.Label>
<div className="mt-1 relative">
<Listbox.Button
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span className="block truncate">{input.value ? clients.find(c => c.id === input.value)!.name : "Choose a client"}</span>
{/*<span className="block truncate">Choose a client</span>*/}
<span
className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
static
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
{clients.filter((c) => c.type === values.type).map((client: any) => (
<Listbox.Option
key={client.id}
className={({active}) =>
classNames(
active ? 'text-white bg-indigo-600' : 'text-gray-900',
'cursor-default select-none relative py-2 pl-3 pr-9'
)
}
value={client.id}
>
{({selected, active}) => (
<>
<span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>
{client.name}
</span>
{selected ? (
<span
className={classNames(
active ? 'text-white' : 'text-indigo-600',
'absolute inset-y-0 right-0 flex items-center pr-4'
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
)} />
</div>
<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="label"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Label
</label>
</div>
<div className="sm:col-span-2">
<Field name="label">
{({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>
<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="save_path"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Save path
</label>
</div>
<div className="sm:col-span-2">
<Field name="save_path">
{({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>
<div className="divide-y px-4 divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">Limit speeds</h3>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
Limit download and upload speed for torrents in this filter. In KB/s.
</p>
</div>
<div className="space-y-6 sm:space-y-5 divide-y divide-gray-200">
<div className="pt-6 sm:pt-5">
<div className="space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<div>
<label
htmlFor="limit_download_speed"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Limit download speed
</label>
</div>
<div className="sm:col-span-2">
<Field name="limit_download_speed">
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...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>
</div>
</div>
<div className="space-y-6 sm:space-y-5 divide-y divide-gray-200">
<div className="pt-6 sm:pt-5">
<div className="space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<div>
<label
htmlFor="limit_upload_speed"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Limit upload speed
</label>
</div>
<div className="sm:col-span-2">
<Field name="limit_upload_speed">
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...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>
</div>
</div>
</div>
</div>
)
default:
return (
<div className="p-4">
<div className="rounded-md bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true"/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">Notice</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>
The test action does nothing except to show if the filter works.
</p>
</div>
</div>
</div>
</div>
</div>
)
}
}
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: "",
enabled: false,
type: "TEST",
watch_folder: "",
exec_cmd: "",
exec_args: "",
category: "",
tags: "",
label: "",
save_path: "",
paused: false,
ignore_rules: false,
limit_upload_speed: 0,
limit_download_speed: 0,
filter_id: filter.id,
client_id: null,
}}
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">
{/* 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 action</Dialog.Title>
<p className="text-sm text-gray-500">
Add filter action.
</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>
{/* Divider container */}
<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"
>
Action 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>
<fieldset>
<div
className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
<div>
<legend
className="text-sm font-medium text-gray-900">Type
</legend>
</div>
<div className="space-y-5 sm:col-span-2">
<div className="space-y-5 sm:mt-0">
<Field
name="type"
type="radio"
render={({input}) => (
<RadioGroup value={values.type} onChange={input.onChange}>
<RadioGroup.Label className="sr-only">Privacy setting</RadioGroup.Label>
<div className="bg-white rounded-md -space-y-px">
{actionTypeOptions.map((setting, settingIdx) => (
<RadioGroup.Option
key={setting.value}
value={setting.value}
className={({checked}) =>
classNames(
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
settingIdx === actionTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
checked ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200',
'relative border p-4 flex cursor-pointer focus:outline-none'
)
}
>
{({
active,
checked
}) => (
<Fragment>
<span
className={classNames(
checked ? 'bg-indigo-600 border-transparent' : 'bg-white border-gray-300',
active ? 'ring-2 ring-offset-2 ring-indigo-500' : '',
'h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center'
)}
aria-hidden="true"
>
<span className="rounded-full bg-white w-1.5 h-1.5"/>
</span>
<div
className="ml-3 flex flex-col">
<RadioGroup.Label
as="span"
className={classNames(checked ? 'text-indigo-900' : 'text-gray-900', 'block text-sm font-medium')}
>
{setting.label}
</RadioGroup.Label>
<RadioGroup.Description
as="span"
className={classNames(checked ? 'text-indigo-700' : 'text-gray-500', 'block text-sm')}
>
{setting.description}
</RadioGroup.Description>
</div>
</Fragment>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
)}
/>
</div>
</div>
</div>
</fieldset>
{TypeForm(values)}
</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"
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"
>
Save
</button>
</div>
</div>
<DEBUG values={values}/>
</form>
)
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default FilterActionUpdateForm;

View file

@ -0,0 +1,151 @@
import React, {Fragment, useEffect} from "react";
import {useMutation} from "react-query";
import {Filter} from "../../domain/interfaces";
import {queryClient} from "../../index";
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";
const required = (value: any) => (value ? undefined : 'Required')
function FilterAddForm({isOpen, toggle}: any) {
const mutation = useMutation((filter: Filter) => APIClient.filters.create(filter), {
onSuccess: () => {
queryClient.invalidateQueries('filter');
toggle()
}
})
useEffect(() => {
// console.log("render add action form")
}, []);
const onSubmit = (data: any) => {
mutation.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: "",
enabled: false,
resolutions: [],
codecs: [],
sources: [],
containers: []
}}
// validate={validate}
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">
{/* 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">Create
filter</Dialog.Title>
<p className="text-sm text-gray-500">
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"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<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
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" validate={required}>
{({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>
</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"
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 FilterAddForm;

11
web/src/forms/index.ts Normal file
View file

@ -0,0 +1,11 @@
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/DownloadClientAddForm";
export { default as DownloadClientUpdateForm } from "./settings/DownloadClientUpdateForm";
export { default as IndexerAddForm } from "./settings/IndexerAddForm";
export { default as IndexerUpdateForm } from "./settings/IndexerUpdateForm";
export { default as IrcNetworkAddForm } from "./settings/IrcNetworkAddForm";

View file

@ -0,0 +1,412 @@
import {Fragment, useState} from "react";
import {useMutation} from "react-query";
import {DOWNLOAD_CLIENT_TYPES, DownloadClient} from "../../domain/interfaces";
import {Dialog, RadioGroup, Transition} from "@headlessui/react";
import {XIcon} from "@heroicons/react/solid";
import {classNames} from "../../styles/utils";
import {Field, Form} from "react-final-form";
import DEBUG from "../../components/debug";
import {SwitchGroup} from "../../components/inputs";
import {queryClient} from "../../index";
import APIClient from "../../api/APIClient";
import {sleep} from "../../utils/utils";
interface radioFieldsetOption {
label: string;
description: string;
value: string;
}
const downloadClientTypeOptions: radioFieldsetOption[] = [
{
label: "qBittorrent",
description: "Add torrents directly to qBittorrent",
value: DOWNLOAD_CLIENT_TYPES.qBittorrent
},
{label: "Deluge", description: "Add torrents directly to Deluge", value: DOWNLOAD_CLIENT_TYPES.Deluge},
];
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']);
toggle()
}
})
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 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">
{/* 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">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">
<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>
<fieldset>
<div
className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
<div>
<legend
className="text-sm font-medium text-gray-900">Type
</legend>
</div>
<div className="space-y-5 sm:col-span-2">
<div className="space-y-5 sm:mt-0">
<Field
name="type"
type="radio"
render={({input}) => (
<RadioGroup value={values.type}
onChange={input.onChange}>
<RadioGroup.Label
className="sr-only">Privacy
setting</RadioGroup.Label>
<div
className="bg-white rounded-md -space-y-px">
{downloadClientTypeOptions.map((setting, settingIdx) => (
<RadioGroup.Option
key={setting.value}
value={setting.value}
className={({checked}) =>
classNames(
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
settingIdx === downloadClientTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
checked ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200',
'relative border p-4 flex cursor-pointer focus:outline-none'
)
}
>
{({
active,
checked
}) => (
<Fragment>
<span
className={classNames(
checked ? 'bg-indigo-600 border-transparent' : 'bg-white border-gray-300',
active ? 'ring-2 ring-offset-2 ring-indigo-500' : '',
'h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center'
)}
aria-hidden="true"
>
<span
className="rounded-full bg-white w-1.5 h-1.5"/>
</span>
<div
className="ml-3 flex flex-col">
<RadioGroup.Label
as="span"
className={classNames(checked ? 'text-indigo-900' : 'text-gray-900', 'block text-sm font-medium')}
>
{setting.label}
</RadioGroup.Label>
<RadioGroup.Description
as="span"
className={classNames(checked ? 'text-indigo-700' : 'text-gray-500', 'block text-sm')}
>
{setting.description}
</RadioGroup.Description>
</div>
</Fragment>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
)}
/>
</div>
</div>
</div>
</fieldset>
<div>
<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="host"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Host
</label>
</div>
<Field name="host">
{({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="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="port"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Port
</label>
</div>
<Field name="port" parse={(v) => v && parseInt(v, 10)}>
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...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="ssl" label="SSL"/>
</div>
<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="username"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Username
</label>
</div>
<Field name="username">
{({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="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="password"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Password
</label>
</div>
<Field name="password">
{({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>
</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={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

@ -0,0 +1,506 @@
import {Fragment, useRef, useState} from "react";
import {useToggle} from "../../hooks/hooks";
import {useMutation} from "react-query";
import {DownloadClient} from "../../domain/interfaces";
import {queryClient} from "../../index";
import {Dialog, RadioGroup, Transition} from "@headlessui/react";
import {ExclamationIcon, XIcon} from "@heroicons/react/solid";
import {classNames} from "../../styles/utils";
import {Field, Form} from "react-final-form";
import DEBUG from "../../components/debug";
import {SwitchGroup} from "../../components/inputs";
import {DownloadClientTypeOptions} from "../../domain/constants";
import APIClient from "../../api/APIClient";
import {sleep} from "../../utils/utils";
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']);
toggle()
}
})
const deleteMutation = useMutation((clientID: number) => APIClient.download_clients.delete(clientID), {
onSuccess: () => {
queryClient.invalidateQueries();
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}>
<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 client
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to remove this client?
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: 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
}}
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">
{/* 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">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">
<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>
<fieldset>
<div
className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
<div>
<legend
className="text-sm font-medium text-gray-900">Type
</legend>
</div>
<div className="space-y-5 sm:col-span-2">
<div className="space-y-5 sm:mt-0">
<Field
name="type"
type="radio"
render={({input}) => (
<RadioGroup value={values.type}
onChange={input.onChange}>
<RadioGroup.Label
className="sr-only">Privacy
setting</RadioGroup.Label>
<div
className="bg-white rounded-md -space-y-px">
{DownloadClientTypeOptions.map((setting, settingIdx) => (
<RadioGroup.Option
key={setting.value}
value={setting.value}
className={({checked}) =>
classNames(
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
settingIdx === DownloadClientTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
checked ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200',
'relative border p-4 flex cursor-pointer focus:outline-none'
)
}
>
{({
active,
checked
}) => (
<Fragment>
<span
className={classNames(
checked ? 'bg-indigo-600 border-transparent' : 'bg-white border-gray-300',
active ? 'ring-2 ring-offset-2 ring-indigo-500' : '',
'h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center'
)}
aria-hidden="true"
>
<span
className="rounded-full bg-white w-1.5 h-1.5"/>
</span>
<div
className="ml-3 flex flex-col">
<RadioGroup.Label
as="span"
className={classNames(checked ? 'text-indigo-900' : 'text-gray-900', 'block text-sm font-medium')}
>
{setting.label}
</RadioGroup.Label>
<RadioGroup.Description
as="span"
className={classNames(checked ? 'text-indigo-700' : 'text-gray-500', 'block text-sm')}
>
{setting.description}
</RadioGroup.Description>
</div>
</Fragment>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
)}
/>
</div>
</div>
</div>
</fieldset>
<div>
<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="host"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Host
</label>
</div>
<Field name="host">
{({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="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="port"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Port
</label>
</div>
<Field name="port" parse={(v) => v && parseInt(v, 10)}>
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...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="ssl" label="SSL"/>
</div>
<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="username"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Username
</label>
</div>
<Field name="username">
{({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="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="password"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Password
</label>
</div>
<Field name="password">
{({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>
</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 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

@ -0,0 +1,241 @@
import React, {Fragment, useEffect} from "react";
import {useMutation, useQuery} from "react-query";
import {Indexer} from "../../domain/interfaces";
import {sleep} from "../../utils/utils";
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 Select from "react-select";
import {queryClient} from "../../index";
import { SwitchGroup } from "../../components/inputs";
import APIClient from "../../api/APIClient";
interface props {
isOpen: boolean;
toggle: any;
}
function IndexerAddForm({isOpen, toggle}: props) {
const {data} = useQuery<any[], Error>('indexerSchema', APIClient.indexers.getSchema,
{
enabled: isOpen,
refetchOnWindowFocus: false
}
)
const mutation = useMutation((indexer: Indexer) => APIClient.indexers.create(indexer), {
onSuccess: () => {
queryClient.invalidateQueries(['indexer']);
sleep(1500)
toggle()
}
})
const onSubmit = (data: any) => {
mutation.mutate(data)
};
const renderSettingFields = (indexer: string) => {
if (indexer !== "") {
// let ind = data.find(i => i.implementation_name === indexer)
let ind = data && data.find(i => i.identifier === indexer)
return (
<div key="opt">
{ind && ind.settings && ind.settings.map((f: any, idx: number) => {
switch (f.type) {
case "text":
return (
<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" key={idx}>
<div>
<label
htmlFor={f.name}
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
{f.label}
</label>
</div>
<div className="sm:col-span-2">
<Field name={"settings."+f.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>
)
}
})}
</div>
)
}
}
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: "",
enabled: true,
identifier: "",
}}
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">
{/* 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">Add
indexer</Dialog.Title>
<p className="text-sm text-gray-500">
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"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<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
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>
<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"
>
Indexer
</label>
</div>
<div className="sm:col-span-2">
<Field
name="identifier"
parse={val => val && val.value}
format={val => data && data.find((o: any) => o.value === val)}
render={({input, meta}) => (
<React.Fragment>
<Select {...input}
isClearable={true}
placeholder="Choose an indexer"
options={data && data.sort((a,b): any => a.name.localeCompare(b.name)).map(v => ({
label: v.name,
value: v.identifier
// value: v.implementation_name
}))}/>
{/*<Error name={input.name} classNames="text-red mt-2 block" />*/}
</React.Fragment>
)}
/>
</div>
</div>
{renderSettingFields(values.identifier)}
</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"
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"
>
Save
</button>
</div>
</div>
<DEBUG values={values}/>
</form>
)
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default IndexerAddForm;

View file

@ -0,0 +1,307 @@
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 } from "../../components/inputs";
import {queryClient} from "../../index";
import {useToggle} from "../../hooks/hooks";
import APIClient from "../../api/APIClient";
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']);
sleep(1500)
toggle()
}
})
const deleteMutation = useMutation((id: number) => APIClient.indexers.delete(id), {
onSuccess: () => {
queryClient.invalidateQueries(['indexer']);
}
})
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 (
<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" key={idx}>
<div>
<label
htmlFor={f.name}
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
{f.label}
</label>
</div>
<div className="sm:col-span-2">
<Field name={"settings."+f.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>
)
}
})}
</div>
)
}
}
// const setss = indexer.settings.reduce((o: any, obj: any) => ({ ...o, [obj.name]: obj.value }), {})
// console.log("setts", setss)
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">
{/* 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
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>
{/* Divider container */}
<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,321 @@
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, TextAreaWide, TextFieldWide} from "../../components/inputs";
import {queryClient} from "../../index";
import arrayMutators from "final-form-arrays";
import { FieldArray } from "react-final-form-arrays";
import {classNames} from "../../styles/utils";
import APIClient from "../../api/APIClient";
// interface radioFieldsetOption {
// label: string;
// description: string;
// value: string;
// }
// const saslTypeOptions: radioFieldsetOption[] = [
// {label: "None", description: "None", value: ""},
// {label: "Plain", description: "SASL plain", value: "PLAIN"},
// {label: "NickServ", description: "/NS identify", value: "NICKSERV"},
// ];
function IrcNetworkAddForm({isOpen, toggle}: any) {
const mutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
onSuccess: data => {
queryClient.invalidateQueries(['networks']);
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.
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 = {} as any;
if (!values.name) {
errors.name = "Required";
}
if (!values.addr) {
errors.addr = "Required";
}
if (!values.nick) {
errors.nick = "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"/>
<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,
addr: "",
tls: false,
nick: "",
pass: "",
// connect_commands: "",
// sasl: {
// mechanism: "",
// plain: {
// username: "",
// password: "",
// }
// },
}}
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">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="addr" label="Address" placeholder="Address:port eg irc.server.net:6697" 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>
<TextFieldWide name="nick" label="Nick" placeholder="Nick" required={true} />
<TextFieldWide name="password" label="Password" placeholder="Network password" />
<TextAreaWide name="connect_commands" label="Connect commands" placeholder="/msg test this" />
{/* <Field*/}
{/* name="sasl.mechanism"*/}
{/* type="select"*/}
{/* render={({input}) => (*/}
{/* <Listbox value={input.value} onChange={input.onChange}>*/}
{/* {({open}) => (*/}
{/* <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>*/}
{/* <Listbox.Label className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2">SASL / auth</Listbox.Label>*/}
{/* </div>*/}
{/* <div className="sm:col-span-2 relative">*/}
{/* <Listbox.Button*/}
{/* className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">*/}
{/* <span className="block truncate">{input.value ? saslTypeOptions.find(c => c.value === input.value)!.label : "Choose auth method"}</span>*/}
{/* <span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">*/}
{/* <SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>*/}
{/*</span>*/}
{/* </Listbox.Button>*/}
{/* <Transition*/}
{/* show={open}*/}
{/* as={Fragment}*/}
{/* leave="transition ease-in duration-100"*/}
{/* leaveFrom="opacity-100"*/}
{/* leaveTo="opacity-0"*/}
{/* >*/}
{/* <Listbox.Options*/}
{/* static*/}
{/* className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"*/}
{/* >*/}
{/* {saslTypeOptions.map((opt: any) => (*/}
{/* <Listbox.Option*/}
{/* key={opt.value}*/}
{/* className={({active}) =>*/}
{/* classNames(*/}
{/* active ? 'text-white bg-indigo-600' : 'text-gray-900',*/}
{/* 'cursor-default select-none relative py-2 pl-3 pr-9'*/}
{/* )*/}
{/* }*/}
{/* value={opt.value}*/}
{/* >*/}
{/* {({selected, active}) => (*/}
{/* <>*/}
{/* <span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>*/}
{/* {opt.label}*/}
{/* </span>*/}
{/* {selected ? (*/}
{/* <span*/}
{/* className={classNames(*/}
{/* active ? 'text-white' : 'text-indigo-600',*/}
{/* 'absolute inset-y-0 right-0 flex items-center pr-4'*/}
{/* )}*/}
{/* >*/}
{/* <CheckIcon className="h-5 w-5" aria-hidden="true"/>*/}
{/* </span>*/}
{/* ) : null}*/}
{/* </>*/}
{/* )}*/}
{/* </Listbox.Option>*/}
{/* ))}*/}
{/* </Listbox.Options>*/}
{/* </Transition>*/}
{/* </div>*/}
{/* </div>*/}
{/* )}*/}
{/* </Listbox>*/}
{/* )} />*/}
</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="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={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

@ -0,0 +1,383 @@
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, TextAreaWide, TextFieldWide} from "../../components/inputs";
import {queryClient} from "../../index";
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";
// interface radioFieldsetOption {
// label: string;
// description: string;
// value: string;
// }
//
// const saslTypeOptions: radioFieldsetOption[] = [
// {label: "None", description: "None", value: ""},
// {label: "Plain", description: "SASL plain", value: "PLAIN"},
// {label: "NickServ", description: "/NS identify", value: "NICKSERV"},
// ];
function IrcNetworkUpdateForm({isOpen, toggle, network}: any) {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
const mutation = useMutation((network: Network) => APIClient.irc.updateNetwork(network), {
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
toggle()
}
})
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
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.addr) {
errors.addr = "Required";
}
if (!values.nick) {
errors.nick = "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,
addr: network.addr,
tls: network.tls,
nick: network.nick,
pass: network.pass,
connect_commands: network.connect_commands,
sasl: network.sasl,
// sasl: {
// mechanism: network.sasl.mechanism,
// plain: {
// username: network.sasl.plain.username,
// password: network.sasl.plain.password,
// }
// },
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>
<TextFieldWide name="addr" label="Address" placeholder="Address:port eg irc.server.net:6697" 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>
<TextFieldWide name="nick" label="Nick" placeholder="Nick" required={true} />
<TextFieldWide name="password" label="Password" placeholder="Network password" />
<TextAreaWide name="connect_commands" label="Connect commands" placeholder="/msg test this" />
{/* <Field*/}
{/* name="sasl.mechanism"*/}
{/* type="select"*/}
{/* render={({input}) => (*/}
{/* <Listbox value={input.value} onChange={input.onChange}>*/}
{/* {({open}) => (*/}
{/* <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>*/}
{/* <Listbox.Label className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2">SASL / auth</Listbox.Label>*/}
{/* </div>*/}
{/* <div className="sm:col-span-2 relative">*/}
{/* <Listbox.Button*/}
{/* className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">*/}
{/* <span className="block truncate">{input.value ? saslTypeOptions.find(c => c.value === input.value)!.label : "Choose a auth type"}</span>*/}
{/* /!*<span className="block truncate">Choose a auth type</span>*!/*/}
{/* <span*/}
{/* className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">*/}
{/* <SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>*/}
{/*</span>*/}
{/* </Listbox.Button>*/}
{/* <Transition*/}
{/* show={open}*/}
{/* as={Fragment}*/}
{/* leave="transition ease-in duration-100"*/}
{/* leaveFrom="opacity-100"*/}
{/* leaveTo="opacity-0"*/}
{/* >*/}
{/* <Listbox.Options*/}
{/* static*/}
{/* className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"*/}
{/* >*/}
{/* {saslTypeOptions.map((opt: any) => (*/}
{/* <Listbox.Option*/}
{/* key={opt.value}*/}
{/* className={({active}) =>*/}
{/* classNames(*/}
{/* active ? 'text-white bg-indigo-600' : 'text-gray-900',*/}
{/* 'cursor-default select-none relative py-2 pl-3 pr-9'*/}
{/* )*/}
{/* }*/}
{/* value={opt.value}*/}
{/* >*/}
{/* {({selected, active}) => (*/}
{/* <>*/}
{/* <span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>*/}
{/* {opt.label}*/}
{/* </span>*/}
{/* {selected ? (*/}
{/* <span*/}
{/* className={classNames(*/}
{/* active ? 'text-white' : 'text-indigo-600',*/}
{/* 'absolute inset-y-0 right-0 flex items-center pr-4'*/}
{/* )}*/}
{/* >*/}
{/* <CheckIcon className="h-5 w-5" aria-hidden="true"/>*/}
{/* </span>*/}
{/* ) : null}*/}
{/* </>*/}
{/* )}*/}
{/* </Listbox.Option>*/}
{/* ))}*/}
{/* </Listbox.Options>*/}
{/* </Transition>*/}
{/* </div>*/}
{/* </div>*/}
{/* )}*/}
{/* </Listbox>*/}
{/* )} />*/}
</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>
{/*<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")}*/}
{/* >*/}
{/* Save*/}
{/* </button>*/}
{/* </div>*/}
{/*</div>*/}
<DEBUG values={values}/>
</form>
)
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default IrcNetworkUpdateForm;