mirror of
https://github.com/idanoo/autobrr
synced 2025-07-25 17:59:14 +00:00
feat: add webui
This commit is contained in:
parent
a838d994a6
commit
773e57afe6
59 changed files with 19794 additions and 0 deletions
202
web/src/screens/Base.tsx
Normal file
202
web/src/screens/Base.tsx
Normal file
|
@ -0,0 +1,202 @@
|
|||
import {Fragment} from 'react'
|
||||
import {Disclosure, Menu, Transition} from '@headlessui/react'
|
||||
import {BellIcon, ChevronDownIcon, MenuIcon, XIcon} from '@heroicons/react/outline'
|
||||
import {NavLink,Link, Route, Switch} from "react-router-dom";
|
||||
import Settings from "./Settings";
|
||||
import { Dashboard } from "./Dashboard";
|
||||
import { FilterDetails, Filters} from "./Filters";
|
||||
|
||||
const profile = ['Settings', 'Sign out']
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export default function Base() {
|
||||
const nav = [{name: 'Dashboard', path: "/"}, {name: 'Filters', path: "/filters"}, {name: "Settings", path: "/settings"}]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="">
|
||||
<Disclosure as="nav" className="bg-gray-800 pb-48">
|
||||
{({open}) => (
|
||||
<>
|
||||
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div className="border-b border-gray-700">
|
||||
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
|
||||
<div className="flex items-center">
|
||||
<div className="hidden md:block">
|
||||
<div className="flex items-baseline space-x-4">
|
||||
{nav.map((item, itemIdx) =>
|
||||
<NavLink
|
||||
key={itemIdx}
|
||||
to={item.path}
|
||||
exact={true}
|
||||
activeClassName="bg-gray-900 text-white "
|
||||
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||
>
|
||||
{item.name}
|
||||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="hidden md:block">*/}
|
||||
{/* <div className="ml-4 flex items-center md:ml-6">*/}
|
||||
{/* <button*/}
|
||||
{/* className="bg-gray-800 p-1 text-gray-400 rounded-full hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">*/}
|
||||
{/* <span className="sr-only">View notifications</span>*/}
|
||||
{/* <BellIcon className="h-6 w-6" aria-hidden="true"/>*/}
|
||||
{/* </button>*/}
|
||||
|
||||
{/* <Menu as="div" className="ml-3 relative">*/}
|
||||
{/* {({open}) => (*/}
|
||||
{/* <>*/}
|
||||
{/* <div>*/}
|
||||
{/* <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">*/}
|
||||
{/* <span*/}
|
||||
{/* className="hidden text-gray-300 text-sm font-medium lg:block">*/}
|
||||
{/* <span className="sr-only">Open user menu for </span>User*/}
|
||||
{/*</span>*/}
|
||||
{/* <ChevronDownIcon*/}
|
||||
{/* className="hidden flex-shrink-0 ml-1 h-5 w-5 text-gray-400 lg:block"*/}
|
||||
{/* aria-hidden="true"*/}
|
||||
{/* />*/}
|
||||
{/* </Menu.Button>*/}
|
||||
{/* </div>*/}
|
||||
{/* <Transition*/}
|
||||
{/* show={open}*/}
|
||||
{/* as={Fragment}*/}
|
||||
{/* enter="transition ease-out duration-100"*/}
|
||||
{/* enterFrom="transform opacity-0 scale-95"*/}
|
||||
{/* enterTo="transform opacity-100 scale-100"*/}
|
||||
{/* leave="transition ease-in duration-75"*/}
|
||||
{/* leaveFrom="transform opacity-100 scale-100"*/}
|
||||
{/* leaveTo="transform opacity-0 scale-95"*/}
|
||||
{/* >*/}
|
||||
{/* <Menu.Items*/}
|
||||
{/* static*/}
|
||||
{/* className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"*/}
|
||||
{/* >*/}
|
||||
{/* <Menu.Item>*/}
|
||||
{/* {({active}) => (*/}
|
||||
{/* <Link*/}
|
||||
{/* to="settings"*/}
|
||||
{/* className={classNames(*/}
|
||||
{/* active ? 'bg-gray-100' : '',*/}
|
||||
{/* 'block px-4 py-2 text-sm text-gray-700'*/}
|
||||
{/* )}*/}
|
||||
{/* >*/}
|
||||
{/* Settings*/}
|
||||
{/* </Link>*/}
|
||||
{/* )}*/}
|
||||
{/* </Menu.Item>*/}
|
||||
{/* <Menu.Item>*/}
|
||||
{/* {({active}) => (*/}
|
||||
{/* <Link*/}
|
||||
{/* to="logout"*/}
|
||||
{/* className={classNames(*/}
|
||||
{/* active ? 'bg-gray-100' : '',*/}
|
||||
{/* 'block px-4 py-2 text-sm text-gray-700'*/}
|
||||
{/* )}*/}
|
||||
{/* >*/}
|
||||
{/* Logout*/}
|
||||
{/* </Link>*/}
|
||||
{/* )}*/}
|
||||
{/* </Menu.Item>*/}
|
||||
{/* </Menu.Items>*/}
|
||||
{/* </Transition>*/}
|
||||
{/* </>*/}
|
||||
{/* )}*/}
|
||||
{/* </Menu>*/}
|
||||
{/* </div>*/}
|
||||
{/* </div>*/}
|
||||
<div className="-mr-2 flex md:hidden">
|
||||
{/* Mobile menu button */}
|
||||
<Disclosure.Button
|
||||
className="bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{open ? (
|
||||
<XIcon className="block h-6 w-6" aria-hidden="true"/>
|
||||
) : (
|
||||
<MenuIcon className="block h-6 w-6" aria-hidden="true"/>
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Disclosure.Panel className="border-b border-gray-700 md:hidden">
|
||||
<div className="px-2 py-3 space-y-1 sm:px-3">
|
||||
{nav.map((item, itemIdx) =>
|
||||
itemIdx === 0 ? (
|
||||
<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}
|
||||
className="bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium">
|
||||
{item.name}
|
||||
</Link>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-4 pb-3 border-t border-gray-700">
|
||||
<div className="flex items-center px-5">
|
||||
<div>
|
||||
<div className="text-base font-medium leading-none text-white">User</div>
|
||||
{/*<div className="text-sm font-medium leading-none text-gray-400">tom@example.com</div>*/}
|
||||
</div>
|
||||
<button
|
||||
className="ml-auto bg-gray-800 flex-shrink-0 p-1 text-gray-400 rounded-full hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
|
||||
<span className="sr-only">View notifications</span>
|
||||
<BellIcon className="h-6 w-6" aria-hidden="true"/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-3 px-2 space-y-1">
|
||||
{profile.map((item) => (
|
||||
<Link
|
||||
key={item}
|
||||
to={item}
|
||||
className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700"
|
||||
>
|
||||
{item}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
|
||||
<Switch>
|
||||
<Route path="/settings">
|
||||
<Settings/>
|
||||
</Route>
|
||||
|
||||
<Route exact={true} path="/filters">
|
||||
<Filters/>
|
||||
</Route>
|
||||
|
||||
<Route path="/filters/:filterId">
|
||||
<FilterDetails />
|
||||
</Route>
|
||||
|
||||
<Route path="/">
|
||||
<Dashboard />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
18
web/src/screens/Dashboard.tsx
Normal file
18
web/src/screens/Dashboard.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
export function Dashboard() {
|
||||
return (
|
||||
<main className="-mt-48">
|
||||
<header className="py-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-3xl font-bold text-white capitalize">Dashboard</h1>
|
||||
</div>
|
||||
</header>
|
||||
<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="border-4 border-dashed border-gray-200 rounded-lg h-96" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
|
833
web/src/screens/Filters.tsx
Normal file
833
web/src/screens/Filters.tsx
Normal file
|
@ -0,0 +1,833 @@
|
|||
import React, {Fragment, useRef, useState} from "react";
|
||||
import {Dialog, Switch, Transition} from "@headlessui/react";
|
||||
import {ChevronRightIcon, ExclamationIcon, } from '@heroicons/react/solid'
|
||||
import {EmptyListState} from "../components/EmptyListState";
|
||||
|
||||
import {
|
||||
Link,
|
||||
NavLink,
|
||||
Route,
|
||||
Switch as RouteSwitch,
|
||||
useHistory,
|
||||
useLocation,
|
||||
useParams,
|
||||
useRouteMatch
|
||||
} from "react-router-dom";
|
||||
import {FilterActionList} from "../components/FilterActionList";
|
||||
import {DownloadClient, Filter, Indexer} from "../domain/interfaces";
|
||||
import {useToggle} from "../hooks/hooks";
|
||||
import {useMutation, useQuery} from "react-query";
|
||||
import {queryClient} from "../index";
|
||||
import {CONTAINER_OPTIONS, CODECS_OPTIONS, RESOLUTION_OPTIONS, SOURCES_OPTIONS} from "../domain/constants";
|
||||
import {Field, Form} from "react-final-form";
|
||||
import {MultiSelectField, TextField} from "../components/inputs";
|
||||
import DEBUG from "../components/debug";
|
||||
import TitleSubtitle from "../components/headings/TitleSubtitle";
|
||||
import { SwitchGroup } from "../components/inputs";
|
||||
import {classNames} from "../styles/utils";
|
||||
import { FilterAddForm, FilterActionAddForm} from "../forms";
|
||||
import Select from "react-select";
|
||||
import APIClient from "../api/APIClient";
|
||||
|
||||
const tabs = [
|
||||
{name: 'General', href: '', current: true},
|
||||
// { name: 'TV', href: 'tv', current: false },
|
||||
// { name: 'Movies', href: 'movies', current: false },
|
||||
{name: 'Movies and TV', href: 'movies-tv', current: false},
|
||||
// { name: 'P2P', href: 'p2p', current: false },
|
||||
{name: 'Advanced', href: 'advanced', current: false},
|
||||
{name: 'Actions', href: 'actions', current: false},
|
||||
]
|
||||
|
||||
function TabNavLink({item, url}: any) {
|
||||
const location = useLocation();
|
||||
|
||||
const {pathname} = location;
|
||||
const splitLocation = pathname.split("/");
|
||||
|
||||
// we need to clean the / if it's a base root path
|
||||
let too = item.href ? `${url}/${item.href}` : url
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={item.name}
|
||||
to={too}
|
||||
exact={true}
|
||||
activeClassName="border-purple-500 text-purple-600"
|
||||
className={classNames(
|
||||
'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm'
|
||||
)}
|
||||
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
|
||||
>
|
||||
{item.name}
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
|
||||
export function Filters() {
|
||||
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false)
|
||||
|
||||
const {isLoading, error, data} = useQuery<Filter[], Error>('filter', APIClient.filters.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (error) return (<p>'An error has occurred: '</p>)
|
||||
|
||||
return (
|
||||
<main className="-mt-48 ">
|
||||
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter}/>
|
||||
|
||||
<header className="py-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
|
||||
<h1 className="text-3xl font-bold text-white capitalize">Filters</h1>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<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"
|
||||
onClick={toggleCreateFilter}
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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="relative inset-0 py-3 px-3 sm:px-3 lg:px-3 h-full">
|
||||
{data && data.length > 0 ? <FilterList filters={data}/> :
|
||||
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter}/>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
interface FilterListProps {
|
||||
filters: Filter[];
|
||||
}
|
||||
|
||||
function FilterList({filters}: FilterListProps) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Enabled
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Indexers
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filters.map((filter: Filter, idx) => (
|
||||
<FilterListItem filter={filter} key={idx} idx={idx}/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface FilterListItemProps {
|
||||
filter: Filter;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
function FilterListItem({filter, idx}: FilterListItemProps) {
|
||||
const [enabled, setEnabled] = useState(filter.enabled)
|
||||
|
||||
const toggleActive = (status: boolean) => {
|
||||
console.log(status)
|
||||
setEnabled(status)
|
||||
// call api
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={filter.name}
|
||||
className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={toggleActive}
|
||||
className={classNames(
|
||||
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">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
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'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</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 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>
|
||||
<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">
|
||||
Edit
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const FormButtonsGroup = ({deleteAction}: any) => {
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
|
||||
|
||||
const cancelButtonRef = useRef(null)
|
||||
|
||||
return (
|
||||
<div className="pt-6 divide-y divide-gray-200">
|
||||
|
||||
<Transition.Root show={deleteModalIsOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
static
|
||||
className="fixed z-10 inset-0 overflow-y-auto"
|
||||
initialFocus={cancelButtonRef}
|
||||
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 filter
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Are you sure you want to remove this filter?
|
||||
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={cancelButtonRef}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
||||
<div className="mt-4 pt-4 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 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"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function FilterDetails() {
|
||||
let {url} = useRouteMatch();
|
||||
let history = useHistory();
|
||||
let {filterId}: any = useParams();
|
||||
|
||||
const {isLoading, data} = useQuery<Filter, Error>(['filter', parseInt(filterId)], () => APIClient.filters.getByID(parseInt(filterId)),
|
||||
{
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
onError: err => {
|
||||
history.push("./")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (<p>Something went wrong</p>)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="-mt-48 ">
|
||||
<header className="py-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center">
|
||||
<h1 className="text-3xl font-bold text-white capitalize">
|
||||
<NavLink to="/filters" exact={true}>
|
||||
Filters
|
||||
</NavLink>
|
||||
</h1>
|
||||
<ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true"/>
|
||||
<h1 className="text-3xl font-bold text-white capitalize">{data.name}</h1>
|
||||
</div>
|
||||
</header>
|
||||
<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="relative mx-auto md:px-6 xl:px-4">
|
||||
<div className="px-4 sm:px-6 md:px-0">
|
||||
<div className="py-6">
|
||||
{/* Tabs */}
|
||||
<div className="lg:hidden">
|
||||
<label htmlFor="selected-tab" className="sr-only">
|
||||
Select a tab
|
||||
</label>
|
||||
<select
|
||||
id="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"
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<option key={tab.name}>{tab.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
{tabs.map((tab) => (
|
||||
<TabNavLink item={tab} url={url} key={tab.href}/>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RouteSwitch>
|
||||
<Route exact path={url}>
|
||||
<FilterTabGeneral filter={data}/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/movies-tv`}>
|
||||
{/*<FilterTabMoviesTv filter={data}/>*/}
|
||||
<FilterTabMoviesTvNew2 filter={data}/>
|
||||
</Route>
|
||||
|
||||
{/*<Route path={`${path}/movies`}>*/}
|
||||
{/* <p>movies</p>*/}
|
||||
{/*</Route>*/}
|
||||
|
||||
{/*<Route path={`${path}/p2p`}>*/}
|
||||
{/* <p>p2p</p>*/}
|
||||
{/*</Route>*/}
|
||||
|
||||
<Route path={`${url}/advanced`}>
|
||||
<FilterTabAdvanced filter={data}/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/actions`}>
|
||||
<FilterTabActions filter={data}/>
|
||||
</Route>
|
||||
|
||||
</RouteSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
interface FilterTabGeneralProps {
|
||||
filter: Filter;
|
||||
}
|
||||
|
||||
function FilterTabGeneral({filter}: FilterTabGeneralProps) {
|
||||
const history = useHistory();
|
||||
|
||||
const { data } = useQuery<Indexer[], Error>('indexerList', APIClient.indexers.getOptions,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
|
||||
const updateMutation = useMutation((filter: Filter) => APIClient.filters.update(filter), {
|
||||
onSuccess: () => {
|
||||
// queryClient.setQueryData(['filter', filter.id], data)
|
||||
queryClient.invalidateQueries(["filter",filter.id]);
|
||||
}
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), {
|
||||
onSuccess: () => {
|
||||
// invalidate filters
|
||||
queryClient.invalidateQueries("filter");
|
||||
// redirect
|
||||
history.push("/filters")
|
||||
}
|
||||
})
|
||||
|
||||
const submitOther = (data: Filter) => {
|
||||
updateMutation.mutate(data)
|
||||
}
|
||||
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate(filter.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
initialValues={{
|
||||
id: filter.id,
|
||||
name: filter.name,
|
||||
enabled: filter.enabled,
|
||||
min_size: filter.min_size,
|
||||
max_size: filter.max_size,
|
||||
delay: filter.delay,
|
||||
shows: filter.shows,
|
||||
years: filter.years,
|
||||
resolutions: filter.resolutions || [],
|
||||
sources: filter.sources || [],
|
||||
codecs: filter.codecs || [],
|
||||
containers: filter.containers || [],
|
||||
seasons: filter.seasons,
|
||||
episodes: filter.episodes,
|
||||
match_releases: filter.match_releases,
|
||||
except_releases: filter.except_releases,
|
||||
match_release_groups: filter.match_release_groups,
|
||||
except_release_groups: filter.except_release_groups,
|
||||
match_categories: filter.match_categories,
|
||||
except_categories: filter.except_categories,
|
||||
match_tags: filter.match_tags,
|
||||
except_tags: filter.except_tags,
|
||||
match_uploaders: filter.match_uploaders,
|
||||
except_uploaders: filter.except_uploaders,
|
||||
indexers: filter.indexers || [],
|
||||
}}
|
||||
// validate={validate}
|
||||
onSubmit={submitOther}
|
||||
>
|
||||
{({handleSubmit, submitting, values, valid}) => {
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<div className="mt-6 lg:pb-8">
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="name" label="Filter name" columns={6} placeholder="eg. Filter 1"/>
|
||||
|
||||
<div className="col-span-6">
|
||||
<label htmlFor="indexers" className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
|
||||
Indexers
|
||||
</label>
|
||||
<Field
|
||||
name="indexers"
|
||||
multiple={true}
|
||||
parse={val => val && val.map((item: any) => ({ id: item.value.id, name: item.value.name, enabled: item.value.enabled, identifier: item.value.identifier}))}
|
||||
format={values => values.map((val: any) => ({ label: val.name, value: val}))}
|
||||
render={({input, meta}) => (
|
||||
<Select {...input}
|
||||
isClearable={true}
|
||||
isMulti={true}
|
||||
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"
|
||||
options={data ? data.map(v => ({
|
||||
label: v.name,
|
||||
value: v
|
||||
})) : []}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-8">
|
||||
<TitleSubtitle title="Rules" subtitle="Set rules"/>
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="min_size" label="Min size" columns={6} placeholder=""/>
|
||||
<TextField name="max_size" label="Max size" columns={6} placeholder=""/>
|
||||
<TextField name="delay" label="Delay" columns={6} placeholder=""/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<SwitchGroup name="enabled" label="Enabled" description="Enabled or disable filter."/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<FormButtonsGroup deleteAction={deleteAction}/>
|
||||
|
||||
<DEBUG values={values}/>
|
||||
</form>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function FilterTabMoviesTvNew2({filter}: FilterTabGeneralProps) {
|
||||
const history = useHistory();
|
||||
|
||||
const updateMutation = useMutation((filter: Filter) => APIClient.filters.update(filter), {
|
||||
onSuccess: () => {
|
||||
// queryClient.setQueryData(['filter', filter.id], data)
|
||||
queryClient.invalidateQueries(["filter",filter.id]);
|
||||
}
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), {
|
||||
onSuccess: () => {
|
||||
// invalidate filters
|
||||
queryClient.invalidateQueries("filter");
|
||||
// redirect
|
||||
history.push("/filters")
|
||||
}
|
||||
})
|
||||
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate(filter.id)
|
||||
}
|
||||
|
||||
const submitOther = (data: Filter) => {
|
||||
updateMutation.mutate(data)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
initialValues={{
|
||||
id: filter.id,
|
||||
name: filter.name,
|
||||
enabled: filter.enabled,
|
||||
min_size: filter.min_size,
|
||||
max_size: filter.max_size,
|
||||
delay: filter.delay,
|
||||
shows: filter.shows,
|
||||
years: filter.years,
|
||||
resolutions: filter.resolutions || [],
|
||||
sources: filter.sources || [],
|
||||
codecs: filter.codecs || [],
|
||||
containers: filter.containers || [],
|
||||
seasons: filter.seasons,
|
||||
episodes: filter.episodes,
|
||||
match_releases: filter.match_releases,
|
||||
except_releases: filter.except_releases,
|
||||
match_release_groups: filter.match_release_groups,
|
||||
except_release_groups: filter.except_release_groups,
|
||||
match_categories: filter.match_categories,
|
||||
except_categories: filter.except_categories,
|
||||
match_tags: filter.match_tags,
|
||||
except_tags: filter.except_tags,
|
||||
match_uploaders: filter.match_uploaders,
|
||||
except_uploaders: filter.except_uploaders,
|
||||
indexers: filter.indexers || [],
|
||||
}}
|
||||
// validate={validate}
|
||||
onSubmit={submitOther}
|
||||
>
|
||||
{({handleSubmit, submitting, values, valid}) => {
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="shows" label="Movies / Shows" columns={8} placeholder="eg. Movie, Show 1, Show?2"/>
|
||||
<TextField name="years" label="Years" columns={4} placeholder="eg. 2018,2019-2021"/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-8">
|
||||
<TitleSubtitle title="Seasons and Episodes" subtitle="Set seaons and episodes"/>
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="seasons" label="Seasons" columns={8} placeholder="eg. 1, 3, 2-6"/>
|
||||
<TextField name="episodes" label="Episodes" columns={4} placeholder="eg. 2, 4, 10-20"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-8">
|
||||
<TitleSubtitle title="Quality" subtitle="Resolution, source etc."/>
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<MultiSelectField name="resolutions" options={RESOLUTION_OPTIONS} label="resolutions" columns={6}/>
|
||||
<MultiSelectField name="sources" options={SOURCES_OPTIONS} label="sources" columns={6}/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<MultiSelectField name="codecs" options={CODECS_OPTIONS} label="codecs" columns={6}/>
|
||||
<MultiSelectField name="containers" options={CONTAINER_OPTIONS} label="containers" columns={6}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormButtonsGroup deleteAction={deleteAction}/>
|
||||
|
||||
<DEBUG values={values}/>
|
||||
</form>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FilterTabAdvanced({filter}: FilterTabGeneralProps) {
|
||||
const history = useHistory();
|
||||
|
||||
const updateMutation = useMutation((filter: Filter) => APIClient.filters.update(filter), {
|
||||
onSuccess: () => {
|
||||
// queryClient.setQueryData(['filter', filter.id], data)
|
||||
queryClient.invalidateQueries(["filter",filter.id]);
|
||||
}
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), {
|
||||
onSuccess: () => {
|
||||
// invalidate filters
|
||||
queryClient.invalidateQueries("filter");
|
||||
// redirect
|
||||
history.push("/filters")
|
||||
}
|
||||
})
|
||||
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate(filter.id)
|
||||
}
|
||||
|
||||
const submitOther = (data: Filter) => {
|
||||
updateMutation.mutate(data)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
initialValues={{
|
||||
id: filter.id,
|
||||
name: filter.name,
|
||||
enabled: filter.enabled,
|
||||
min_size: filter.min_size,
|
||||
max_size: filter.max_size,
|
||||
delay: filter.delay,
|
||||
shows: filter.shows,
|
||||
years: filter.years,
|
||||
resolutions: filter.resolutions || [],
|
||||
sources: filter.sources || [],
|
||||
codecs: filter.codecs || [],
|
||||
containers: filter.containers || [],
|
||||
seasons: filter.seasons,
|
||||
episodes: filter.episodes,
|
||||
match_releases: filter.match_releases,
|
||||
except_releases: filter.except_releases,
|
||||
match_release_groups: filter.match_release_groups,
|
||||
except_release_groups: filter.except_release_groups,
|
||||
match_categories: filter.match_categories,
|
||||
except_categories: filter.except_categories,
|
||||
match_tags: filter.match_tags,
|
||||
except_tags: filter.except_tags,
|
||||
match_uploaders: filter.match_uploaders,
|
||||
except_uploaders: filter.except_uploaders,
|
||||
indexers: filter.indexers || [],
|
||||
}}
|
||||
// validate={validate}
|
||||
onSubmit={submitOther}
|
||||
>
|
||||
{({handleSubmit, submitting, values, valid}) => {
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mt-6 lg:pb-8">
|
||||
<TitleSubtitle title="Releases" subtitle="Releases"/>
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="match_releases" label="Match releases" columns={6} placeholder=""/>
|
||||
<TextField name="except_releases" label="Except releases" columns={6}
|
||||
placeholder=""/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-8">
|
||||
<TitleSubtitle title="Release groups"
|
||||
subtitle="Match or ignore certain release groups"/>
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="match_release_groups" label="Match groups" columns={6}
|
||||
placeholder=""/>
|
||||
<TextField name="except_release_groups" label="Except groups" columns={6}
|
||||
placeholder=""/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-6 lg:pb-8">
|
||||
<TitleSubtitle title="Categories" subtitle="Match or ignore certain categories"/>
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="match_categories" label="Match categories" columns={6}
|
||||
placeholder=""/>
|
||||
<TextField name="except_categories" label="Except categories" columns={6}
|
||||
placeholder=""/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-8">
|
||||
<TitleSubtitle title="Tags" subtitle="Match or ignore certain tags"/>
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="match_tags" label="Match tags" columns={6} placeholder=""/>
|
||||
<TextField name="except_tags" label="Except tags" columns={6} placeholder=""/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-8">
|
||||
<TitleSubtitle title="Uploaders" subtitle="Match or ignore certain uploaders"/>
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="match_uploaders" label="Match uploaders" columns={6}
|
||||
placeholder=""/>
|
||||
<TextField name="except_uploaders" label="Except uploaders" columns={6}
|
||||
placeholder=""/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormButtonsGroup deleteAction={deleteAction}/>
|
||||
|
||||
<DEBUG values={values}/>
|
||||
|
||||
</form>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FilterTabActions({filter}: FilterTabGeneralProps) {
|
||||
const [addActionIsOpen, toggleAddAction] = useToggle(false)
|
||||
|
||||
const {data} = useQuery<DownloadClient[], Error>('downloadClients', APIClient.download_clients.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="mt-10">
|
||||
{addActionIsOpen &&
|
||||
<FilterActionAddForm filter={filter} clients={data || []} isOpen={addActionIsOpen} toggle={toggleAddAction}/>
|
||||
}
|
||||
<div>
|
||||
<div className="-ml-4 -mt-4 mb-6 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Actions
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<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"
|
||||
onClick={toggleAddAction}
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{filter.actions ? <FilterActionList actions={filter.actions} clients={data || []} filterID={filter.id}/> :
|
||||
<EmptyListState text="No actions yet!"/>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
144
web/src/screens/Settings.tsx
Normal file
144
web/src/screens/Settings.tsx
Normal file
|
@ -0,0 +1,144 @@
|
|||
import React from 'react'
|
||||
import {CogIcon, DownloadIcon, KeyIcon} from '@heroicons/react/outline'
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
NavLink,
|
||||
Route,
|
||||
Switch as RouteSwitch,
|
||||
useLocation,
|
||||
useRouteMatch
|
||||
} from "react-router-dom";
|
||||
import IndexerSettings from "./settings/Indexer";
|
||||
import IrcSettings from "./settings/Irc";
|
||||
import ApplicationSettings from "./settings/Application";
|
||||
import DownloadClientSettings from "./settings/DownloadClient";
|
||||
import {classNames} from "../styles/utils";
|
||||
import ActionSettings from "./settings/Action";
|
||||
import {useRecoilValue} from "recoil";
|
||||
import {configState} from "../state/state";
|
||||
|
||||
const subNavigation = [
|
||||
{name: 'Application', href: '', icon: CogIcon, current: true},
|
||||
{name: 'Indexers', href: 'indexers', icon: KeyIcon, current: false},
|
||||
{name: 'IRC', href: 'irc', icon: KeyIcon, current: false},
|
||||
{name: 'Clients', href: 'clients', icon: DownloadIcon, current: false},
|
||||
// {name: 'Actions', href: 'actions', icon: PlayIcon, current: false},
|
||||
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
|
||||
// {name: 'Notifications', href: 'notifications', icon: BellIcon, current: false},
|
||||
]
|
||||
|
||||
function SubNavLink({item, url}: any) {
|
||||
const location = useLocation();
|
||||
|
||||
const {pathname} = location;
|
||||
|
||||
const splitLocation = pathname.split("/");
|
||||
|
||||
// we need to clean the / if it's a base root path
|
||||
let too = item.href ? `${url}/${item.href}` : url
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={item.name}
|
||||
to={too}
|
||||
exact={true}
|
||||
activeClassName="bg-teal-50 border-teal-500 text-teal-700 hover:bg-teal-50 hover:text-teal-700"
|
||||
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'
|
||||
)}
|
||||
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
|
||||
>
|
||||
<item.icon
|
||||
className={classNames(
|
||||
splitLocation[2] === item.href
|
||||
? 'text-teal-500 group-hover:text-teal-500'
|
||||
: 'text-gray-400 group-hover:text-gray-500',
|
||||
'flex-shrink-0 -ml-1 mr-3 h-6 w-6'
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="truncate">{item.name}</span>
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarNav({subNavigation, url}: any) {
|
||||
return (
|
||||
<aside className="py-6 lg:col-span-3">
|
||||
<nav className="space-y-1">
|
||||
{subNavigation.map((item: any) => (
|
||||
<SubNavLink item={item} url={url} key={item.href}/>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
export function buildPath(...args: string[]): string {
|
||||
const [first] = args;
|
||||
const firstTrimmed = first.trim();
|
||||
const result = args
|
||||
.map((part) => part.trim())
|
||||
.map((part, i) => {
|
||||
if (i === 0) {
|
||||
return part.replace(/[/]*$/g, '');
|
||||
} else {
|
||||
return part.replace(/(^[/]*|[/]*$)/g, '');
|
||||
}
|
||||
})
|
||||
.filter((x) => x.length)
|
||||
.join('/');
|
||||
|
||||
return firstTrimmed === '/' ? `/${result}` : result;
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
const config = useRecoilValue(configState)
|
||||
|
||||
let { url } = useRouteMatch();
|
||||
let p = config.base_url ? buildPath(config.base_url, url) : url
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<main className="relative -mt-48">
|
||||
<header className="py-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-3xl font-bold text-white capitalize">Settings</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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="divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
|
||||
<SidebarNav url={p} subNavigation={subNavigation}/>
|
||||
|
||||
<RouteSwitch>
|
||||
<Route exact path={p}>
|
||||
<ApplicationSettings />
|
||||
</Route>
|
||||
|
||||
<Route path={`${p}/indexers`}>
|
||||
<IndexerSettings />
|
||||
</Route>
|
||||
|
||||
<Route path={`${p}/irc`}>
|
||||
<IrcSettings />
|
||||
</Route>
|
||||
|
||||
<Route path={`${p}/clients`}>
|
||||
<DownloadClientSettings />
|
||||
</Route>
|
||||
|
||||
<Route path={`${p}/actions`}>
|
||||
<ActionSettings />
|
||||
</Route>
|
||||
|
||||
</RouteSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
|
101
web/src/screens/settings/Action.tsx
Normal file
101
web/src/screens/settings/Action.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
import React from "react";
|
||||
|
||||
|
||||
function ActionSettings() {
|
||||
// const [addClientIsOpen, toggleAddClient] = useToggle(false)
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
|
||||
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
{/*{addClientIsOpen &&*/}
|
||||
{/*<AddNewClientForm isOpen={addClientIsOpen} toggle={toggleAddClient}/>*/}
|
||||
{/*}*/}
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Manage actions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<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"
|
||||
// onClick={toggleAddClient}
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-6">
|
||||
<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="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Port
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Enabled
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>empty</td>
|
||||
</tr>
|
||||
{/*{downloadclients.map((client, personIdx) => (*/}
|
||||
{/* <tr key={client.name}*/}
|
||||
{/* className={personIdx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>*/}
|
||||
{/* <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 text-gray-500">{client.type}</td>*/}
|
||||
{/* <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{client.port}</td>*/}
|
||||
{/* <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{client.enabled}</td>*/}
|
||||
{/* <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">*/}
|
||||
{/* <Link to="edit" className="text-indigo-600 hover:text-indigo-900">*/}
|
||||
{/* Edit*/}
|
||||
{/* </Link>*/}
|
||||
{/* </td>*/}
|
||||
{/* </tr>*/}
|
||||
{/*))}*/}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default ActionSettings;
|
119
web/src/screens/settings/Application.tsx
Normal file
119
web/src/screens/settings/Application.tsx
Normal file
|
@ -0,0 +1,119 @@
|
|||
import React, {useState} from "react";
|
||||
import {Switch} from "@headlessui/react";
|
||||
import { classNames } from "../../styles/utils";
|
||||
import {useRecoilState} from "recoil";
|
||||
import {configState} from "../../state/state";
|
||||
|
||||
function ApplicationSettings() {
|
||||
const [isDebug, setIsDebug] = useState(true)
|
||||
const [config] = useRecoilState(configState)
|
||||
|
||||
return (
|
||||
<form className="divide-y divide-gray-200 lg:col-span-9" action="#" method="POST">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900">Application</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Application settings. Change in config.toml and restart to take effect.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="host" className="block text-sm font-medium text-gray-700">
|
||||
Host
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
id="host"
|
||||
value={config.host}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="port" className="block text-sm font-medium text-gray-700">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="port"
|
||||
id="port"
|
||||
value={config.port}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="base_url" className="block text-sm font-medium text-gray-700">
|
||||
Base url
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="base_url"
|
||||
id="base_url"
|
||||
value={config.base_url}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 pb-6 divide-y divide-gray-200">
|
||||
<div className="px-4 sm:px-6">
|
||||
<ul className="mt-2 divide-y divide-gray-200">
|
||||
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900"
|
||||
passive>
|
||||
Debug
|
||||
</Switch.Label>
|
||||
<Switch.Description className="text-sm text-gray-500">
|
||||
Enable debug mode to get more logs.
|
||||
</Switch.Description>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isDebug}
|
||||
onChange={setIsDebug}
|
||||
className={classNames(
|
||||
isDebug ? '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'
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
isDebug ? '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'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</Switch.Group>
|
||||
</ul>
|
||||
</div>
|
||||
{/*<div className="mt-4 py-4 px-4 flex justify-end sm:px-6">*/}
|
||||
{/* <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-indigo-500"*/}
|
||||
{/* >*/}
|
||||
{/* Cancel*/}
|
||||
{/* </button>*/}
|
||||
{/* <button*/}
|
||||
{/* type="submit"*/}
|
||||
{/* className="ml-5 bg-indigo-700 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-indigo-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"*/}
|
||||
{/* >*/}
|
||||
{/* Save*/}
|
||||
{/* </button>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default ApplicationSettings;
|
148
web/src/screens/settings/DownloadClient.tsx
Normal file
148
web/src/screens/settings/DownloadClient.tsx
Normal file
|
@ -0,0 +1,148 @@
|
|||
import {useState} from "react";
|
||||
import {DownloadClient} from "../../domain/interfaces";
|
||||
import {useToggle} from "../../hooks/hooks";
|
||||
import {Switch} from "@headlessui/react";
|
||||
import {useQuery} from "react-query";
|
||||
import {classNames} from "../../styles/utils";
|
||||
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
|
||||
import EmptySimple from "../../components/empty/EmptySimple";
|
||||
import APIClient from "../../api/APIClient";
|
||||
|
||||
interface DownloadLClientSettingsListItemProps {
|
||||
client: DownloadClient;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
function DownloadClientSettingsListItem({ client, idx }: DownloadLClientSettingsListItemProps) {
|
||||
const [enabled, setEnabled] = useState(client.enabled)
|
||||
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false)
|
||||
|
||||
const toggleActive = (status: boolean) => {
|
||||
console.log(status)
|
||||
setEnabled(status)
|
||||
// call api
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={client.name} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
{updateClientIsOpen &&
|
||||
<DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient}/>
|
||||
}
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<Switch
|
||||
checked={client.enabled}
|
||||
onChange={toggleActive}
|
||||
className={classNames(
|
||||
client.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">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
client.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'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</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 text-gray-500">{client.type}</td>
|
||||
<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}>
|
||||
Edit
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function DownloadClientSettings() {
|
||||
const [addClientIsOpen, toggleAddClient] = useToggle(false)
|
||||
|
||||
const { error, data } = useQuery<DownloadClient[], Error>('downloadClients', APIClient.download_clients.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
})
|
||||
|
||||
if (error) return (<p>'An error has occurred: '</p>);
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
|
||||
{addClientIsOpen &&
|
||||
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient}/>
|
||||
}
|
||||
|
||||
<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">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Clients</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Manage download clients.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<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"
|
||||
onClick={toggleAddClient}
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-6">
|
||||
{data && data.length > 0 ?
|
||||
<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="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Enabled
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data && data.map((client, idx) => (
|
||||
<DownloadClientSettingsListItem client={client} idx={idx} key={idx} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: <EmptySimple title="No download clients" subtitle="Add a new client" buttonText="New client" buttonAction={toggleAddClient} />
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default DownloadClientSettings;
|
127
web/src/screens/settings/Indexer.tsx
Normal file
127
web/src/screens/settings/Indexer.tsx
Normal file
|
@ -0,0 +1,127 @@
|
|||
import {useToggle} from "../../hooks/hooks";
|
||||
import {useQuery} from "react-query";
|
||||
import React, {useEffect} from "react";
|
||||
import {IndexerAddForm, IndexerUpdateForm} from "../../forms";
|
||||
import {Indexer} from "../../domain/interfaces";
|
||||
import {Switch} from "@headlessui/react";
|
||||
import {classNames} from "../../styles/utils";
|
||||
import EmptySimple from "../../components/empty/EmptySimple";
|
||||
import APIClient from "../../api/APIClient";
|
||||
|
||||
const ListItem = ({ indexer }: any) => {
|
||||
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
||||
|
||||
return (
|
||||
<tr key={indexer.name}>
|
||||
{updateIsOpen && <IndexerUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} indexer={indexer} />}
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Switch
|
||||
checked={indexer.enabled}
|
||||
onChange={toggleUpdate}
|
||||
className={classNames(
|
||||
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(
|
||||
indexer.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'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</td>
|
||||
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900">{indexer.name}</td>
|
||||
<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}>
|
||||
Edit
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function IndexerSettings() {
|
||||
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false)
|
||||
|
||||
const {error, data} = useQuery<any[], Error>('indexer', APIClient.indexers.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
}, []);
|
||||
|
||||
if (error) return (<p>An error has occurred</p>)
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
|
||||
<IndexerAddForm isOpen={addIndexerIsOpen} toggle={toggleAddIndexer} />
|
||||
|
||||
<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">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Indexers</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Indexer settings.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-6">
|
||||
{data && data.length > 0 ?
|
||||
<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="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Enabled
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{data && data.map((indexer: Indexer, idx: number) => (
|
||||
<ListItem indexer={indexer} key={idx}/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: <EmptySimple title="No indexers" subtitle="Add a new indexer" buttonText="New indexer" buttonAction={toggleAddIndexer}/>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default IndexerSettings;
|
153
web/src/screens/settings/Irc.tsx
Normal file
153
web/src/screens/settings/Irc.tsx
Normal file
|
@ -0,0 +1,153 @@
|
|||
import React, {useEffect} from "react";
|
||||
import {IrcNetworkAddForm} from "../../forms";
|
||||
import {useToggle} from "../../hooks/hooks";
|
||||
import {useQuery} from "react-query";
|
||||
import IrcNetworkUpdateForm from "../../forms/settings/IrcNetworkUpdateForm";
|
||||
import {Switch} from "@headlessui/react";
|
||||
import {classNames} from "../../styles/utils";
|
||||
import EmptySimple from "../../components/empty/EmptySimple";
|
||||
import APIClient from "../../api/APIClient";
|
||||
|
||||
interface IrcNetwork {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
addr: string;
|
||||
nick: string;
|
||||
username: string;
|
||||
realname: string;
|
||||
pass: string;
|
||||
// connect_commands: string;
|
||||
}
|
||||
|
||||
function IrcSettings() {
|
||||
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false)
|
||||
|
||||
useEffect(() => {
|
||||
}, []);
|
||||
|
||||
const { data } = useQuery<any[], Error>('networks', APIClient.irc.getNetworks,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
|
||||
{addNetworkIsOpen &&
|
||||
<IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork}/>
|
||||
}
|
||||
|
||||
<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">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">IRC</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
IRC networks and channels.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-6">
|
||||
{data && data.length > 0 ?
|
||||
<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="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Enabled
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Network
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Addr
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Nick
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data && data.map((network: IrcNetwork, idx) => (
|
||||
<ListItem key={idx} idx={idx} network={network}/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: <EmptySimple title="No networks" subtitle="Add a new network" buttonText="New network" buttonAction={toggleAddNetwork}/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ListItem = ({ idx, network }: any) => {
|
||||
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
||||
|
||||
return (
|
||||
<tr key={network.name} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
{updateIsOpen && <IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} />}
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Switch
|
||||
checked={network.enabled}
|
||||
onChange={toggleUpdate}
|
||||
className={classNames(
|
||||
network.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(
|
||||
network.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'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</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 text-gray-500">{network.addr} {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">{network.nick}</td>
|
||||
<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}>
|
||||
Edit
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default IrcSettings;
|
Loading…
Add table
Add a link
Reference in a new issue