mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 16:59:12 +00:00
feat: add webui
This commit is contained in:
parent
a838d994a6
commit
773e57afe6
59 changed files with 19794 additions and 0 deletions
829
web/src/forms/filters/FilterActionAddForm.tsx
Normal file
829
web/src/forms/filters/FilterActionAddForm.tsx
Normal 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;
|
834
web/src/forms/filters/FilterActionUpdateForm.tsx
Normal file
834
web/src/forms/filters/FilterActionUpdateForm.tsx
Normal 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;
|
151
web/src/forms/filters/FilterAddForm.tsx
Normal file
151
web/src/forms/filters/FilterAddForm.tsx
Normal 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
11
web/src/forms/index.ts
Normal 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";
|
412
web/src/forms/settings/DownloadClientAddForm.tsx
Normal file
412
web/src/forms/settings/DownloadClientAddForm.tsx
Normal 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;
|
506
web/src/forms/settings/DownloadClientUpdateForm.tsx
Normal file
506
web/src/forms/settings/DownloadClientUpdateForm.tsx
Normal 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">
|
||||
​
|
||||
</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;
|
241
web/src/forms/settings/IndexerAddForm.tsx
Normal file
241
web/src/forms/settings/IndexerAddForm.tsx
Normal 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;
|
307
web/src/forms/settings/IndexerUpdateForm.tsx
Normal file
307
web/src/forms/settings/IndexerUpdateForm.tsx
Normal 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">
|
||||
​
|
||||
</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;
|
321
web/src/forms/settings/IrcNetworkAddForm.tsx
Normal file
321
web/src/forms/settings/IrcNetworkAddForm.tsx
Normal 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;
|
383
web/src/forms/settings/IrcNetworkUpdateForm.tsx
Normal file
383
web/src/forms/settings/IrcNetworkUpdateForm.tsx
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue