mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat: dark mode (#32)
This commit is contained in:
parent
974ca95d80
commit
66048c5533
49 changed files with 1736 additions and 1992 deletions
|
@ -26,7 +26,7 @@
|
||||||
</script>
|
</script>
|
||||||
<!-- {{end}} -->
|
<!-- {{end}} -->
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100">
|
<body class="bg-gray-100 dark:bg-gray-900">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import React from "react";
|
import { QueryClient, QueryClientProvider } from "react-query";
|
||||||
import {QueryClient, QueryClientProvider} from "react-query";
|
import { BrowserRouter as Router, Route } from "react-router-dom";
|
||||||
import {BrowserRouter as Router, Route, Switch} from "react-router-dom";
|
|
||||||
import Login from "./screens/auth/login";
|
import Login from "./screens/auth/login";
|
||||||
import Logout from "./screens/auth/logout";
|
import Logout from "./screens/auth/logout";
|
||||||
import Base from "./screens/Base";
|
import Base from "./screens/Base";
|
||||||
import {ReactQueryDevtools} from "react-query/devtools";
|
import { ReactQueryDevtools } from "react-query/devtools";
|
||||||
import Layout from "./components/Layout";
|
import Layout from "./components/Layout";
|
||||||
import {baseUrl} from "./utils/utils";
|
import { baseUrl } from "./utils/utils";
|
||||||
|
|
||||||
function Protected() {
|
function Protected() {
|
||||||
return (
|
return (
|
||||||
|
@ -22,11 +21,11 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Router basename={baseUrl()}>
|
<Router basename={baseUrl()}>
|
||||||
<Route exact={true} path="/login" component={Login}/>
|
<Route exact={true} path="/login" component={Login} />
|
||||||
<Route exact={true} path="/logout" component={Logout}/>
|
<Route exact={true} path="/logout" component={Logout} />
|
||||||
<Route exact={true} path="/*" component={Protected}/>
|
<Route exact={true} path="/*" component={Protected} />
|
||||||
</Router>
|
</Router>
|
||||||
<ReactQueryDevtools initialIsOpen={false}/>
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface props {
|
interface props {
|
||||||
text: string;
|
text: string;
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
|
@ -9,11 +7,11 @@ interface props {
|
||||||
export function EmptyListState({ text, buttonText, buttonOnClick }: props) {
|
export function EmptyListState({ text, buttonText, buttonOnClick }: props) {
|
||||||
return (
|
return (
|
||||||
<div className="px-4 py-12 flex flex-col items-center">
|
<div className="px-4 py-12 flex flex-col items-center">
|
||||||
<p className="text-center text-gray-500">{text}</p>
|
<p className="text-center text-gray-500 dark:text-white">{text}</p>
|
||||||
{buttonText && buttonOnClick && (
|
{buttonText && buttonOnClick && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="relative inline-flex items-center px-4 py-2 mt-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="relative inline-flex items-center px-4 py-2 mt-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
onClick={buttonOnClick}
|
onClick={buttonOnClick}
|
||||||
>
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
|
|
|
@ -199,7 +199,7 @@ function ListItem({ action, clients, filterID, idx }: ListItemProps) {
|
||||||
enabledMutation.mutate(action.id);
|
enabledMutation.mutate(action.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {}, [action]);
|
useEffect(() => { }, [action]);
|
||||||
|
|
||||||
const cancelButtonRef = useRef(null);
|
const cancelButtonRef = useRef(null);
|
||||||
|
|
||||||
|
@ -358,7 +358,7 @@ function ListItem({ action, clients, filterID, idx }: ListItemProps) {
|
||||||
onChange={toggleActive}
|
onChange={toggleActive}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
action.enabled ? "bg-teal-500" : "bg-gray-200",
|
action.enabled ? "bg-teal-500" : "bg-gray-200",
|
||||||
"z-10 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500"
|
"z-10 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Use setting</span>
|
<span className="sr-only">Use setting</span>
|
||||||
|
@ -469,7 +469,7 @@ function ListItem({ action, clients, filterID, idx }: ListItemProps) {
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500"
|
className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const DEBUG = ({ values }: any) => {
|
const DEBUG = ({ values }: any) => {
|
||||||
if (process.env.NODE_ENV !== "development") {
|
if (process.env.NODE_ENV !== "development") {
|
||||||
return null;
|
return null;
|
||||||
|
@ -7,7 +5,7 @@ const DEBUG = ({ values }: any) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-1/2 mx-auto mt-2 flex flex-col mt-12 mb-12">
|
<div className="w-1/2 mx-auto mt-2 flex flex-col mt-12 mb-12">
|
||||||
<pre className="mt-2">{JSON.stringify(values, 0 as any, 2)}</pre>
|
<pre className="mt-2 dark:text-gray-500">{JSON.stringify(values, 0 as any, 2)}</pre>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {PlusIcon} from "@heroicons/react/solid";
|
import { PlusIcon } from "@heroicons/react/solid";
|
||||||
|
|
||||||
interface props {
|
interface props {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -7,15 +7,15 @@ interface props {
|
||||||
buttonAction: any;
|
buttonAction: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmptySimple = ({ title, subtitle, buttonText, buttonAction}: props) => (
|
const EmptySimple = ({ title, subtitle, buttonText, buttonAction }: props) => (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900">{title}</h3>
|
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">{title}</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500">{subtitle}</p>
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-200">{subtitle}</p>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={buttonAction}
|
onClick={buttonAction}
|
||||||
className="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="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 dark:focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
|
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
|
||||||
{buttonText}
|
{buttonText}
|
||||||
|
|
|
@ -7,8 +7,8 @@ interface Props {
|
||||||
|
|
||||||
const TitleSubtitle: React.FC<Props> = ({ title, subtitle }) => (
|
const TitleSubtitle: React.FC<Props> = ({ title, subtitle }) => (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg leading-6 font-medium text-gray-900">{title}</h2>
|
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{title}</h2>
|
||||||
<p className="mt-1 text-sm text-gray-500">{subtitle}</p>
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Field } from "react-final-form";
|
import { Field } from "react-final-form";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Error from "./Error";
|
import Error from "./Error";
|
||||||
import {classNames} from "../../styles/utils";
|
import { classNames } from "../../styles/utils";
|
||||||
|
|
||||||
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||||
|
|
||||||
|
@ -14,30 +14,30 @@ interface Props {
|
||||||
autoComplete?: string;
|
autoComplete?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PasswordField: React.FC<Props> = ({ name, label, placeholder, columns , className, autoComplete}) => (
|
const PasswordField: React.FC<Props> = ({ name, label, placeholder, columns, className, autoComplete }) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
columns ? `col-span-${columns}` : "col-span-12"
|
columns ? `col-span-${columns}` : "col-span-12"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label && (
|
{label && (
|
||||||
<label htmlFor={name} className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
|
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-white uppercase tracking-wide">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<Field
|
<Field
|
||||||
name={name}
|
name={name}
|
||||||
render={({input, meta}) => (
|
render={({ input }) => (
|
||||||
<input
|
<input
|
||||||
{...input}
|
{...input}
|
||||||
id={name}
|
id={name}
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
className="mt-2 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm"
|
className="mt-2 block w-full border border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-white rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Error name={name} classNames="text-red mt-2" />
|
<Error name={name} classNames="text-red mt-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Switch} from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
import {Field} from "react-final-form";
|
import { Field } from "react-final-form";
|
||||||
import {classNames} from "../../styles/utils";
|
import { classNames } from "../../styles/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -11,32 +11,32 @@ interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SwitchGroup: React.FC<Props> = ({name, label, description, defaultValue}) => (
|
const SwitchGroup: React.FC<Props> = ({ name, label, description, defaultValue }) => (
|
||||||
<ul className="mt-2 divide-y divide-gray-200">
|
<ul className="mt-2 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"
|
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white"
|
||||||
passive>
|
passive>
|
||||||
{label}
|
{label}
|
||||||
</Switch.Label>
|
</Switch.Label>
|
||||||
{description && (
|
{description && (
|
||||||
<Switch.Description className="text-sm text-gray-500">
|
<Switch.Description className="text-sm text-gray-500 dark:text-gray-700">
|
||||||
{description}
|
{description}
|
||||||
</Switch.Description>
|
</Switch.Description>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
name={name}
|
name={name}
|
||||||
defaultValue={defaultValue as any}
|
defaultValue={defaultValue as any}
|
||||||
render={({input: {onChange, checked, value}}) => (
|
render={({ input: { onChange, checked, value } }) => (
|
||||||
<Switch
|
<Switch
|
||||||
value={value}
|
value={value}
|
||||||
checked={value}
|
checked={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
value ? 'bg-teal-500' : 'bg-gray-200',
|
value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-500',
|
||||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Use setting</span>
|
<span className="sr-only">Use setting</span>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Field } from "react-final-form";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Field } from "react-final-form";
|
||||||
import Error from "./Error";
|
import Error from "./Error";
|
||||||
import {classNames} from "../../styles/utils";
|
import { classNames } from "../../styles/utils";
|
||||||
|
|
||||||
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||||
|
|
||||||
|
@ -14,31 +14,31 @@ interface Props {
|
||||||
autoComplete?: string;
|
autoComplete?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextField: React.FC<Props> = ({ name, label, placeholder, columns , className, autoComplete}) => (
|
const TextField: React.FC<Props> = ({ name, label, placeholder, columns, className, autoComplete }) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
columns ? `col-span-${columns}` : "col-span-12"
|
columns ? `col-span-${columns}` : "col-span-12"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label && (
|
{label && (
|
||||||
<label htmlFor={name} className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
|
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-white uppercase tracking-wide">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<Field
|
<Field
|
||||||
name={name}
|
name={name}
|
||||||
render={({input, meta}) => (
|
render={({ input }) => (
|
||||||
<input
|
<input
|
||||||
{...input}
|
{...input}
|
||||||
id={name}
|
id={name}
|
||||||
type="text"
|
type="text"
|
||||||
value={input.value}
|
value={input.value}
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
className="mt-2 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm"
|
className="mt-2 block w-full border border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-white rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Error name={name} classNames="text-red mt-2" />
|
<Error name={name} classNames="text-red mt-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Field } from "react-final-form";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Field } from "react-final-form";
|
||||||
import Error from "./Error";
|
import Error from "./Error";
|
||||||
import { classNames } from "../../styles/utils";
|
import { classNames } from "../../styles/utils";
|
||||||
|
|
||||||
|
@ -14,12 +14,12 @@ interface Props {
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextFieldWide: React.FC<Props> = ({ name, label, help, placeholder, defaultValue, required, hidden, className}) => (
|
const TextFieldWide: React.FC<Props> = ({ name, label, help, placeholder, defaultValue, required, hidden, className }) => (
|
||||||
<div hidden={hidden}
|
<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">
|
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>
|
<div>
|
||||||
|
|
||||||
<label htmlFor={name} className="block text-sm font-medium text-gray-900 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>}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,7 +32,7 @@ const TextFieldWide: React.FC<Props> = ({ name, label, help, placeholder, defaul
|
||||||
{...input}
|
{...input}
|
||||||
id={name}
|
id={name}
|
||||||
type="text"
|
type="text"
|
||||||
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300", "block w-full shadow-sm sm:text-sm rounded-md")}
|
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md")}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
hidden={hidden}
|
hidden={hidden}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Field } from "react-final-form";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Field } from "react-final-form";
|
||||||
import Error from "../Error";
|
import Error from "../Error";
|
||||||
import { classNames } from "../../../styles/utils";
|
import { classNames } from "../../../styles/utils";
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ const NumberFieldWide: React.FC<Props> = ({
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor={name}
|
htmlFor={name}
|
||||||
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
|
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
||||||
>
|
>
|
||||||
{label} {required && <span className="text-gray-500">*</span>}
|
{label} {required && <span className="text-gray-500">*</span>}
|
||||||
</label>
|
</label>
|
||||||
|
@ -46,15 +46,15 @@ const NumberFieldWide: React.FC<Props> = ({
|
||||||
className={classNames(
|
className={classNames(
|
||||||
meta.touched && meta.error
|
meta.touched && meta.error
|
||||||
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
||||||
: "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300",
|
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",
|
||||||
"block w-full shadow-sm sm:text-sm rounded-md"
|
"block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md"
|
||||||
)}
|
)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{help && (
|
{help && (
|
||||||
<p className="mt-2 text-sm text-gray-500" id={`${name}-description`}>{help}</p>
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-200" id={`${name}-description`}>{help}</p>
|
||||||
)}
|
)}
|
||||||
<Error name={name} classNames="block text-red-500 mt-2" />
|
<Error name={name} classNames="block text-red-500 mt-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,7 +21,7 @@ function PasswordField({ name, label, placeholder, defaultValue, help, required
|
||||||
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:px-6 sm:py-5">
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<label htmlFor={name} className="block text-sm font-medium text-gray-900 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>}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,7 +35,7 @@ function PasswordField({ name, label, placeholder, defaultValue, help, required
|
||||||
{...input}
|
{...input}
|
||||||
id={name}
|
id={name}
|
||||||
type={isVisible ? "text" : "password"}
|
type={isVisible ? "text" : "password"}
|
||||||
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300", "block w-full shadow-sm sm:text-sm rounded-md")}
|
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "block w-full dark:bg-gray-800 shadow-sm dark:text-gray-100 sm:text-sm rounded-md")}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
||||||
|
|
|
@ -17,7 +17,7 @@ function RadioFieldsetWide({ name, legend, options }: props) {
|
||||||
<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:px-6 sm:py-5">
|
||||||
<div>
|
<div>
|
||||||
<legend className="text-sm font-medium text-gray-900">
|
<legend className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{legend}
|
{legend}
|
||||||
</legend>
|
</legend>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,7 +31,7 @@ function RadioFieldsetWide({ name, legend, options }: props) {
|
||||||
<RadioGroup.Label className="sr-only">
|
<RadioGroup.Label className="sr-only">
|
||||||
Privacy setting
|
Privacy setting
|
||||||
</RadioGroup.Label>
|
</RadioGroup.Label>
|
||||||
<div className="bg-white rounded-md -space-y-px">
|
<div className="bg-white dark:bg-gray-800 rounded-md -space-y-px">
|
||||||
{options.map((setting, settingIdx) => (
|
{options.map((setting, settingIdx) => (
|
||||||
<RadioGroup.Option
|
<RadioGroup.Option
|
||||||
key={setting.value}
|
key={setting.value}
|
||||||
|
@ -45,8 +45,8 @@ function RadioFieldsetWide({ name, legend, options }: props) {
|
||||||
? "rounded-bl-md rounded-br-md"
|
? "rounded-bl-md rounded-br-md"
|
||||||
: "",
|
: "",
|
||||||
checked
|
checked
|
||||||
? "bg-indigo-50 border-indigo-200 z-10"
|
? "bg-indigo-50 dark:bg-gray-700 border-indigo-200 dark:border-blue-600 z-10"
|
||||||
: "border-gray-200",
|
: "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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -56,10 +56,10 @@ function RadioFieldsetWide({ name, legend, options }: props) {
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
checked
|
checked
|
||||||
? "bg-indigo-600 border-transparent"
|
? "bg-indigo-600 dark:bg-blue-600 border-transparent"
|
||||||
: "bg-white border-gray-300",
|
: "bg-white border-gray-300 dark:border-gray-300",
|
||||||
active
|
active
|
||||||
? "ring-2 ring-offset-2 ring-indigo-500"
|
? "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"
|
"h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
|
||||||
)}
|
)}
|
||||||
|
@ -71,7 +71,7 @@ function RadioFieldsetWide({ name, legend, options }: props) {
|
||||||
<RadioGroup.Label
|
<RadioGroup.Label
|
||||||
as="span"
|
as="span"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
checked ? "text-indigo-900" : "text-gray-900",
|
checked ? "text-indigo-900 dark:text-blue-500" : "text-gray-900 dark:text-gray-300",
|
||||||
"block text-sm font-medium"
|
"block text-sm font-medium"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -80,7 +80,7 @@ function RadioFieldsetWide({ name, legend, options }: props) {
|
||||||
<RadioGroup.Description
|
<RadioGroup.Description
|
||||||
as="span"
|
as="span"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
checked ? "text-indigo-700" : "text-gray-500",
|
checked ? "text-indigo-700 dark:text-blue-500" : "text-gray-500",
|
||||||
"block text-sm"
|
"block text-sm"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {Fragment} from "react";
|
import { Fragment } from "react";
|
||||||
import {Dialog, Transition} from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import {ExclamationIcon} from "@heroicons/react/solid";
|
import { ExclamationIcon } from "@heroicons/react/solid";
|
||||||
|
|
||||||
interface props {
|
interface props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
@ -34,10 +34,9 @@ const DeleteModal = ({ isOpen, buttonRef, toggle, deleteAction, title, text }: p
|
||||||
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||||
</Transition.Child>
|
</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 className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||||
​
|
​
|
||||||
</span>
|
</span>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
|
@ -48,24 +47,24 @@ const DeleteModal = ({ isOpen, buttonRef, toggle, deleteAction, title, text }: p
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
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="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="bg-white dark:bg-gray-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="sm:flex sm:items-start">
|
<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">
|
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-400 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true" />
|
<ExclamationIcon className="h-6 w-6 text-red-600 dark:text-red-600" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<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">
|
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||||
{title}
|
{title}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500 dark:text-gray-300">
|
||||||
{text}
|
{text}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||||
<button
|
<button
|
||||||
type="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"
|
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"
|
||||||
|
@ -75,7 +74,7 @@ const DeleteModal = ({ isOpen, buttonRef, toggle, deleteAction, title, text }: p
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="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"
|
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from '@heroicons/react/solid'
|
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from '@heroicons/react/solid'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { classNames } from '../../styles/utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: 'error' | 'success' | 'warning'
|
type: 'error' | 'success' | 'warning'
|
||||||
|
@ -14,9 +15,9 @@ const Toast: FC<Props> = ({
|
||||||
t
|
t
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={`${
|
<div className={classNames(
|
||||||
t.visible ? 'animate-enter' : 'animate-leave'
|
t.visible ? 'animate-enter' : 'animate-leave',
|
||||||
} max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden transition-all`}>
|
"max-w-sm w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden transition-all")}>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
|
@ -25,16 +26,16 @@ const Toast: FC<Props> = ({
|
||||||
{type === 'warning' && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
|
{type === 'warning' && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 w-0 flex-1 pt-0.5">
|
<div className="ml-3 w-0 flex-1 pt-0.5">
|
||||||
<p className="text-sm font-medium text-gray-900">
|
<p className="text-sm font-medium text-gray-900 dark:text-gray-200">
|
||||||
{type === 'success' && "Success"}
|
{type === 'success' && "Success"}
|
||||||
{type === 'error' && "Error"}
|
{type === 'error' && "Error"}
|
||||||
{type === 'warning' && "Warning"}
|
{type === 'warning' && "Warning"}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-sm text-gray-500">{body}</p>
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 flex-shrink-0 flex">
|
<div className="ml-4 flex-shrink-0 flex">
|
||||||
<button
|
<button
|
||||||
className="bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
className="bg-white dark:bg-gray-700 rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
toast.dismiss(t.id)
|
toast.dismiss(t.id)
|
||||||
}}
|
}}
|
||||||
|
|
140
web/src/components/panels/SlideOver.tsx
Normal file
140
web/src/components/panels/SlideOver.tsx
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import { Fragment, useRef } from "react";
|
||||||
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { Form } from "react-final-form";
|
||||||
|
import DEBUG from "../../components/debug";
|
||||||
|
import { useToggle } from "../../hooks/hooks";
|
||||||
|
import { DeleteModal } from "../../components/modals";
|
||||||
|
import { classNames } from "../../styles/utils";
|
||||||
|
|
||||||
|
interface props {
|
||||||
|
title: string;
|
||||||
|
initialValues: any;
|
||||||
|
mutators?: any;
|
||||||
|
validate?: any;
|
||||||
|
onSubmit: any;
|
||||||
|
isOpen: boolean;
|
||||||
|
toggle: any;
|
||||||
|
children?: (values: any) => React.ReactNode;
|
||||||
|
deleteAction?: any
|
||||||
|
type: "CREATE" | "UPDATE";
|
||||||
|
}
|
||||||
|
|
||||||
|
function SlideOver({ title, initialValues, mutators, validate, onSubmit, deleteAction, isOpen, toggle, type, children }: props) {
|
||||||
|
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
|
||||||
|
|
||||||
|
const cancelModalButtonRef = useRef(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
|
||||||
|
{deleteAction && (
|
||||||
|
<DeleteModal
|
||||||
|
isOpen={deleteModalIsOpen}
|
||||||
|
toggle={toggleDeleteModal}
|
||||||
|
buttonRef={cancelModalButtonRef}
|
||||||
|
deleteAction={deleteAction}
|
||||||
|
title={`Remove ${title}`}
|
||||||
|
text={`Are you sure you want to remove this ${title}? 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 dark:border-gray-700 border-l">
|
||||||
|
|
||||||
|
<Form
|
||||||
|
initialValues={initialValues}
|
||||||
|
mutators={mutators}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
validate={validate}
|
||||||
|
>
|
||||||
|
{({ handleSubmit, values }) => {
|
||||||
|
return (
|
||||||
|
<form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||||
|
onSubmit={handleSubmit}>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||||
|
<div className="flex items-start justify-between space-x-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">{type === "CREATE" ? "Create" : "Update"} {title}</Dialog.Title>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{type === "CREATE" ? "Create" : "Update"} {title}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="h-7 flex items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-white dark:bg-gray-900 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close panel</span>
|
||||||
|
<XIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children !== undefined && children(values)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
||||||
|
<div className={classNames(type === "CREATE" ? "justify-end" : "justify-between", "space-x-3 flex")}>
|
||||||
|
{type === "UPDATE" && (
|
||||||
|
<button
|
||||||
|
type="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-900 bg-red-100 dark:bg-red-500 hover:bg-red-200 dark:hover:bg-red-600 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 dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="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 dark:bg-blue-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
{type === "CREATE" ? "Create" : "Save"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DEBUG values={values} />
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SlideOver;
|
1
web/src/components/panels/index.ts
Normal file
1
web/src/components/panels/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default as SlideOver } from "./SlideOver";
|
|
@ -1,20 +1,17 @@
|
||||||
import React, {Fragment, useEffect} from "react";
|
import { Fragment, useEffect } from "react";
|
||||||
import {useMutation} from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import {Filter} from "../../domain/interfaces";
|
import { Filter } from "../../domain/interfaces";
|
||||||
import {queryClient} from "../../App";
|
import { queryClient } from "../../App";
|
||||||
import {XIcon} from "@heroicons/react/solid";
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
import {Dialog, Transition} from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import {Field, Form} from "react-final-form";
|
import { Field, Form } from "react-final-form";
|
||||||
import DEBUG from "../../components/debug";
|
import DEBUG from "../../components/debug";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import Toast from '../../components/notifications/Toast';
|
import Toast from '../../components/notifications/Toast';
|
||||||
|
|
||||||
|
function FilterAddForm({ isOpen, toggle }: any) {
|
||||||
const required = (value: any) => (value ? undefined : 'Required')
|
|
||||||
|
|
||||||
function FilterAddForm({isOpen, toggle}: any) {
|
|
||||||
const mutation = useMutation((filter: Filter) => APIClient.filters.create(filter), {
|
const mutation = useMutation((filter: Filter) => APIClient.filters.create(filter), {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries('filter');
|
queryClient.invalidateQueries('filter');
|
||||||
|
@ -32,11 +29,21 @@ function FilterAddForm({isOpen, toggle}: any) {
|
||||||
mutation.mutate(data)
|
mutation.mutate(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validate = (values: any) => {
|
||||||
|
const errors = {} as any;
|
||||||
|
|
||||||
|
if (!values.name) {
|
||||||
|
errors.name = "Required";
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
@ -48,7 +55,7 @@ function FilterAddForm({isOpen, toggle}: any) {
|
||||||
leaveFrom="translate-x-0"
|
leaveFrom="translate-x-0"
|
||||||
leaveTo="translate-x-full"
|
leaveTo="translate-x-full"
|
||||||
>
|
>
|
||||||
<div className="w-screen max-w-2xl">
|
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
initialValues={{
|
initialValues={{
|
||||||
|
@ -59,38 +66,34 @@ function FilterAddForm({isOpen, toggle}: any) {
|
||||||
sources: [],
|
sources: [],
|
||||||
containers: []
|
containers: []
|
||||||
}}
|
}}
|
||||||
// validate={validate}
|
validate={validate}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
{({handleSubmit, values}) => {
|
{({ handleSubmit, values }) => {
|
||||||
return (
|
return (
|
||||||
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll" onSubmit={handleSubmit}>
|
<form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll" onSubmit={handleSubmit}>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{/* Header */}
|
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||||
<div className="px-4 py-6 bg-gray-50 sm:px-6">
|
|
||||||
<div className="flex items-start justify-between space-x-3">
|
<div className="flex items-start justify-between space-x-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Dialog.Title
|
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Create filter</Dialog.Title>
|
||||||
className="text-lg font-medium text-gray-900">Create
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
filter</Dialog.Title>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Add new filter.
|
Add new filter.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-7 flex items-center">
|
<div className="h-7 flex items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
className="light:bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
onClick={toggle}
|
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>
|
||||||
|
|
||||||
{/* Divider container */}
|
|
||||||
<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
|
||||||
|
@ -98,21 +101,21 @@ function FilterAddForm({isOpen, toggle}: any) {
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="name"
|
htmlFor="name"
|
||||||
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
|
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
||||||
>
|
>
|
||||||
Name
|
Name
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<Field name="name" validate={required}>
|
<Field name="name">
|
||||||
{({input, meta}) => (
|
{({ input, meta }) => (
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
{...input}
|
{...input}
|
||||||
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md"
|
className="block w-full shadow-sm dark:bg-gray-800 border-gray-300 dark:border-gray-700 sm:text-sm dark:text-white focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 rounded-md"
|
||||||
/>
|
/>
|
||||||
{meta.touched && meta.error &&
|
{meta.touched && meta.error &&
|
||||||
<span>{meta.error}</span>}
|
<span className="block mt-2 text-red-500">{meta.error}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
@ -122,24 +125,24 @@ function FilterAddForm({isOpen, toggle}: any) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
|
className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
||||||
<div className="space-x-3 flex justify-end">
|
<div className="space-x-3 flex justify-end">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
className="bg-white dark:bg-gray-800 py-2 px-4 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DEBUG values={values}/>
|
<DEBUG values={values} />
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -2,10 +2,6 @@ export { default as FilterAddForm } from "./filters/FilterAddForm";
|
||||||
export { default as FilterActionAddForm } from "./filters/FilterActionAddForm";
|
export { default as FilterActionAddForm } from "./filters/FilterActionAddForm";
|
||||||
export { default as FilterActionUpdateForm } from "./filters/FilterActionUpdateForm";
|
export { default as FilterActionUpdateForm } from "./filters/FilterActionUpdateForm";
|
||||||
|
|
||||||
export { default as DownloadClientAddForm } from "./settings/downloadclient/DownloadClientAddForm";
|
export { DownloadClientAddForm, DownloadClientUpdateForm } from "./settings/DownloadClientForms";
|
||||||
export { default as DownloadClientUpdateForm } from "./settings/downloadclient/DownloadClientUpdateForm";
|
export { IndexerAddForm, IndexerUpdateForm } from "./settings/IndexerForms";
|
||||||
|
export { IrcNetworkAddForm, IrcNetworkUpdateForm } from "./settings/IrcForms";
|
||||||
export { default as IndexerAddForm } from "./settings/IndexerAddForm";
|
|
||||||
export { default as IndexerUpdateForm } from "./settings/IndexerUpdateForm";
|
|
||||||
|
|
||||||
export { default as IrcNetworkAddForm } from "./settings/IrcNetworkAddForm";
|
|
||||||
|
|
617
web/src/forms/settings/DownloadClientForms.tsx
Normal file
617
web/src/forms/settings/DownloadClientForms.tsx
Normal file
|
@ -0,0 +1,617 @@
|
||||||
|
import { Fragment, useRef, useState } from "react";
|
||||||
|
import { useMutation } from "react-query";
|
||||||
|
import {
|
||||||
|
DOWNLOAD_CLIENT_TYPES,
|
||||||
|
DownloadClient,
|
||||||
|
} from "../../domain/interfaces";
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
|
import { classNames } from "../../styles/utils";
|
||||||
|
import { Form, useField } from "react-final-form";
|
||||||
|
import DEBUG from "../../components/debug";
|
||||||
|
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
|
||||||
|
import { queryClient } from "../../App";
|
||||||
|
import APIClient from "../../api/APIClient";
|
||||||
|
import { sleep } from "../../utils/utils";
|
||||||
|
import { DownloadClientTypeOptions } from "../../domain/constants";
|
||||||
|
import { NumberFieldWide, PasswordFieldWide, RadioFieldsetWide } from "../../components/inputs/wide";
|
||||||
|
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import Toast from '../../components/notifications/Toast';
|
||||||
|
import { useToggle } from "../../hooks/hooks";
|
||||||
|
import { DeleteModal } from "../../components/modals";
|
||||||
|
|
||||||
|
function FormFieldsDefault() {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<TextFieldWide name="host" label="Host" help="Url domain.ltd/client" />
|
||||||
|
|
||||||
|
<NumberFieldWide name="port" label="Port" />
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroup name="ssl" label="SSL" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextFieldWide name="username" label="Username" />
|
||||||
|
<PasswordFieldWide name="password" label="Password" />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormFieldsArr() {
|
||||||
|
const { input } = useField("settings.basic.auth");
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<TextFieldWide name="host" label="Host" help="Full url like http(s)://domain.ltd/" />
|
||||||
|
|
||||||
|
<PasswordFieldWide name="settings.apikey" label="API key" />
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroup name="settings.basic.auth" label="Basic auth" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{input.value === true && (
|
||||||
|
<Fragment>
|
||||||
|
<TextFieldWide name="settings.basic.username" label="Username" />
|
||||||
|
<PasswordFieldWide name="settings.basic.password" label="Password" />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const componentMap: any = {
|
||||||
|
DELUGE_V1: <FormFieldsDefault />,
|
||||||
|
DELUGE_V2: <FormFieldsDefault />,
|
||||||
|
QBITTORRENT: <FormFieldsDefault />,
|
||||||
|
RADARR: <FormFieldsArr />,
|
||||||
|
SONARR: <FormFieldsArr />,
|
||||||
|
LIDARR: <FormFieldsArr />,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function FormFieldsRulesBasic() {
|
||||||
|
const { input: enabled } = useField("settings.rules.enabled");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||||
|
|
||||||
|
<div className="px-6 space-y-1">
|
||||||
|
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Rules</Dialog.Title>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Manage max downloads.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{enabled.value === true && (
|
||||||
|
<Fragment>
|
||||||
|
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormFieldsRules() {
|
||||||
|
const { input } = useField("settings.rules.ignore_slow_torrents");
|
||||||
|
const { input: enabled } = useField("settings.rules.enabled");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||||
|
|
||||||
|
<div className="px-6 space-y-1">
|
||||||
|
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Rules</Dialog.Title>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Manage max downloads etc.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{enabled.value === true && (
|
||||||
|
<Fragment>
|
||||||
|
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroup name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{input.value === true && (
|
||||||
|
<Fragment>
|
||||||
|
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold" placeholder="in KB/s" help="If download speed is below this when max active downloads is hit, download anyways. KB/s" />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rulesComponentMap: any = {
|
||||||
|
DELUGE_V1: <FormFieldsRulesBasic />,
|
||||||
|
DELUGE_V2: <FormFieldsRulesBasic />,
|
||||||
|
QBITTORRENT: <FormFieldsRules />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
||||||
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
|
||||||
|
const [isErrorTest, setIsErrorTest] = useState(false);
|
||||||
|
|
||||||
|
const mutation = useMutation(
|
||||||
|
(client: DownloadClient) => APIClient.download_clients.create(client),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(["downloadClients"]);
|
||||||
|
toast.custom((t) => <Toast type="success" body="Client was added" t={t} />)
|
||||||
|
|
||||||
|
toggle();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.custom((t) => <Toast type="error" body="Client could not be added" t={t} />)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const testClientMutation = useMutation(
|
||||||
|
(client: DownloadClient) => APIClient.download_clients.test(client),
|
||||||
|
{
|
||||||
|
onMutate: () => {
|
||||||
|
setIsTesting(true);
|
||||||
|
setIsErrorTest(false);
|
||||||
|
setIsSuccessfulTest(false);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
sleep(1000)
|
||||||
|
.then(() => {
|
||||||
|
setIsTesting(false);
|
||||||
|
setIsSuccessfulTest(true);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
sleep(2500).then(() => {
|
||||||
|
setIsSuccessfulTest(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.log('not added')
|
||||||
|
setIsTesting(false);
|
||||||
|
setIsErrorTest(true);
|
||||||
|
sleep(2500).then(() => {
|
||||||
|
setIsErrorTest(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = (data: any) => {
|
||||||
|
mutation.mutate(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const testClient = (data: any) => {
|
||||||
|
testClientMutation.mutate(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
static
|
||||||
|
className="fixed inset-0 overflow-hidden"
|
||||||
|
open={isOpen}
|
||||||
|
onClose={toggle}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<Dialog.Overlay className="absolute inset-0" />
|
||||||
|
|
||||||
|
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||||
|
enterFrom="translate-x-full"
|
||||||
|
enterTo="translate-x-0"
|
||||||
|
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
||||||
|
leaveFrom="translate-x-0"
|
||||||
|
leaveTo="translate-x-full"
|
||||||
|
>
|
||||||
|
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
|
||||||
|
<Form
|
||||||
|
initialValues={{
|
||||||
|
name: "",
|
||||||
|
type: DOWNLOAD_CLIENT_TYPES.qBittorrent,
|
||||||
|
enabled: true,
|
||||||
|
host: "",
|
||||||
|
port: 10000,
|
||||||
|
ssl: false,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
}}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
{({ handleSubmit, values }) => {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||||
|
<div className="flex items-start justify-between space-x-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
|
Add client
|
||||||
|
</Dialog.Title>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Add download client.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="h-7 flex items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close panel</span>
|
||||||
|
<XIcon
|
||||||
|
className="h-6 w-6"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
||||||
|
<TextFieldWide name="name" label="Name" />
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700">
|
||||||
|
<SwitchGroup name="enabled" label="Enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RadioFieldsetWide
|
||||||
|
name="type"
|
||||||
|
legend="Type"
|
||||||
|
options={DownloadClientTypeOptions}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>{componentMap[values.type]}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rulesComponentMap[values.type]}
|
||||||
|
|
||||||
|
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
||||||
|
<div className="space-x-3 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames(
|
||||||
|
isSuccessfulTest
|
||||||
|
? "text-green-500 border-green-500 bg-green-50"
|
||||||
|
: isErrorTest
|
||||||
|
? "text-red-500 border-red-500 bg-red-50"
|
||||||
|
: "border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-400 bg-white dark:bg-gray-700 hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
|
||||||
|
isTesting ? "cursor-not-allowed" : "",
|
||||||
|
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
)}
|
||||||
|
disabled={isTesting}
|
||||||
|
onClick={() => testClient(values)}
|
||||||
|
>
|
||||||
|
{isTesting ? (
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 text-green-500"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
) : isSuccessfulTest ? (
|
||||||
|
"OK!"
|
||||||
|
) : isErrorTest ? (
|
||||||
|
"ERROR"
|
||||||
|
) : (
|
||||||
|
"Test"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DEBUG values={values} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
||||||
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
|
||||||
|
const [isErrorTest, setIsErrorTest] = useState(false);
|
||||||
|
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||||
|
|
||||||
|
const mutation = useMutation(
|
||||||
|
(client: DownloadClient) => APIClient.download_clients.update(client),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(["downloadClients"]);
|
||||||
|
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t} />)
|
||||||
|
toggle();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteMutation = useMutation(
|
||||||
|
(clientID: number) => APIClient.download_clients.delete(clientID),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries();
|
||||||
|
toast.custom((t) => <Toast type="success" body={`${client.name} was deleted.`} t={t} />)
|
||||||
|
toggleDeleteModal();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const testClientMutation = useMutation(
|
||||||
|
(client: DownloadClient) => APIClient.download_clients.test(client),
|
||||||
|
{
|
||||||
|
onMutate: () => {
|
||||||
|
setIsTesting(true);
|
||||||
|
setIsErrorTest(false);
|
||||||
|
setIsSuccessfulTest(false);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
sleep(1000)
|
||||||
|
.then(() => {
|
||||||
|
setIsTesting(false);
|
||||||
|
setIsSuccessfulTest(true);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
sleep(2500).then(() => {
|
||||||
|
setIsSuccessfulTest(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsTesting(false);
|
||||||
|
setIsErrorTest(true);
|
||||||
|
sleep(2500).then(() => {
|
||||||
|
setIsErrorTest(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = (data: any) => {
|
||||||
|
mutation.mutate(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelButtonRef = useRef(null);
|
||||||
|
const cancelModalButtonRef = useRef(null);
|
||||||
|
|
||||||
|
const deleteAction = () => {
|
||||||
|
deleteMutation.mutate(client.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const testClient = (data: any) => {
|
||||||
|
testClientMutation.mutate(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
static
|
||||||
|
className="fixed inset-0 overflow-hidden"
|
||||||
|
open={isOpen}
|
||||||
|
onClose={toggle}
|
||||||
|
initialFocus={cancelButtonRef}
|
||||||
|
>
|
||||||
|
<DeleteModal
|
||||||
|
isOpen={deleteModalIsOpen}
|
||||||
|
toggle={toggleDeleteModal}
|
||||||
|
buttonRef={cancelModalButtonRef}
|
||||||
|
deleteAction={deleteAction}
|
||||||
|
title="Remove download client"
|
||||||
|
text="Are you sure you want to remove this download client? This action cannot be undone."
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<Dialog.Overlay className="absolute inset-0" />
|
||||||
|
|
||||||
|
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||||
|
enterFrom="translate-x-full"
|
||||||
|
enterTo="translate-x-0"
|
||||||
|
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
||||||
|
leaveFrom="translate-x-0"
|
||||||
|
leaveTo="translate-x-full"
|
||||||
|
>
|
||||||
|
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
|
||||||
|
<Form
|
||||||
|
initialValues={{
|
||||||
|
id: client.id,
|
||||||
|
name: client.name,
|
||||||
|
type: client.type,
|
||||||
|
enabled: client.enabled,
|
||||||
|
host: client.host,
|
||||||
|
port: client.port,
|
||||||
|
ssl: client.ssl,
|
||||||
|
username: client.username,
|
||||||
|
password: client.password,
|
||||||
|
settings: client.settings,
|
||||||
|
}}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
{({ handleSubmit, values }) => {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||||
|
<div className="flex items-start justify-between space-x-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
|
Edit client
|
||||||
|
</Dialog.Title>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Edit download client settings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="h-7 flex items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close panel</span>
|
||||||
|
<XIcon
|
||||||
|
className="h-6 w-6"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
||||||
|
<TextFieldWide name="name" label="Name" />
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroup name="enabled" label="Enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RadioFieldsetWide
|
||||||
|
name="type"
|
||||||
|
legend="Type"
|
||||||
|
options={DownloadClientTypeOptions}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>{componentMap[values.type]}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rulesComponentMap[values.type]}
|
||||||
|
|
||||||
|
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
||||||
|
<div className="space-x-3 flex justify-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
||||||
|
onClick={toggleDeleteModal}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
<div className="flex">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames(
|
||||||
|
isSuccessfulTest
|
||||||
|
? "text-green-500 border-green-500 bg-green-50"
|
||||||
|
: isErrorTest
|
||||||
|
? "text-red-500 border-red-500 bg-red-50"
|
||||||
|
: "border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-400 bg-white dark:bg-gray-700 hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
|
||||||
|
isTesting ? "cursor-not-allowed" : "",
|
||||||
|
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
)}
|
||||||
|
disabled={isTesting}
|
||||||
|
onClick={() => testClient(values)}
|
||||||
|
>
|
||||||
|
{isTesting ? (
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 text-green-500"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
) : isSuccessfulTest ? (
|
||||||
|
"OK!"
|
||||||
|
) : isErrorTest ? (
|
||||||
|
"ERROR"
|
||||||
|
) : (
|
||||||
|
"Test"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mr-4 bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DEBUG values={values} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
}
|
|
@ -13,12 +13,14 @@ import APIClient from "../../api/APIClient";
|
||||||
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
|
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import Toast from '../../components/notifications/Toast';
|
import Toast from '../../components/notifications/Toast';
|
||||||
interface props {
|
import { SlideOver } from "../../components/panels";
|
||||||
|
|
||||||
|
interface AddProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
toggle: any;
|
toggle: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function IndexerAddForm({ isOpen, toggle }: props) {
|
export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
||||||
const { data } = useQuery<IndexerSchema[], Error>('indexerSchema', APIClient.indexers.getSchema,
|
const { data } = useQuery<IndexerSchema[], Error>('indexerSchema', APIClient.indexers.getSchema,
|
||||||
{
|
{
|
||||||
enabled: isOpen,
|
enabled: isOpen,
|
||||||
|
@ -40,7 +42,7 @@ function IndexerAddForm({ isOpen, toggle }: props) {
|
||||||
|
|
||||||
const ircMutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
|
const ircMutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
console.log("irc mutation: ", data);
|
// console.log("irc mutation: ", data);
|
||||||
|
|
||||||
// queryClient.invalidateQueries(['indexer']);
|
// queryClient.invalidateQueries(['indexer']);
|
||||||
// sleep(1500)
|
// sleep(1500)
|
||||||
|
@ -97,13 +99,14 @@ function IndexerAddForm({ isOpen, toggle }: props) {
|
||||||
switch (f.type) {
|
switch (f.type) {
|
||||||
case "text":
|
case "text":
|
||||||
return (
|
return (
|
||||||
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue=""/>
|
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue="" />
|
||||||
)
|
)
|
||||||
case "secret":
|
case "secret":
|
||||||
return (
|
return (
|
||||||
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue="" />
|
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue="" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
})}
|
})}
|
||||||
<div hidden={true}>
|
<div hidden={true}>
|
||||||
<TextFieldWide name={`name`} label="Name" defaultValue={ind?.name} />
|
<TextFieldWide name={`name`} label="Name" defaultValue={ind?.name} />
|
||||||
|
@ -121,10 +124,10 @@ function IndexerAddForm({ isOpen, toggle }: props) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{ind && ind.irc && ind.irc.settings && (
|
{ind && ind.irc && ind.irc.settings && (
|
||||||
<div className="border-t border-gray-200 py-5">
|
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||||
<div className="px-6 space-y-1">
|
<div className="px-6 space-y-1">
|
||||||
<Dialog.Title className="text-lg font-medium text-gray-900">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">
|
<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.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -135,6 +138,7 @@ function IndexerAddForm({ isOpen, toggle }: props) {
|
||||||
case "secret":
|
case "secret":
|
||||||
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} />
|
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} />
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div hidden={true}>
|
<div hidden={true}>
|
||||||
|
@ -165,7 +169,7 @@ function IndexerAddForm({ isOpen, toggle }: props) {
|
||||||
leaveFrom="translate-x-0"
|
leaveFrom="translate-x-0"
|
||||||
leaveTo="translate-x-full"
|
leaveTo="translate-x-full"
|
||||||
>
|
>
|
||||||
<div className="w-screen max-w-2xl">
|
<div className="w-screen max-w-2xl dark:border-gray-700 border-l">
|
||||||
<Form
|
<Form
|
||||||
initialValues={{
|
initialValues={{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -176,23 +180,23 @@ function IndexerAddForm({ isOpen, toggle }: props) {
|
||||||
>
|
>
|
||||||
{({ handleSubmit, values }) => {
|
{({ handleSubmit, values }) => {
|
||||||
return (
|
return (
|
||||||
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
|
<form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||||
onSubmit={handleSubmit}>
|
onSubmit={handleSubmit}>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="px-4 py-6 bg-gray-50 sm:px-6">
|
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||||
<div className="flex items-start justify-between space-x-3">
|
<div className="flex items-start justify-between space-x-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
className="text-lg font-medium text-gray-900">Add
|
className="text-lg font-medium text-gray-900 dark:text-white">Add
|
||||||
indexer</Dialog.Title>
|
indexer</Dialog.Title>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500 dark:text-gray-200">
|
||||||
Add indexer.
|
Add indexer.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-7 flex items-center">
|
<div className="h-7 flex items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Close panel</span>
|
<span className="sr-only">Close panel</span>
|
||||||
|
@ -203,14 +207,14 @@ function IndexerAddForm({ isOpen, toggle }: props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
className="py-6 space-y-6 py-0 space-y-0 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
|
||||||
<div
|
<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:px-6 sm:py-5">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="identifier"
|
htmlFor="identifier"
|
||||||
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
|
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
||||||
>
|
>
|
||||||
Indexer
|
Indexer
|
||||||
</label>
|
</label>
|
||||||
|
@ -249,18 +253,18 @@ function IndexerAddForm({ isOpen, toggle }: props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
|
className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
||||||
<div className="space-x-3 flex justify-end">
|
<div className="space-x-3 flex justify-end">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
@ -282,4 +286,120 @@ function IndexerAddForm({ isOpen, toggle }: props) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IndexerAddForm;
|
interface UpdateProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
toggle: any;
|
||||||
|
indexer: Indexer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
|
||||||
|
const mutation = useMutation((indexer: Indexer) => APIClient.indexers.update(indexer), {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['indexer']);
|
||||||
|
toast.custom((t) => <Toast type="success" body={`${indexer.name} was updated successfully`} t={t} />)
|
||||||
|
sleep(1500)
|
||||||
|
|
||||||
|
toggle()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteMutation = useMutation((id: number) => APIClient.indexers.delete(id), {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['indexer']);
|
||||||
|
toast.custom((t) => <Toast type="success" body={`${indexer.name} was deleted.`} t={t} />)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (data: any) => {
|
||||||
|
// TODO clear data depending on type
|
||||||
|
mutation.mutate(data)
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAction = () => {
|
||||||
|
deleteMutation.mutate(indexer.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderSettingFields = (settings: any[]) => {
|
||||||
|
if (settings !== []) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key="opt">
|
||||||
|
{settings && settings.map((f: any, idx: number) => {
|
||||||
|
switch (f.type) {
|
||||||
|
case "text":
|
||||||
|
return (
|
||||||
|
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
|
||||||
|
)
|
||||||
|
case "secret":
|
||||||
|
return (
|
||||||
|
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let initialValues = {
|
||||||
|
id: indexer.id,
|
||||||
|
name: indexer.name,
|
||||||
|
enabled: indexer.enabled,
|
||||||
|
identifier: indexer.identifier,
|
||||||
|
settings: indexer.settings.reduce((o: any, obj: any) => ({ ...o, [obj.name]: obj.value }), {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SlideOver
|
||||||
|
type="UPDATE"
|
||||||
|
title="Indexer"
|
||||||
|
isOpen={isOpen}
|
||||||
|
toggle={toggle}
|
||||||
|
deleteAction={deleteAction}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
initialValues={initialValues}
|
||||||
|
>
|
||||||
|
{({ values }: any) => (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="py-6 space-y-6 sm:py-0 sm:space-y-0 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<div
|
||||||
|
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Field name="name">
|
||||||
|
{({ input, meta }) => (
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
{...input}
|
||||||
|
className="block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 dark:border-gray-700 rounded-md"
|
||||||
|
/>
|
||||||
|
{meta.touched && meta.error &&
|
||||||
|
<span>{meta.error}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
|
||||||
|
<SwitchGroup name="enabled" label="Enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renderSettingFields(indexer.settings)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</SlideOver>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,288 +0,0 @@
|
||||||
import { Fragment, useRef } from "react";
|
|
||||||
import { useMutation } from "react-query";
|
|
||||||
import { Indexer } from "../../domain/interfaces";
|
|
||||||
import { sleep } from "../../utils/utils";
|
|
||||||
import { ExclamationIcon, XIcon } from "@heroicons/react/solid";
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
import { Field, Form } from "react-final-form";
|
|
||||||
import DEBUG from "../../components/debug";
|
|
||||||
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
|
|
||||||
import { useToggle } from "../../hooks/hooks";
|
|
||||||
import APIClient from "../../api/APIClient";
|
|
||||||
import { queryClient } from "../../App";
|
|
||||||
import { PasswordFieldWide } from "../../components/inputs/wide";
|
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import Toast from '../../components/notifications/Toast';
|
|
||||||
|
|
||||||
interface props {
|
|
||||||
isOpen: boolean;
|
|
||||||
toggle: any;
|
|
||||||
indexer: Indexer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function IndexerUpdateForm({ isOpen, toggle, indexer }: props) {
|
|
||||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
|
|
||||||
|
|
||||||
const mutation = useMutation((indexer: Indexer) => APIClient.indexers.update(indexer), {
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(['indexer']);
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${indexer.name} was updated successfully`} t={t}/>)
|
|
||||||
sleep(1500)
|
|
||||||
|
|
||||||
toggle()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteMutation = useMutation((id: number) => APIClient.indexers.delete(id), {
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(['indexer']);
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${indexer.name} was deleted.`} t={t}/>)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const cancelModalButtonRef = useRef(null)
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
|
||||||
// TODO clear data depending on type
|
|
||||||
mutation.mutate(data)
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteAction = () => {
|
|
||||||
deleteMutation.mutate(indexer.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderSettingFields = (settings: any[]) => {
|
|
||||||
if (settings !== []) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key="opt">
|
|
||||||
{settings && settings.map((f: any, idx: number) => {
|
|
||||||
switch (f.type) {
|
|
||||||
case "text":
|
|
||||||
return (
|
|
||||||
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
|
|
||||||
)
|
|
||||||
case "secret":
|
|
||||||
return (
|
|
||||||
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
|
||||||
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
|
|
||||||
|
|
||||||
<Transition.Root show={deleteModalIsOpen} as={Fragment}>
|
|
||||||
<Dialog
|
|
||||||
as="div"
|
|
||||||
static
|
|
||||||
className="fixed z-10 inset-0 overflow-y-auto"
|
|
||||||
initialFocus={cancelModalButtonRef}
|
|
||||||
open={deleteModalIsOpen}
|
|
||||||
onClose={toggleDeleteModal}
|
|
||||||
>
|
|
||||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
{/* This element is to trick the browser into centering the modal contents. */}
|
|
||||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
|
||||||
​
|
|
||||||
</span>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
>
|
|
||||||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
|
||||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
||||||
<div className="sm:flex sm:items-start">
|
|
||||||
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
|
||||||
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
|
||||||
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900">
|
|
||||||
Remove indexer
|
|
||||||
</Dialog.Title>
|
|
||||||
<div className="mt-2">
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Are you sure you want to remove this indexer?
|
|
||||||
This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
|
|
||||||
onClick={deleteAction}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
|
||||||
onClick={toggleDeleteModal}
|
|
||||||
ref={cancelModalButtonRef}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
<Dialog.Overlay className="absolute inset-0" />
|
|
||||||
|
|
||||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
enterFrom="translate-x-full"
|
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="translate-x-full"
|
|
||||||
>
|
|
||||||
<div className="w-screen max-w-2xl">
|
|
||||||
<Form
|
|
||||||
initialValues={{
|
|
||||||
id: indexer.id,
|
|
||||||
name: indexer.name,
|
|
||||||
enabled: indexer.enabled,
|
|
||||||
identifier: indexer.identifier,
|
|
||||||
settings: indexer.settings.reduce((o: any, obj: any) => ({ ...o, [obj.name]: obj.value }), {}),
|
|
||||||
}}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
{({ handleSubmit, values }) => {
|
|
||||||
return (
|
|
||||||
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
|
|
||||||
onSubmit={handleSubmit}>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="px-4 py-6 bg-gray-50 sm:px-6">
|
|
||||||
<div className="flex items-start justify-between space-x-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Dialog.Title
|
|
||||||
className="text-lg font-medium text-gray-900">Update
|
|
||||||
indexer</Dialog.Title>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Update indexer.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-7 flex items-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close panel</span>
|
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<div
|
|
||||||
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<Field name="name">
|
|
||||||
{({ input, meta }) => (
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
{...input}
|
|
||||||
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md"
|
|
||||||
/>
|
|
||||||
{meta.touched && meta.error &&
|
|
||||||
<span>{meta.error}</span>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="enabled" label="Enabled" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{renderSettingFields(indexer.settings)}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
|
|
||||||
<div className="space-x-3 flex justify-between">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
|
||||||
onClick={toggleDeleteModal}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="ml-4 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DEBUG values={values} />
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IndexerUpdateForm;
|
|
347
web/src/forms/settings/IrcForms.tsx
Normal file
347
web/src/forms/settings/IrcForms.tsx
Normal file
|
@ -0,0 +1,347 @@
|
||||||
|
import { useMutation } from "react-query";
|
||||||
|
import { Network } from "../../domain/interfaces";
|
||||||
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
|
import { Field } from "react-final-form";
|
||||||
|
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
|
||||||
|
import { queryClient } from "../../App";
|
||||||
|
|
||||||
|
import arrayMutators from "final-form-arrays";
|
||||||
|
import { FieldArray } from "react-final-form-arrays";
|
||||||
|
import APIClient from "../../api/APIClient";
|
||||||
|
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
|
||||||
|
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import Toast from '../../components/notifications/Toast';
|
||||||
|
import { SlideOver } from "../../components/panels";
|
||||||
|
|
||||||
|
export function IrcNetworkAddForm({ isOpen, toggle }: any) {
|
||||||
|
const mutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries(['networks']);
|
||||||
|
toast.custom((t) => <Toast type="success" body="IRC Network added" t={t} />)
|
||||||
|
toggle()
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (data: any) => {
|
||||||
|
// easy way to split textarea lines into array of strings for each newline.
|
||||||
|
// parse on the field didn't really work.
|
||||||
|
let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g, "\n").split("\n") : [];
|
||||||
|
data.connect_commands = cmds
|
||||||
|
console.log("formated", data)
|
||||||
|
|
||||||
|
mutation.mutate(data)
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = (values: any) => {
|
||||||
|
const errors = {
|
||||||
|
nickserv: {
|
||||||
|
account: null,
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
if (!values.name) {
|
||||||
|
errors.name = "Required";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.port) {
|
||||||
|
errors.port = "Required";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.server) {
|
||||||
|
errors.server = "Required";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.nickserv?.account) {
|
||||||
|
errors.nickserv.account = "Required";
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
name: "",
|
||||||
|
enabled: true,
|
||||||
|
server: "",
|
||||||
|
tls: false,
|
||||||
|
pass: "",
|
||||||
|
nickserv: {
|
||||||
|
account: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutators = {
|
||||||
|
...arrayMutators
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SlideOver
|
||||||
|
type="CREATE"
|
||||||
|
title="Network"
|
||||||
|
isOpen={isOpen}
|
||||||
|
toggle={toggle}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
initialValues={initialValues}
|
||||||
|
mutators={mutators}
|
||||||
|
validate={validate}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<>
|
||||||
|
|
||||||
|
|
||||||
|
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
|
||||||
|
|
||||||
|
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
|
||||||
|
<SwitchGroup name="enabled" label="Enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
|
||||||
|
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroup name="tls" label="TLS" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PasswordFieldWide name="pass" label="Password" help="Network password" />
|
||||||
|
|
||||||
|
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
|
||||||
|
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
|
||||||
|
|
||||||
|
<PasswordFieldWide name="invite_command" label="Invite command" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
|
||||||
|
<FieldArray name="channels">
|
||||||
|
{({ fields }) => (
|
||||||
|
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
|
||||||
|
{fields && (fields.length as any) > 0 ? (
|
||||||
|
fields.map((name, index) => (
|
||||||
|
<div key={name} className="flex justify-between">
|
||||||
|
<div className="flex">
|
||||||
|
<Field
|
||||||
|
name={`${name}.name`}
|
||||||
|
component="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="#Channel"
|
||||||
|
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
name={`${name}.password`}
|
||||||
|
component="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Password"
|
||||||
|
className="dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={() => fields.remove(index)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Remove</span>
|
||||||
|
<XIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-center text-sm text-grey-darker dark:text-white">
|
||||||
|
No channels!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 rounded self-center text-center"
|
||||||
|
onClick={() => fields.push({ name: "", password: "" })}
|
||||||
|
>
|
||||||
|
Add Channel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FieldArray>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SlideOver>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) {
|
||||||
|
const mutation = useMutation((network: Network) => APIClient.irc.updateNetwork(network), {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['networks']);
|
||||||
|
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />)
|
||||||
|
toggle()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['networks']);
|
||||||
|
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />)
|
||||||
|
|
||||||
|
toggle()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (data: any) => {
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
|
// easy way to split textarea lines into array of strings for each newline.
|
||||||
|
// parse on the field didn't really work.
|
||||||
|
// TODO fix connect_commands on network update
|
||||||
|
// let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
|
||||||
|
// data.connect_commands = cmds
|
||||||
|
// console.log("formatted", data)
|
||||||
|
|
||||||
|
mutation.mutate(data)
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = (values: any) => {
|
||||||
|
const errors = {} as any;
|
||||||
|
|
||||||
|
if (!values.name) {
|
||||||
|
errors.name = "Required";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.server) {
|
||||||
|
errors.server = "Required";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.port) {
|
||||||
|
errors.port = "Required";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.nickserv?.account) {
|
||||||
|
errors.nickserv.account = "Required";
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAction = () => {
|
||||||
|
deleteMutation.mutate(network.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
id: network.id,
|
||||||
|
name: network.name,
|
||||||
|
enabled: network.enabled,
|
||||||
|
server: network.server,
|
||||||
|
port: network.port,
|
||||||
|
tls: network.tls,
|
||||||
|
nickserv: network.nickserv,
|
||||||
|
pass: network.pass,
|
||||||
|
invite_command: network.invite_command,
|
||||||
|
// connect_commands: network.connect_commands,
|
||||||
|
channels: network.channels
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutators = {
|
||||||
|
...arrayMutators
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SlideOver
|
||||||
|
type="UPDATE"
|
||||||
|
title="Network"
|
||||||
|
isOpen={isOpen}
|
||||||
|
toggle={toggle}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
deleteAction={deleteAction}
|
||||||
|
initialValues={initialValues}
|
||||||
|
mutators={mutators}
|
||||||
|
validate={validate}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
|
||||||
|
|
||||||
|
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0">
|
||||||
|
<SwitchGroup name="enabled" label="Enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
|
||||||
|
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroup name="tls" label="TLS" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PasswordFieldWide name="pass" label="Password" help="Network password" />
|
||||||
|
|
||||||
|
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
|
||||||
|
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
|
||||||
|
|
||||||
|
<PasswordFieldWide name="invite_command" label="Invite command" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
|
||||||
|
<FieldArray name="channels">
|
||||||
|
{({ fields }) => (
|
||||||
|
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
|
||||||
|
{fields && (fields.length as any) > 0 ? (
|
||||||
|
fields.map((name, index) => (
|
||||||
|
<div key={name} className="flex justify-between">
|
||||||
|
<div className="flex">
|
||||||
|
<Field
|
||||||
|
name={`${name}.name`}
|
||||||
|
component="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="#Channel"
|
||||||
|
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
name={`${name}.password`}
|
||||||
|
component="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Password"
|
||||||
|
className="dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={() => fields.remove(index)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Remove</span>
|
||||||
|
<XIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-center text-sm text-grey-darker dark:text-white">
|
||||||
|
No channels!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500 rounded self-center text-center"
|
||||||
|
onClick={() => fields.push({ name: "", password: "" })}
|
||||||
|
>
|
||||||
|
Add Channel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FieldArray>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SlideOver>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,254 +0,0 @@
|
||||||
import {Fragment} from "react";
|
|
||||||
import {useMutation} from "react-query";
|
|
||||||
import {Network} from "../../domain/interfaces";
|
|
||||||
import {Dialog, Transition} from "@headlessui/react";
|
|
||||||
import {XIcon} from "@heroicons/react/solid";
|
|
||||||
import {Field, Form} from "react-final-form";
|
|
||||||
import DEBUG from "../../components/debug";
|
|
||||||
import {SwitchGroup, TextFieldWide} from "../../components/inputs";
|
|
||||||
import {queryClient} from "../../App";
|
|
||||||
|
|
||||||
import arrayMutators from "final-form-arrays";
|
|
||||||
import { FieldArray } from "react-final-form-arrays";
|
|
||||||
import {classNames} from "../../styles/utils";
|
|
||||||
import APIClient from "../../api/APIClient";
|
|
||||||
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
|
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast';
|
|
||||||
import Toast from '../../components/notifications/Toast';
|
|
||||||
|
|
||||||
type FormValues = {
|
|
||||||
name: string
|
|
||||||
server: string
|
|
||||||
nickserv: {
|
|
||||||
account: string
|
|
||||||
}
|
|
||||||
port: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function IrcNetworkAddForm({isOpen, toggle}: any) {
|
|
||||||
const mutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
queryClient.invalidateQueries(['networks']);
|
|
||||||
toast.custom((t) => <Toast type="success" body="IRC Network added" t={t} />)
|
|
||||||
toggle()
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t}/>)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
|
||||||
// easy way to split textarea lines into array of strings for each newline.
|
|
||||||
// parse on the field didn't really work.
|
|
||||||
let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
|
|
||||||
data.connect_commands = cmds
|
|
||||||
console.log("formated", data)
|
|
||||||
|
|
||||||
mutation.mutate(data)
|
|
||||||
};
|
|
||||||
|
|
||||||
const validate = (values: any) => {
|
|
||||||
const errors = {
|
|
||||||
nickserv: {
|
|
||||||
account: null,
|
|
||||||
}
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
if (!values.name) {
|
|
||||||
errors.name = "Required";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.port) {
|
|
||||||
errors.port = "Required";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.server) {
|
|
||||||
errors.server = "Required";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!values.nickserv?.account) {
|
|
||||||
errors.nickserv.account = "Required";
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
|
||||||
<Dialog as="div" static className="fixed inset-0 overflow-hidden transition-all" open={isOpen} onClose={toggle}>
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
<Dialog.Overlay className="absolute inset-0"/>
|
|
||||||
|
|
||||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
enterFrom="translate-x-full"
|
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="translate-x-full"
|
|
||||||
>
|
|
||||||
<div className="w-screen max-w-2xl">
|
|
||||||
|
|
||||||
<Form
|
|
||||||
initialValues={{
|
|
||||||
name: "",
|
|
||||||
enabled: true,
|
|
||||||
server: "",
|
|
||||||
tls: false,
|
|
||||||
pass: "",
|
|
||||||
nickserv: {
|
|
||||||
account: ""
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
mutators={{
|
|
||||||
...arrayMutators
|
|
||||||
}}
|
|
||||||
validate={validate}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
{({handleSubmit, values, pristine, invalid}) => {
|
|
||||||
return (
|
|
||||||
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
|
|
||||||
onSubmit={handleSubmit}>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="px-4 py-6 bg-gray-50 sm:px-6">
|
|
||||||
<div className="flex items-start justify-between space-x-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Dialog.Title
|
|
||||||
className="text-lg font-medium text-gray-900">Add
|
|
||||||
network</Dialog.Title>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Add irc network.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-7 flex items-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close panel</span>
|
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
|
|
||||||
|
|
||||||
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="enabled" label="Enabled"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
|
|
||||||
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="tls" label="TLS"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<PasswordFieldWide name="pass" label="Password" help="Network password" />
|
|
||||||
|
|
||||||
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
|
|
||||||
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
|
|
||||||
|
|
||||||
<PasswordFieldWide name="invite_command" label="Invite command" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6">
|
|
||||||
|
|
||||||
<FieldArray name="channels">
|
|
||||||
{({ fields }) => (
|
|
||||||
<div className="flex flex-col border-2 border-dashed p-4">
|
|
||||||
{fields && (fields.length as any) > 0 ? (
|
|
||||||
fields.map((name, index) => (
|
|
||||||
<div key={name} className="flex justify-between">
|
|
||||||
<div className="flex">
|
|
||||||
<Field
|
|
||||||
name={`${name}.name`}
|
|
||||||
component="input"
|
|
||||||
type="text"
|
|
||||||
placeholder="#Channel"
|
|
||||||
className="mr-4 focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 block w-full shadow-sm sm:text-sm rounded-md"
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
name={`${name}.password`}
|
|
||||||
component="input"
|
|
||||||
type="text"
|
|
||||||
placeholder="Password"
|
|
||||||
className="focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 block w-full shadow-sm sm:text-sm rounded-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
||||||
onClick={() => fields.remove(index)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Remove</span>
|
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span className="text-center text-sm text-grey-darker">
|
|
||||||
No channels!
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="border my-4 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded self-center text-center"
|
|
||||||
onClick={() => fields.push({ name: "", password: "" })}
|
|
||||||
>
|
|
||||||
Add Channel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FieldArray>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
|
|
||||||
<div className="space-x-3 flex justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={pristine || invalid}
|
|
||||||
className={classNames(pristine || invalid ? "bg-indigo-300" : "bg-indigo-600 hover:bg-indigo-700","inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500")}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DEBUG values={values}/>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IrcNetworkAddForm;
|
|
|
@ -1,297 +0,0 @@
|
||||||
import { Fragment, useEffect, useRef } from "react";
|
|
||||||
import { useMutation } from "react-query";
|
|
||||||
import { Network } from "../../domain/interfaces";
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
|
||||||
import { Field, Form } from "react-final-form";
|
|
||||||
import DEBUG from "../../components/debug";
|
|
||||||
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
|
|
||||||
import { queryClient } from "../../App";
|
|
||||||
|
|
||||||
import arrayMutators from "final-form-arrays";
|
|
||||||
import { FieldArray } from "react-final-form-arrays";
|
|
||||||
import { classNames } from "../../styles/utils";
|
|
||||||
import { useToggle } from "../../hooks/hooks";
|
|
||||||
import { DeleteModal } from "../../components/modals";
|
|
||||||
import APIClient from "../../api/APIClient";
|
|
||||||
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
|
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast';
|
|
||||||
import Toast from '../../components/notifications/Toast';
|
|
||||||
|
|
||||||
|
|
||||||
function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) {
|
|
||||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
|
|
||||||
const mutation = useMutation((network: Network) => APIClient.irc.updateNetwork(network), {
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(['networks']);
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />)
|
|
||||||
toggle()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(['networks']);
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />)
|
|
||||||
|
|
||||||
toggle()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("render add network form")
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
|
||||||
console.log(data)
|
|
||||||
|
|
||||||
// easy way to split textarea lines into array of strings for each newline.
|
|
||||||
// parse on the field didn't really work.
|
|
||||||
// TODO fix connect_commands on network update
|
|
||||||
// let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
|
|
||||||
// data.connect_commands = cmds
|
|
||||||
// console.log("formatted", data)
|
|
||||||
|
|
||||||
mutation.mutate(data)
|
|
||||||
};
|
|
||||||
|
|
||||||
const validate = (values: any) => {
|
|
||||||
const errors = {} as any;
|
|
||||||
|
|
||||||
if (!values.name) {
|
|
||||||
errors.name = "Required";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.server) {
|
|
||||||
errors.server = "Required";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.port) {
|
|
||||||
errors.port = "Required";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!values.nickserv?.account) {
|
|
||||||
errors.nickserv.account = "Required";
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelModalButtonRef = useRef(null)
|
|
||||||
const deleteAction = () => {
|
|
||||||
deleteMutation.mutate(network.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
|
||||||
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
|
|
||||||
<DeleteModal
|
|
||||||
isOpen={deleteModalIsOpen}
|
|
||||||
toggle={toggleDeleteModal}
|
|
||||||
buttonRef={cancelModalButtonRef}
|
|
||||||
deleteAction={deleteAction}
|
|
||||||
title="Remove network"
|
|
||||||
text="Are you sure you want to remove this network and channels? This action cannot be undone."
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
<Dialog.Overlay className="absolute inset-0" />
|
|
||||||
|
|
||||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
enterFrom="translate-x-full"
|
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="translate-x-full"
|
|
||||||
>
|
|
||||||
<div className="w-screen max-w-2xl">
|
|
||||||
|
|
||||||
<Form
|
|
||||||
initialValues={{
|
|
||||||
id: network.id,
|
|
||||||
name: network.name,
|
|
||||||
enabled: network.enabled,
|
|
||||||
server: network.server,
|
|
||||||
port: network.port,
|
|
||||||
tls: network.tls,
|
|
||||||
nickserv: network.nickserv,
|
|
||||||
pass: network.pass,
|
|
||||||
invite_command: network.invite_command,
|
|
||||||
// connect_commands: network.connect_commands,
|
|
||||||
channels: network.channels
|
|
||||||
}}
|
|
||||||
mutators={{
|
|
||||||
...arrayMutators
|
|
||||||
}}
|
|
||||||
validate={validate}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
{({ handleSubmit, values, pristine, invalid }) => {
|
|
||||||
return (
|
|
||||||
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
|
|
||||||
onSubmit={handleSubmit}>
|
|
||||||
<div className="flex-1">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="px-4 py-6 bg-gray-50 sm:px-6">
|
|
||||||
<div className="flex items-start justify-between space-x-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Dialog.Title
|
|
||||||
className="text-lg font-medium text-gray-900">Update network</Dialog.Title>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Update irc network.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-7 flex items-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close panel</span>
|
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
|
|
||||||
|
|
||||||
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="enabled" label="Enabled" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="px-6 space-y-1 mt-6">
|
|
||||||
<Dialog.Title className="text-lg font-medium text-gray-900">Connection</Dialog.Title>
|
|
||||||
{/* <p className="text-sm text-gray-500">
|
|
||||||
Networks, channels and invite commands are configured automatically.
|
|
||||||
</p> */}
|
|
||||||
</div>
|
|
||||||
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
|
|
||||||
<NumberFieldWide name="port" label="Port" />
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="tls" label="TLS" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<PasswordFieldWide name="pass" label="Password" help="Network password" />
|
|
||||||
|
|
||||||
<div className="px-6 space-y-1 border-t pt-6">
|
|
||||||
<Dialog.Title className="text-lg font-medium text-gray-900">Account</Dialog.Title>
|
|
||||||
{/* <p className="text-sm text-gray-500">
|
|
||||||
Networks, channels and invite commands are configured automatically.
|
|
||||||
</p> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TextFieldWide name="nickserv.account" label="NickServ Account" required={true} />
|
|
||||||
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
|
|
||||||
|
|
||||||
<PasswordFieldWide name="invite_command" label="Invite command" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6">
|
|
||||||
|
|
||||||
<FieldArray name="channels">
|
|
||||||
{({ fields }) => (
|
|
||||||
<div className="flex flex-col border-2 border-dashed p-4">
|
|
||||||
{fields && (fields.length as any) > 0 ? (
|
|
||||||
fields.map((name, index) => (
|
|
||||||
<div key={name} className="flex justify-between">
|
|
||||||
<div className="flex">
|
|
||||||
<Field
|
|
||||||
name={`${name}.name`}
|
|
||||||
component="input"
|
|
||||||
type="text"
|
|
||||||
placeholder="#Channel"
|
|
||||||
className="focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 block w-full shadow-sm sm:text-sm rounded-md"
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
name={`${name}.password`}
|
|
||||||
component="input"
|
|
||||||
type="text"
|
|
||||||
placeholder="Password"
|
|
||||||
className="focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 block w-full shadow-sm sm:text-sm rounded-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
||||||
onClick={() => fields.remove(index)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Remove</span>
|
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span className="text-center text-sm text-grey-darker">
|
|
||||||
No channels!
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="border my-4 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded self-center text-center"
|
|
||||||
onClick={() => fields.push({ name: "", password: "" })}
|
|
||||||
>
|
|
||||||
Add Channel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FieldArray>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
|
|
||||||
<div className="space-x-3 flex justify-between">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
|
||||||
onClick={toggleDeleteModal}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mr-4 bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
// disabled={pristine || invalid}
|
|
||||||
className={classNames(pristine || invalid ? "bg-indigo-300" : "bg-indigo-600 hover:bg-indigo-700", "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500")}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DEBUG values={values} />
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IrcNetworkUpdateForm;
|
|
|
@ -1,245 +0,0 @@
|
||||||
import { Fragment, useState } from "react";
|
|
||||||
import { useMutation } from "react-query";
|
|
||||||
import {
|
|
||||||
DOWNLOAD_CLIENT_TYPES,
|
|
||||||
DownloadClient,
|
|
||||||
} from "../../../domain/interfaces";
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
|
||||||
import { classNames } from "../../../styles/utils";
|
|
||||||
import { Form } from "react-final-form";
|
|
||||||
import DEBUG from "../../../components/debug";
|
|
||||||
import { SwitchGroup, TextFieldWide } from "../../../components/inputs";
|
|
||||||
import { queryClient } from "../../../App";
|
|
||||||
import APIClient from "../../../api/APIClient";
|
|
||||||
import { sleep } from "../../../utils/utils";
|
|
||||||
import { DownloadClientTypeOptions } from "../../../domain/constants";
|
|
||||||
import { RadioFieldsetWide } from "../../../components/inputs/wide";
|
|
||||||
import { componentMap, rulesComponentMap } from "./shared";
|
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import Toast from '../../../components/notifications/Toast';
|
|
||||||
|
|
||||||
function DownloadClientAddForm({ isOpen, toggle }: any) {
|
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
|
||||||
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
|
|
||||||
const [isErrorTest, setIsErrorTest] = useState(false);
|
|
||||||
|
|
||||||
const mutation = useMutation(
|
|
||||||
(client: DownloadClient) => APIClient.download_clients.create(client),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(["downloadClients"]);
|
|
||||||
toast.custom((t) => <Toast type="success" body="Client was added" t={t} />)
|
|
||||||
|
|
||||||
toggle();
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast.custom((t) => <Toast type="error" body="Client could not be added" t={t} />)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const testClientMutation = useMutation(
|
|
||||||
(client: DownloadClient) => APIClient.download_clients.test(client),
|
|
||||||
{
|
|
||||||
onMutate: () => {
|
|
||||||
setIsTesting(true);
|
|
||||||
setIsErrorTest(false);
|
|
||||||
setIsSuccessfulTest(false);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
sleep(1000)
|
|
||||||
.then(() => {
|
|
||||||
setIsTesting(false);
|
|
||||||
setIsSuccessfulTest(true);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
sleep(2500).then(() => {
|
|
||||||
setIsSuccessfulTest(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.log('not added')
|
|
||||||
setIsTesting(false);
|
|
||||||
setIsErrorTest(true);
|
|
||||||
sleep(2500).then(() => {
|
|
||||||
setIsErrorTest(false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
|
||||||
mutation.mutate(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const testClient = (data: any) => {
|
|
||||||
testClientMutation.mutate(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
|
||||||
<Dialog
|
|
||||||
as="div"
|
|
||||||
static
|
|
||||||
className="fixed inset-0 overflow-hidden"
|
|
||||||
open={isOpen}
|
|
||||||
onClose={toggle}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
<Dialog.Overlay className="absolute inset-0" />
|
|
||||||
|
|
||||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
enterFrom="translate-x-full"
|
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="translate-x-full"
|
|
||||||
>
|
|
||||||
<div className="w-screen max-w-2xl">
|
|
||||||
<Form
|
|
||||||
initialValues={{
|
|
||||||
name: "",
|
|
||||||
type: DOWNLOAD_CLIENT_TYPES.qBittorrent,
|
|
||||||
enabled: true,
|
|
||||||
host: "",
|
|
||||||
port: 10000,
|
|
||||||
ssl: false,
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
}}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
{({ handleSubmit, values }) => {
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="px-4 py-6 bg-gray-50 sm:px-6">
|
|
||||||
<div className="flex items-start justify-between space-x-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Dialog.Title className="text-lg font-medium text-gray-900">
|
|
||||||
Add client
|
|
||||||
</Dialog.Title>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Add download client.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-7 flex items-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close panel</span>
|
|
||||||
<XIcon
|
|
||||||
className="h-6 w-6"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<TextFieldWide name="name" label="Name" />
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="enabled" label="Enabled" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<RadioFieldsetWide
|
|
||||||
name="type"
|
|
||||||
legend="Type"
|
|
||||||
options={DownloadClientTypeOptions}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>{componentMap[values.type]}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{rulesComponentMap[values.type]}
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
|
|
||||||
<div className="space-x-3 flex justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={classNames(
|
|
||||||
isSuccessfulTest
|
|
||||||
? "text-green-500 border-green-500 bg-green-50"
|
|
||||||
: isErrorTest
|
|
||||||
? "text-red-500 border-red-500 bg-red-50"
|
|
||||||
: "border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
|
|
||||||
isTesting ? "cursor-not-allowed" : "",
|
|
||||||
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150"
|
|
||||||
)}
|
|
||||||
disabled={isTesting}
|
|
||||||
onClick={() => testClient(values)}
|
|
||||||
>
|
|
||||||
{isTesting ? (
|
|
||||||
<svg
|
|
||||||
className="animate-spin h-5 w-5 text-green-500"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
className="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
></circle>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
) : isSuccessfulTest ? (
|
|
||||||
"OK!"
|
|
||||||
) : isErrorTest ? (
|
|
||||||
"ERROR"
|
|
||||||
) : (
|
|
||||||
"Test"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DEBUG values={values} />
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DownloadClientAddForm;
|
|
|
@ -1,279 +0,0 @@
|
||||||
import { Fragment, useRef, useState } from "react";
|
|
||||||
import { useToggle } from "../../../hooks/hooks";
|
|
||||||
import { useMutation } from "react-query";
|
|
||||||
import { DownloadClient } from "../../../domain/interfaces";
|
|
||||||
import { queryClient } from "../../../App";
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
|
||||||
import { classNames } from "../../../styles/utils";
|
|
||||||
import { Form } from "react-final-form";
|
|
||||||
import DEBUG from "../../../components/debug";
|
|
||||||
import { SwitchGroup, TextFieldWide } from "../../../components/inputs";
|
|
||||||
import { DownloadClientTypeOptions } from "../../../domain/constants";
|
|
||||||
import APIClient from "../../../api/APIClient";
|
|
||||||
import { sleep } from "../../../utils/utils";
|
|
||||||
import { componentMap, rulesComponentMap } from "./shared";
|
|
||||||
import { RadioFieldsetWide } from "../../../components/inputs/wide";
|
|
||||||
import { DeleteModal } from "../../../components/modals";
|
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import Toast from '../../../components/notifications/Toast';
|
|
||||||
|
|
||||||
|
|
||||||
function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
|
||||||
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
|
|
||||||
const [isErrorTest, setIsErrorTest] = useState(false);
|
|
||||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
|
||||||
|
|
||||||
const mutation = useMutation(
|
|
||||||
(client: DownloadClient) => APIClient.download_clients.update(client),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(["downloadClients"]);
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t} />)
|
|
||||||
toggle();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const deleteMutation = useMutation(
|
|
||||||
(clientID: number) => APIClient.download_clients.delete(clientID),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries();
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${client.name} was deleted.`} t={t}/>)
|
|
||||||
toggleDeleteModal();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const testClientMutation = useMutation(
|
|
||||||
(client: DownloadClient) => APIClient.download_clients.test(client),
|
|
||||||
{
|
|
||||||
onMutate: () => {
|
|
||||||
setIsTesting(true);
|
|
||||||
setIsErrorTest(false);
|
|
||||||
setIsSuccessfulTest(false);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
sleep(1000)
|
|
||||||
.then(() => {
|
|
||||||
setIsTesting(false);
|
|
||||||
setIsSuccessfulTest(true);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
sleep(2500).then(() => {
|
|
||||||
setIsSuccessfulTest(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setIsTesting(false);
|
|
||||||
setIsErrorTest(true);
|
|
||||||
sleep(2500).then(() => {
|
|
||||||
setIsErrorTest(false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
|
||||||
mutation.mutate(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelButtonRef = useRef(null);
|
|
||||||
const cancelModalButtonRef = useRef(null);
|
|
||||||
|
|
||||||
const deleteAction = () => {
|
|
||||||
deleteMutation.mutate(client.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const testClient = (data: any) => {
|
|
||||||
testClientMutation.mutate(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
|
||||||
<Dialog
|
|
||||||
as="div"
|
|
||||||
static
|
|
||||||
className="fixed inset-0 overflow-hidden"
|
|
||||||
open={isOpen}
|
|
||||||
onClose={toggle}
|
|
||||||
initialFocus={cancelButtonRef}
|
|
||||||
>
|
|
||||||
<DeleteModal
|
|
||||||
isOpen={deleteModalIsOpen}
|
|
||||||
toggle={toggleDeleteModal}
|
|
||||||
buttonRef={cancelModalButtonRef}
|
|
||||||
deleteAction={deleteAction}
|
|
||||||
title="Remove download client"
|
|
||||||
text="Are you sure you want to remove this download client? This action cannot be undone."
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
<Dialog.Overlay className="absolute inset-0" />
|
|
||||||
|
|
||||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
enterFrom="translate-x-full"
|
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="translate-x-full"
|
|
||||||
>
|
|
||||||
<div className="w-screen max-w-2xl">
|
|
||||||
<Form
|
|
||||||
initialValues={{
|
|
||||||
id: client.id,
|
|
||||||
name: client.name,
|
|
||||||
type: client.type,
|
|
||||||
enabled: client.enabled,
|
|
||||||
host: client.host,
|
|
||||||
port: client.port,
|
|
||||||
ssl: client.ssl,
|
|
||||||
username: client.username,
|
|
||||||
password: client.password,
|
|
||||||
settings: client.settings,
|
|
||||||
}}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
{({ handleSubmit, values }) => {
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="px-4 py-6 bg-gray-50 sm:px-6">
|
|
||||||
<div className="flex items-start justify-between space-x-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Dialog.Title className="text-lg font-medium text-gray-900">
|
|
||||||
Edit client
|
|
||||||
</Dialog.Title>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Edit download client settings.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-7 flex items-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close panel</span>
|
|
||||||
<XIcon
|
|
||||||
className="h-6 w-6"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<TextFieldWide name="name" label="Name" />
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="enabled" label="Enabled" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<RadioFieldsetWide
|
|
||||||
name="type"
|
|
||||||
legend="Type"
|
|
||||||
options={DownloadClientTypeOptions}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>{componentMap[values.type]}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{rulesComponentMap[values.type]}
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
|
|
||||||
<div className="space-x-3 flex justify-between">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
|
||||||
onClick={toggleDeleteModal}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
<div className="flex">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={classNames(
|
|
||||||
isSuccessfulTest
|
|
||||||
? "text-green-500 border-green-500 bg-green-50"
|
|
||||||
: isErrorTest
|
|
||||||
? "text-red-500 border-red-500 bg-red-50"
|
|
||||||
: "border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
|
|
||||||
isTesting ? "cursor-not-allowed" : "",
|
|
||||||
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150"
|
|
||||||
)}
|
|
||||||
disabled={isTesting}
|
|
||||||
onClick={() => testClient(values)}
|
|
||||||
>
|
|
||||||
{isTesting ? (
|
|
||||||
<svg
|
|
||||||
className="animate-spin h-5 w-5 text-green-500"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
className="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
></circle>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
) : isSuccessfulTest ? (
|
|
||||||
"OK!"
|
|
||||||
) : isErrorTest ? (
|
|
||||||
"ERROR"
|
|
||||||
) : (
|
|
||||||
"Test"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="ml-4 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DEBUG values={values} />
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DownloadClientUpdateForm;
|
|
|
@ -1,122 +0,0 @@
|
||||||
import { Fragment } from "react";
|
|
||||||
import { SwitchGroup, TextFieldWide } from "../../../components/inputs";
|
|
||||||
import { NumberFieldWide, PasswordFieldWide } from "../../../components/inputs/wide";
|
|
||||||
import { useField } from "react-final-form";
|
|
||||||
import { Dialog } from "@headlessui/react";
|
|
||||||
|
|
||||||
function FormFieldsDefault() {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<TextFieldWide name="host" label="Host" help="Url domain.ltd/client" />
|
|
||||||
|
|
||||||
<NumberFieldWide name="port" label="Port" />
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="ssl" label="SSL" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TextFieldWide name="username" label="Username" />
|
|
||||||
<PasswordFieldWide name="password" label="Password" />
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormFieldsArr() {
|
|
||||||
const { input } = useField("settings.basic.auth");
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<TextFieldWide name="host" label="Host" help="Full url like http(s)://domain.ltd/" />
|
|
||||||
|
|
||||||
<PasswordFieldWide name="settings.apikey" label="API key" />
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="settings.basic.auth" label="Basic auth" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{input.value === true && (
|
|
||||||
<Fragment>
|
|
||||||
<TextFieldWide name="settings.basic.username" label="Username" />
|
|
||||||
<PasswordFieldWide name="settings.basic.password" label="Password" />
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const componentMap: any = {
|
|
||||||
DELUGE_V1: <FormFieldsDefault />,
|
|
||||||
DELUGE_V2: <FormFieldsDefault />,
|
|
||||||
QBITTORRENT: <FormFieldsDefault />,
|
|
||||||
RADARR: <FormFieldsArr />,
|
|
||||||
SONARR: <FormFieldsArr />,
|
|
||||||
LIDARR: <FormFieldsArr />,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function FormFieldsRulesBasic() {
|
|
||||||
const { input: enabled } = useField("settings.rules.enabled");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="border-t border-gray-200 py-5">
|
|
||||||
|
|
||||||
<div className="px-6 space-y-1">
|
|
||||||
<Dialog.Title className="text-lg font-medium text-gray-900">Rules</Dialog.Title>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Manage max downloads.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{enabled.value === true && (
|
|
||||||
<Fragment>
|
|
||||||
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormFieldsRules() {
|
|
||||||
const { input } = useField("settings.rules.ignore_slow_torrents");
|
|
||||||
const { input: enabled } = useField("settings.rules.enabled");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="border-t border-gray-200 py-5">
|
|
||||||
|
|
||||||
<div className="px-6 space-y-1">
|
|
||||||
<Dialog.Title className="text-lg font-medium text-gray-900">Rules</Dialog.Title>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Manage max downloads etc.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{enabled.value === true && (
|
|
||||||
<Fragment>
|
|
||||||
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{input.value === true && (
|
|
||||||
<Fragment>
|
|
||||||
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold" placeholder="in KB/s" help="If download speed is below this when max active downloads is hit, download anyways. KB/s" />
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rulesComponentMap: any = {
|
|
||||||
DELUGE_V1: <FormFieldsRulesBasic />,
|
|
||||||
DELUGE_V2: <FormFieldsRulesBasic />,
|
|
||||||
QBITTORRENT: <FormFieldsRules />,
|
|
||||||
};
|
|
|
@ -17,7 +17,7 @@ export default function Base() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
<Disclosure as="nav" className="bg-gray-800 pb-48">
|
<Disclosure as="nav" className="bg-gray-900 pb-48">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
@ -40,11 +40,34 @@ export default function Base() {
|
||||||
<div className="flex items-baseline space-x-4">
|
<div className="flex items-baseline space-x-4">
|
||||||
{nav.map((item, itemIdx) =>
|
{nav.map((item, itemIdx) =>
|
||||||
<NavLink
|
<NavLink
|
||||||
key={itemIdx}
|
key={item.name + itemIdx}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
exact={true}
|
strict={true}
|
||||||
activeClassName="bg-gray-900 text-white "
|
isActive={(match, location) => {
|
||||||
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
if (match?.url === "/" && item.path === "/" && location.pathname === "/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (item.path ==="/" && location.pathname ==="/") {
|
||||||
|
// console.log("match base");
|
||||||
|
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.url === "/") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
activeClassName="bg-gray-900 dark:bg-gray-700 text-white "
|
||||||
|
className={classNames(
|
||||||
|
"text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
@ -59,7 +82,7 @@ export default function Base() {
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Menu.Button
|
<Menu.Button
|
||||||
className="max-w-xs bg-gray-800 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
|
className="max-w-xs rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
|
||||||
<span
|
<span
|
||||||
className="hidden text-gray-300 text-sm font-medium sm:block">
|
className="hidden text-gray-300 text-sm font-medium sm:block">
|
||||||
<span className="sr-only">Open user menu for </span>User
|
<span className="sr-only">Open user menu for </span>User
|
||||||
|
@ -82,15 +105,15 @@ export default function Base() {
|
||||||
>
|
>
|
||||||
<Menu.Items
|
<Menu.Items
|
||||||
static
|
static
|
||||||
className="origin-top-right absolute right-0 mt-2 w-48 z-10 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
|
className="origin-top-right absolute right-0 mt-2 w-48 z-10 rounded-md shadow-lg py-1 bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
>
|
>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<Link
|
<Link
|
||||||
to="settings"
|
to="settings"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? 'bg-gray-100' : '',
|
active ? 'bg-gray-100 dark:bg-gray-600' : '',
|
||||||
'block px-4 py-2 text-sm text-gray-700'
|
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
|
@ -102,8 +125,8 @@ export default function Base() {
|
||||||
<Link
|
<Link
|
||||||
to="/logout"
|
to="/logout"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? 'bg-gray-100' : '',
|
active ? 'bg-gray-100 dark:bg-gray-600' : '',
|
||||||
'block px-4 py-2 text-sm text-gray-700'
|
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
|
@ -138,7 +161,6 @@ export default function Base() {
|
||||||
{nav.map((item, itemIdx) =>
|
{nav.map((item, itemIdx) =>
|
||||||
itemIdx === 0 ? (
|
itemIdx === 0 ? (
|
||||||
<Fragment key={item.path}>
|
<Fragment key={item.path}>
|
||||||
{/* Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" */}
|
|
||||||
<Link to={item.path}
|
<Link to={item.path}
|
||||||
className="bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium">
|
className="bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium">
|
||||||
{item.name}
|
{item.name}
|
||||||
|
|
|
@ -7,12 +7,10 @@ export function Dashboard() {
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow px-5 py-6 sm:px-6">
|
||||||
<div className="border-4 border-dashed border-gray-200 rounded-lg h-96" />
|
<div className="border-4 border-dashed border-gray-200 dark:border-gray-700 rounded-lg h-96" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default function Logs() {
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="max-w-7xl mx-auto pb-12 px-2 sm:px-4 lg:px-8">
|
<div className="max-w-7xl mx-auto pb-12 px-2 sm:px-4 lg:px-8">
|
||||||
<div className="bg-white rounded-lg shadow px-2 sm:px-4 py-3 sm:py-4">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow px-2 sm:px-4 py-3 sm:py-4">
|
||||||
<div className=" overflow-y-auto p-2 rounded-lg h-96 bg-gray-900">
|
<div className=" overflow-y-auto p-2 rounded-lg h-96 bg-gray-900">
|
||||||
{logs.map((a, idx) => (
|
{logs.map((a, idx) => (
|
||||||
<p key={idx}>
|
<p key={idx}>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import React from 'react'
|
|
||||||
import {CogIcon, DownloadIcon, KeyIcon} from '@heroicons/react/outline'
|
import {CogIcon, DownloadIcon, KeyIcon} from '@heroicons/react/outline'
|
||||||
import {NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch} from "react-router-dom";
|
import {NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch} from "react-router-dom";
|
||||||
import IndexerSettings from "./settings/Indexer";
|
import IndexerSettings from "./settings/Indexer";
|
||||||
|
@ -33,17 +32,17 @@ function SubNavLink({item, url}: any) {
|
||||||
key={item.name}
|
key={item.name}
|
||||||
to={too}
|
to={too}
|
||||||
exact={true}
|
exact={true}
|
||||||
activeClassName="bg-teal-50 border-teal-500 text-teal-700 hover:bg-teal-50 hover:text-teal-700"
|
activeClassName="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"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 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'
|
||||||
)}
|
)}
|
||||||
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
|
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
|
||||||
>
|
>
|
||||||
<item.icon
|
<item.icon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
splitLocation[2] === item.href
|
splitLocation[2] === item.href
|
||||||
? 'text-teal-500 group-hover:text-teal-500'
|
? 'text-teal-500 dark:text-blue-600 group-hover:text-teal-500 dark:group-hover:text-blue-600'
|
||||||
: 'text-gray-400 group-hover:text-gray-500',
|
: 'text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300',
|
||||||
'flex-shrink-0 -ml-1 mr-3 h-6 w-6'
|
'flex-shrink-0 -ml-1 mr-3 h-6 w-6'
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
@ -77,8 +76,8 @@ export default function Settings() {
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
||||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
||||||
<div className="divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
|
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
|
||||||
<SidebarNav url={url} subNavigation={subNavigation}/>
|
<SidebarNav url={url} subNavigation={subNavigation}/>
|
||||||
|
|
||||||
<RouteSwitch>
|
<RouteSwitch>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import {useMutation} from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
import {Form} from "react-final-form";
|
import { Form } from "react-final-form";
|
||||||
import {PasswordField, TextField} from "../../components/inputs";
|
import { PasswordField, TextField } from "../../components/inputs";
|
||||||
import {useRecoilState} from "recoil";
|
import { useRecoilState } from "recoil";
|
||||||
import {isLoggedIn} from "../../state/state";
|
import { isLoggedIn } from "../../state/state";
|
||||||
import {useHistory} from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import {useEffect} from "react";
|
import { useEffect } from "react";
|
||||||
import logo from "../../logo.png"
|
import logo from "../../logo.png"
|
||||||
|
|
||||||
interface loginData {
|
interface loginData {
|
||||||
|
@ -18,7 +18,7 @@ function Login() {
|
||||||
let history = useHistory();
|
let history = useHistory();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(loggedIn) {
|
if (loggedIn) {
|
||||||
// setLoading(false);
|
// setLoading(false);
|
||||||
history.push('/');
|
history.push('/');
|
||||||
} else {
|
} else {
|
||||||
|
@ -38,7 +38,7 @@ function Login() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
<div className="sm:mx-auto sm:w-full sm:max-w-md mb-6">
|
<div className="sm:mx-auto sm:w-full sm:max-w-md mb-6">
|
||||||
<img
|
<img
|
||||||
className="mx-auto h-12 w-auto"
|
className="mx-auto h-12 w-auto"
|
||||||
|
@ -48,7 +48,7 @@ function Login() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
<div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
initialValues={{
|
initialValues={{
|
||||||
|
@ -57,11 +57,11 @@ function Login() {
|
||||||
}}
|
}}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
{({handleSubmit, values}) => {
|
{({ handleSubmit, values }) => {
|
||||||
return (
|
return (
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||||
<TextField name="username" label="Username" autoComplete="username" />
|
<TextField name="username" label="Username" autoComplete="username" />
|
||||||
<PasswordField name="password" label="password" autoComplete="current-password"/>
|
<PasswordField name="password" label="password" autoComplete="current-password" />
|
||||||
|
|
||||||
{/*<div className="flex items-center justify-between">*/}
|
{/*<div className="flex items-center justify-between">*/}
|
||||||
{/* <div className="flex items-center">*/}
|
{/* <div className="flex items-center">*/}
|
||||||
|
@ -86,7 +86,7 @@ function Login() {
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -9,18 +9,18 @@ function Logout() {
|
||||||
const [loggedIn, setLoggedIn] = useRecoilState(isLoggedIn);
|
const [loggedIn, setLoggedIn] = useRecoilState(isLoggedIn);
|
||||||
let history = useHistory();
|
let history = useHistory();
|
||||||
|
|
||||||
const [_, removeCookie] = useCookies(['user_session']);
|
const [,, removeCookie] = useCookies(['user_session']);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
APIClient.auth.logout().then(r => {
|
APIClient.auth.logout().then(r => {
|
||||||
removeCookie("user_session", "")
|
removeCookie("user_session")
|
||||||
setLoggedIn(false);
|
setLoggedIn(false);
|
||||||
history.push('/login');
|
history.push('/login');
|
||||||
})
|
})
|
||||||
}, [loggedIn, history, removeCookie, setLoggedIn])
|
}, [loggedIn, history, removeCookie, setLoggedIn])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-800 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
<p>Logged out</p>
|
<p>Logged out</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Fragment, useRef } from "react";
|
import { Fragment, useRef } from "react";
|
||||||
import { Dialog, Transition, Switch as SwitchBasic } from "@headlessui/react";
|
import { Dialog, Transition, Switch as SwitchBasic } from "@headlessui/react";
|
||||||
import { ChevronDownIcon, ChevronRightIcon, ExclamationIcon, } from '@heroicons/react/solid'
|
import { ChevronDownIcon, ChevronRightIcon, ExclamationIcon, } from '@heroicons/react/solid'
|
||||||
import { EmptyListState } from "../../components/EmptyListState";
|
import { EmptyListState } from "../../components/EmptyListState";
|
||||||
|
@ -55,9 +55,9 @@ function TabNavLink({ item, url }: any) {
|
||||||
key={item.name}
|
key={item.name}
|
||||||
to={too}
|
to={too}
|
||||||
exact={true}
|
exact={true}
|
||||||
activeClassName="border-purple-600 text-purple-600"
|
activeClassName="border-purple-600 dark:border-blue-500 text-purple-600 dark:text-white"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'border-transparent text-gray-500 hover:text-purple-600 hover:border-purple-600 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm'
|
'border-transparent text-gray-500 hover:text-purple-600 dark:hover:text-white hover:border-purple-600 dark:hover:border-blue-500 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm'
|
||||||
)}
|
)}
|
||||||
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
|
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
|
||||||
>
|
>
|
||||||
|
@ -72,7 +72,7 @@ const FormButtonsGroup = ({ deleteAction, reset, dirty }: any) => {
|
||||||
const cancelButtonRef = useRef(null)
|
const cancelButtonRef = useRef(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-6 divide-y divide-gray-200">
|
<div className="pt-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
|
||||||
<Transition.Root show={deleteModalIsOpen} as={Fragment}>
|
<Transition.Root show={deleteModalIsOpen} as={Fragment}>
|
||||||
<Dialog
|
<Dialog
|
||||||
|
@ -97,7 +97,6 @@ const FormButtonsGroup = ({ deleteAction, reset, dirty }: any) => {
|
||||||
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||||
</Transition.Child>
|
</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 className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||||
​
|
​
|
||||||
</span>
|
</span>
|
||||||
|
@ -142,7 +141,7 @@ const FormButtonsGroup = ({ deleteAction, reset, dirty }: any) => {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="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"
|
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 light:bg-white text-base font-medium text-gray-700 dark:text-red-500 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}
|
onClick={toggleDeleteModal}
|
||||||
ref={cancelButtonRef}
|
ref={cancelButtonRef}
|
||||||
>
|
>
|
||||||
|
@ -158,7 +157,7 @@ const FormButtonsGroup = ({ deleteAction, reset, dirty }: any) => {
|
||||||
<div className="mt-4 pt-4 flex justify-between">
|
<div className="mt-4 pt-4 flex justify-between">
|
||||||
<button
|
<button
|
||||||
type="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"
|
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-500 light:bg-red-100 light:hover:bg-red-200 dark:hover:text-red-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
||||||
onClick={toggleDeleteModal}
|
onClick={toggleDeleteModal}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
|
@ -168,14 +167,14 @@ const FormButtonsGroup = ({ deleteAction, reset, dirty }: any) => {
|
||||||
{/* {dirty && <span className="mr-4 text-sm text-gray-500">Unsaved changes..</span>} */}
|
{/* {dirty && <span className="mr-4 text-sm text-gray-500">Unsaved changes..</span>} */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500"
|
className="light:bg-white light:border light:border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 dark:text-gray-500 light:hover:bg-gray-50 dark:hover:text-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
onClick={reset}
|
onClick={reset}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="ml-4 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="ml-4 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"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
@ -266,7 +265,7 @@ export default function FilterDetails() {
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div className="bg-white rounded-lg shadow">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||||
<div className="relative mx-auto md:px-6 xl:px-4">
|
<div className="relative mx-auto md:px-6 xl:px-4">
|
||||||
<div className="px-4 sm:px-6 md:px-0">
|
<div className="px-4 sm:px-6 md:px-0">
|
||||||
<div className="pt-2 pb-6">
|
<div className="pt-2 pb-6">
|
||||||
|
@ -278,7 +277,7 @@ export default function FilterDetails() {
|
||||||
<select
|
<select
|
||||||
id="selected-tab"
|
id="selected-tab"
|
||||||
name="selected-tab"
|
name="selected-tab"
|
||||||
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm rounded-md"
|
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-700 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm rounded-md"
|
||||||
>
|
>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<option key={tab.name} onClick={(e) => handleMobileNav(e, tab.href)}>
|
<option key={tab.name} onClick={(e) => handleMobileNav(e, tab.href)}>
|
||||||
|
@ -288,7 +287,7 @@ export default function FilterDetails() {
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:block">
|
<div className="hidden sm:block">
|
||||||
<div className="border-b border-gray-200">
|
<div className="border-b border-gray-200 dark:border-gray-700">
|
||||||
<nav className="-mb-px flex space-x-8">
|
<nav className="-mb-px flex space-x-8">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<TabNavLink item={tab} url={url} key={tab.href} />
|
<TabNavLink item={tab} url={url} key={tab.href} />
|
||||||
|
@ -385,7 +384,7 @@ function General({ indexers }: GeneralProps) {
|
||||||
<TextField name="name" label="Filter name" columns={6} placeholder="eg. Filter 1" />
|
<TextField name="name" label="Filter name" columns={6} placeholder="eg. Filter 1" />
|
||||||
|
|
||||||
<div className="col-span-6">
|
<div className="col-span-6">
|
||||||
<label htmlFor="indexers" className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
|
<label htmlFor="indexers" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
Indexers
|
Indexers
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
@ -409,7 +408,7 @@ function General({ indexers }: GeneralProps) {
|
||||||
isClearable={true}
|
isClearable={true}
|
||||||
isMulti={true}
|
isMulti={true}
|
||||||
placeholder="Choose indexers"
|
placeholder="Choose indexers"
|
||||||
className="mt-2 block w-full focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm"
|
className="mt-2 block w-full focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
options={opts}
|
options={opts}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -429,7 +428,7 @@ function General({ indexers }: GeneralProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t">
|
<div className="border-t dark:border-gray-700">
|
||||||
<SwitchGroup name="enabled" label="Enabled" description="Enabled or disable filter." />
|
<SwitchGroup name="enabled" label="Enabled" description="Enabled or disable filter." />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -485,11 +484,11 @@ function Advanced() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleReleases}>
|
<div className="flex justify-between items-center cursor-pointer" onClick={toggleReleases}>
|
||||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Releases</h3>
|
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Releases</h3>
|
||||||
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore</p>
|
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match or ignore</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
<button
|
<button
|
||||||
|
@ -508,11 +507,11 @@ function Advanced() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleGroups}>
|
<div className="flex justify-between items-center cursor-pointer" onClick={toggleGroups}>
|
||||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Groups</h3>
|
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Groups</h3>
|
||||||
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore</p>
|
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match or ignore</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
<button
|
<button
|
||||||
|
@ -531,11 +530,11 @@ function Advanced() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleCategories}>
|
<div className="flex justify-between items-center cursor-pointer" onClick={toggleCategories}>
|
||||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Categories and tags</h3>
|
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Categories and tags</h3>
|
||||||
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore categories or tags</p>
|
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match or ignore categories or tags</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
<button
|
<button
|
||||||
|
@ -557,11 +556,11 @@ function Advanced() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleUploaders}>
|
<div className="flex justify-between items-center cursor-pointer" onClick={toggleUploaders}>
|
||||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Uploaders</h3>
|
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Uploaders</h3>
|
||||||
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore uploaders</p>
|
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match or ignore uploaders</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
<button
|
<button
|
||||||
|
@ -580,11 +579,11 @@ function Advanced() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleFreeleech}>
|
<div className="flex justify-between items-center cursor-pointer" onClick={toggleFreeleech}>
|
||||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Freeleech</h3>
|
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Freeleech</h3>
|
||||||
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match only freeleech and freeleech percent</p>
|
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match only freeleech and freeleech percent</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
<button
|
<button
|
||||||
|
@ -615,8 +614,6 @@ interface FilterActionsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function FilterActions({ filter, values }: FilterActionsProps) {
|
function FilterActions({ filter, values }: FilterActionsProps) {
|
||||||
// const [addActionIsOpen, toggleAddAction] = useToggle(false)
|
|
||||||
|
|
||||||
const { data } = useQuery<DownloadClient[], Error>('downloadClients', APIClient.download_clients.getAll,
|
const { data } = useQuery<DownloadClient[], Error>('downloadClients', APIClient.download_clients.getAll,
|
||||||
{
|
{
|
||||||
refetchOnWindowFocus: false
|
refetchOnWindowFocus: false
|
||||||
|
@ -644,36 +641,35 @@ function FilterActions({ filter, values }: FilterActionsProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
{/* {addActionIsOpen &&
|
|
||||||
<FilterActionAddForm filter={filter} clients={data || []} isOpen={addActionIsOpen} toggle={toggleAddAction} />
|
|
||||||
} */}
|
|
||||||
<FieldArray name="actions">
|
<FieldArray name="actions">
|
||||||
{({ remove, push }) => (
|
{({ remove, push }) => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="-ml-4 -mt-4 mb-6 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
<div className="-ml-4 -mt-4 mb-6 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 dark:text-gray-200">Actions</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Add to download clients or run custom commands.
|
Add to download clients or run custom commands.
|
||||||
</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 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
onClick={() => push(newAction)}
|
onClick={() => push(newAction)}
|
||||||
>
|
>
|
||||||
Add new
|
Add new
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white shadow sm:rounded-md">
|
|
||||||
|
<div className="light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||||
{values.actions.length > 0 ?
|
{values.actions.length > 0 ?
|
||||||
values.actions.map((action: any, index: any) => (
|
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<ul className="divide-y divide-gray-200" key={index}>
|
{values.actions.map((action: any, index: any) => (
|
||||||
<FilterActionsItem action={action} clients={data!} idx={index} remove={remove} />
|
<FilterActionsItem action={action} clients={data!} idx={index} remove={remove} />
|
||||||
</ul>
|
))}
|
||||||
)) : <EmptyListState text="No actions yet!" />
|
</ul>
|
||||||
|
: <EmptyListState text="No actions yet!" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -867,8 +863,8 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
idx % 2 === 0 ? "bg-white" : "bg-gray-50",
|
idx % 2 === 0 ? "bg-white dark:bg-gray-800" : "bg-gray-50 dark:bg-gray-700",
|
||||||
"flex items-center sm:px-6 hover:bg-gray-50"
|
"flex items-center sm:px-6 hover:bg-gray-50 dark:hover:bg-gray-600"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Field name={`actions.${idx}.enabled`} type="checkbox">
|
<Field name={`actions.${idx}.enabled`} type="checkbox">
|
||||||
|
@ -885,8 +881,8 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
|
||||||
setFieldValue(field?.name ?? '', value)
|
setFieldValue(field?.name ?? '', value)
|
||||||
}}
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
field.value ? 'bg-teal-500' : 'bg-gray-200',
|
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">toggle enabled</span>
|
<span className="sr-only">toggle enabled</span>
|
||||||
|
@ -905,14 +901,14 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
|
||||||
<div className="min-w-0 flex-1 sm:flex sm:items-center sm:justify-between">
|
<div className="min-w-0 flex-1 sm:flex sm:items-center sm:justify-between">
|
||||||
<div className="truncate">
|
<div className="truncate">
|
||||||
<div className="flex text-sm">
|
<div className="flex text-sm">
|
||||||
<p className="ml-4 font-medium text-indigo-600 truncate">
|
<p className="ml-4 font-medium text-indigo-600 dark:text-gray-100 truncate">
|
||||||
{action.name}
|
{action.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex-shrink-0 sm:mt-0 sm:ml-5">
|
<div className="mt-4 flex-shrink-0 sm:mt-0 sm:ml-5">
|
||||||
<div className="flex overflow-hidden -space-x-1">
|
<div className="flex overflow-hidden -space-x-1">
|
||||||
<span className="text-sm font-normal text-gray-500">
|
<span className="text-sm font-normal text-gray-500 dark:text-gray-400">
|
||||||
{ActionTypeNameMap[action.type]}
|
{ActionTypeNameMap[action.type]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -928,7 +924,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{edit && (
|
{edit && (
|
||||||
<div className="px-4 py-4 flex items-center sm:px-6">
|
<div className="px-4 py-4 flex items-center sm:px-6 border dark:border-gray-600">
|
||||||
<Transition.Root show={deleteModalIsOpen} as={Fragment}>
|
<Transition.Root show={deleteModalIsOpen} as={Fragment}>
|
||||||
<Dialog
|
<Dialog
|
||||||
as="div"
|
as="div"
|
||||||
|
@ -968,7 +964,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
|
||||||
<div className="mt-4 pt-4 flex justify-between">
|
<div className="mt-4 pt-4 flex justify-between">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex items-center justify-center py-2 border border-transparent font-medium rounded-md text-red-700 hover:text-red-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
className="inline-flex items-center justify-center py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-500 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
||||||
onClick={toggleDeleteModal}
|
onClick={toggleDeleteModal}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
|
@ -977,7 +973,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500"
|
className="light:bg-white light:border light:border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 dark:text-gray-500 light:hover:bg-gray-50 dark:hover:text-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
onClick={toggleEdit}
|
onClick={toggleEdit}
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Transition, Listbox } from "@headlessui/react";
|
import { Transition, Listbox } from "@headlessui/react";
|
||||||
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid';
|
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid';
|
||||||
import { Action, DownloadClient } from "../../../domain/interfaces";
|
import { Action, DownloadClient } from "../../../domain/interfaces";
|
||||||
|
@ -21,17 +21,17 @@ export default function DownloadClientSelect({
|
||||||
field,
|
field,
|
||||||
form: { setFieldValue },
|
form: { setFieldValue },
|
||||||
}: any) => (
|
}: any) => (
|
||||||
<Listbox
|
<Listbox
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={(value: any) => setFieldValue(field?.name, value)}
|
onChange={(value: any) => setFieldValue(field?.name, value)}
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Listbox.Label className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
|
<Listbox.Label className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
Client
|
Client
|
||||||
</Listbox.Label>
|
</Listbox.Label>
|
||||||
<div className="mt-2 relative">
|
<div className="mt-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">
|
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
|
||||||
<span className="block truncate">
|
<span className="block truncate">
|
||||||
{field.value
|
{field.value
|
||||||
? clients.find((c) => c.id === field.value)!.name
|
? clients.find((c) => c.id === field.value)!.name
|
||||||
|
@ -40,7 +40,7 @@ export default function DownloadClientSelect({
|
||||||
{/*<span className="block truncate">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">
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<SelectorIcon
|
<SelectorIcon
|
||||||
className="h-5 w-5 text-gray-400"
|
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
||||||
aria-hidden="true" />
|
aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
</Listbox.Button>
|
</Listbox.Button>
|
||||||
|
@ -54,7 +54,7 @@ export default function DownloadClientSelect({
|
||||||
>
|
>
|
||||||
<Listbox.Options
|
<Listbox.Options
|
||||||
static
|
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"
|
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 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
|
{clients
|
||||||
.filter((c) => c.type === action.type)
|
.filter((c) => c.type === action.type)
|
||||||
|
@ -63,8 +63,8 @@ export default function DownloadClientSelect({
|
||||||
key={client.id}
|
key={client.id}
|
||||||
className={({ active }) => classNames(
|
className={({ active }) => classNames(
|
||||||
active
|
active
|
||||||
? "text-white bg-indigo-600"
|
? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
|
||||||
: "text-gray-900",
|
: "text-gray-900 dark:text-gray-300",
|
||||||
"cursor-default select-none relative py-2 pl-3 pr-9"
|
"cursor-default select-none relative py-2 pl-3 pr-9"
|
||||||
)}
|
)}
|
||||||
value={client.id}
|
value={client.id}
|
||||||
|
@ -83,7 +83,7 @@ export default function DownloadClientSelect({
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-indigo-600",
|
active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
|
||||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -24,7 +24,7 @@ const MultiSelect: React.FC<Props> = ({
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
|
className="block uppercase tracking-wide text-gray-700 dark:text-gray-200 text-xs font-bold mb-2"
|
||||||
htmlFor={label}
|
htmlFor={label}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
@ -34,22 +34,21 @@ const MultiSelect: React.FC<Props> = ({
|
||||||
{({
|
{({
|
||||||
field,
|
field,
|
||||||
form: { setFieldValue },
|
form: { setFieldValue },
|
||||||
}: any) => {
|
}: any) => (
|
||||||
return (
|
<RMSC
|
||||||
<RMSC
|
{...field}
|
||||||
{...field}
|
type="select"
|
||||||
type="select"
|
options={options}
|
||||||
options={options}
|
labelledBy={name}
|
||||||
labelledBy={name}
|
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value === item))}
|
||||||
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value === item))}
|
onChange={(values: any) => {
|
||||||
onChange={(values: any) => {
|
let am = values && values.map((i: any) => i.value)
|
||||||
let am = values && values.map((i: any) => i.value)
|
|
||||||
|
|
||||||
setFieldValue(field.name, am)
|
setFieldValue(field.name, am)
|
||||||
}}
|
}}
|
||||||
/>
|
className="dark:bg-gray-700"
|
||||||
)
|
/>
|
||||||
}}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Field } from "formik";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Field } from "formik";
|
||||||
import { classNames } from "../../../styles/utils";
|
import { classNames } from "../../../styles/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -18,14 +18,13 @@ const NumberField: React.FC<Props> = ({
|
||||||
className,
|
className,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<div className="col-span-12 sm:col-span-6">
|
||||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
|
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Field name={name} type="number">
|
<Field name={name} type="number">
|
||||||
{({
|
{({
|
||||||
field,
|
field,
|
||||||
form: { touched, errors },
|
|
||||||
meta,
|
meta,
|
||||||
}: any) => (
|
}: any) => (
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
|
@ -35,8 +34,8 @@ const NumberField: React.FC<Props> = ({
|
||||||
className={classNames(
|
className={classNames(
|
||||||
meta.touched && meta.error
|
meta.touched && meta.error
|
||||||
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
||||||
: "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300",
|
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300",
|
||||||
"block w-full shadow-sm sm:text-sm rounded-md"
|
"mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 shadow-sm dark:text-gray-100 sm:text-sm rounded-md"
|
||||||
)}
|
)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -30,11 +30,11 @@ function Select({ name, label, optionDefaultText, options }: props) {
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Listbox.Label className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
|
<Listbox.Label className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
{label}
|
{label}
|
||||||
</Listbox.Label>
|
</Listbox.Label>
|
||||||
<div className="mt-2 relative">
|
<div className="mt-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">
|
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
|
||||||
<span className="block truncate">
|
<span className="block truncate">
|
||||||
{field.value
|
{field.value
|
||||||
? options.find((c) => c.value === field.value)!.label
|
? options.find((c) => c.value === field.value)!.label
|
||||||
|
@ -43,7 +43,7 @@ function Select({ name, label, optionDefaultText, options }: props) {
|
||||||
</span>
|
</span>
|
||||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<SelectorIcon
|
<SelectorIcon
|
||||||
className="h-5 w-5 text-gray-400"
|
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -58,7 +58,7 @@ function Select({ name, label, optionDefaultText, options }: props) {
|
||||||
>
|
>
|
||||||
<Listbox.Options
|
<Listbox.Options
|
||||||
static
|
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"
|
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 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"
|
||||||
>
|
>
|
||||||
{options.map((opt) => (
|
{options.map((opt) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
|
@ -66,8 +66,8 @@ function Select({ name, label, optionDefaultText, options }: props) {
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
classNames(
|
classNames(
|
||||||
active
|
active
|
||||||
? "text-white bg-indigo-600"
|
? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
|
||||||
: "text-gray-900",
|
: "text-gray-900 dark:text-gray-300",
|
||||||
"cursor-default select-none relative py-2 pl-3 pr-9"
|
"cursor-default select-none relative py-2 pl-3 pr-9"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ function Select({ name, label, optionDefaultText, options }: props) {
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-indigo-600",
|
active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
|
||||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import React, { InputHTMLAttributes } from 'react'
|
||||||
import { Switch as HeadlessSwitch } from '@headlessui/react'
|
import { Switch as HeadlessSwitch } from '@headlessui/react'
|
||||||
import { FieldInputProps, FieldMetaProps, FieldProps, FormikProps, FormikValues } from 'formik'
|
import { FieldInputProps, FieldMetaProps, FieldProps, FormikProps, FormikValues } from 'formik'
|
||||||
import React, { InputHTMLAttributes } from 'react'
|
|
||||||
import { classNames } from "../../../styles/utils";
|
import { classNames } from "../../../styles/utils";
|
||||||
|
|
||||||
type SwitchProps<V = any> = {
|
type SwitchProps<V = any> = {
|
||||||
|
@ -37,8 +37,8 @@ export const Switch: React.FC<SwitchProps> = ({
|
||||||
}}
|
}}
|
||||||
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
checked ? 'bg-teal-500' : 'bg-gray-200',
|
checked ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{({ checked }) => (
|
{({ checked }) => (
|
||||||
|
|
|
@ -15,12 +15,12 @@ const SwitchGroup: React.FC<Props> = ({ name, label, description, defaultValue }
|
||||||
<ul className="mt-2 divide-y divide-gray-200">
|
<ul className="mt-2 divide-y divide-gray-200">
|
||||||
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
||||||
{label && <div className="flex flex-col">
|
{label && <div className="flex flex-col">
|
||||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900"
|
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
passive>
|
passive>
|
||||||
{label}
|
{label}
|
||||||
</Switch.Label>
|
</Switch.Label>
|
||||||
{description && (
|
{description && (
|
||||||
<Switch.Description className="text-sm text-gray-500">
|
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{description}
|
{description}
|
||||||
</Switch.Description>
|
</Switch.Description>
|
||||||
)}
|
)}
|
||||||
|
@ -41,8 +41,8 @@ const SwitchGroup: React.FC<Props> = ({ name, label, description, defaultValue }
|
||||||
setFieldValue(field?.name ?? '', value)
|
setFieldValue(field?.name ?? '', value)
|
||||||
}}
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
field.value ? 'bg-teal-500' : 'bg-gray-200',
|
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200',
|
||||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* <span className="sr-only">{label}</span> */}
|
{/* <span className="sr-only">{label}</span> */}
|
||||||
|
@ -68,7 +68,7 @@ const SwitchGroup: React.FC<Props> = ({ name, label, description, defaultValue }
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
value ? 'bg-teal-500' : 'bg-gray-200',
|
value ? 'bg-teal-500' : 'bg-gray-200',
|
||||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Use setting</span>
|
<span className="sr-only">Use setting</span>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Field } from "formik";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Field } from "formik";
|
||||||
import { classNames } from "../../../styles/utils";
|
import { classNames } from "../../../styles/utils";
|
||||||
|
|
||||||
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||||
|
@ -20,7 +20,7 @@ const TextField: React.FC<Props> = ({ name, label, placeholder, columns, classNa
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label && (
|
{label && (
|
||||||
<label htmlFor={name} className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
|
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
@ -35,7 +35,7 @@ const TextField: React.FC<Props> = ({ name, label, placeholder, columns, classNa
|
||||||
id={name}
|
id={name}
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
className="mt-2 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 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"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Switch } from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
import { EmptyListState } from "../../components/EmptyListState";
|
import { EmptyListState } from "../../components/EmptyListState";
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ export default function Filters() {
|
||||||
<div className="flex-shrink-0">
|
<div className="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 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
onClick={toggleCreateFilter}
|
onClick={toggleCreateFilter}
|
||||||
>
|
>
|
||||||
Add new
|
Add new
|
||||||
|
@ -48,8 +48,8 @@ export default function Filters() {
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div className="bg-white rounded-lg shadow">
|
<div className="bg-white dark:bg-gray-900 light:rounded-lg light:shadow">
|
||||||
<div className="relative inset-0 py-3 px-3 sm:px-3 lg:px-3 h-full">
|
<div className="relative inset-0 light:py-3 light:px-3 light:sm:px-3 light:lg:px-3 h-full">
|
||||||
{data && data.length > 0 ? <FilterList filters={data} /> :
|
{data && data.length > 0 ? <FilterList filters={data} /> :
|
||||||
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />}
|
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />}
|
||||||
</div>
|
</div>
|
||||||
|
@ -67,26 +67,26 @@ function FilterList({ filters }: FilterListProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
<div className="light:py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
<div className="shadow overflow-hidden border-b border-gray-200 dark:border-gray-800 sm:rounded-lg">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50 dark:bg-gray-900 text-gray-500 dark:text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
<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 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 uppercase tracking-wider"
|
className="px-6 py-3 text-left text-xs font-medium 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 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Indexers
|
Indexers
|
||||||
</th>
|
</th>
|
||||||
|
@ -95,7 +95,7 @@ function FilterList({ filters }: FilterListProps) {
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
|
||||||
{filters.map((filter: Filter, idx) => (
|
{filters.map((filter: Filter, idx) => (
|
||||||
<FilterListItem filter={filter} key={idx} idx={idx} />
|
<FilterListItem filter={filter} key={idx} idx={idx} />
|
||||||
))}
|
))}
|
||||||
|
@ -124,14 +124,14 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={filter.name}
|
<tr key={filter.name}
|
||||||
className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
className={idx % 2 === 0 ? 'bg-white dark:bg-gray-900' : 'bg-gray-50 dark:bg-gray-900'}>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100">
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled}
|
checked={enabled}
|
||||||
onChange={toggleActive}
|
onChange={toggleActive}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
enabled ? 'bg-teal-500' : 'bg-gray-200',
|
enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700',
|
||||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Use setting</span>
|
<span className="sr-only">Use setting</span>
|
||||||
|
@ -139,16 +139,16 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
enabled ? 'translate-x-5' : 'translate-x-0',
|
enabled ? 'translate-x-5' : 'translate-x-0',
|
||||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
'inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900">{filter.name}</td>
|
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">{filter.name}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{filter.indexers && filter.indexers.map(t =>
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{filter.indexers && filter.indexers.map(t =>
|
||||||
<span key={t.id} className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-100 text-gray-800">{t.name}</span>)}</td>
|
<span key={t.id} className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-400">{t.name}</span>)}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<Link to={`filters/${filter.id.toString()}`} className="text-indigo-600 hover:text-indigo-900">
|
<Link to={`filters/${filter.id.toString()}`} className="text-indigo-600 dark:text-gray-200 hover:text-indigo-900 dark:hover:text-gray-400">
|
||||||
Edit
|
Edit
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -22,11 +22,11 @@ function ApplicationSettings() {
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="divide-y divide-gray-200 lg:col-span-9" action="#" method="POST">
|
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
|
||||||
<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">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">
|
<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>
|
||||||
|
@ -35,7 +35,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-sm font-medium text-gray-700">
|
<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
|
||||||
|
@ -44,12 +44,12 @@ function ApplicationSettings() {
|
||||||
id="host"
|
id="host"
|
||||||
value={data.host}
|
value={data.host}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm"
|
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"
|
||||||
/>
|
/>
|
||||||
</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-sm font-medium text-gray-700">
|
<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
|
||||||
|
@ -58,12 +58,12 @@ function ApplicationSettings() {
|
||||||
id="port"
|
id="port"
|
||||||
value={data.port}
|
value={data.port}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm"
|
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"
|
||||||
/>
|
/>
|
||||||
</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-sm font-medium text-gray-700">
|
<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
|
||||||
|
@ -72,23 +72,23 @@ function ApplicationSettings() {
|
||||||
id="base_url"
|
id="base_url"
|
||||||
value={data.base_url}
|
value={data.base_url}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-6 pb-6 divide-y divide-gray-200">
|
<div className="pt-6 pb-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<div className="px-4 sm:px-6">
|
<div className="px-4 sm:px-6">
|
||||||
<ul className="mt-2 divide-y divide-gray-200">
|
<ul className="mt-2 divide-y divide-gray-200">
|
||||||
<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"
|
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white"
|
||||||
passive>
|
passive>
|
||||||
Debug
|
Debug
|
||||||
</Switch.Label>
|
</Switch.Label>
|
||||||
<Switch.Description className="text-sm text-gray-500">
|
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
Enable debug mode to get more logs.
|
Enable debug mode to get more logs.
|
||||||
</Switch.Description>
|
</Switch.Description>
|
||||||
</div>
|
</div>
|
||||||
|
@ -96,8 +96,8 @@ function ApplicationSettings() {
|
||||||
checked={isDebug}
|
checked={isDebug}
|
||||||
onChange={setIsDebug}
|
onChange={setIsDebug}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isDebug ? 'bg-teal-500' : 'bg-gray-200',
|
isDebug ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700',
|
||||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Use setting</span>
|
<span className="sr-only">Use setting</span>
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import {useState} from "react";
|
import { DownloadClient } from "../../domain/interfaces";
|
||||||
import {DownloadClient} from "../../domain/interfaces";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import {useToggle} from "../../hooks/hooks";
|
import { Switch } from "@headlessui/react";
|
||||||
import {Switch} from "@headlessui/react";
|
import { useQuery } from "react-query";
|
||||||
import {useQuery} from "react-query";
|
import { classNames } from "../../styles/utils";
|
||||||
import {classNames} from "../../styles/utils";
|
|
||||||
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
|
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
|
||||||
import EmptySimple from "../../components/empty/EmptySimple";
|
import EmptySimple from "../../components/empty/EmptySimple";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
import {DownloadClientTypeNameMap} from "../../domain/constants";
|
import { DownloadClientTypeNameMap } from "../../domain/constants";
|
||||||
|
|
||||||
interface DownloadLClientSettingsListItemProps {
|
interface DownloadLClientSettingsListItemProps {
|
||||||
client: DownloadClient;
|
client: DownloadClient;
|
||||||
|
@ -18,17 +17,17 @@ function DownloadClientSettingsListItem({ client, idx }: DownloadLClientSettings
|
||||||
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false)
|
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={client.name} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
<tr key={client.name} className={idx % 2 === 0 ? 'light:bg-white' : 'light:bg-gray-50'}>
|
||||||
{updateClientIsOpen &&
|
{updateClientIsOpen &&
|
||||||
<DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient}/>
|
<DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient} />
|
||||||
}
|
}
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
<Switch
|
<Switch
|
||||||
checked={client.enabled}
|
checked={client.enabled}
|
||||||
onChange={toggleUpdateClient}
|
onChange={toggleUpdateClient}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
client.enabled ? 'bg-teal-500' : 'bg-gray-200',
|
client.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Use setting</span>
|
<span className="sr-only">Use setting</span>
|
||||||
|
@ -41,11 +40,11 @@ function DownloadClientSettingsListItem({ client, idx }: DownloadLClientSettings
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{client.name}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{client.name}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{client.host}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{client.host}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{DownloadClientTypeNameMap[client.type]}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{DownloadClientTypeNameMap[client.type]}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<span className="text-indigo-600 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateClient}>
|
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateClient}>
|
||||||
Edit
|
Edit
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -67,21 +66,21 @@ function DownloadClientSettings() {
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||||
|
|
||||||
{addClientIsOpen &&
|
{addClientIsOpen &&
|
||||||
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient}/>
|
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} />
|
||||||
}
|
}
|
||||||
|
|
||||||
<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">Clients</h3>
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Clients</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Manage download clients.
|
Manage download clients.
|
||||||
</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 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"
|
||||||
onClick={toggleAddClient}
|
onClick={toggleAddClient}
|
||||||
>
|
>
|
||||||
Add new
|
Add new
|
||||||
|
@ -93,43 +92,43 @@ function DownloadClientSettings() {
|
||||||
{data && data.length > 0 ?
|
{data && data.length > 0 ?
|
||||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
<div className="light:shadow overflow-hidden light:border-b light:border-gray-200 sm:rounded-lg">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<thead className="bg-gray-50">
|
<thead className="light:bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<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 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 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 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 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>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{data && data.map((client, idx) => (
|
{data && data.map((client, idx) => (
|
||||||
<DownloadClientSettingsListItem client={client} idx={idx} key={idx} />
|
<DownloadClientSettingsListItem client={client} idx={idx} key={idx} />
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,52 +1,53 @@
|
||||||
import {useToggle} from "../../hooks/hooks";
|
import { useEffect } from "react";
|
||||||
import {useQuery} from "react-query";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import React, {useEffect} from "react";
|
import { useQuery } from "react-query";
|
||||||
import {IndexerAddForm, IndexerUpdateForm} from "../../forms";
|
import { IndexerAddForm, IndexerUpdateForm } from "../../forms";
|
||||||
import {Indexer} from "../../domain/interfaces";
|
import { Indexer } from "../../domain/interfaces";
|
||||||
import {Switch} from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
import {classNames} from "../../styles/utils";
|
import { classNames } from "../../styles/utils";
|
||||||
import EmptySimple from "../../components/empty/EmptySimple";
|
import EmptySimple from "../../components/empty/EmptySimple";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
|
|
||||||
const ListItem = ({ indexer }: any) => {
|
const ListItem = ({ indexer }: any) => {
|
||||||
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={indexer.name}>
|
<tr key={indexer.name}>
|
||||||
{updateIsOpen && <IndexerUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} indexer={indexer} />}
|
<IndexerUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} indexer={indexer} />
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<Switch
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
checked={indexer.enabled}
|
<Switch
|
||||||
onChange={toggleUpdate}
|
checked={indexer.enabled}
|
||||||
className={classNames(
|
onChange={toggleUpdate}
|
||||||
indexer.enabled ? 'bg-teal-500' : 'bg-gray-200',
|
|
||||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Enable</span>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
indexer.enabled ? 'translate-x-5' : 'translate-x-0',
|
indexer.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
</Switch>
|
<span className="sr-only">Enable</span>
|
||||||
</td>
|
<span
|
||||||
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900">{indexer.name}</td>
|
aria-hidden="true"
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
className={classNames(
|
||||||
<span className="text-indigo-600 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}>
|
indexer.enabled ? 'translate-x-5' : 'translate-x-0',
|
||||||
Edit
|
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||||
</span>
|
)}
|
||||||
</td>
|
/>
|
||||||
</tr>
|
</Switch>
|
||||||
)
|
</td>
|
||||||
|
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{indexer.name}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 dark:hover:text-blue-500 cursor-pointer" onClick={toggleUpdate}>
|
||||||
|
Edit
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function IndexerSettings() {
|
function IndexerSettings() {
|
||||||
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false)
|
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false)
|
||||||
|
|
||||||
const {error, data} = useQuery<any[], Error>('indexer', APIClient.indexers.getAll,
|
const { error, data } = useQuery<any[], Error>('indexer', APIClient.indexers.getAll,
|
||||||
{
|
{
|
||||||
refetchOnWindowFocus: false
|
refetchOnWindowFocus: false
|
||||||
}
|
}
|
||||||
|
@ -65,8 +66,8 @@ function IndexerSettings() {
|
||||||
<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">Indexers</h3>
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Indexers</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Indexer settings.
|
Indexer settings.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -74,7 +75,7 @@ function IndexerSettings() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleAddIndexer}
|
onClick={toggleAddIndexer}
|
||||||
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 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
Add new
|
Add new
|
||||||
</button>
|
</button>
|
||||||
|
@ -85,37 +86,37 @@ function IndexerSettings() {
|
||||||
{data && data.length > 0 ?
|
{data && data.length > 0 ?
|
||||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
<div className="light:shadow overflow-hidden light:border-b light:border-gray-200 dark:border-gray-700 sm:rounded-lg">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<thead className="bg-gray-50">
|
<thead className="light:bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<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 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 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 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>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{data && data.map((indexer: Indexer, idx: number) => (
|
{data && data.map((indexer: Indexer, idx: number) => (
|
||||||
<ListItem indexer={indexer} key={idx}/>
|
<ListItem indexer={indexer} key={idx} />
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
: <EmptySimple title="No indexers" subtitle="Add a new indexer" buttonText="New indexer" buttonAction={toggleAddIndexer}/>
|
: <EmptySimple title="No indexers" subtitle="Add a new indexer" buttonText="New indexer" buttonAction={toggleAddIndexer} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import React, {useEffect} from "react";
|
import { useEffect } from "react";
|
||||||
import {IrcNetworkAddForm} from "../../forms";
|
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
|
||||||
import {useToggle} from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import {useQuery} from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import IrcNetworkUpdateForm from "../../forms/settings/IrcNetworkUpdateForm";
|
import { Switch } from "@headlessui/react";
|
||||||
import {Switch} from "@headlessui/react";
|
import { classNames } from "../../styles/utils";
|
||||||
import {classNames} from "../../styles/utils";
|
|
||||||
import EmptySimple from "../../components/empty/EmptySimple";
|
import EmptySimple from "../../components/empty/EmptySimple";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
|
|
||||||
|
@ -34,16 +33,13 @@ function IrcSettings() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||||
|
<IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork} />
|
||||||
{addNetworkIsOpen &&
|
|
||||||
<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">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">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
IRC networks and channels.
|
IRC networks and channels.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,7 +47,7 @@ function IrcSettings() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
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 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 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>
|
||||||
|
@ -62,49 +58,49 @@ function IrcSettings() {
|
||||||
{data && data.length > 0 ?
|
{data && data.length > 0 ?
|
||||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
<div className="light:shadow overflow-hidden light:border-b light:border-gray-200 sm:rounded-lg">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<thead className="bg-gray-50">
|
<thead className="light:bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<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 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 uppercase tracking-wider"
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Network
|
Network
|
||||||
</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 dark:text-gray-400 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Server
|
Server
|
||||||
</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 dark:text-gray-400 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Nick
|
Nick
|
||||||
</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>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{data && data.map((network: IrcNetwork, idx) => (
|
{data && data.map((network: IrcNetwork, idx) => (
|
||||||
<ListItem key={idx} idx={idx} network={network}/>
|
<ListItem key={idx} idx={idx} network={network} />
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
: <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>
|
||||||
|
@ -116,15 +112,16 @@ const ListItem = ({ idx, network }: any) => {
|
||||||
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={network.name} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
<tr key={network.name} className={idx % 2 === 0 ? 'light:bg-white' : 'light:bg-gray-50'}>
|
||||||
{updateIsOpen && <IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} />}
|
<IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} />
|
||||||
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<Switch
|
<Switch
|
||||||
checked={network.enabled}
|
checked={network.enabled}
|
||||||
onChange={toggleUpdate}
|
onChange={toggleUpdate}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
network.enabled ? 'bg-teal-500' : 'bg-gray-200',
|
network.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Enable</span>
|
<span className="sr-only">Enable</span>
|
||||||
|
@ -137,11 +134,11 @@ const ListItem = ({ idx, network }: any) => {
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{network.name}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{network.name}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{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 text-green-800">TLS</span>}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><span>{network.server}:{network.port}</span> {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>}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{network.nickserv?.account}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{network.nickserv?.account}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<span className="text-indigo-600 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>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -21,7 +21,7 @@ module.exports = {
|
||||||
'col-span-12',
|
'col-span-12',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
darkMode: false, // or 'media' or 'class'
|
darkMode: 'media', // or 'media' or 'class'
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue