feat(web): mobile UI improvements (#359)

* enhancement: improved alerts component contrast
enhancement: simplified and improved radio switch group look
fix: fixed inconsistent spacing in input components (there's still some work left to be done)
fix: made slideover panel display on full width on mobile devices
enhancement: made forms more accessible to mobile users, adapter changes in accordance with the previous input components fix
fix: fixed misspelling in NotificationForms filename
chore: cleaned up code
fix: made filter table top edges less round and improved look
fix: fixed a bug where when a modal/slideover component was opened, a 1px white bar would be shown in one of the modal parent elements (for the fix see L89 in screens/settings/DwonloadClient.tsx)
enhancement: improved responsiveness for irc network list

* Fixed 2 small comma warnings from ESLint

Co-authored-by: anonymous <anonymous>
This commit is contained in:
stacksmash76 2022-07-17 23:34:49 +02:00 committed by GitHub
parent f961115dac
commit 3da594ec75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 758 additions and 640 deletions

View file

@ -99,7 +99,7 @@ export const APIClient = {
toggleEnable: (id: number, enabled: boolean) => appClient.Patch(`api/feeds/${id}/enabled`, { enabled }), toggleEnable: (id: number, enabled: boolean) => appClient.Patch(`api/feeds/${id}/enabled`, { enabled }),
update: (feed: Feed) => appClient.Put(`api/feeds/${feed.id}`, feed), update: (feed: Feed) => appClient.Put(`api/feeds/${feed.id}`, feed),
delete: (id: number) => appClient.Delete(`api/feeds/${id}`), delete: (id: number) => appClient.Delete(`api/feeds/${id}`),
test: (feed: Feed) => appClient.Post("api/feeds/test", feed), test: (feed: Feed) => appClient.Post("api/feeds/test", feed)
}, },
indexers: { indexers: {
// returns indexer options for all currently present/enabled indexers // returns indexer options for all currently present/enabled indexers

View file

@ -2,26 +2,26 @@
import { ExclamationIcon } from "@heroicons/react/solid"; import { ExclamationIcon } from "@heroicons/react/solid";
interface props { interface props {
title: string; title?: string;
text: string; text: string;
} }
export function AlertWarning({ title, text }: props) { export function AlertWarning({ title, text }: props) {
return ( return (
<div className="p-4"> <div className="my-4 rounded-md bg-yellow-50 dark:bg-yellow-100 p-4 border border-yellow-300 dark:border-none">
<div className="rounded-md bg-yellow-50 p-4"> <div className="flex">
<div className="flex"> <div className="flex-shrink-0">
<div className="flex-shrink-0"> <ExclamationIcon
<ExclamationIcon className="h-5 w-5 text-yellow-400 dark:text-yellow-600"
className="h-5 w-5 text-yellow-400" aria-hidden="true"
aria-hidden="true" />
/> </div>
</div> <div className="ml-3">
<div className="ml-3"> {title ? (
<h3 className="text-sm font-medium text-yellow-800">{title}</h3> <h3 className="mb-1 text-md font-medium text-yellow-800">{title}</h3>
<div className="mt-2 text-sm text-yellow-700"> ) : null}
<p>{text}</p> <div className="text-sm text-yellow-800">
</div> <p>{text}</p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -11,7 +11,7 @@ const DEBUG: FC<DebugProps> = ({ values }) => {
return ( return (
<div className="w-full p-2 flex flex-col mt-6 bg-gray-100 dark:bg-gray-900"> <div className="w-full p-2 flex flex-col mt-6 bg-gray-100 dark:bg-gray-900">
<pre className="dark:text-gray-400">{JSON.stringify(values, null, 2)}</pre> <pre className="dark:text-gray-400 break-all whitespace-pre-wrap">{JSON.stringify(values, null, 2)}</pre>
</div> </div>
); );
}; };

View file

@ -25,7 +25,7 @@ export const TextFieldWide = ({
required, required,
hidden hidden
}: TextFieldWideProps) => ( }: TextFieldWideProps) => (
<div hidden={hidden} 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 hidden={hidden} className="space-y-1 p-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4">
<div> <div>
<label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"> <label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
{label} {required && <span className="text-gray-500">*</span>} {label} {required && <span className="text-gray-500">*</span>}
@ -80,7 +80,7 @@ export const PasswordFieldWide = ({
const [isVisible, toggleVisibility] = useToggle(defaultVisible); const [isVisible, toggleVisibility] = useToggle(defaultVisible);
return ( 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"> <div className="space-y-1 p-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4">
<div> <div>
<label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"> <label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
{label} {required && <span className="text-gray-500">*</span>} {label} {required && <span className="text-gray-500">*</span>}
@ -134,7 +134,7 @@ export const NumberFieldWide = ({
defaultValue, defaultValue,
required required
}: NumberFieldWideProps) => ( }: NumberFieldWideProps) => (
<div className="px-4 space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5"> <div className="px-4 space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-4">
<div> <div>
<label <label
htmlFor={name} htmlFor={name}
@ -187,7 +187,7 @@ export const SwitchGroupWide = ({
description, description,
defaultValue defaultValue
}: SwitchGroupWideProps) => ( }: SwitchGroupWideProps) => (
<ul className="mt-2 divide-y divide-gray-200 dark:divide-gray-700"> <ul className="mt-2 px-4 divide-y divide-gray-200 dark:divide-gray-700">
<Switch.Group as="li" className="py-4 flex items-center justify-between"> <Switch.Group as="li" className="py-4 flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white" <Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white"
@ -212,7 +212,7 @@ export const SwitchGroupWide = ({
type="button" type="button"
value={field.value} value={field.value}
checked={field.checked ?? false} checked={field.checked ?? false}
onChange={value => { onChange={(value: unknown) => {
form.setFieldValue(field?.name ?? "", value); form.setFieldValue(field?.name ?? "", value);
}} }}
className={classNames( className={classNames(

View file

@ -1,4 +1,3 @@
import { Fragment } from "react";
import { Field, useFormikContext } from "formik"; import { Field, useFormikContext } from "formik";
import { RadioGroup } from "@headlessui/react"; import { RadioGroup } from "@headlessui/react";
import { classNames } from "../../utils"; import { classNames } from "../../utils";
@ -25,14 +24,13 @@ function RadioFieldsetWide({ name, legend, options }: props) {
setFieldValue setFieldValue
} = useFormikContext<anyObj>(); } = useFormikContext<anyObj>();
const onChange = (value: string) => { const onChange = (value: string) => {
setFieldValue(name, value); setFieldValue(name, value);
}; };
return ( return (
<fieldset> <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 className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:py-4">
<div> <div>
<legend className="text-sm font-medium text-gray-900 dark:text-white"> <legend className="text-sm font-medium text-gray-900 dark:text-white">
{legend} {legend}
@ -60,49 +58,41 @@ function RadioFieldsetWide({ name, legend, options }: props) {
? "rounded-bl-md rounded-br-md" ? "rounded-bl-md rounded-br-md"
: "", : "",
checked checked
? "bg-indigo-50 dark:bg-gray-700 border-indigo-200 dark:border-blue-600 z-10" ? "border-1 bg-indigo-100 dark:bg-blue-900 border-indigo-400 dark:border-blue-600 z-10"
: "border-gray-200 dark:border-gray-700", : "border-gray-200 dark:border-gray-700",
"relative border p-4 flex cursor-pointer focus:outline-none" "relative border p-4 flex cursor-pointer focus:outline-none"
) )
} }
> >
{({ active, checked }) => ( {({ checked }) => (
<Fragment> <>
<span <span
className={classNames( className={classNames(
checked checked
? "bg-indigo-600 dark:bg-blue-600 border-transparent" ? "bg-indigo-600 dark:bg-blue-500 border-transparent"
: "bg-white border-gray-300 dark:border-gray-300", : "bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-300",
active "h-6 w-6 mt-1 cursor-pointer rounded-full border flex items-center justify-center"
? "ring-2 ring-offset-2 ring-indigo-500 dark:ring-blue-500"
: "",
"h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
)} )}
aria-hidden="true" aria-hidden="true"
> />
<span className="rounded-full bg-white w-1.5 h-1.5" />
</span>
<div className="ml-3 flex flex-col"> <div className="ml-3 flex flex-col">
<RadioGroup.Label <RadioGroup.Label
as="span" as="span"
className={classNames( className={classNames(
checked ? "text-indigo-900 dark:text-blue-500" : "text-gray-900 dark:text-gray-300", "block text-md text-gray-900 dark:text-gray-300",
"block text-sm font-medium" checked ? "font-bold" : "font-medium"
)} )}
> >
{setting.label} {setting.label}
</RadioGroup.Label> </RadioGroup.Label>
<RadioGroup.Description <RadioGroup.Description
as="span" as="span"
className={classNames( className="block text-sm text-gray-700 dark:text-gray-400"
checked ? "text-indigo-700 dark:text-blue-500" : "text-gray-500",
"block text-sm"
)}
> >
{setting.description} {setting.description}
</RadioGroup.Description> </RadioGroup.Description>
</div> </div>
</Fragment> </>
)} )}
</RadioGroup.Option> </RadioGroup.Option>
))} ))}

View file

@ -354,7 +354,7 @@ export const SelectWide = ({
return ( return (
<div className="py-6 px-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">
<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 className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-4">
<Field name={name} type="select"> <Field name={name} type="select">
{({ {({
field, field,

View file

@ -35,7 +35,7 @@ export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, d
</Transition.Child> </Transition.Child>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"> <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203; &#8203;
</span> </span>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}

View file

@ -36,7 +36,7 @@ function SlideOver<DataType>({
testFn, testFn,
isTesting, isTesting,
isTestSuccessful, isTestSuccessful,
isTestError, isTestError
}: SlideOverProps<DataType>): React.ReactElement { }: SlideOverProps<DataType>): React.ReactElement {
const cancelModalButtonRef = useRef<HTMLInputElement | null>(null); const cancelModalButtonRef = useRef<HTMLInputElement | null>(null);
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
@ -64,7 +64,7 @@ function SlideOver<DataType>({
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" /> <Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16"> <div className="fixed inset-y-0 right-0 max-w-full flex">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700" enter="transform transition ease-in-out duration-500 sm:duration-700"

View file

@ -338,5 +338,5 @@ export const EventOptions: SelectOption[] = [
label: "New update", label: "New update",
value: "APP_UPDATE_AVAILABLE", value: "APP_UPDATE_AVAILABLE",
description: "Get notified on updates" description: "Get notified on updates"
}, }
]; ];

View file

@ -96,7 +96,7 @@ function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
<div <div
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<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"> className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-4">
<div> <div>
<label <label
htmlFor="name" htmlFor="name"

View file

@ -50,24 +50,31 @@ function FormFieldsDefault() {
} = useFormikContext<InitialValues>(); } = useFormikContext<InitialValues>();
return ( return (
<Fragment> <div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide name="host" label="Host" help="Eg. client.domain.ltd, domain.ltd/client, domain.ltd:port"/> <TextFieldWide
name="host"
label="Host"
help="Eg. client.domain.ltd, domain.ltd/client, domain.ltd:port"
/>
<NumberFieldWide name="port" label="Port" help="WebUI port for qBittorrent and daemon port for Deluge"/> <NumberFieldWide
name="port"
label="Port"
help="WebUI port for qBittorrent and daemon port for Deluge"
/>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700"> <SwitchGroupWide name="tls" label="TLS" />
<SwitchGroupWide name="tls" label="TLS"/>
{tls && ( {tls && (
<Fragment> <SwitchGroupWide
<SwitchGroupWide name="tls_skip_verify" label="Skip TLS verification (insecure)"/> name="tls_skip_verify"
</Fragment> label="Skip TLS verification (insecure)"
)} />
</div> )}
<TextFieldWide name="username" label="Username"/> <TextFieldWide name="username" label="Username" />
<PasswordFieldWide name="password" label="Password"/> <PasswordFieldWide name="password" label="Password" />
</Fragment> </div>
); );
} }
@ -77,22 +84,24 @@ function FormFieldsArr() {
} = useFormikContext<InitialValues>(); } = useFormikContext<InitialValues>();
return ( return (
<Fragment> <div className="flex flex-col space-y-4 px-1 mb-4 sm:py-0 sm:space-y-0">
<TextFieldWide name="host" label="Host" help="Full url http(s)://domain.ltd and/or subdomain/subfolder"/> <TextFieldWide
name="host"
label="Host"
help="Full url http(s)://domain.ltd and/or subdomain/subfolder"
/>
<PasswordFieldWide name="settings.apikey" label="API key"/> <PasswordFieldWide name="settings.apikey" label="API key" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> <SwitchGroupWide name="settings.basic.auth" label="Basic auth" />
<SwitchGroupWide name="settings.basic.auth" label="Basic auth"/>
</div>
{settings.basic?.auth === true && ( {settings.basic?.auth === true && (
<Fragment> <>
<TextFieldWide name="settings.basic.username" label="Username"/> <TextFieldWide name="settings.basic.username" label="Username" />
<PasswordFieldWide name="settings.basic.password" label="Password"/> <PasswordFieldWide name="settings.basic.password" label="Password" />
</Fragment> </>
)} )}
</Fragment> </div>
); );
} }
@ -102,37 +111,42 @@ function FormFieldsQbit() {
} = useFormikContext<InitialValues>(); } = useFormikContext<InitialValues>();
return ( return (
<Fragment> <div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide name="host" label="Host" help="Eg. client.domain.ltd, domain.ltd/client, domain.ltd:port" /> <TextFieldWide
name="host"
label="Host"
help="Eg. client.domain.ltd, domain.ltd/client, domain.ltd:port"
/>
{port > 0 && ( {port > 0 && (
<NumberFieldWide name="port" label="Port" help="WebUI port for qBittorrent" /> <NumberFieldWide
name="port"
label="Port"
help="WebUI port for qBittorrent"
/>
)} )}
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700"> <SwitchGroupWide name="tls" label="TLS" />
<SwitchGroupWide name="tls" label="TLS" />
{tls && ( {tls && (
<Fragment> <SwitchGroupWide
<SwitchGroupWide name="tls_skip_verify" label="Skip TLS verification (insecure)" /> name="tls_skip_verify"
</Fragment> label="Skip TLS verification (insecure)"
)} />
</div> )}
<TextFieldWide name="username" label="Username" /> <TextFieldWide name="username" label="Username" />
<PasswordFieldWide name="password" label="Password" /> <PasswordFieldWide name="password" label="Password" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> <SwitchGroupWide name="settings.basic.auth" label="Basic auth" />
<SwitchGroupWide name="settings.basic.auth" label="Basic auth" />
</div>
{settings.basic?.auth === true && ( {settings.basic?.auth === true && (
<Fragment> <>
<TextFieldWide name="settings.basic.username" label="Username" /> <TextFieldWide name="settings.basic.username" label="Username" />
<PasswordFieldWide name="settings.basic.password" label="Password" /> <PasswordFieldWide name="settings.basic.password" label="Password" />
</Fragment> </>
)} )}
</Fragment> </div>
); );
} }
@ -142,24 +156,27 @@ function FormFieldsTransmission() {
} = useFormikContext<InitialValues>(); } = useFormikContext<InitialValues>();
return ( return (
<Fragment> <div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide name="host" label="Host" help="Eg. client.domain.ltd, domain.ltd/client, domain.ltd"/> <TextFieldWide
name="host"
label="Host"
help="Eg. client.domain.ltd, domain.ltd/client, domain.ltd"
/>
<NumberFieldWide name="port" label="Port" help="Port for Transmission"/> <NumberFieldWide name="port" label="Port" help="Port for Transmission" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700"> <SwitchGroupWide name="tls" label="TLS" />
<SwitchGroupWide name="tls" label="TLS"/>
{tls && ( {tls && (
<Fragment> <SwitchGroupWide
<SwitchGroupWide name="tls_skip_verify" label="Skip TLS verification (insecure)"/> name="tls_skip_verify"
</Fragment> label="Skip TLS verification (insecure)"
)} />
</div> )}
<TextFieldWide name="username" label="Username"/> <TextFieldWide name="username" label="Username" />
<PasswordFieldWide name="password" label="Password"/> <PasswordFieldWide name="password" label="Password" />
</Fragment> </div>
); );
} }
@ -186,21 +203,17 @@ function FormFieldsRulesBasic() {
return ( return (
<div className="border-t border-gray-200 dark:border-gray-700 py-5"> <div className="border-t border-gray-200 dark:border-gray-700 py-5">
<div className="px-6 space-y-1"> <div className="px-4 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Rules</Dialog.Title> <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Rules</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
Manage max downloads. Manage max downloads.
</p> </p>
</div> </div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> <SwitchGroupWide name="settings.rules.enabled" label="Enabled"/>
<SwitchGroupWide name="settings.rules.enabled" label="Enabled"/>
</div>
{settings && settings.rules?.enabled === true && ( {settings && settings.rules?.enabled === true && (
<Fragment> <NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads"/>
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads"/>
</Fragment>
)} )}
</div> </div>
); );
@ -212,34 +225,38 @@ function FormFieldsRules() {
} = useFormikContext<InitialValues>(); } = useFormikContext<InitialValues>();
return ( return (
<div className="border-t border-gray-200 dark:border-gray-700 py-5"> <div className="border-t border-gray-200 dark:border-gray-700 py-5 px-2">
<div className="px-4 space-y-1">
<div className="px-6 space-y-1"> <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Rules</Dialog.Title> Rules
</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
Manage max downloads etc. Manage max downloads etc.
</p> </p>
</div> </div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> <SwitchGroupWide name="settings.rules.enabled" label="Enabled" />
<SwitchGroupWide name="settings.rules.enabled" label="Enabled"/>
</div>
{settings.rules?.enabled === true && ( {settings.rules?.enabled === true && (
<Fragment> <>
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads"/> <NumberFieldWide
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> name="settings.rules.max_active_downloads"
<SwitchGroupWide name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents"/> label="Max active downloads"
</div> />
<SwitchGroupWide
name="settings.rules.ignore_slow_torrents"
label="Ignore slow torrents"
/>
{settings.rules?.ignore_slow_torrents === true && ( {settings.rules?.ignore_slow_torrents === true && (
<Fragment> <NumberFieldWide
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold" name="settings.rules.download_speed_threshold"
placeholder="in KB/s" label="Download speed threshold"
help="If download speed is below this when max active downloads is hit, download anyways. KB/s"/> placeholder="in KB/s"
</Fragment> help="If download speed is below this when max active downloads is hit, download anyways. KB/s"
/>
)} )}
</Fragment> </>
)} )}
</div> </div>
); );
@ -248,7 +265,7 @@ function FormFieldsRules() {
export const rulesComponentMap: componentMapType = { export const rulesComponentMap: componentMapType = {
DELUGE_V1: <FormFieldsRulesBasic/>, DELUGE_V1: <FormFieldsRulesBasic/>,
DELUGE_V2: <FormFieldsRulesBasic/>, DELUGE_V2: <FormFieldsRulesBasic/>,
QBITTORRENT: <FormFieldsRules/>, QBITTORRENT: <FormFieldsRules/>
}; };
interface formButtonsProps { interface formButtonsProps {
@ -490,20 +507,14 @@ export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
</div> </div>
</div> </div>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700"> <div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide name="name" label="Name"/> <TextFieldWide name="name" label="Name"/>
<SwitchGroupWide name="enabled" label="Enabled"/>
<div
className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700">
<SwitchGroupWide name="enabled" label="Enabled"/>
</div>
<RadioFieldsetWide <RadioFieldsetWide
name="type" name="type"
legend="Type" legend="Type"
options={DownloadClientTypeOptions} options={DownloadClientTypeOptions}
/> />
<div>{componentMap[values.type]}</div> <div>{componentMap[values.type]}</div>
</div> </div>
</div> </div>
@ -697,17 +708,12 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700"> <div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<TextFieldWide name="name" label="Name"/> <TextFieldWide name="name" label="Name"/>
<SwitchGroupWide name="enabled" label="Enabled"/>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroupWide name="enabled" label="Enabled"/>
</div>
<RadioFieldsetWide <RadioFieldsetWide
name="type" name="type"
legend="Type" legend="Type"
options={DownloadClientTypeOptions} options={DownloadClientTypeOptions}
/> />
<div>{componentMap[values.type]}</div> <div>{componentMap[values.type]}</div>
</div> </div>
</div> </div>

View file

@ -116,7 +116,7 @@ export function FeedUpdateForm({ isOpen, toggle, feed }: UpdateProps) {
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700"> <div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
<div <div
className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5"> className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-4">
<div> <div>
<label <label
htmlFor="type" htmlFor="type"

View file

@ -63,7 +63,7 @@ const IrcSettingFields = (ind: IndexerDefinition, indexer: string) => {
<Fragment> <Fragment>
{ind && ind.irc && ind.irc.settings && ( {ind && ind.irc && ind.irc.settings && (
<div className="border-t border-gray-200 dark:border-gray-700 py-5"> <div className="border-t border-gray-200 dark:border-gray-700 py-5">
<div className="px-6 space-y-1"> <div className="px-4 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">IRC</Dialog.Title> <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">IRC</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-200"> <p className="text-sm text-gray-500 dark:text-gray-200">
Networks, channels and invite commands are configured automatically. Networks, channels and invite commands are configured automatically.
@ -94,7 +94,7 @@ const FeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
<Fragment> <Fragment>
{ind && ind.torznab && ind.torznab.settings && ( {ind && ind.torznab && ind.torznab.settings && (
<div className=""> <div className="">
<div className="px-6 space-y-1"> <div className="px-4 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Torznab</Dialog.Title> <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Torznab</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-200"> <p className="text-sm text-gray-500 dark:text-gray-200">
Torznab feed Torznab feed
@ -329,13 +329,13 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
</div> </div>
<div className="py-6 space-y-4 divide-y divide-gray-200 dark:divide-gray-700"> <div className="py-6 space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
<div className="py-4 flex items-center justify-between 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 className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-4">
<div> <div>
<label <label
htmlFor="identifier" htmlFor="identifier"
className="block text-sm font-medium text-gray-900 dark:text-white" className="block text-sm font-medium text-gray-900 dark:text-white"
> >
Indexer Indexer
</label> </label>
</div> </div>
<div className="sm:col-span-2"> <div className="sm:col-span-2">
@ -391,9 +391,7 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
</div> </div>
</div> </div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> <SwitchGroupWide name="enabled" label="Enabled" />
<SwitchGroupWide name="enabled" label="Enabled" />
</div>
{SettingFields(indexer, values.identifier)} {SettingFields(indexer, values.identifier)}
@ -504,7 +502,7 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
...o, ...o,
[obj.name]: obj.value [obj.name]: obj.value
} as Record<string, string>), } as Record<string, string>),
{} as Record<string, string> {} as Record<string, string>
) )
}; };
@ -519,16 +517,14 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
initialValues={initialValues} initialValues={initialValues}
> >
{() => ( {() => (
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 divide-y divide-gray-200 dark:divide-gray-700"> <div className="py-2 space-y-6 sm:py-0 sm:space-y-0 divide-y divide-gray-200 dark:divide-gray-700">
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5"> <div className="space-y-1 p-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4">
<div> <label
<label htmlFor="name"
htmlFor="name" className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2" >
> Name
Name </label>
</label>
</div>
<Field name="name"> <Field name="name">
{({ field, meta }: FieldProps) => ( {({ field, meta }: FieldProps) => (
<div className="sm:col-span-2"> <div className="sm:col-span-2">
@ -542,11 +538,7 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
)} )}
</Field> </Field>
</div> </div>
<SwitchGroupWide name="enabled" label="Enabled" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
<SwitchGroupWide name="enabled" label="Enabled" />
</div>
{renderSettingFields(indexer.settings)} {renderSettingFields(indexer.settings)}
</div> </div>
)} )}

View file

@ -24,7 +24,7 @@ const ChannelsFieldArray = ({ channels }: ChannelsFieldArrayProps) => (
<div className="p-6"> <div className="p-6">
<FieldArray name="channels"> <FieldArray name="channels">
{({ remove, push }) => ( {({ remove, push }) => (
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4"> <div className="flex flex-col space-y-2 border-2 border-dashed dark:border-gray-700 p-4">
{channels && channels.length > 0 ? ( {channels && channels.length > 0 ? (
channels.map((_channel: IrcChannel, index: number) => ( channels.map((_channel: IrcChannel, index: number) => (
<div key={index} className="flex justify-between"> <div key={index} className="flex justify-between">
@ -68,7 +68,7 @@ const ChannelsFieldArray = ({ channels }: ChannelsFieldArrayProps) => (
)) ))
) : ( ) : (
<span className="text-center text-sm text-grey-darker dark:text-white"> <span className="text-center text-sm text-grey-darker dark:text-white">
No channels! No channels!
</span> </span>
)} )}
<button <button
@ -76,7 +76,7 @@ const ChannelsFieldArray = ({ channels }: ChannelsFieldArrayProps) => (
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 rounded self-center text-center" className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 rounded self-center text-center"
onClick={() => push({ name: "", password: "" })} onClick={() => push({ name: "", password: "" })}
> >
Add Channel Add Channel
</button> </button>
</div> </div>
)} )}
@ -159,34 +159,47 @@ export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
validate={validate} validate={validate}
> >
{(values) => ( {(values) => (
<> <div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} /> <TextFieldWide
name="name"
label="Name"
placeholder="Name"
required={true}
/>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700"> <SwitchGroupWide name="enabled" label="Enabled" />
<TextFieldWide
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700"> name="server"
<SwitchGroupWide name="enabled" label="Enabled" /> label="Server"
</div> placeholder="Address: Eg irc.server.net"
required={true}
<div> />
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} /> <NumberFieldWide
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} /> name="port"
label="Port"
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> placeholder="Eg 6667"
<SwitchGroupWide name="tls" label="TLS" /> required={true}
</div> />
<SwitchGroupWide name="tls" label="TLS" />
<PasswordFieldWide name="pass" label="Password" help="Network password" /> <PasswordFieldWide
name="pass"
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} /> label="Password"
<PasswordFieldWide name="nickserv.password" label="NickServ Password" /> help="Network password"
/>
<PasswordFieldWide name="invite_command" label="Invite command" /> <TextFieldWide
</div> name="nickserv.account"
</div> label="NickServ Account"
placeholder="NickServ Account"
required={true}
/>
<PasswordFieldWide
name="nickserv.password"
label="NickServ Password"
/>
<PasswordFieldWide name="invite_command" label="Invite command" />
<ChannelsFieldArray channels={values.channels} /> <ChannelsFieldArray channels={values.channels} />
</> </div>
)} )}
</SlideOver> </SlideOver>
); );
@ -290,34 +303,51 @@ export function IrcNetworkUpdateForm({
validate={validate} validate={validate}
> >
{(values) => ( {(values) => (
<> <div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} /> <TextFieldWide
name="name"
label="Name"
placeholder="Name"
required={true}
/>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700"> <SwitchGroupWide name="enabled" label="Enabled" />
<TextFieldWide
name="server"
label="Server"
placeholder="Address: Eg irc.server.net"
required={true}
/>
<NumberFieldWide
name="port"
label="Port"
placeholder="Eg 6667"
required={true}
/>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0"> <SwitchGroupWide name="tls" label="TLS" />
<SwitchGroupWide name="enabled" label="Enabled" />
</div>
<div> <PasswordFieldWide
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} /> name="pass"
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} /> label="Password"
help="Network password"
/>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> <TextFieldWide
<SwitchGroupWide name="tls" label="TLS" /> name="nickserv.account"
</div> label="NickServ Account"
placeholder="NickServ Account"
required={true}
/>
<PasswordFieldWide
name="nickserv.password"
label="NickServ Password"
/>
<PasswordFieldWide name="pass" label="Password" help="Network password" /> <PasswordFieldWide name="invite_command" label="Invite command" />
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<PasswordFieldWide name="invite_command" label="Invite command" />
</div>
</div>
<ChannelsFieldArray channels={values.channels} /> <ChannelsFieldArray channels={values.channels} />
</> </div>
)} )}
</SlideOver> </SlideOver>
); );

View file

@ -62,11 +62,11 @@ const Option = (props: OptionProps) => {
function FormFieldsDiscord() { function FormFieldsDiscord() {
return ( return (
<div className="border-t border-gray-200 dark:border-gray-700 py-5"> <div className="border-t border-gray-200 dark:border-gray-700 py-4">
<div className="px-6 space-y-1"> <div className="px-4 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Settings</Dialog.Title> <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Settings</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
Create a <a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks" rel="noopener noreferrer" target="_blank" className="font-medium text-blue-500">webhook integration</a> in your server. Create a <a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks" rel="noopener noreferrer" target="_blank" className="font-medium text-blue-500 underline underline-offset-1 hover:text-blue-400">webhook integration</a> in your server.
</p> </p>
</div> </div>
@ -82,11 +82,11 @@ function FormFieldsDiscord() {
function FormFieldsTelegram() { function FormFieldsTelegram() {
return ( return (
<div className="border-t border-gray-200 dark:border-gray-700 py-5"> <div className="border-t border-gray-200 dark:border-gray-700 py-4">
<div className="px-6 space-y-1"> <div className="px-4 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Settings</Dialog.Title> <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Settings</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
Read how to <a href="https://core.telegram.org/bots#3-how-do-i-create-a-bot" rel="noopener noreferrer" target="_blank" className="font-medium text-blue-500">create a bot</a>. Read how to <a href="https://core.telegram.org/bots#3-how-do-i-create-a-bot" rel="noopener noreferrer" target="_blank" className="font-medium text-blue-500 underline underline-offset-1 hover:text-blue-400">create a bot</a>.
</p> </p>
</div> </div>
@ -161,9 +161,15 @@ export function NotificationAddForm({ isOpen, toggle }: AddProps) {
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}> <Dialog
as="div"
static
className="fixed inset-0 overflow-hidden"
open={isOpen}
onClose={toggle}
>
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0"/> <Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16"> <div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child <Transition.Child
@ -208,16 +214,20 @@ export function NotificationAddForm({ isOpen, toggle }: AddProps) {
onClick={toggle} onClick={toggle}
> >
<span className="sr-only">Close panel</span> <span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true"/> <XIcon className="h-6 w-6" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<TextFieldWide name="name" label="Name" required={true}/> <div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide
name="name"
label="Name"
required={true}
/>
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700"> <div className="flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4">
<div className="py-4 flex items-center justify-between 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> <div>
<label <label
htmlFor="type" htmlFor="type"
@ -232,10 +242,16 @@ export function NotificationAddForm({ isOpen, toggle }: AddProps) {
field, field,
form: { setFieldValue, resetForm } form: { setFieldValue, resetForm }
}: FieldProps) => ( }: FieldProps) => (
<Select {...field} <Select
{...field}
isClearable={true} isClearable={true}
isSearchable={true} isSearchable={true}
components={{ Input, Control, Menu, Option }} components={{
Input,
Control,
Menu,
Option
}}
placeholder="Choose a type" placeholder="Choose a type"
styles={{ styles={{
singleValue: (base) => ({ singleValue: (base) => ({
@ -257,39 +273,39 @@ export function NotificationAddForm({ isOpen, toggle }: AddProps) {
const opt = option as SelectOption; const opt = option as SelectOption;
// setFieldValue("name", option?.label ?? "") // setFieldValue("name", option?.label ?? "")
setFieldValue(field.name, opt.value ?? ""); setFieldValue(
field.name,
opt.value ?? ""
);
}} }}
options={NotificationTypeOptions} options={NotificationTypeOptions}
/> />
)} )}
</Field> </Field>
</div> </div>
</div> </div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> <SwitchGroupWide name="enabled" label="Enabled" />
<SwitchGroupWide name="enabled" label="Enabled"/>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 py-5"> <div className="border-t mt-2 border-gray-200 dark:border-gray-700 py-4">
<div className="px-6 space-y-1"> <div className="px-4 space-y-1">
<Dialog.Title <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
className="text-lg font-medium text-gray-900 dark:text-white">Events</Dialog.Title> Events
</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
Select what events to trigger on Select what events to trigger on
</p> </p>
</div> </div>
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:gap-4 sm:px-6 sm:py-5"> <div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:gap-4 sm:py-4">
<EventCheckBoxes /> <EventCheckBoxes />
</div> </div>
</div> </div>
</div> </div>
{componentMap[values.type]} {componentMap[values.type]}
</div> </div>
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6"> <div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-4 sm:px-6">
<div className="space-x-3 flex justify-end"> <div className="space-x-3 flex justify-end">
<button <button
type="button" type="button"
@ -314,12 +330,11 @@ export function NotificationAddForm({ isOpen, toggle }: AddProps) {
</div> </div>
</div> </div>
<DEBUG values={values}/> <DEBUG values={values} />
</Form> </Form>
)} )}
</Formik> </Formik>
</div> </div>
</Transition.Child> </Transition.Child>
</div> </div>
</div> </div>
@ -432,8 +447,8 @@ export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateP
<div> <div>
<TextFieldWide name="name" label="Name" required={true}/> <TextFieldWide name="name" label="Name" required={true}/>
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700"> <div className="space-y-2 divide-y divide-gray-200 dark:divide-gray-700">
<div className="py-4 flex items-center justify-between 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 className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-4">
<div> <div>
<label <label
htmlFor="type" htmlFor="type"
@ -477,13 +492,9 @@ export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateP
</Field> </Field>
</div> </div>
</div> </div>
<SwitchGroupWide name="enabled" label="Enabled"/>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> <div className="border-t border-gray-200 dark:border-gray-700 py-4">
<SwitchGroupWide name="enabled" label="Enabled"/> <div className="px-4 space-y-1">
</div>
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
<div className="px-6 space-y-1">
<Dialog.Title <Dialog.Title
className="text-lg font-medium text-gray-900 dark:text-white">Events</Dialog.Title> className="text-lg font-medium text-gray-900 dark:text-white">Events</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
@ -491,7 +502,7 @@ export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateP
</p> </p>
</div> </div>
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:gap-4 sm:px-6 sm:py-5"> <div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:gap-4 sm:py-2">
<EventCheckBoxes /> <EventCheckBoxes />
</div> </div>
</div> </div>

View file

@ -46,7 +46,7 @@ function SubNavLink({ item }: NavLinkProps) {
className={({ isActive }) => classNames( className={({ isActive }) => classNames(
"border-transparent text-gray-900 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-300 group border-l-4 px-3 py-2 flex items-center text-sm font-medium", "border-transparent text-gray-900 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-300 group border-l-4 px-3 py-2 flex items-center text-sm font-medium",
isActive ? isActive ?
"bg-teal-50 dark:bg-gray-700 border-teal-500 dark:border-blue-500 text-teal-700 dark:text-white hover:bg-teal-50 dark:hover:bg-gray-500 hover:text-teal-700 dark:hover:text-gray-200" : "" "font-bold bg-teal-50 dark:bg-gray-700 border-teal-500 dark:border-blue-500 text-teal-700 dark:text-white hover:bg-teal-50 dark:hover:bg-gray-500 hover:text-teal-700 dark:hover:text-gray-200" : ""
)} )}
aria-current={splitLocation[2] === item.href ? "page" : undefined} aria-current={splitLocation[2] === item.href ? "page" : undefined}
> >

View file

@ -609,6 +609,266 @@ export function FilterActions({ filter, values }: FilterActionsProps) {
); );
} }
interface TypeFormProps {
action: Action;
idx: number;
clients: Array<DownloadClient>;
}
const TypeForm = ({ action, idx, clients }: TypeFormProps) => {
switch (action.type) {
case "TEST":
return (
<AlertWarning
text="The test action does nothing except to show if the filter works."
/>
);
case "EXEC":
return (
<div>
<div className="mt-6 grid grid-cols-12 gap-6">
<TextField
name={`actions.${idx}.exec_cmd`}
label="Command"
columns={6}
placeholder="Path to program eg. /bin/test"
/>
<TextField
name={`actions.${idx}.exec_args`}
label="Arguments"
columns={6}
placeholder="Arguments eg. --test"
/>
</div>
</div>
);
case "WATCH_FOLDER":
return (
<div className="mt-6 grid grid-cols-12 gap-6">
<TextField
name={`actions.${idx}.watch_folder`}
label="Watch folder"
columns={6}
placeholder="Watch directory eg. /home/user/rwatch"
/>
</div>
);
case "WEBHOOK":
return (
<div className="mt-6 grid grid-cols-12 gap-6">
<TextField
name={`actions.${idx}.webhook_host`}
label="Host"
columns={6}
placeholder="Host eg. http://localhost/webhook"
/>
<TextField
name={`actions.${idx}.webhook_data`}
label="Data (json)"
columns={6}
placeholder={"Request data: { \"key\": \"value\" }"}
/>
</div>
);
case "QBITTORRENT":
return (
<div className="w-full">
<div className="mt-6 grid grid-cols-12 gap-6">
<DownloadClientSelect
name={`actions.${idx}.client_id`}
action={action}
clients={clients}
/>
<div className="col-span-6 sm:col-span-6">
<TextField
name={`actions.${idx}.save_path`}
label="Save path"
columns={6}
placeholder="eg. /full/path/to/watch_folder"
/>
</div>
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<TextField
name={`actions.${idx}.category`}
label="Category"
columns={6}
placeholder="eg. category"
/>
<TextField
name={`actions.${idx}.tags`}
label="Tags"
columns={6}
placeholder="eg. tag1,tag2"
/>
</div>
<CollapsableSection title="Rules" subtitle="client options">
<div className="col-span-12">
<div className="mt-6 grid grid-cols-12 gap-6">
<NumberField
name={`actions.${idx}.limit_download_speed`}
label="Limit download speed (KB/s)"
/>
<NumberField
name={`actions.${idx}.limit_upload_speed`}
label="Limit upload speed (KB/s)"
/>
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<NumberField
name={`actions.${idx}.limit_ratio`}
label="Ratio limit"
step={0.5}
/>
<NumberField
name={`actions.${idx}.limit_seed_time`}
label="Seed time limit (seconds)"
/>
</div>
</div>
<div className="col-span-6">
<SwitchGroup
name={`actions.${idx}.paused`}
label="Add paused"
description="Add torrent as paused"
/>
<SwitchGroup
name={`actions.${idx}.ignore_rules`}
label="Ignore client rules"
description="Download if max active reached"
/>
</div>
</CollapsableSection>
<CollapsableSection title="Advanced" subtitle="Advanced options">
<div className="col-span-12">
<div className="mt-6 grid grid-cols-12 gap-6">
<NumberField
name={`actions.${idx}.reannounce_interval`}
label="Reannounce interval. Run every X seconds"
/>
<NumberField
name={`actions.${idx}.reannounce_max_attempts`}
label="Run reannounce Y times"
/>
</div>
</div>
<div className="col-span-6">
<SwitchGroup
name={`actions.${idx}.reannounce_skip`}
label="Skip reannounce"
description="If reannounce is not needed, skip"
/>
<SwitchGroup
name={`actions.${idx}.reannounce_delete`}
label="Delete stalled"
description="Delete stalled torrents after X attempts"
/>
</div>
</CollapsableSection>
</div>
);
case "DELUGE_V1":
case "DELUGE_V2":
return (
<div>
<div className="mt-6 grid grid-cols-12 gap-6">
<DownloadClientSelect
name={`actions.${idx}.client_id`}
action={action}
clients={clients}
/>
<div className="col-span-12 sm:col-span-6">
<TextField
name={`actions.${idx}.save_path`}
label="Save path"
columns={6}
/>
</div>
</div>
<div className="mt-6 col-span-12 sm:col-span-6">
<TextField
name={`actions.${idx}.label`}
label="Label"
columns={6}
/>
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<NumberField
name={`actions.${idx}.limit_download_speed`}
label="Limit download speed (KB/s)"
/>
<NumberField
name={`actions.${idx}.limit_upload_speed`}
label="Limit upload speed (KB/s)"
/>
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<div className="col-span-6">
<SwitchGroup
name={`actions.${idx}.paused`}
label="Add paused"
/>
</div>
</div>
</div>
);
case "TRANSMISSION":
return (
<div>
<div className="mt-6 grid grid-cols-12 gap-6">
<DownloadClientSelect
name={`actions.${idx}.client_id`}
action={action}
clients={clients}
/>
<div className="col-span-12 sm:col-span-6">
<TextField
name={`actions.${idx}.save_path`}
label="Save path"
columns={6}
/>
</div>
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<div className="col-span-6">
<SwitchGroup
name={`actions.${idx}.paused`}
label="Add paused"
/>
</div>
</div>
</div>
);
case "RADARR":
case "SONARR":
case "LIDARR":
case "WHISPARR":
return (
<div className="mt-6 grid grid-cols-12 gap-6">
<DownloadClientSelect
name={`actions.${idx}.client_id`}
action={action}
clients={clients}
/>
</div>
);
default:
return null;
}
};
interface FilterActionsItemProps { interface FilterActionsItemProps {
action: Action; action: Action;
clients: DownloadClient[]; clients: DownloadClient[];
@ -617,255 +877,10 @@ interface FilterActionsItemProps {
} }
function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemProps) { function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemProps) {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const [edit, toggleEdit] = useToggle(false);
const cancelButtonRef = useRef(null); const cancelButtonRef = useRef(null);
const TypeForm = (actionType: ActionType) => { const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
switch (actionType) { const [edit, toggleEdit] = useToggle(false);
case "TEST":
return (
<AlertWarning
title="Notice"
text="The test action does nothing except to show if the filter works."
/>
);
case "EXEC":
return (
<div>
<div className="mt-6 grid grid-cols-12 gap-6">
<TextField
name={`actions.${idx}.exec_cmd`}
label="Command"
columns={6}
placeholder="Path to program eg. /bin/test"
/>
<TextField
name={`actions.${idx}.exec_args`}
label="Arguments"
columns={6}
placeholder="Arguments eg. --test"
/>
</div>
</div>
);
case "WATCH_FOLDER":
return (
<div className="mt-6 grid grid-cols-12 gap-6">
<TextField
name={`actions.${idx}.watch_folder`}
label="Watch folder"
columns={6}
placeholder="Watch directory eg. /home/user/rwatch"
/>
</div>
);
case "WEBHOOK":
return (
<div className="mt-6 grid grid-cols-12 gap-6">
<TextField
name={`actions.${idx}.webhook_host`}
label="Host"
columns={6}
placeholder="Host eg. http://localhost/webhook"
/>
<TextField
name={`actions.${idx}.webhook_data`}
label="Data (json)"
columns={6}
placeholder={"Request data: { \"key\": \"value\" }"}
/>
</div>
);
case "QBITTORRENT":
return (
<div className="w-full">
<div className="mt-6 grid grid-cols-12 gap-6">
<DownloadClientSelect
name={`actions.${idx}.client_id`}
action={action}
clients={clients}
/>
<div className="col-span-6 sm:col-span-6">
<TextField
name={`actions.${idx}.save_path`}
label="Save path"
columns={6}
placeholder="eg. /full/path/to/watch_folder"
/>
</div>
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<TextField name={`actions.${idx}.category`} label="Category" columns={6} placeholder="eg. category" />
<TextField name={`actions.${idx}.tags`} label="Tags" columns={6} placeholder="eg. tag1,tag2" />
</div>
<CollapsableSection title="Rules" subtitle="client options">
<div className="col-span-12">
<div className="mt-6 grid grid-cols-12 gap-6">
<NumberField
name={`actions.${idx}.limit_download_speed`}
label="Limit download speed (KB/s)"
/>
<NumberField
name={`actions.${idx}.limit_upload_speed`}
label="Limit upload speed (KB/s)"
/>
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<NumberField
name={`actions.${idx}.limit_ratio`}
label="Ratio limit"
step={0.5}
/>
<NumberField
name={`actions.${idx}.limit_seed_time`}
label="Seed time limit (seconds)"
/>
</div>
</div>
<div className="col-span-6">
<SwitchGroup
name={`actions.${idx}.paused`}
label="Add paused"
description="Add torrent as paused"
/>
<SwitchGroup
name={`actions.${idx}.ignore_rules`}
label="Ignore client rules"
description="Download if max active reached"
/>
</div>
</CollapsableSection>
<CollapsableSection title="Advanced" subtitle="Advanced options">
<div className="col-span-12">
<div className="mt-6 grid grid-cols-12 gap-6">
<NumberField
name={`actions.${idx}.reannounce_interval`}
label="Reannounce interval. Run every X seconds"
/>
<NumberField
name={`actions.${idx}.reannounce_max_attempts`}
label="Run reannounce Y times"
/>
</div>
</div>
<div className="col-span-6">
<SwitchGroup
name={`actions.${idx}.reannounce_skip`}
label="Skip reannounce"
description="If reannounce is not needed, skip"
/>
<SwitchGroup
name={`actions.${idx}.reannounce_delete`}
label="Delete stalled"
description="Delete stalled torrents after X attempts"
/>
</div>
</CollapsableSection>
</div>
);
case "DELUGE_V1":
case "DELUGE_V2":
return (
<div>
<div className="mt-6 grid grid-cols-12 gap-6">
<DownloadClientSelect
name={`actions.${idx}.client_id`}
action={action}
clients={clients}
/>
<div className="col-span-12 sm:col-span-6">
<TextField
name={`actions.${idx}.save_path`}
label="Save path"
columns={6}
/>
</div>
</div>
<div className="mt-6 col-span-12 sm:col-span-6">
<TextField
name={`actions.${idx}.label`}
label="Label"
columns={6}
/>
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<NumberField
name={`actions.${idx}.limit_download_speed`}
label="Limit download speed (KB/s)"
/>
<NumberField
name={`actions.${idx}.limit_upload_speed`}
label="Limit upload speed (KB/s)"
/>
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<div className="col-span-6">
<SwitchGroup
name={`actions.${idx}.paused`}
label="Add paused"
/>
</div>
</div>
</div>
);
case "TRANSMISSION":
return (
<div>
<div className="mt-6 grid grid-cols-12 gap-6">
<DownloadClientSelect
name={`actions.${idx}.client_id`}
action={action}
clients={clients}
/>
<div className="col-span-12 sm:col-span-6">
<TextField
name={`actions.${idx}.save_path`}
label="Save path"
columns={6}
/>
</div>
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<div className="col-span-6">
<SwitchGroup
name={`actions.${idx}.paused`}
label="Add paused"
/>
</div>
</div>
</div>
);
case "RADARR":
case "SONARR":
case "LIDARR":
case "WHISPARR":
return (
<div className="mt-6 grid grid-cols-12 gap-6">
<DownloadClientSelect
name={`actions.${idx}.client_id`}
action={action}
clients={clients}
/>
</div>
);
default:
return null;
}
};
return ( return (
<li> <li>
@ -966,7 +981,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
<TextField name={`actions.${idx}.name`} label="Name" columns={6} /> <TextField name={`actions.${idx}.name`} label="Name" columns={6} />
</div> </div>
{TypeForm(action.type)} <TypeForm action={action} clients={clients} idx={idx} />
<div className="pt-6 divide-y divide-gray-200"> <div className="pt-6 divide-y divide-gray-200">
<div className="mt-4 pt-4 flex justify-between"> <div className="mt-4 pt-4 flex justify-between">

View file

@ -72,7 +72,7 @@ interface FilterListProps {
function FilterList({ filters }: FilterListProps) { function FilterList({ filters }: FilterListProps) {
return ( return (
<div className="overflow-x-auto align-middle min-w-full rounded-lg shadow-lg"> <div className="overflow-x-auto align-middle min-w-full rounded-t-md rounded-b-lg shadow-lg">
<table className="min-w-full"> <table className="min-w-full">
<thead className="bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700"> <thead className="bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
<tr> <tr>
@ -80,7 +80,7 @@ function FilterList({ filters }: FilterListProps) {
<th <th
key={`th-${label}`} key={`th-${label}`}
scope="col" scope="col"
className="px-6 py-2.5 text-left text-xs font-medium uppercase tracking-wider" className="px-6 pt-4 pb-3 text-left text-xs font-medium uppercase tracking-wider"
> >
{label} {label}
</th> </th>

View file

@ -1,26 +1,20 @@
function ActionSettings() { function ActionSettings() {
return ( return (
<div className="divide-y divide-gray-200 lg:col-span-9"> <div className="lg:col-span-9">
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
{/*{addClientIsOpen &&*/}
{/*<AddNewClientForm isOpen={addClientIsOpen} toggle={toggleAddClient}/>*/}
{/*}*/}
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"> <div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3> <h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3>
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500">
Manage actions. Manage actions.
</p> </p>
</div> </div>
<div className="ml-4 mt-4 flex-shrink-0"> <div className="ml-4 mt-4 flex-shrink-0">
<button <button
type="button" type="button"
className="relative inline-flex items-center px-4 py-2 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="relative inline-flex items-center px-4 py-2 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"
// onClick={toggleAddClient}
> >
Add new Add new
</button> </button>
</div> </div>
</div> </div>
@ -36,25 +30,25 @@ function ActionSettings() {
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
> >
Name Name
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
> >
Type Type
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
> >
Port Port
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
> >
Enabled Enabled
</th> </th>
<th scope="col" className="relative px-6 py-3"> <th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span> <span className="sr-only">Edit</span>

View file

@ -23,7 +23,7 @@ function ApplicationSettings() {
<div> <div>
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2> <h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Application settings. Change in config.toml and restart to take effect. Application settings. Change in config.toml and restart to take effect.
</p> </p>
</div> </div>
@ -31,7 +31,7 @@ function ApplicationSettings() {
<div className="mt-6 grid grid-cols-12 gap-6"> <div className="mt-6 grid grid-cols-12 gap-6">
<div className="col-span-6 sm:col-span-4"> <div className="col-span-6 sm:col-span-4">
<label htmlFor="host" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide"> <label htmlFor="host" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
Host Host
</label> </label>
<input <input
type="text" type="text"
@ -39,13 +39,13 @@ function ApplicationSettings() {
id="host" id="host"
value={data.host} value={data.host}
disabled={true} disabled={true}
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm" className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
/> />
</div> </div>
<div className="col-span-6 sm:col-span-4"> <div className="col-span-6 sm:col-span-4">
<label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide"> <label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
Port Port
</label> </label>
<input <input
type="text" type="text"
@ -53,13 +53,13 @@ function ApplicationSettings() {
id="port" id="port"
value={data.port} value={data.port}
disabled={true} disabled={true}
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm" className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
/> />
</div> </div>
<div className="col-span-6 sm:col-span-4"> <div className="col-span-6 sm:col-span-4">
<label htmlFor="base_url" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide"> <label htmlFor="base_url" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
Base url Base url
</label> </label>
<input <input
type="text" type="text"
@ -67,7 +67,7 @@ function ApplicationSettings() {
id="base_url" id="base_url"
value={data.base_url} value={data.base_url}
disabled={true} disabled={true}
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm" className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
/> />
</div> </div>
</div> </div>
@ -80,19 +80,21 @@ function ApplicationSettings() {
{data?.version ? ( {data?.version ? (
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6"> <div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
<dt className="font-medium text-gray-500 dark:text-white">Version:</dt> <dt className="font-medium text-gray-500 dark:text-white">Version:</dt>
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.version}</dd> <dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2 break-all">
{data?.version}
</dd>
</div> </div>
) : null} ) : null}
{data?.commit ? ( {data?.commit ? (
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6"> <div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
<dt className="font-medium text-gray-500 dark:text-white">Commit:</dt> <dt className="font-medium text-gray-500 dark:text-white">Commit:</dt>
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data.commit}</dd> <dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2 break-all">{data.commit}</dd>
</div> </div>
) : null} ) : null}
{data?.date ? ( {data?.date ? (
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6"> <div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
<dt className="font-medium text-gray-500 dark:text-white">Date:</dt> <dt className="font-medium text-gray-500 dark:text-white">Date:</dt>
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.date}</dd> <dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2 break-all">{data?.date}</dd>
</div> </div>
) : null} ) : null}
</dl> </dl>
@ -123,7 +125,6 @@ function ApplicationSettings() {
</ul> </ul>
</div> </div>
</form> </form>
); );
} }

View file

@ -86,7 +86,7 @@ function DownloadClientSettings() {
return (<p>An error has occurred: </p>); return (<p>An error has occurred: </p>);
return ( return (
<div className="divide-y divide-gray-200 lg:col-span-9"> <div className="lg:col-span-9">
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} /> <DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} />
@ -121,25 +121,25 @@ function DownloadClientSettings() {
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Enabled Enabled
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Name Name
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Host Host
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Type Type
</th> </th>
<th scope="col" className="relative px-6 py-3"> <th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span> <span className="sr-only">Edit</span>

View file

@ -27,13 +27,13 @@ function FeedSettings() {
); );
return ( return (
<div className="divide-y divide-gray-200 lg:col-span-9"> <div className="lg:col-span-9">
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"> <div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Feeds</h3> <h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Feeds</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage Torznab feeds. Manage Torznab feeds.
</p> </p>
</div> </div>
</div> </div>

View file

@ -82,7 +82,7 @@ function IndexerSettings() {
return (<p>An error has occurred</p>); return (<p>An error has occurred</p>);
return ( return (
<div className="divide-y divide-gray-200 lg:col-span-9"> <div className="lg:col-span-9">
<IndexerAddForm isOpen={addIndexerIsOpen} toggle={toggleAddIndexer} /> <IndexerAddForm isOpen={addIndexerIsOpen} toggle={toggleAddIndexer} />

View file

@ -1,41 +1,35 @@
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { import { simplifyDate, IsEmptyDate, classNames } from "../../utils";
simplifyDate, import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
IsEmptyDate
} from "../../utils";
import {
IrcNetworkAddForm,
IrcNetworkUpdateForm
} from "../../forms";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "../../hooks/hooks";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { EmptySimple } from "../../components/emptystates"; import { EmptySimple } from "../../components/emptystates";
import {ExclamationCircleIcon} from "@heroicons/react/outline"; import { ExclamationCircleIcon } from "@heroicons/react/outline";
import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/solid";
export const IrcSettings = () => { export const IrcSettings = () => {
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false); const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false);
const { data } = useQuery( const { data } = useQuery("networks", () => APIClient.irc.getNetworks(), {
"networks", refetchOnWindowFocus: false,
() => APIClient.irc.getNetworks(), // Refetch every 3 seconds
{ refetchInterval: 3000
refetchOnWindowFocus: false, });
// Refetch every 3 seconds
refetchInterval: 3000
}
);
return ( return (
<div className="divide-y divide-gray-200 lg:col-span-9"> <div className="lg:col-span-9">
<IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork} /> <IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork} />
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"> <div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">IRC</h3> <h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
IRC
</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
IRC networks and channels. Click on a network to view channel status. IRC networks and channels. Click on a network to view channel
status.
</p> </p>
</div> </div>
<div className="ml-4 mt-4 flex-shrink-0"> <div className="ml-4 mt-4 flex-shrink-0">
@ -44,35 +38,46 @@ export const IrcSettings = () => {
onClick={toggleAddNetwork} onClick={toggleAddNetwork}
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
> >
Add new Add new
</button> </button>
</div> </div>
</div> </div>
{data && data.length > 0 ? ( {data && data.length > 0 ? (
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md"> <section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
<ol className="min-w-full"> <ol className="min-w-full">
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700"> <li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
{/* <div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div> */} <div className="col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<div className="col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Network</div> Network
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Server</div> </div>
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Nick</div> <div className="col-span-5 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Server
</div>
<div className="col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Nick
</div>
</li> </li>
{data &&
{data && data.map((network, idx) => ( data.map((network, idx) => (
<ListItem key={idx} idx={idx} network={network} /> <ListItem key={idx} idx={idx} network={network} />
))} ))}
</ol> </ol>
</section> </section>
) : <EmptySimple title="No networks" subtitle="Add a new network" buttonText="New network" buttonAction={toggleAddNetwork} />} ) : (
<EmptySimple
title="No networks"
subtitle="Add a new network"
buttonText="New network"
buttonAction={toggleAddNetwork}
/>
)}
</div> </div>
</div> </div>
); );
}; };
interface ListItemProps { interface ListItemProps {
idx: number; idx: number;
network: IrcNetworkWithHealth; network: IrcNetworkWithHealth;
} }
const ListItem = ({ idx, network }: ListItemProps) => { const ListItem = ({ idx, network }: ListItemProps) => {
@ -80,33 +85,91 @@ const ListItem = ({ idx, network }: ListItemProps) => {
const [edit, toggleEdit] = useToggle(false); const [edit, toggleEdit] = useToggle(false);
return ( return (
<li key={idx}>
<div className="grid grid-cols-12 gap-2 lg:gap-4 items-center hover:bg-gray-50 dark:hover:bg-gray-700 py-4">
<IrcNetworkUpdateForm
isOpen={updateIsOpen}
toggle={toggleUpdate}
network={network}
/>
<li key={idx} > <div
<div className="grid grid-cols-12 gap-4 items-center hover:bg-gray-50 dark:hover:bg-gray-700 py-4"> className="col-span-3 items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white cursor-pointer"
<IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} /> onClick={toggleEdit}
>
<div className="col-span-3 items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white cursor-pointer" onClick={toggleEdit}> <div className="flex">
<span className="relative inline-flex items-center"> <span className="relative inline-flex items-center ml-1">
{ {network.enabled ? (
network.enabled ? ( IsNetworkHealthy(network) ? (
networkHealthy(network) ? ( <span
<span className="mr-3 flex h-3 w-3 relative" title={`Connected since: ${simplifyDate(network.connected_since)}`}> className="mr-3 flex h-3 w-3 relative"
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/> title={`Connected since: ${simplifyDate(network.connected_since)}`}
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/> >
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500" />
</span> </span>
) : <span className="mr-3 flex items-center" title={network.connection_errors.toString()}><ExclamationCircleIcon className="h-4 w-4 text-red-400 hover:text-red-600" /></span> ) : (
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" /> <span
} className="mr-3 flex items-center"
{network.name} title={network.connection_errors.toString()}
</span> >
<ExclamationCircleIcon className="h-4 w-4 text-red-400 hover:text-red-600" />
</span>
)
) : (
<span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
)}
</span>
<div className="overflow-x-auto flex">
{network.name}
</div>
</div>
</div>
<div
className="col-span-5 sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer"
onClick={toggleEdit}
>
<div
className="overflow-x-auto flex items-center"
title={network.tls ? "Secured using TLS" : "Insecure, not using TLS"}
>
<div className="min-h-2 min-w-2">
{network.tls ? (
<LockClosedIcon
className={classNames(
"mr-2 h-4 w-4",
network.enabled ? "text-green-600" : "text-gray-500"
)}
/>
) : (
<LockOpenIcon className={classNames(
"mr-2 h-4 w-4",
network.enabled ? "text-red-500" : "text-yellow-500"
)} />
)}
</div>
<p className="break-all">
{network.server}:{network.port}
</p>
</div>
</div> </div>
<div className="col-span-4 flex justify-between items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.server}:{network.port} {network.tls && <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-300 text-green-800 dark:text-green-900">TLS</span>}</div>
{network.nickserv && network.nickserv.account ? ( {network.nickserv && network.nickserv.account ? (
<div className="col-span-4 items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.nickserv.account}</div> <div
) : <div className="col-span-4" />} className="col-span-3 items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer"
onClick={toggleEdit}
>
<div className="overflow-x-auto flex">
{network.nickserv.account}
</div>
</div>
) : (
<div className="col-span-3" />
)}
<div className="col-span-1 text-sm text-gray-500 dark:text-gray-400"> <div className="col-span-1 text-sm text-gray-500 dark:text-gray-400">
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}> <span
className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer"
onClick={toggleUpdate}
>
Edit Edit
</span> </span>
</div> </div>
@ -117,39 +180,58 @@ const ListItem = ({ idx, network }: ListItemProps) => {
{network.channels.length > 0 ? ( {network.channels.length > 0 ? (
<ol> <ol>
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700"> <li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Channel</div> <div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Monitoring since</div> Channel
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Last announce</div> </div>
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Monitoring since
</div>
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Last announce
</div>
</li> </li>
{network.channels.map(c => ( {network.channels.map((c) => (
<li key={c.id} className="text-gray-500 dark:text-gray-400"> <li key={c.id} className="text-gray-500 dark:text-gray-400">
<div className="grid grid-cols-12 gap-4 items-center py-4"> <div className="grid grid-cols-12 gap-4 items-center py-4">
<div className="col-span-4 flex items-center sm:px-6 "> <div className="col-span-4 flex items-center sm:px-6 ">
<span className="relative inline-flex items-center"> <span className="relative inline-flex items-center">
{ {network.enabled ? (
network.enabled ? ( c.monitoring ? (
c.monitoring ? ( <span
<span className="mr-3 flex h-3 w-3 relative" title="monitoring"> className="mr-3 flex h-3 w-3 relative"
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/> title="monitoring"
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/> >
</span> <span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" /> <span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500" />
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" /> </span>
} ) : (
<span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
)
) : (
<span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
)}
{c.name} {c.name}
</span> </span>
</div> </div>
<div className="col-span-4 flex items-center sm:px-6 "> <div className="col-span-4 flex items-center sm:px-6 ">
<span className="" title={simplifyDate(c.monitoring_since)}>{IsEmptyDate(c.monitoring_since)}</span> <span title={simplifyDate(c.monitoring_since)}>
{IsEmptyDate(c.monitoring_since)}
</span>
</div> </div>
<div className="col-span-4 flex items-center sm:px-6 "> <div className="col-span-4 flex items-center sm:px-6 ">
<span className="" title={simplifyDate(c.last_announce)}>{IsEmptyDate(c.last_announce)}</span> <span title={simplifyDate(c.last_announce)}>
{IsEmptyDate(c.last_announce)}
</span>
</div> </div>
</div> </div>
</li> </li>
))} ))}
</ol> </ol>
) : <div className="flex text-center justify-center py-4 dark:text-gray-500"><p>No channels!</p></div>} ) : (
<div className="flex text-center justify-center py-4 dark:text-gray-500">
<p>No channels!</p>
</div>
)}
</div> </div>
</div> </div>
)} )}
@ -157,10 +239,5 @@ const ListItem = ({ idx, network }: ListItemProps) => {
); );
}; };
function networkHealthy(network: IrcNetworkWithHealth): boolean { const IsNetworkHealthy = (network: IrcNetworkWithHealth) =>
if (network.connection_errors.length > 0) { network.connection_errors.length <= 0;
return false
}
return true
}

View file

@ -2,7 +2,7 @@ import { useQuery } from "react-query";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { EmptySimple } from "../../components/emptystates"; import { EmptySimple } from "../../components/emptystates";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "../../hooks/hooks";
import { NotificationAddForm, NotificationUpdateForm } from "../../forms/settings/NotifiactionForms"; import { NotificationAddForm, NotificationUpdateForm } from "../../forms/settings/NotificationForms";
import { Switch } from "@headlessui/react"; import { Switch } from "@headlessui/react";
import { classNames } from "../../utils"; import { classNames } from "../../utils";
import { componentMapType } from "../../forms/settings/DownloadClientForms"; import { componentMapType } from "../../forms/settings/DownloadClientForms";

View file

@ -28,7 +28,11 @@ function ReleaseSettings() {
const cancelModalButtonRef = useRef(null); const cancelModalButtonRef = useRef(null);
return ( return (
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST"> <form
className="lg:col-span-9"
action="#"
method="POST"
>
<DeleteModal <DeleteModal
isOpen={deleteModalIsOpen} isOpen={deleteModalIsOpen}
toggle={toggleDeleteModal} toggle={toggleDeleteModal}
@ -40,9 +44,11 @@ function ReleaseSettings() {
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div> <div>
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Releases</h2> <h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
Releases
</h2>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Release settings. Reset state. Release settings. Reset state.
</p> </p>
</div> </div>
</div> </div>
@ -50,25 +56,21 @@ function ReleaseSettings() {
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700"> <div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
<div className="px-4 py-5 sm:p-0"> <div className="px-4 py-5 sm:p-0">
<div className="px-4 py-5 sm:p-6"> <div className="px-4 py-5 sm:p-6">
<div> <div>
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Danger Zone</h3> <h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
Danger Zone
</h3>
</div> </div>
<ul className="p-4 mt-6 divide-y divide-gray-200 dark:divide-gray-700 border-red-500 border rounded-lg"> <div className="flex justify-between items-center p-4 mt-6 max-w-sm m-auto">
<div className="flex justify-between items-center py-2"> <button
<p className="text-sm text-gray-500 dark:text-gray-400"> type="button"
Delete all releases onClick={toggleDeleteModal}
</p> className="w-full inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 hover:text-red-900 dark:text-white bg-red-100 dark:bg-red-800 hover:bg-red-200 dark:hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
<button >
type="button" Delete all releases
onClick={toggleDeleteModal} </button>
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-100 bg-red-100 dark:bg-red-500 hover:bg-red-200 dark:hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm" </div>
>
Delete all releases
</button>
</div>
</ul>
</div> </div>
</div> </div>
</div> </div>