mirror of
https://github.com/idanoo/autobrr
synced 2025-07-24 17:29:12 +00:00
refactor(web) add eslint (#222)
* fix(tsconfig.json): changed skipLibCheck to false. refactor(eslint): moved configuration from package.json to .eslintrc.js and added a typescript plugin for future use * feat: wip eslint and types * feat: fix identation * feat: get rid of last any types
This commit is contained in:
parent
7f06a4c707
commit
cb8f280e86
70 changed files with 6797 additions and 6541 deletions
|
@ -1,6 +1,6 @@
|
|||
import { Fragment } from "react";
|
||||
import { NavLink, Link, Route, Switch } from "react-router-dom";
|
||||
import type { match } from "react-router-dom";
|
||||
import { Link, NavLink, Route, Switch } from "react-router-dom";
|
||||
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
||||
import { ExternalLinkIcon } from "@heroicons/react/solid";
|
||||
import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
|
||||
|
@ -11,9 +11,9 @@ import { Logs } from "./Logs";
|
|||
import { Releases } from "./releases";
|
||||
import { Dashboard } from "./dashboard";
|
||||
import { FilterDetails, Filters } from "./filters";
|
||||
import { AuthContext } from '../utils/Context';
|
||||
import { AuthContext } from "../utils/Context";
|
||||
|
||||
import logo from '../logo.png';
|
||||
import logo from "../logo.png";
|
||||
|
||||
interface NavItem {
|
||||
name: string;
|
||||
|
@ -21,229 +21,230 @@ interface NavItem {
|
|||
}
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
const isActiveMatcher = (
|
||||
match: match<any> | null,
|
||||
location: { pathname: string },
|
||||
item: NavItem
|
||||
match: match | null,
|
||||
location: { pathname: string },
|
||||
item: NavItem
|
||||
) => {
|
||||
if (!match)
|
||||
return false;
|
||||
|
||||
if (match?.url === "/" && item.path === "/" && location.pathname === "/")
|
||||
return true
|
||||
return true;
|
||||
|
||||
if (match.url === "/")
|
||||
return false;
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export default function Base() {
|
||||
const authContext = AuthContext.useValue();
|
||||
const nav: Array<NavItem> = [
|
||||
{ name: 'Dashboard', path: "/" },
|
||||
{ name: 'Filters', path: "/filters" },
|
||||
{ name: 'Releases', path: "/releases" },
|
||||
{ name: "Settings", path: "/settings" },
|
||||
{ name: "Logs", path: "/logs" }
|
||||
];
|
||||
const authContext = AuthContext.useValue();
|
||||
const nav: Array<NavItem> = [
|
||||
{ name: "Dashboard", path: "/" },
|
||||
{ name: "Filters", path: "/filters" },
|
||||
{ name: "Releases", path: "/releases" },
|
||||
{ name: "Settings", path: "/settings" },
|
||||
{ name: "Logs", path: "/logs" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Disclosure
|
||||
as="nav"
|
||||
className="bg-gradient-to-b from-gray-100 dark:from-[#141414]"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="max-w-screen-xl mx-auto sm:px-6 lg:px-8">
|
||||
<div className="border-b border-gray-300 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<img
|
||||
className="block lg:hidden h-10 w-auto"
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
/>
|
||||
<img
|
||||
className="hidden lg:block h-10 w-auto"
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:ml-3 hidden sm:block">
|
||||
<div className="flex items-baseline space-x-4">
|
||||
{nav.map((item, itemIdx) =>
|
||||
<NavLink
|
||||
key={item.name + itemIdx}
|
||||
to={item.path}
|
||||
strict
|
||||
className={classNames(
|
||||
"text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium",
|
||||
"transition-colors duration-200"
|
||||
)}
|
||||
activeClassName="text-black dark:text-gray-50 !font-bold"
|
||||
isActive={(match, location) => isActiveMatcher(match, location, item)}
|
||||
>
|
||||
{item.name}
|
||||
</NavLink>
|
||||
)}
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://autobrr.com/docs/configuration/indexers"
|
||||
className={classNames(
|
||||
"text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium",
|
||||
"transition-colors duration-200 flex items-center justify-center"
|
||||
)}
|
||||
>
|
||||
Docs
|
||||
<ExternalLinkIcon className="inline ml-1 h-5 w-5" aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<div className="ml-4 flex items-center sm:ml-6">
|
||||
<Menu as="div" className="ml-3 relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Menu.Button
|
||||
className={classNames(
|
||||
open ? "bg-gray-200 dark:bg-gray-800" : "",
|
||||
"text-gray-800 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-800",
|
||||
"max-w-xs rounded-full flex items-center text-sm px-3 py-2",
|
||||
"transition-colors duration-200"
|
||||
)}
|
||||
>
|
||||
<span className="hidden text-sm font-medium sm:block">
|
||||
<span className="sr-only">Open user menu for </span>
|
||||
{authContext.username}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className="hidden flex-shrink-0 ml-1 h-5 w-5 text-gray-800 dark:text-gray-300 sm:block"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<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 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>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
to="/settings"
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 dark:bg-gray-600' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
|
||||
)}
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
to="/logout"
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 dark:bg-gray-600' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
|
||||
)}
|
||||
>
|
||||
Logout
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mr-2 flex sm:hidden">
|
||||
{/* Mobile menu button */}
|
||||
<Disclosure.Button
|
||||
className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
|
||||
<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-300 dark:border-gray-700 md:hidden">
|
||||
<div className="px-2 py-3 space-y-1 sm:px-3">
|
||||
{nav.map((item) =>
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
strict
|
||||
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||
activeClassName="font-bold bg-gray-300 text-black"
|
||||
isActive={(match, location) => isActiveMatcher(match, location, item)}
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Disclosure
|
||||
as="nav"
|
||||
className="bg-gradient-to-b from-gray-100 dark:from-[#141414]"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="max-w-screen-xl mx-auto sm:px-6 lg:px-8">
|
||||
<div className="border-b border-gray-300 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<img
|
||||
className="block lg:hidden h-10 w-auto"
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
/>
|
||||
<img
|
||||
className="hidden lg:block h-10 w-auto"
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:ml-3 hidden sm:block">
|
||||
<div className="flex items-baseline space-x-4">
|
||||
{nav.map((item, itemIdx) =>
|
||||
<NavLink
|
||||
key={item.name + itemIdx}
|
||||
to={item.path}
|
||||
strict
|
||||
className={classNames(
|
||||
"text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium",
|
||||
"transition-colors duration-200"
|
||||
)}
|
||||
activeClassName="text-black dark:text-gray-50 !font-bold"
|
||||
isActive={(match, location) => isActiveMatcher(match, location, item)}
|
||||
>
|
||||
{item.name}
|
||||
</NavLink>
|
||||
)}
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://autobrr.com/docs/configuration/indexers"
|
||||
className={classNames(
|
||||
"text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium",
|
||||
"transition-colors duration-200 flex items-center justify-center"
|
||||
)}
|
||||
>
|
||||
Docs
|
||||
<ExternalLinkIcon className="inline ml-1 h-5 w-5"
|
||||
aria-hidden="true"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<div className="ml-4 flex items-center sm:ml-6">
|
||||
<Menu as="div" className="ml-3 relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Menu.Button
|
||||
className={classNames(
|
||||
open ? "bg-gray-200 dark:bg-gray-800" : "",
|
||||
"text-gray-800 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-800",
|
||||
"max-w-xs rounded-full flex items-center text-sm px-3 py-2",
|
||||
"transition-colors duration-200"
|
||||
)}
|
||||
>
|
||||
<span className="hidden text-sm font-medium sm:block">
|
||||
<span className="sr-only">Open user menu for </span>
|
||||
{authContext.username}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className="hidden flex-shrink-0 ml-1 h-5 w-5 text-gray-800 dark:text-gray-300 sm:block"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<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 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>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
to="/settings"
|
||||
className={classNames(
|
||||
active ? "bg-gray-100 dark:bg-gray-600" : "",
|
||||
"block px-4 py-2 text-sm text-gray-700 dark:text-gray-200"
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</NavLink>
|
||||
)}
|
||||
<Link
|
||||
to="/logout"
|
||||
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||
>
|
||||
Logout
|
||||
</Link>
|
||||
</div>
|
||||
Settings
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
to="/logout"
|
||||
className={classNames(
|
||||
active ? "bg-gray-100 dark:bg-gray-600" : "",
|
||||
"block px-4 py-2 text-sm text-gray-700 dark:text-gray-200"
|
||||
)}
|
||||
>
|
||||
Logout
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mr-2 flex sm:hidden">
|
||||
{/* Mobile menu button */}
|
||||
<Disclosure.Button
|
||||
className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
|
||||
<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>
|
||||
</>
|
||||
<Disclosure.Panel className="border-b border-gray-300 dark:border-gray-700 md:hidden">
|
||||
<div className="px-2 py-3 space-y-1 sm:px-3">
|
||||
{nav.map((item) =>
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
strict
|
||||
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||
activeClassName="font-bold bg-gray-300 text-black"
|
||||
isActive={(match, location) => isActiveMatcher(match, location, item)}
|
||||
>
|
||||
{item.name}
|
||||
</NavLink>
|
||||
)}
|
||||
</Disclosure>
|
||||
<Link
|
||||
to="/logout"
|
||||
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||
>
|
||||
Logout
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Switch>
|
||||
<Route path="/logs">
|
||||
<Logs />
|
||||
</Route>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
|
||||
<Route path="/settings">
|
||||
<Settings />
|
||||
</Route>
|
||||
<Switch>
|
||||
<Route path="/logs">
|
||||
<Logs/>
|
||||
</Route>
|
||||
|
||||
<Route path="/releases">
|
||||
<Releases />
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
<Settings/>
|
||||
</Route>
|
||||
|
||||
<Route exact={true} path="/filters">
|
||||
<Filters />
|
||||
</Route>
|
||||
<Route path="/releases">
|
||||
<Releases/>
|
||||
</Route>
|
||||
|
||||
<Route path="/filters/:filterId">
|
||||
<FilterDetails />
|
||||
</Route>
|
||||
<Route exact={true} path="/filters">
|
||||
<Filters/>
|
||||
</Route>
|
||||
|
||||
<Route exact path="/">
|
||||
<Dashboard />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
)
|
||||
<Route path="/filters/:filterId">
|
||||
<FilterDetails/>
|
||||
</Route>
|
||||
|
||||
<Route exact path="/">
|
||||
<Dashboard/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ const LogColors: Record<LogLevel, string> = {
|
|||
"TRACE": "text-purple-300",
|
||||
"DEBUG": "text-yellow-500",
|
||||
"INFO": "text-green-500",
|
||||
"ERROR": "text-red-500",
|
||||
"ERROR": "text-red-500"
|
||||
};
|
||||
|
||||
export const Logs = () => {
|
||||
|
@ -29,7 +29,7 @@ export const Logs = () => {
|
|||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto" });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const es = APIClient.events.logs();
|
||||
|
@ -40,7 +40,7 @@ export const Logs = () => {
|
|||
|
||||
if (settings.scrollOnNewLog)
|
||||
scrollToBottom();
|
||||
}
|
||||
};
|
||||
|
||||
return () => es.close();
|
||||
}, [setLogs, settings]);
|
||||
|
@ -96,7 +96,7 @@ export const Logs = () => {
|
|||
key={idx}
|
||||
className={classNames(
|
||||
settings.indentLogLines ? "grid justify-start grid-flow-col" : "",
|
||||
settings.hideWrappedText ? "truncate hover:text-ellipsis hover:whitespace-normal" : "",
|
||||
settings.hideWrappedText ? "truncate hover:text-ellipsis hover:whitespace-normal" : ""
|
||||
)}
|
||||
>
|
||||
<span
|
||||
|
@ -112,7 +112,7 @@ export const Logs = () => {
|
|||
)}
|
||||
>
|
||||
{a.level}
|
||||
{' '}
|
||||
{" "}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="ml-2 text-black dark:text-gray-300">
|
||||
|
@ -125,5 +125,5 @@ export const Logs = () => {
|
|||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,120 +1,137 @@
|
|||
import {BellIcon, ChatAlt2Icon, CogIcon, CollectionIcon, DownloadIcon, KeyIcon, RssIcon} from '@heroicons/react/outline'
|
||||
import {NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch} from "react-router-dom";
|
||||
import { BellIcon, ChatAlt2Icon, CogIcon, CollectionIcon, DownloadIcon, KeyIcon, RssIcon } from "@heroicons/react/outline";
|
||||
import { NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch } from "react-router-dom";
|
||||
|
||||
import { classNames } from "../utils";
|
||||
import IndexerSettings from "./settings/Indexer";
|
||||
import { IrcSettings } from "./settings/Irc";
|
||||
import ApplicationSettings from "./settings/Application";
|
||||
import DownloadClientSettings from "./settings/DownloadClient";
|
||||
import { RegexPlayground } from './settings/RegexPlayground';
|
||||
import { RegexPlayground } from "./settings/RegexPlayground";
|
||||
import ReleaseSettings from "./settings/Releases";
|
||||
import NotificationSettings from "./settings/Notifications";
|
||||
import FeedSettings from "./settings/Feed";
|
||||
|
||||
const subNavigation = [
|
||||
{name: 'Application', href: '', icon: CogIcon, current: true},
|
||||
{name: 'Indexers', href: 'indexers', icon: KeyIcon, current: false},
|
||||
{name: 'IRC', href: 'irc', icon: ChatAlt2Icon, current: false},
|
||||
{name: 'Feeds', href: 'feeds', icon: RssIcon, current: false},
|
||||
{name: 'Clients', href: 'clients', icon: DownloadIcon, current: false},
|
||||
{name: 'Notifications', href: 'notifications', icon: BellIcon, current: false},
|
||||
{name: 'Releases', href: 'releases', icon: CollectionIcon, current: false},
|
||||
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
|
||||
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, 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
|
||||
const too = item.href ? `${url}/${item.href}` : url
|
||||
return (
|
||||
<NavLink
|
||||
key={item.name}
|
||||
to={too}
|
||||
exact={true}
|
||||
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(
|
||||
'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}
|
||||
>
|
||||
<item.icon
|
||||
className="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"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="truncate">{item.name}</span>
|
||||
</NavLink>
|
||||
)
|
||||
interface NavTabType {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: typeof CogIcon;
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
function SidebarNav({subNavigation, url}: any) {
|
||||
return (
|
||||
<aside className="py-2 lg:col-span-3">
|
||||
<nav className="space-y-1">
|
||||
{subNavigation.map((item: any) => (
|
||||
<SubNavLink item={item} url={url} key={item.href}/>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
)
|
||||
const subNavigation: NavTabType[] = [
|
||||
{ name: "Application", href: "", icon: CogIcon, current: true },
|
||||
{ name: "Indexers", href: "indexers", icon: KeyIcon, current: false },
|
||||
{ name: "IRC", href: "irc", icon: ChatAlt2Icon, current: false },
|
||||
{ name: "Feeds", href: "feeds", icon: RssIcon, current: false },
|
||||
{ name: "Clients", href: "clients", icon: DownloadIcon, current: false },
|
||||
{ name: "Notifications", href: "notifications", icon: BellIcon, current: false },
|
||||
{ name: "Releases", href: "releases", icon: CollectionIcon, current: false }
|
||||
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
|
||||
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
|
||||
];
|
||||
|
||||
interface NavLinkProps {
|
||||
item: NavTabType;
|
||||
url: string;
|
||||
}
|
||||
|
||||
function SubNavLink({ item, url }: NavLinkProps) {
|
||||
const location = useLocation();
|
||||
const { pathname } = location;
|
||||
|
||||
const splitLocation = pathname.split("/");
|
||||
|
||||
// we need to clean the / if it's a base root path
|
||||
const too = item.href ? `${url}/${item.href}` : url;
|
||||
return (
|
||||
<NavLink
|
||||
key={item.name}
|
||||
to={too}
|
||||
exact={true}
|
||||
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(
|
||||
"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}
|
||||
>
|
||||
<item.icon
|
||||
className="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"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="truncate">{item.name}</span>
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
interface SidebarNavProps {
|
||||
subNavigation: NavTabType[];
|
||||
url: string;
|
||||
}
|
||||
|
||||
function SidebarNav({ subNavigation, url }: SidebarNavProps) {
|
||||
return (
|
||||
<aside className="py-2 lg:col-span-3">
|
||||
<nav className="space-y-1">
|
||||
{subNavigation.map((item) => (
|
||||
<SubNavLink item={item} url={url} key={item.href}/>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
const { url } = useRouteMatch();
|
||||
return (
|
||||
<main>
|
||||
<header className="py-10">
|
||||
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-3xl font-bold text-black dark:text-white">Settings</h1>
|
||||
</div>
|
||||
</header>
|
||||
const { url } = useRouteMatch();
|
||||
return (
|
||||
<main>
|
||||
<header className="py-10">
|
||||
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-3xl font-bold text-black dark:text-white">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 dark:bg-gray-800 rounded-lg shadow-lg">
|
||||
<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}/>
|
||||
<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 dark:bg-gray-800 rounded-lg shadow-lg">
|
||||
<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}/>
|
||||
|
||||
<RouteSwitch>
|
||||
<Route exact path={url}>
|
||||
<ApplicationSettings/>
|
||||
</Route>
|
||||
<RouteSwitch>
|
||||
<Route exact path={url}>
|
||||
<ApplicationSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/indexers`}>
|
||||
<IndexerSettings/>
|
||||
</Route>
|
||||
<Route path={`${url}/indexers`}>
|
||||
<IndexerSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/feeds`}>
|
||||
<FeedSettings/>
|
||||
</Route>
|
||||
<Route path={`${url}/feeds`}>
|
||||
<FeedSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/irc`}>
|
||||
<IrcSettings/>
|
||||
</Route>
|
||||
<Route path={`${url}/irc`}>
|
||||
<IrcSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/clients`}>
|
||||
<DownloadClientSettings/>
|
||||
</Route>
|
||||
<Route path={`${url}/clients`}>
|
||||
<DownloadClientSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/notifications`}>
|
||||
<NotificationSettings />
|
||||
</Route>
|
||||
<Route path={`${url}/notifications`}>
|
||||
<NotificationSettings />
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/releases`}>
|
||||
<ReleaseSettings/>
|
||||
</Route>
|
||||
<Route path={`${url}/releases`}>
|
||||
<ReleaseSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/regex-playground`}>
|
||||
<RegexPlayground />
|
||||
</Route>
|
||||
</RouteSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
<Route path={`${url}/regex-playground`}>
|
||||
<RegexPlayground />
|
||||
</Route>
|
||||
</RouteSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -34,11 +34,11 @@ export const Login = () => {
|
|||
isLoggedIn: true
|
||||
});
|
||||
history.push("/");
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleSubmit = (data: any) => mutation.mutate(data);
|
||||
const handleSubmit = (data: LoginData) => mutation.mutate(data);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
|
@ -75,4 +75,4 @@ export const Login = () => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -29,4 +29,4 @@ export const Logout = () => {
|
|||
<p>Logged out</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -27,7 +27,7 @@ export const Onboarding = () => {
|
|||
if (values.password1 !== values.password2)
|
||||
obj.password2 = "Passwords don't match!";
|
||||
|
||||
return obj;
|
||||
return obj;
|
||||
};
|
||||
|
||||
const history = useHistory();
|
||||
|
@ -37,7 +37,7 @@ export const Onboarding = () => {
|
|||
{
|
||||
onSuccess: () => {
|
||||
history.push("/login");
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -81,5 +81,5 @@ export const Onboarding = () => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
useFilters,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination
|
||||
usePagination, FilterProps, Column
|
||||
} from "react-table";
|
||||
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
|
@ -17,17 +17,17 @@ import * as DataTable from "../../components/data-table";
|
|||
// This is a custom filter UI for selecting
|
||||
// a unique option from a list
|
||||
function SelectColumnFilter({
|
||||
column: { filterValue, setFilter, preFilteredRows, id, render },
|
||||
}: any) {
|
||||
column: { filterValue, setFilter, preFilteredRows, id, render }
|
||||
}: FilterProps<object>) {
|
||||
// Calculate the options for filtering
|
||||
// using the preFilteredRows
|
||||
const options = React.useMemo(() => {
|
||||
const options: any = new Set()
|
||||
preFilteredRows.forEach((row: { values: { [x: string]: unknown } }) => {
|
||||
options.add(row.values[id])
|
||||
})
|
||||
return [...options.values()]
|
||||
}, [id, preFilteredRows])
|
||||
const options = new Set<string>();
|
||||
preFilteredRows.forEach((row: { values: { [x: string]: string } }) => {
|
||||
options.add(row.values[id]);
|
||||
});
|
||||
return [...options.values()];
|
||||
}, [id, preFilteredRows]);
|
||||
|
||||
// Render a multi-select box
|
||||
return (
|
||||
|
@ -39,7 +39,7 @@ function SelectColumnFilter({
|
|||
id={id}
|
||||
value={filterValue}
|
||||
onChange={e => {
|
||||
setFilter(e.target.value || undefined)
|
||||
setFilter(e.target.value || undefined);
|
||||
}}
|
||||
>
|
||||
<option value="">All</option>
|
||||
|
@ -50,17 +50,22 @@ function SelectColumnFilter({
|
|||
))}
|
||||
</select>
|
||||
</label>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Table({ columns, data }: any) {
|
||||
interface TableProps {
|
||||
columns: Column[];
|
||||
data: Release[];
|
||||
}
|
||||
|
||||
function Table({ columns, data }: TableProps) {
|
||||
// Use the state and functions returned from useTable to build your UI
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
prepareRow,
|
||||
page, // Instead of using 'rows', we'll use page,
|
||||
page // Instead of using 'rows', we'll use page,
|
||||
} = useTable(
|
||||
{ columns, data },
|
||||
useFilters,
|
||||
|
@ -94,7 +99,7 @@ function Table({ columns, data }: any) {
|
|||
{...columnRest}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{column.render('Header')}
|
||||
{column.render("Header")}
|
||||
{/* Add a sort direction indicator */}
|
||||
<span>
|
||||
{column.isSorted ? (
|
||||
|
@ -119,12 +124,12 @@ function Table({ columns, data }: any) {
|
|||
{...getTableBodyProps()}
|
||||
className="divide-y divide-gray-200 dark:divide-gray-700"
|
||||
>
|
||||
{page.map((row: any) => {
|
||||
{page.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
||||
return (
|
||||
<tr key={bodyRowKey} {...bodyRowRest}>
|
||||
{row.cells.map((cell: any) => {
|
||||
{row.cells.map((cell) => {
|
||||
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
||||
return (
|
||||
<td
|
||||
|
@ -133,12 +138,12 @@ function Table({ columns, data }: any) {
|
|||
role="cell"
|
||||
{...cellRowRest}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
{cell.render("Cell")}
|
||||
</td>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -151,30 +156,30 @@ export const ActivityTable = () => {
|
|||
const columns = React.useMemo(() => [
|
||||
{
|
||||
Header: "Age",
|
||||
accessor: 'timestamp',
|
||||
Cell: DataTable.AgeCell,
|
||||
accessor: "timestamp",
|
||||
Cell: DataTable.AgeCell
|
||||
},
|
||||
{
|
||||
Header: "Release",
|
||||
accessor: 'torrent_name',
|
||||
Cell: DataTable.TitleCell,
|
||||
accessor: "torrent_name",
|
||||
Cell: DataTable.TitleCell
|
||||
},
|
||||
{
|
||||
Header: "Actions",
|
||||
accessor: 'action_status',
|
||||
Cell: DataTable.ReleaseStatusCell,
|
||||
accessor: "action_status",
|
||||
Cell: DataTable.ReleaseStatusCell
|
||||
},
|
||||
{
|
||||
Header: "Indexer",
|
||||
accessor: 'indexer',
|
||||
accessor: "indexer",
|
||||
Cell: DataTable.TitleCell,
|
||||
Filter: SelectColumnFilter,
|
||||
filter: 'includes',
|
||||
},
|
||||
], [])
|
||||
filter: "includes"
|
||||
}
|
||||
], []);
|
||||
|
||||
const { isLoading, data } = useQuery(
|
||||
'dash_release',
|
||||
"dash_release",
|
||||
() => APIClient.release.find("?limit=10"),
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
@ -188,7 +193,7 @@ export const ActivityTable = () => {
|
|||
Recent activity
|
||||
</h3>
|
||||
|
||||
<Table columns={columns} data={data?.data} />
|
||||
<Table columns={columns} data={data?.data ?? []} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -8,41 +8,41 @@ interface StatsItemProps {
|
|||
|
||||
const StatsItem = ({ name, value }: StatsItemProps) => (
|
||||
<div
|
||||
className="relative px-4 py-5 overflow-hidden bg-white rounded-lg shadow-lg dark:bg-gray-800"
|
||||
title="All time"
|
||||
className="relative px-4 py-5 overflow-hidden bg-white rounded-lg shadow-lg dark:bg-gray-800"
|
||||
title="All time"
|
||||
>
|
||||
<dt>
|
||||
<p className="pb-1 text-sm font-medium text-gray-500 truncate">{name}</p>
|
||||
</dt>
|
||||
<dt>
|
||||
<p className="pb-1 text-sm font-medium text-gray-500 truncate">{name}</p>
|
||||
</dt>
|
||||
|
||||
<dd className="flex items-baseline">
|
||||
<p className="text-3xl font-extrabold text-gray-900 dark:text-gray-200">{value}</p>
|
||||
</dd>
|
||||
<dd className="flex items-baseline">
|
||||
<p className="text-3xl font-extrabold text-gray-900 dark:text-gray-200">{value}</p>
|
||||
</dd>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export const Stats = () => {
|
||||
const { isLoading, data } = useQuery(
|
||||
"dash_release_stats",
|
||||
() => APIClient.release.stats(),
|
||||
{ refetchOnWindowFocus: false }
|
||||
"dash_release_stats",
|
||||
() => APIClient.release.stats(),
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
if (isLoading)
|
||||
return null;
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
|
||||
<div>
|
||||
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
|
||||
Stats
|
||||
</h3>
|
||||
</h3>
|
||||
|
||||
<dl className="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<StatsItem name="Filtered Releases" value={data?.filtered_count} />
|
||||
{/* <StatsItem name="Filter Rejected Releases" stat={data?.filter_rejected_count} /> */}
|
||||
<StatsItem name="Rejected Pushes" value={data?.push_rejected_count} />
|
||||
<StatsItem name="Approved Pushes" value={data?.push_approved_count} />
|
||||
</dl>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<dl className="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<StatsItem name="Filtered Releases" value={data?.filtered_count} />
|
||||
{/* <StatsItem name="Filter Rejected Releases" stat={data?.filter_rejected_count} /> */}
|
||||
<StatsItem name="Rejected Pushes" value={data?.push_rejected_count} />
|
||||
<StatsItem name="Approved Pushes" value={data?.push_approved_count} />
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -2,10 +2,10 @@ import { Stats } from "./Stats";
|
|||
import { ActivityTable } from "./ActivityTable";
|
||||
|
||||
export const Dashboard = () => (
|
||||
<main className="py-10">
|
||||
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
||||
<Stats />
|
||||
<ActivityTable />
|
||||
</div>
|
||||
</main>
|
||||
<main className="py-10">
|
||||
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
||||
<Stats />
|
||||
<ActivityTable />
|
||||
</div>
|
||||
</main>
|
||||
);
|
File diff suppressed because it is too large
Load diff
|
@ -4,10 +4,10 @@ import { toast } from "react-hot-toast";
|
|||
import { Menu, Switch, Transition } from "@headlessui/react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import {
|
||||
TrashIcon,
|
||||
PencilAltIcon,
|
||||
SwitchHorizontalIcon,
|
||||
DotsHorizontalIcon, DuplicateIcon,
|
||||
TrashIcon,
|
||||
PencilAltIcon,
|
||||
SwitchHorizontalIcon,
|
||||
DotsHorizontalIcon, DuplicateIcon
|
||||
} from "@heroicons/react/outline";
|
||||
|
||||
import { queryClient } from "../../App";
|
||||
|
@ -20,50 +20,50 @@ import { EmptyListState } from "../../components/emptystates";
|
|||
import { DeleteModal } from "../../components/modals";
|
||||
|
||||
export default function Filters() {
|
||||
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false)
|
||||
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false);
|
||||
|
||||
const { isLoading, error, data } = useQuery(
|
||||
["filters"],
|
||||
APIClient.filters.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
const { isLoading, error, data } = useQuery(
|
||||
["filters"],
|
||||
APIClient.filters.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
if (isLoading)
|
||||
return null;
|
||||
if (isLoading)
|
||||
return null;
|
||||
|
||||
if (error)
|
||||
return (<p>An error has occurred: </p>);
|
||||
if (error)
|
||||
return (<p>An error has occurred: </p>);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} />
|
||||
return (
|
||||
<main>
|
||||
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} />
|
||||
|
||||
<header className="py-10">
|
||||
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
|
||||
<h1 className="text-3xl font-bold text-black dark:text-white">
|
||||
<header className="py-10">
|
||||
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
|
||||
<h1 className="text-3xl font-bold text-black dark:text-white">
|
||||
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 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}
|
||||
>
|
||||
</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 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}
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="max-w-screen-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8 relative">
|
||||
{data && data.length > 0 ? (
|
||||
<FilterList filters={data} />
|
||||
) : (
|
||||
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
<div className="max-w-screen-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8 relative">
|
||||
{data && data.length > 0 ? (
|
||||
<FilterList filters={data} />
|
||||
) : (
|
||||
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
interface FilterListProps {
|
||||
|
@ -71,33 +71,33 @@ interface FilterListProps {
|
|||
}
|
||||
|
||||
function FilterList({ filters }: FilterListProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto align-middle min-w-full rounded-lg shadow-lg">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
|
||||
<tr>
|
||||
{["Enabled", "Name", "Indexers"].map((label) => (
|
||||
<th
|
||||
key={`th-${label}`}
|
||||
scope="col"
|
||||
className="px-6 py-2.5 text-left text-xs font-medium uppercase tracking-wider"
|
||||
>
|
||||
{label}
|
||||
</th>
|
||||
))}
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
{filters.map((filter: Filter, idx) => (
|
||||
<FilterListItem filter={filter} key={filter.id} idx={idx} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="overflow-x-auto align-middle min-w-full rounded-lg shadow-lg">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
|
||||
<tr>
|
||||
{["Enabled", "Name", "Indexers"].map((label) => (
|
||||
<th
|
||||
key={`th-${label}`}
|
||||
scope="col"
|
||||
className="px-6 py-2.5 text-left text-xs font-medium uppercase tracking-wider"
|
||||
>
|
||||
{label}
|
||||
</th>
|
||||
))}
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
{filters.map((filter: Filter, idx) => (
|
||||
<FilterListItem filter={filter} key={filter.id} idx={idx} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface FilterItemDropdownProps {
|
||||
|
@ -106,157 +106,157 @@ interface FilterItemDropdownProps {
|
|||
}
|
||||
|
||||
const FilterItemDropdown = ({
|
||||
filter,
|
||||
onToggle
|
||||
filter,
|
||||
onToggle
|
||||
}: FilterItemDropdownProps) => {
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const deleteMutation = useMutation(
|
||||
(id: number) => APIClient.filters.delete(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["filters"]);
|
||||
queryClient.invalidateQueries(["filters", filter.id]);
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const deleteMutation = useMutation(
|
||||
(id: number) => APIClient.filters.delete(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["filters"]);
|
||||
queryClient.invalidateQueries(["filters", filter.id]);
|
||||
|
||||
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} was deleted`} t={t} />);
|
||||
}
|
||||
}
|
||||
);
|
||||
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} was deleted`} t={t} />);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const duplicateMutation = useMutation(
|
||||
(id: number) => APIClient.filters.duplicate(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["filters"]);
|
||||
const duplicateMutation = useMutation(
|
||||
(id: number) => APIClient.filters.duplicate(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["filters"]);
|
||||
|
||||
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} duplicated`} t={t} />);
|
||||
}
|
||||
}
|
||||
);
|
||||
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} duplicated`} t={t} />);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu as="div">
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={() => {
|
||||
deleteMutation.mutate(filter.id);
|
||||
toggleDeleteModal();
|
||||
}}
|
||||
title={`Remove filter: ${filter.name}`}
|
||||
text="Are you sure you want to remove this filter? This action cannot be undone."
|
||||
/>
|
||||
<Menu.Button className="px-4 py-2">
|
||||
<DotsHorizontalIcon
|
||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
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
|
||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||
return (
|
||||
<Menu as="div">
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={() => {
|
||||
deleteMutation.mutate(filter.id);
|
||||
toggleDeleteModal();
|
||||
}}
|
||||
title={`Remove filter: ${filter.name}`}
|
||||
text="Are you sure you want to remove this filter? This action cannot be undone."
|
||||
/>
|
||||
<Menu.Button className="px-4 py-2">
|
||||
<DotsHorizontalIcon
|
||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
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
|
||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
to={`filters/${filter.id.toString()}`}
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
to={`filters/${filter.id.toString()}`}
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
>
|
||||
<PencilAltIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<PencilAltIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Edit
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => onToggle(!filter.enabled)}
|
||||
>
|
||||
<SwitchHorizontalIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => onToggle(!filter.enabled)}
|
||||
>
|
||||
<SwitchHorizontalIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Toggle
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => duplicateMutation.mutate(filter.id)}
|
||||
>
|
||||
<DuplicateIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => duplicateMutation.mutate(filter.id)}
|
||||
>
|
||||
<DuplicateIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Duplicate
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => toggleDeleteModal()}
|
||||
>
|
||||
<TrashIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-red-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => toggleDeleteModal()}
|
||||
>
|
||||
<TrashIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-red-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
interface FilterListItemProps {
|
||||
filter: Filter;
|
||||
|
@ -264,83 +264,83 @@ interface FilterListItemProps {
|
|||
}
|
||||
|
||||
function FilterListItem({ filter, idx }: FilterListItemProps) {
|
||||
const [enabled, setEnabled] = useState(filter.enabled)
|
||||
const [enabled, setEnabled] = useState(filter.enabled);
|
||||
|
||||
const updateMutation = useMutation(
|
||||
(status: boolean) => APIClient.filters.toggleEnable(filter.id, status),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was ${enabled ? "disabled" : "enabled"} successfully`} t={t} />)
|
||||
const updateMutation = useMutation(
|
||||
(status: boolean) => APIClient.filters.toggleEnable(filter.id, status),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was ${enabled ? "disabled" : "enabled"} successfully`} t={t} />);
|
||||
|
||||
// We need to invalidate both keys here.
|
||||
// The filters key is used on the /filters page,
|
||||
// while the ["filter", filter.id] key is used on the details page.
|
||||
queryClient.invalidateQueries(["filters"]);
|
||||
queryClient.invalidateQueries(["filters", filter?.id]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const toggleActive = (status: boolean) => {
|
||||
setEnabled(status);
|
||||
updateMutation.mutate(status);
|
||||
// We need to invalidate both keys here.
|
||||
// The filters key is used on the /filters page,
|
||||
// while the ["filter", filter.id] key is used on the details page.
|
||||
queryClient.invalidateQueries(["filters"]);
|
||||
queryClient.invalidateQueries(["filters", filter?.id]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={filter.id}
|
||||
className={classNames(
|
||||
idx % 2 === 0 ?
|
||||
"bg-white dark:bg-[#2e2e31]" :
|
||||
"bg-gray-50 dark:bg-gray-800",
|
||||
"hover:bg-gray-100 dark:hover:bg-[#222225]"
|
||||
)}
|
||||
const toggleActive = (status: boolean) => {
|
||||
setEnabled(status);
|
||||
updateMutation.mutate(status);
|
||||
};
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={filter.id}
|
||||
className={classNames(
|
||||
idx % 2 === 0 ?
|
||||
"bg-white dark:bg-[#2e2e31]" :
|
||||
"bg-gray-50 dark:bg-gray-800",
|
||||
"hover:bg-gray-100 dark:hover:bg-[#222225]"
|
||||
)}
|
||||
>
|
||||
<td
|
||||
className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100"
|
||||
>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={toggleActive}
|
||||
className={classNames(
|
||||
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-blue-500"
|
||||
)}
|
||||
>
|
||||
<td
|
||||
className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100"
|
||||
>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={toggleActive}
|
||||
className={classNames(
|
||||
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-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 dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</td>
|
||||
<td className="px-6 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<Link
|
||||
to={`filters/${filter.id.toString()}`}
|
||||
className="hover:text-black dark:hover:text-gray-300 w-full py-4 flex"
|
||||
>
|
||||
{filter.name}
|
||||
</Link>
|
||||
</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-200 dark:bg-gray-700 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">
|
||||
<FilterItemDropdown
|
||||
filter={filter}
|
||||
onToggle={toggleActive}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
<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 dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</td>
|
||||
<td className="px-6 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<Link
|
||||
to={`filters/${filter.id.toString()}`}
|
||||
className="hover:text-black dark:hover:text-gray-300 w-full py-4 flex"
|
||||
>
|
||||
{filter.name}
|
||||
</Link>
|
||||
</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-200 dark:bg-gray-700 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">
|
||||
<FilterItemDropdown
|
||||
filter={filter}
|
||||
onToggle={toggleActive}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
|
@ -2,90 +2,91 @@ import * as React from "react";
|
|||
import { useQuery } from "react-query";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon
|
||||
} from "@heroicons/react/solid";
|
||||
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { classNames } from "../../utils";
|
||||
import { PushStatusOptions } from "../../domain/constants";
|
||||
import { FilterProps } from "react-table";
|
||||
|
||||
interface ListboxFilterProps {
|
||||
id: string;
|
||||
label: string;
|
||||
currentValue: string;
|
||||
onChange: (newValue: string) => void;
|
||||
children: any;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ListboxFilter = ({
|
||||
id,
|
||||
label,
|
||||
currentValue,
|
||||
onChange,
|
||||
children
|
||||
id,
|
||||
label,
|
||||
currentValue,
|
||||
onChange,
|
||||
children
|
||||
}: ListboxFilterProps) => (
|
||||
<div className="w-48">
|
||||
<Listbox
|
||||
refName={id}
|
||||
value={currentValue}
|
||||
onChange={onChange}
|
||||
<div className="w-48">
|
||||
<Listbox
|
||||
refName={id}
|
||||
value={currentValue}
|
||||
onChange={onChange}
|
||||
>
|
||||
<div className="relative mt-1">
|
||||
<Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white dark:bg-gray-800 rounded-lg shadow-md cursor-default dark:text-gray-400 sm:text-sm">
|
||||
<span className="block truncate">{label}</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<ChevronDownIcon
|
||||
className="w-5 h-5 ml-2 -mr-1 text-gray-600 hover:text-gray-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="relative mt-1">
|
||||
<Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white dark:bg-gray-800 rounded-lg shadow-md cursor-default dark:text-gray-400 sm:text-sm">
|
||||
<span className="block truncate">{label}</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<ChevronDownIcon
|
||||
className="w-5 h-5 ml-2 -mr-1 text-gray-600 hover:text-gray-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options
|
||||
className="absolute w-full mt-1 overflow-auto text-base bg-white dark:bg-gray-800 rounded-md shadow-lg max-h-60 border border-opacity-5 border-black dark:border-gray-700 dark:border-opacity-40 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<FilterOption label="All" />
|
||||
{children}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
<Listbox.Options
|
||||
className="absolute w-full mt-1 overflow-auto text-base bg-white dark:bg-gray-800 rounded-md shadow-lg max-h-60 border border-opacity-5 border-black dark:border-gray-700 dark:border-opacity-40 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<FilterOption label="All" />
|
||||
{children}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
);
|
||||
|
||||
// a unique option from a list
|
||||
export const IndexerSelectColumnFilter = ({
|
||||
column: { filterValue, setFilter, id }
|
||||
}: any) => {
|
||||
const { data, isSuccess } = useQuery(
|
||||
"release_indexers",
|
||||
() => APIClient.release.indexerOptions(),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
column: { filterValue, setFilter, id }
|
||||
}: FilterProps<object>) => {
|
||||
const { data, isSuccess } = useQuery(
|
||||
"release_indexers",
|
||||
() => APIClient.release.indexerOptions(),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: Infinity
|
||||
}
|
||||
);
|
||||
|
||||
// Render a multi-select box
|
||||
return (
|
||||
<ListboxFilter
|
||||
id={id}
|
||||
label={filterValue ?? "Indexer"}
|
||||
currentValue={filterValue}
|
||||
onChange={setFilter}
|
||||
>
|
||||
{isSuccess && data?.map((indexer, idx) => (
|
||||
<FilterOption key={idx} label={indexer} value={indexer} />
|
||||
))}
|
||||
</ListboxFilter>
|
||||
)
|
||||
}
|
||||
// Render a multi-select box
|
||||
return (
|
||||
<ListboxFilter
|
||||
id={id}
|
||||
label={filterValue ?? "Indexer"}
|
||||
currentValue={filterValue}
|
||||
onChange={setFilter}
|
||||
>
|
||||
{isSuccess && data?.map((indexer, idx) => (
|
||||
<FilterOption key={idx} label={indexer} value={indexer} />
|
||||
))}
|
||||
</ListboxFilter>
|
||||
);
|
||||
};
|
||||
|
||||
interface FilterOptionProps {
|
||||
label: string;
|
||||
|
@ -93,50 +94,48 @@ interface FilterOptionProps {
|
|||
}
|
||||
|
||||
const FilterOption = ({ label, value }: FilterOptionProps) => (
|
||||
<Listbox.Option
|
||||
className={({ active }) => classNames(
|
||||
"cursor-pointer select-none relative py-2 pl-10 pr-4",
|
||||
active ? 'text-black dark:text-gray-200 bg-gray-100 dark:bg-gray-900' : 'text-gray-700 dark:text-gray-400'
|
||||
)}
|
||||
value={value}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
"block truncate",
|
||||
selected ? "font-medium text-black dark:text-white" : "font-normal"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500 dark:text-gray-400">
|
||||
<CheckIcon className="w-5 h-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
<Listbox.Option
|
||||
className={({ active }) => classNames(
|
||||
"cursor-pointer select-none relative py-2 pl-10 pr-4",
|
||||
active ? "text-black dark:text-gray-200 bg-gray-100 dark:bg-gray-900" : "text-gray-700 dark:text-gray-400"
|
||||
)}
|
||||
value={value}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
"block truncate",
|
||||
selected ? "font-medium text-black dark:text-white" : "font-normal"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500 dark:text-gray-400">
|
||||
<CheckIcon className="w-5 h-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
);
|
||||
|
||||
export const PushStatusSelectColumnFilter = ({
|
||||
column: { filterValue, setFilter, id }
|
||||
}: any) => (
|
||||
column: { filterValue, setFilter, id }
|
||||
}: FilterProps<object>) => {
|
||||
const label = filterValue ? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label : "Push status";
|
||||
return (
|
||||
<div className="mr-3">
|
||||
<ListboxFilter
|
||||
id={id}
|
||||
label={
|
||||
filterValue
|
||||
? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label
|
||||
: "Push status"
|
||||
}
|
||||
currentValue={filterValue}
|
||||
onChange={setFilter}
|
||||
>
|
||||
{PushStatusOptions.map((status, idx) => (
|
||||
<FilterOption key={idx} value={status.value} label={status.label} />
|
||||
))}
|
||||
</ListboxFilter>
|
||||
<ListboxFilter
|
||||
id={id}
|
||||
label={label ?? "Push status"}
|
||||
currentValue={filterValue}
|
||||
onChange={setFilter}
|
||||
>
|
||||
{PushStatusOptions.map((status, idx) => (
|
||||
<FilterOption key={idx} value={status.value} label={status.label} />
|
||||
))}
|
||||
</ListboxFilter>
|
||||
</div>
|
||||
);
|
||||
);};
|
|
@ -1,17 +1,17 @@
|
|||
import * as React from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import {
|
||||
useTable,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useFilters,
|
||||
Column
|
||||
useTable,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useFilters,
|
||||
Column
|
||||
} from "react-table";
|
||||
import {
|
||||
ChevronDoubleLeftIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronDoubleRightIcon
|
||||
ChevronDoubleLeftIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronDoubleRightIcon
|
||||
} from "@heroicons/react/solid";
|
||||
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
|
@ -21,295 +21,310 @@ import * as Icons from "../../components/Icons";
|
|||
import * as DataTable from "../../components/data-table";
|
||||
|
||||
import {
|
||||
IndexerSelectColumnFilter,
|
||||
PushStatusSelectColumnFilter
|
||||
IndexerSelectColumnFilter,
|
||||
PushStatusSelectColumnFilter
|
||||
} from "./Filters";
|
||||
|
||||
const initialState = {
|
||||
queryPageIndex: 0,
|
||||
queryPageSize: 10,
|
||||
totalCount: null,
|
||||
queryFilters: []
|
||||
type TableState = {
|
||||
queryPageIndex: number;
|
||||
queryPageSize: number;
|
||||
totalCount: number;
|
||||
queryFilters: ReleaseFilter[];
|
||||
};
|
||||
|
||||
const PAGE_CHANGED = 'PAGE_CHANGED';
|
||||
const PAGE_SIZE_CHANGED = 'PAGE_SIZE_CHANGED';
|
||||
const TOTAL_COUNT_CHANGED = 'TOTAL_COUNT_CHANGED';
|
||||
const FILTER_CHANGED = 'FILTER_CHANGED';
|
||||
const initialState: TableState = {
|
||||
queryPageIndex: 0,
|
||||
queryPageSize: 10,
|
||||
totalCount: 0,
|
||||
queryFilters: []
|
||||
};
|
||||
|
||||
const TableReducer = (state: any, { type, payload }: any) => {
|
||||
switch (type) {
|
||||
case PAGE_CHANGED:
|
||||
return { ...state, queryPageIndex: payload };
|
||||
case PAGE_SIZE_CHANGED:
|
||||
return { ...state, queryPageSize: payload };
|
||||
case TOTAL_COUNT_CHANGED:
|
||||
return { ...state, totalCount: payload };
|
||||
case FILTER_CHANGED:
|
||||
return { ...state, queryFilters: payload };
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${type}`);
|
||||
}
|
||||
enum ActionType {
|
||||
PAGE_CHANGED = "PAGE_CHANGED",
|
||||
PAGE_SIZE_CHANGED = "PAGE_SIZE_CHANGED",
|
||||
TOTAL_COUNT_CHANGED = "TOTAL_COUNT_CHANGED",
|
||||
FILTER_CHANGED = "FILTER_CHANGED"
|
||||
}
|
||||
|
||||
type Actions =
|
||||
| { type: ActionType.FILTER_CHANGED; payload: ReleaseFilter[]; }
|
||||
| { type: ActionType.PAGE_CHANGED; payload: number; }
|
||||
| { type: ActionType.PAGE_SIZE_CHANGED; payload: number; }
|
||||
| { type: ActionType.TOTAL_COUNT_CHANGED; payload: number; };
|
||||
|
||||
const TableReducer = (state: TableState, action: Actions): TableState => {
|
||||
switch (action.type) {
|
||||
case ActionType.PAGE_CHANGED:
|
||||
return { ...state, queryPageIndex: action.payload };
|
||||
case ActionType.PAGE_SIZE_CHANGED:
|
||||
return { ...state, queryPageSize: action.payload };
|
||||
case ActionType.FILTER_CHANGED:
|
||||
return { ...state, queryFilters: action.payload };
|
||||
case ActionType.TOTAL_COUNT_CHANGED:
|
||||
return { ...state, totalCount: action.payload };
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${action}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const ReleaseTable = () => {
|
||||
const columns = React.useMemo(() => [
|
||||
{
|
||||
Header: "Age",
|
||||
accessor: 'timestamp',
|
||||
Cell: DataTable.AgeCell,
|
||||
},
|
||||
{
|
||||
Header: "Release",
|
||||
accessor: 'torrent_name',
|
||||
Cell: DataTable.TitleCell,
|
||||
},
|
||||
{
|
||||
Header: "Actions",
|
||||
accessor: 'action_status',
|
||||
Cell: DataTable.ReleaseStatusCell,
|
||||
Filter: PushStatusSelectColumnFilter,
|
||||
},
|
||||
{
|
||||
Header: "Indexer",
|
||||
accessor: 'indexer',
|
||||
Cell: DataTable.TitleCell,
|
||||
Filter: IndexerSelectColumnFilter,
|
||||
filter: 'equal',
|
||||
},
|
||||
] as Column<Release>[], [])
|
||||
const columns = React.useMemo(() => [
|
||||
{
|
||||
Header: "Age",
|
||||
accessor: "timestamp",
|
||||
Cell: DataTable.AgeCell
|
||||
},
|
||||
{
|
||||
Header: "Release",
|
||||
accessor: "torrent_name",
|
||||
Cell: DataTable.TitleCell
|
||||
},
|
||||
{
|
||||
Header: "Actions",
|
||||
accessor: "action_status",
|
||||
Cell: DataTable.ReleaseStatusCell,
|
||||
Filter: PushStatusSelectColumnFilter
|
||||
},
|
||||
{
|
||||
Header: "Indexer",
|
||||
accessor: "indexer",
|
||||
Cell: DataTable.TitleCell,
|
||||
Filter: IndexerSelectColumnFilter,
|
||||
filter: "equal"
|
||||
}
|
||||
] as Column<Release>[], []);
|
||||
|
||||
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
|
||||
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
|
||||
React.useReducer(TableReducer, initialState);
|
||||
|
||||
const { isLoading, error, data, isSuccess } = useQuery(
|
||||
['releases', queryPageIndex, queryPageSize, queryFilters],
|
||||
() => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 5000,
|
||||
}
|
||||
);
|
||||
const { isLoading, error, data, isSuccess } = useQuery(
|
||||
["releases", queryPageIndex, queryPageSize, queryFilters],
|
||||
() => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 5000
|
||||
}
|
||||
);
|
||||
|
||||
// Use the state and functions returned from useTable to build your UI
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
prepareRow,
|
||||
page, // Instead of using 'rows', we'll use page,
|
||||
// which has only the rows for the active page
|
||||
// Use the state and functions returned from useTable to build your UI
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
prepareRow,
|
||||
page, // Instead of using 'rows', we'll use page,
|
||||
// which has only the rows for the active page
|
||||
|
||||
// The rest of these things are super handy, too ;)
|
||||
canPreviousPage,
|
||||
canNextPage,
|
||||
pageOptions,
|
||||
pageCount,
|
||||
gotoPage,
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageSize,
|
||||
state: { pageIndex, pageSize, filters }
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data: data && isSuccess ? data.data : [],
|
||||
initialState: {
|
||||
pageIndex: queryPageIndex,
|
||||
pageSize: queryPageSize,
|
||||
filters: []
|
||||
},
|
||||
manualPagination: true,
|
||||
manualFilters: true,
|
||||
manualSortBy: true,
|
||||
pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : 0,
|
||||
autoResetSortBy: false,
|
||||
autoResetExpanded: false,
|
||||
autoResetPage: false
|
||||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
);
|
||||
// The rest of these things are super handy, too ;)
|
||||
canPreviousPage,
|
||||
canNextPage,
|
||||
pageOptions,
|
||||
pageCount,
|
||||
gotoPage,
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageSize,
|
||||
state: { pageIndex, pageSize, filters }
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data: data && isSuccess ? data.data : [],
|
||||
initialState: {
|
||||
pageIndex: queryPageIndex,
|
||||
pageSize: queryPageSize,
|
||||
filters: []
|
||||
},
|
||||
manualPagination: true,
|
||||
manualFilters: true,
|
||||
manualSortBy: true,
|
||||
pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : 0,
|
||||
autoResetSortBy: false,
|
||||
autoResetExpanded: false,
|
||||
autoResetPage: false
|
||||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
usePagination
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: PAGE_CHANGED, payload: pageIndex });
|
||||
}, [pageIndex]);
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: ActionType.PAGE_CHANGED, payload: pageIndex });
|
||||
}, [pageIndex]);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: PAGE_SIZE_CHANGED, payload: pageSize });
|
||||
gotoPage(0);
|
||||
}, [pageSize, gotoPage]);
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: ActionType.PAGE_SIZE_CHANGED, payload: pageSize });
|
||||
gotoPage(0);
|
||||
}, [pageSize, gotoPage]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (data?.count) {
|
||||
dispatch({
|
||||
type: TOTAL_COUNT_CHANGED,
|
||||
payload: data.count,
|
||||
});
|
||||
}
|
||||
}, [data?.count]);
|
||||
React.useEffect(() => {
|
||||
if (data?.count) {
|
||||
dispatch({
|
||||
type: ActionType.TOTAL_COUNT_CHANGED,
|
||||
payload: data.count
|
||||
});
|
||||
}
|
||||
}, [data?.count]);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: FILTER_CHANGED, payload: filters });
|
||||
}, [filters]);
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: ActionType.FILTER_CHANGED, payload: filters });
|
||||
}, [filters]);
|
||||
|
||||
if (error)
|
||||
return <p>Error</p>;
|
||||
if (error)
|
||||
return <p>Error</p>;
|
||||
|
||||
if (isLoading)
|
||||
return <p>Loading...</p>;
|
||||
if (isLoading)
|
||||
return <p>Loading...</p>;
|
||||
|
||||
if (!data)
|
||||
return <EmptyListState text="No recent activity" />
|
||||
if (!data)
|
||||
return <EmptyListState text="No recent activity" />;
|
||||
|
||||
// Render the UI for your table
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex mb-6">
|
||||
{headerGroups.map((headerGroup: { headers: any[] }) =>
|
||||
headerGroup.headers.map((column) => (
|
||||
column.Filter ? (
|
||||
<div className="mt-2 sm:mt-0" key={column.id}>
|
||||
{column.render("Filter")}
|
||||
</div>
|
||||
) : null
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-auto bg-white shadow-lg dark:bg-gray-800 rounded-lg">
|
||||
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<tr key={rowKey} {...rowRest}>
|
||||
{headerGroup.headers.map((column) => {
|
||||
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps());
|
||||
return (
|
||||
// Add the sorting props to control sorting. For this example
|
||||
// we can add them into the header props
|
||||
<th
|
||||
key={`${rowKey}-${columnKey}`}
|
||||
scope="col"
|
||||
className="first:pl-5 pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group"
|
||||
{...columnRest}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{column.render('Header')}
|
||||
{/* Add a sort direction indicator */}
|
||||
<span>
|
||||
{column.isSorted ? (
|
||||
column.isSortedDesc ? (
|
||||
<Icons.SortDownIcon className="w-4 h-4 text-gray-400" />
|
||||
) : (
|
||||
<Icons.SortUpIcon className="w-4 h-4 text-gray-400" />
|
||||
)
|
||||
) : (
|
||||
<Icons.SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
{...getTableBodyProps()}
|
||||
className="divide-y divide-gray-200 dark:divide-gray-700"
|
||||
>
|
||||
{page.map((row: any) => {
|
||||
prepareRow(row);
|
||||
// Render the UI for your table
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex mb-6">
|
||||
{headerGroups.map((headerGroup) =>
|
||||
headerGroup.headers.map((column) => (
|
||||
column.Filter ? (
|
||||
<div className="mt-2 sm:mt-0" key={column.id}>
|
||||
{column.render("Filter")}
|
||||
</div>
|
||||
) : null
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-auto bg-white shadow-lg dark:bg-gray-800 rounded-lg">
|
||||
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<tr key={rowKey} {...rowRest}>
|
||||
{headerGroup.headers.map((column) => {
|
||||
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps());
|
||||
return (
|
||||
// Add the sorting props to control sorting. For this example
|
||||
// we can add them into the header props
|
||||
<th
|
||||
key={`${rowKey}-${columnKey}`}
|
||||
scope="col"
|
||||
className="first:pl-5 pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group"
|
||||
{...columnRest}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{column.render("Header")}
|
||||
{/* Add a sort direction indicator */}
|
||||
<span>
|
||||
{column.isSorted ? (
|
||||
column.isSortedDesc ? (
|
||||
<Icons.SortDownIcon className="w-4 h-4 text-gray-400" />
|
||||
) : (
|
||||
<Icons.SortUpIcon className="w-4 h-4 text-gray-400" />
|
||||
)
|
||||
) : (
|
||||
<Icons.SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
{...getTableBodyProps()}
|
||||
className="divide-y divide-gray-200 dark:divide-gray-700"
|
||||
>
|
||||
{page.map((row) => {
|
||||
prepareRow(row);
|
||||
|
||||
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
||||
return (
|
||||
<tr key={bodyRowKey} {...bodyRowRest}>
|
||||
{row.cells.map((cell: any) => {
|
||||
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
||||
return (
|
||||
<td
|
||||
key={cellRowKey}
|
||||
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
||||
role="cell"
|
||||
{...cellRowRest}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
||||
return (
|
||||
<tr key={bodyRowKey} {...bodyRowRest}>
|
||||
{row.cells.map((cell) => {
|
||||
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
||||
return (
|
||||
<td
|
||||
key={cellRowKey}
|
||||
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
||||
role="cell"
|
||||
{...cellRowRest}
|
||||
>
|
||||
{cell.render("Cell")}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between flex-1 sm:hidden">
|
||||
<DataTable.Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</DataTable.Button>
|
||||
<DataTable.Button onClick={() => nextPage()} disabled={!canNextPage}>Next</DataTable.Button>
|
||||
</div>
|
||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div className="flex items-baseline gap-x-2">
|
||||
<span className="text-sm text-gray-700">
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between flex-1 sm:hidden">
|
||||
<DataTable.Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</DataTable.Button>
|
||||
<DataTable.Button onClick={() => nextPage()} disabled={!canNextPage}>Next</DataTable.Button>
|
||||
</div>
|
||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div className="flex items-baseline gap-x-2">
|
||||
<span className="text-sm text-gray-700">
|
||||
Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
|
||||
</span>
|
||||
<label>
|
||||
<span className="sr-only bg-gray-700">Items Per Page</span>
|
||||
<select
|
||||
className="block w-full border-gray-300 rounded-md shadow-sm cursor-pointer dark:bg-gray-800 dark:border-gray-800 dark:text-gray-600 dark:hover:text-gray-500 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
value={pageSize}
|
||||
onChange={e => {
|
||||
setPageSize(Number(e.target.value))
|
||||
}}
|
||||
>
|
||||
{[5, 10, 20, 50].map(pageSize => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
</span>
|
||||
<label>
|
||||
<span className="sr-only bg-gray-700">Items Per Page</span>
|
||||
<select
|
||||
className="block w-full border-gray-300 rounded-md shadow-sm cursor-pointer dark:bg-gray-800 dark:border-gray-800 dark:text-gray-600 dark:hover:text-gray-500 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
value={pageSize}
|
||||
onChange={e => {
|
||||
setPageSize(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{[5, 10, 20, 50].map(pageSize => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
Show {pageSize}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<nav className="relative z-0 inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||
<DataTable.PageButton
|
||||
className="rounded-l-md"
|
||||
onClick={() => gotoPage(0)}
|
||||
disabled={!canPreviousPage}
|
||||
>
|
||||
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">First</span>
|
||||
<ChevronDoubleLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
||||
</DataTable.PageButton>
|
||||
<DataTable.PageButton
|
||||
onClick={() => previousPage()}
|
||||
disabled={!canPreviousPage}
|
||||
>
|
||||
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Previous</span>
|
||||
<ChevronLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
||||
</DataTable.PageButton>
|
||||
<DataTable.PageButton
|
||||
onClick={() => nextPage()}
|
||||
disabled={!canNextPage}>
|
||||
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Next</span>
|
||||
<ChevronRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
||||
</DataTable.PageButton>
|
||||
<DataTable.PageButton
|
||||
className="rounded-r-md"
|
||||
onClick={() => gotoPage(pageCount - 1)}
|
||||
disabled={!canNextPage}
|
||||
>
|
||||
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Last</span>
|
||||
<ChevronDoubleRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
||||
</DataTable.PageButton>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<nav className="relative z-0 inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||
<DataTable.PageButton
|
||||
className="rounded-l-md"
|
||||
onClick={() => gotoPage(0)}
|
||||
disabled={!canPreviousPage}
|
||||
>
|
||||
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">First</span>
|
||||
<ChevronDoubleLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
||||
</DataTable.PageButton>
|
||||
<DataTable.PageButton
|
||||
onClick={() => previousPage()}
|
||||
disabled={!canPreviousPage}
|
||||
>
|
||||
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Previous</span>
|
||||
<ChevronLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
||||
</DataTable.PageButton>
|
||||
<DataTable.PageButton
|
||||
onClick={() => nextPage()}
|
||||
disabled={!canNextPage}>
|
||||
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Next</span>
|
||||
<ChevronRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
||||
</DataTable.PageButton>
|
||||
<DataTable.PageButton
|
||||
className="rounded-r-md"
|
||||
onClick={() => gotoPage(pageCount - 1)}
|
||||
disabled={!canNextPage}
|
||||
>
|
||||
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Last</span>
|
||||
<ChevronDoubleRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
||||
</DataTable.PageButton>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { ReleaseTable } from "./ReleaseTable";
|
||||
|
||||
export const Releases = () => (
|
||||
<main>
|
||||
<header className="py-10">
|
||||
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
|
||||
<h1 className="text-3xl font-bold text-black dark:text-white">Releases</h1>
|
||||
</div>
|
||||
</header>
|
||||
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
||||
<ReleaseTable />
|
||||
</div>
|
||||
</main>
|
||||
<main>
|
||||
<header className="py-10">
|
||||
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
|
||||
<h1 className="text-3xl font-bold text-black dark:text-white">Releases</h1>
|
||||
</div>
|
||||
</header>
|
||||
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
||||
<ReleaseTable />
|
||||
</div>
|
||||
</main>
|
||||
);
|
|
@ -1,79 +1,79 @@
|
|||
function ActionSettings() {
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
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">
|
||||
<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}
|
||||
>
|
||||
</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>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionSettings;
|
|
@ -4,128 +4,127 @@ import { APIClient } from "../../api/APIClient";
|
|||
import { Checkbox } from "../../components/Checkbox";
|
||||
import { SettingsContext } from "../../utils/Context";
|
||||
|
||||
|
||||
function ApplicationSettings() {
|
||||
const [settings, setSettings] = SettingsContext.use();
|
||||
const [settings, setSettings] = SettingsContext.use();
|
||||
|
||||
const { isLoading, data } = useQuery(
|
||||
['config'],
|
||||
() => APIClient.config.get(),
|
||||
{
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
onError: err => console.log(err)
|
||||
}
|
||||
);
|
||||
const { isLoading, data } = useQuery(
|
||||
["config"],
|
||||
() => APIClient.config.get(),
|
||||
{
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
onError: err => console.log(err)
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<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>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
return (
|
||||
<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>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Application settings. Change in config.toml and restart to take effect.
|
||||
</p>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!isLoading && data && (
|
||||
<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-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
{!isLoading && data && (
|
||||
<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-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
Host
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
id="host"
|
||||
value={data.host}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
id="host"
|
||||
value={data.host}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="port"
|
||||
id="port"
|
||||
value={data.port}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="port"
|
||||
id="port"
|
||||
value={data.port}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="base_url" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="base_url" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
Base url
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="base_url"
|
||||
id="base_url"
|
||||
value={data.base_url}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="base_url"
|
||||
id="base_url"
|
||||
value={data.base_url}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 py-5 sm:p-0">
|
||||
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{data?.version ? (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-500 dark:text-white">Version:</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.version}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{data?.commit ? (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-500 dark:text-white">Commit:</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data.commit}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{data?.date ? (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-500 dark:text-white">Date:</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.date}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</dl>
|
||||
</div>
|
||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 sm:px-6 py-1">
|
||||
<Checkbox
|
||||
label="Debug"
|
||||
description="Enable debug mode to get more logs."
|
||||
value={settings.debug}
|
||||
setValue={(newValue: boolean) => setSettings({
|
||||
...settings,
|
||||
debug: newValue
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 sm:px-6 py-1">
|
||||
<Checkbox
|
||||
label="Dark theme"
|
||||
description="Switch between dark and light theme."
|
||||
value={settings.darkTheme}
|
||||
setValue={(newValue: boolean) => setSettings({
|
||||
...settings,
|
||||
darkTheme: newValue
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 py-5 sm:p-0">
|
||||
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{data?.version ? (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-500 dark:text-white">Version:</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.version}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{data?.commit ? (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-500 dark:text-white">Commit:</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data.commit}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{data?.date ? (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-500 dark:text-white">Date:</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.date}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</dl>
|
||||
</div>
|
||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 sm:px-6 py-1">
|
||||
<Checkbox
|
||||
label="Debug"
|
||||
description="Enable debug mode to get more logs."
|
||||
value={settings.debug}
|
||||
setValue={(newValue: boolean) => setSettings({
|
||||
...settings,
|
||||
debug: newValue
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 sm:px-6 py-1">
|
||||
<Checkbox
|
||||
label="Dark theme"
|
||||
description="Switch between dark and light theme."
|
||||
value={settings.darkTheme}
|
||||
setValue={(newValue: boolean) => setSettings({
|
||||
...settings,
|
||||
darkTheme: newValue
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ApplicationSettings;
|
|
@ -13,132 +13,132 @@ interface DLSettingsItemProps {
|
|||
}
|
||||
|
||||
function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
|
||||
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false)
|
||||
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false);
|
||||
|
||||
return (
|
||||
<tr key={client.name} className={idx % 2 === 0 ? 'light:bg-white' : 'light:bg-gray-50'}>
|
||||
<DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient} />
|
||||
return (
|
||||
<tr key={client.name} className={idx % 2 === 0 ? "light:bg-white" : "light:bg-gray-50"}>
|
||||
<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={toggleUpdateClient}
|
||||
className={classNames(
|
||||
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-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 dark:text-white">{client.name}</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 dark:text-gray-400">{DownloadClientTypeNameMap[client.type]}</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 cursor-pointer" onClick={toggleUpdateClient}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<Switch
|
||||
checked={client.enabled}
|
||||
onChange={toggleUpdateClient}
|
||||
className={classNames(
|
||||
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-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 dark:text-white">{client.name}</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 dark:text-gray-400">{DownloadClientTypeNameMap[client.type]}</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 cursor-pointer" onClick={toggleUpdateClient}>
|
||||
Edit
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function DownloadClientSettings() {
|
||||
const [addClientIsOpen, toggleAddClient] = useToggle(false)
|
||||
const [addClientIsOpen, toggleAddClient] = useToggle(false);
|
||||
|
||||
const { error, data } = useQuery(
|
||||
'downloadClients',
|
||||
APIClient.download_clients.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
const { error, data } = useQuery(
|
||||
"downloadClients",
|
||||
APIClient.download_clients.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
if (error)
|
||||
return (<p>An error has occurred: </p>);
|
||||
if (error)
|
||||
return (<p>An error has occurred: </p>);
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
|
||||
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} />
|
||||
<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 dark:text-white">Clients</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<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 dark:text-white">Clients</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
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 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}
|
||||
>
|
||||
</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 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}
|
||||
>
|
||||
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="light:shadow overflow-hidden light:border-b light:border-gray-200 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="light:bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Enabled
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Host
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{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>
|
||||
</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="light:shadow overflow-hidden light:border-b light:border-gray-200 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="light:bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Enabled
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Host
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{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;
|
|
@ -3,147 +3,148 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
|
|||
import { APIClient } from "../../api/APIClient";
|
||||
import { Menu, Switch, Transition } from "@headlessui/react";
|
||||
|
||||
import {classNames} from "../../utils";
|
||||
import {Fragment, useRef, useState} from "react";
|
||||
import {toast} from "react-hot-toast";
|
||||
import { classNames } from "../../utils";
|
||||
import { Fragment, useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
import {queryClient} from "../../App";
|
||||
import {DeleteModal} from "../../components/modals";
|
||||
import { queryClient } from "../../App";
|
||||
import { DeleteModal } from "../../components/modals";
|
||||
import {
|
||||
DotsHorizontalIcon,
|
||||
PencilAltIcon,
|
||||
SwitchHorizontalIcon,
|
||||
TrashIcon
|
||||
DotsHorizontalIcon,
|
||||
PencilAltIcon,
|
||||
SwitchHorizontalIcon,
|
||||
TrashIcon
|
||||
} from "@heroicons/react/outline";
|
||||
import {FeedUpdateForm} from "../../forms/settings/FeedForms";
|
||||
import { FeedUpdateForm } from "../../forms/settings/FeedForms";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import { componentMapType } from "../../forms/settings/DownloadClientForms";
|
||||
|
||||
function FeedSettings() {
|
||||
const {data} = useQuery<Feed[], Error>('feeds', APIClient.feeds.find,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
const { data } = useQuery<Feed[], Error>("feeds", APIClient.feeds.find,
|
||||
{
|
||||
refetchOnWindowFocus: 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">
|
||||
<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 dark:text-white">Feeds</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
<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 dark:text-white">Feeds</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Manage Torznab feeds.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data && data.length > 0 ?
|
||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full relative">
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div
|
||||
className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled
|
||||
</div>
|
||||
<div
|
||||
className="col-span-6 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name
|
||||
</div>
|
||||
<div
|
||||
className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type
|
||||
</div>
|
||||
{/*<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>*/}
|
||||
</li>
|
||||
|
||||
{data && data.map((f) => (
|
||||
<ListItem key={f.id} feed={f}/>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
: <EmptySimple title="No feeds" subtitle="Setup via indexers" />}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
{data && data.length > 0 ?
|
||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full relative">
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div
|
||||
className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled
|
||||
</div>
|
||||
<div
|
||||
className="col-span-6 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name
|
||||
</div>
|
||||
<div
|
||||
className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type
|
||||
</div>
|
||||
{/*<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>*/}
|
||||
</li>
|
||||
|
||||
{data && data.map((f) => (
|
||||
<ListItem key={f.id} feed={f}/>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
: <EmptySimple title="No feeds" subtitle="Setup via indexers" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ImplementationTorznab = () => (
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
|
||||
>
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
|
||||
>
|
||||
Torznab
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
);
|
||||
|
||||
export const ImplementationMap: any = {
|
||||
"TORZNAB": <ImplementationTorznab/>,
|
||||
export const ImplementationMap: componentMapType = {
|
||||
"TORZNAB": <ImplementationTorznab/>
|
||||
};
|
||||
|
||||
interface ListItemProps {
|
||||
feed: Feed;
|
||||
}
|
||||
|
||||
function ListItem({feed}: ListItemProps) {
|
||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false)
|
||||
function ListItem({ feed }: ListItemProps) {
|
||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false);
|
||||
|
||||
const [enabled, setEnabled] = useState(feed.enabled)
|
||||
const [enabled, setEnabled] = useState(feed.enabled);
|
||||
|
||||
const updateMutation = useMutation(
|
||||
(status: boolean) => APIClient.feeds.toggleEnable(feed.id, status),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success"
|
||||
body={`${feed.name} was ${enabled ? "disabled" : "enabled"} successfully`}
|
||||
t={t}/>)
|
||||
const updateMutation = useMutation(
|
||||
(status: boolean) => APIClient.feeds.toggleEnable(feed.id, status),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success"
|
||||
body={`${feed.name} was ${enabled ? "disabled" : "enabled"} successfully`}
|
||||
t={t}/>);
|
||||
|
||||
queryClient.invalidateQueries(["feeds"]);
|
||||
queryClient.invalidateQueries(["feeds", feed?.id]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const toggleActive = (status: boolean) => {
|
||||
setEnabled(status);
|
||||
updateMutation.mutate(status);
|
||||
queryClient.invalidateQueries(["feeds"]);
|
||||
queryClient.invalidateQueries(["feeds", feed?.id]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={feed.id} className="text-gray-500 dark:text-gray-400">
|
||||
<FeedUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} feed={feed}/>
|
||||
const toggleActive = (status: boolean) => {
|
||||
setEnabled(status);
|
||||
updateMutation.mutate(status);
|
||||
};
|
||||
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
<Switch
|
||||
checked={feed.enabled}
|
||||
onChange={toggleActive}
|
||||
className={classNames(
|
||||
feed.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-blue-500'
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
feed.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>
|
||||
</div>
|
||||
<div className="col-span-6 flex items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{feed.name}
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6">
|
||||
{ImplementationMap[feed.type]}
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center sm:px-6">
|
||||
<FeedItemDropdown
|
||||
feed={feed}
|
||||
onToggle={toggleActive}
|
||||
toggleUpdate={toggleUpdateForm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
return (
|
||||
<li key={feed.id} className="text-gray-500 dark:text-gray-400">
|
||||
<FeedUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} feed={feed}/>
|
||||
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
<Switch
|
||||
checked={feed.enabled}
|
||||
onChange={toggleActive}
|
||||
className={classNames(
|
||||
feed.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-blue-500"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
feed.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>
|
||||
</div>
|
||||
<div className="col-span-6 flex items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{feed.name}
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6">
|
||||
{ImplementationMap[feed.type]}
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center sm:px-6">
|
||||
<FeedItemDropdown
|
||||
feed={feed}
|
||||
onToggle={toggleActive}
|
||||
toggleUpdate={toggleUpdateForm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
interface FeedItemDropdownProps {
|
||||
|
@ -153,126 +154,126 @@ interface FeedItemDropdownProps {
|
|||
}
|
||||
|
||||
const FeedItemDropdown = ({
|
||||
feed,
|
||||
onToggle,
|
||||
toggleUpdate,
|
||||
}: FeedItemDropdownProps) => {
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
feed,
|
||||
onToggle,
|
||||
toggleUpdate
|
||||
}: FeedItemDropdownProps) => {
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const deleteMutation = useMutation(
|
||||
(id: number) => APIClient.feeds.delete(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["feeds"]);
|
||||
queryClient.invalidateQueries(["feeds", feed.id]);
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const deleteMutation = useMutation(
|
||||
(id: number) => APIClient.feeds.delete(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["feeds"]);
|
||||
queryClient.invalidateQueries(["feeds", feed.id]);
|
||||
|
||||
toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} was deleted`} t={t}/>);
|
||||
}
|
||||
}
|
||||
);
|
||||
toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} was deleted`} t={t}/>);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu as="div">
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={() => {
|
||||
deleteMutation.mutate(feed.id);
|
||||
toggleDeleteModal();
|
||||
}}
|
||||
title={`Remove feed: ${feed.name}`}
|
||||
text="Are you sure you want to remove this feed? This action cannot be undone."
|
||||
/>
|
||||
<Menu.Button className="px-4 py-2">
|
||||
<DotsHorizontalIcon
|
||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
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
|
||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||
return (
|
||||
<Menu as="div">
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={() => {
|
||||
deleteMutation.mutate(feed.id);
|
||||
toggleDeleteModal();
|
||||
}}
|
||||
title={`Remove feed: ${feed.name}`}
|
||||
text="Are you sure you want to remove this feed? This action cannot be undone."
|
||||
/>
|
||||
<Menu.Button className="px-4 py-2">
|
||||
<DotsHorizontalIcon
|
||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
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
|
||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => toggleUpdate()}
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({active}) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => toggleUpdate()}
|
||||
>
|
||||
<PencilAltIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<PencilAltIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({active}) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => onToggle(!feed.enabled)}
|
||||
>
|
||||
<SwitchHorizontalIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => onToggle(!feed.enabled)}
|
||||
>
|
||||
<SwitchHorizontalIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Toggle
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({active}) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => toggleDeleteModal()}
|
||||
>
|
||||
<TrashIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-red-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => toggleDeleteModal()}
|
||||
>
|
||||
<TrashIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-red-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedSettings;
|
|
@ -5,148 +5,153 @@ import { Switch } from "@headlessui/react";
|
|||
import { classNames } from "../../utils";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { componentMapType } from "../../forms/settings/DownloadClientForms";
|
||||
|
||||
const ImplementationIRC = () => (
|
||||
<span
|
||||
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-green-200 dark:bg-green-400 text-green-800 dark:text-green-800"
|
||||
>
|
||||
<span
|
||||
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-green-200 dark:bg-green-400 text-green-800 dark:text-green-800"
|
||||
>
|
||||
IRC
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
);
|
||||
|
||||
const ImplementationTorznab = () => (
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
|
||||
>
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
|
||||
>
|
||||
Torznab
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
);
|
||||
|
||||
const implementationMap: any = {
|
||||
"irc": <ImplementationIRC/>,
|
||||
"torznab": <ImplementationTorznab />,
|
||||
const implementationMap: componentMapType = {
|
||||
"irc": <ImplementationIRC/>,
|
||||
"torznab": <ImplementationTorznab />
|
||||
};
|
||||
|
||||
const ListItem = ({ indexer }: any) => {
|
||||
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
||||
|
||||
return (
|
||||
<tr key={indexer.name}>
|
||||
<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 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-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 dark:text-white">{indexer.name}</td>
|
||||
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{implementationMap[indexer.implementation]}</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>
|
||||
)
|
||||
interface ListItemProps {
|
||||
indexer: IndexerDefinition;
|
||||
}
|
||||
|
||||
const ListItem = ({ indexer }: ListItemProps) => {
|
||||
const [updateIsOpen, toggleUpdate] = useToggle(false);
|
||||
|
||||
return (
|
||||
<tr key={indexer.name}>
|
||||
<IndexerUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} indexer={indexer} />
|
||||
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Switch
|
||||
checked={indexer.enabled ?? false}
|
||||
onChange={toggleUpdate}
|
||||
className={classNames(
|
||||
indexer.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-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 dark:text-white">{indexer.name}</td>
|
||||
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{implementationMap[indexer.implementation]}</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() {
|
||||
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false)
|
||||
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false);
|
||||
|
||||
const { error, data } = useQuery(
|
||||
'indexer',
|
||||
APIClient.indexers.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
const { error, data } = useQuery(
|
||||
"indexer",
|
||||
APIClient.indexers.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
if (error)
|
||||
return (<p>An error has occurred</p>);
|
||||
if (error)
|
||||
return (<p>An error has occurred</p>);
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
|
||||
<IndexerAddForm isOpen={addIndexerIsOpen} toggle={toggleAddIndexer} />
|
||||
<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 dark:text-white">Indexers</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<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 dark:text-white">Indexers</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
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 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"
|
||||
>
|
||||
</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 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
|
||||
</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="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 dark:divide-gray-700">
|
||||
<thead className="light:bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Enabled
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Implementation
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{data && data.map((indexer: IndexerDefinition, 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>
|
||||
</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="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 dark:divide-gray-700">
|
||||
<thead className="light:bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Enabled
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Implementation
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{data && data.map((indexer: IndexerDefinition, 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;
|
|
@ -1,73 +1,73 @@
|
|||
import { useQuery } from "react-query";
|
||||
|
||||
import {
|
||||
simplifyDate,
|
||||
IsEmptyDate
|
||||
simplifyDate,
|
||||
IsEmptyDate
|
||||
} from "../../utils";
|
||||
import {
|
||||
IrcNetworkAddForm,
|
||||
IrcNetworkUpdateForm
|
||||
IrcNetworkAddForm,
|
||||
IrcNetworkUpdateForm
|
||||
} from "../../forms";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
|
||||
export const IrcSettings = () => {
|
||||
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false)
|
||||
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false);
|
||||
|
||||
const { data } = useQuery(
|
||||
"networks",
|
||||
APIClient.irc.getNetworks,
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
// Refetch every 3 seconds
|
||||
refetchInterval: 3000
|
||||
}
|
||||
);
|
||||
const { data } = useQuery(
|
||||
"networks",
|
||||
APIClient.irc.getNetworks,
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
// Refetch every 3 seconds
|
||||
refetchInterval: 3000
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
<IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork} />
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
<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 dark:text-white">IRC</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<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 dark:text-white">IRC</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
IRC networks and channels. Click on a network to view channel status.
|
||||
</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 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"
|
||||
>
|
||||
</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 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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data && data.length > 0 ? (
|
||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full">
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
{/* <div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div> */}
|
||||
<div className="col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Network</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Server</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Nick</div>
|
||||
</li>
|
||||
|
||||
{data && data.map((network, idx) => (
|
||||
<ListItem key={idx} idx={idx} network={network} />
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
) : <EmptySimple title="No networks" subtitle="Add a new network" buttonText="New network" buttonAction={toggleAddNetwork} />}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{data && data.length > 0 ? (
|
||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full">
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
{/* <div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div> */}
|
||||
<div className="col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Network</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Server</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Nick</div>
|
||||
</li>
|
||||
|
||||
{data && data.map((network, idx) => (
|
||||
<ListItem key={idx} idx={idx} network={network} />
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
) : <EmptySimple title="No networks" subtitle="Add a new network" buttonText="New network" buttonAction={toggleAddNetwork} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ListItemProps {
|
||||
idx: number;
|
||||
|
@ -75,83 +75,83 @@ interface ListItemProps {
|
|||
}
|
||||
|
||||
const ListItem = ({ idx, network }: ListItemProps) => {
|
||||
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
||||
const [edit, toggleEdit] = useToggle(false);
|
||||
const [updateIsOpen, toggleUpdate] = useToggle(false);
|
||||
const [edit, toggleEdit] = useToggle(false);
|
||||
|
||||
return (
|
||||
return (
|
||||
|
||||
<li key={idx} >
|
||||
<div className="grid grid-cols-12 gap-4 items-center hover:bg-gray-50 dark:hover:bg-gray-700 py-4">
|
||||
<IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} />
|
||||
<li key={idx} >
|
||||
<div className="grid grid-cols-12 gap-4 items-center hover:bg-gray-50 dark:hover:bg-gray-700 py-4">
|
||||
<IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} />
|
||||
|
||||
<div className="col-span-3 items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white cursor-pointer" onClick={toggleEdit}>
|
||||
<span className="relative inline-flex items-center">
|
||||
{
|
||||
network.enabled ? (
|
||||
network.connected ? (
|
||||
<span className="mr-3 flex h-3 w-3 relative" title={`Connected since: ${simplifyDate(network.connected_since)}`}>
|
||||
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/>
|
||||
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
||||
</span>
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
||||
}
|
||||
{network.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-3 items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white cursor-pointer" onClick={toggleEdit}>
|
||||
<span className="relative inline-flex items-center">
|
||||
{
|
||||
network.enabled ? (
|
||||
network.connected ? (
|
||||
<span className="mr-3 flex h-3 w-3 relative" title={`Connected since: ${simplifyDate(network.connected_since)}`}>
|
||||
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/>
|
||||
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
||||
</span>
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
||||
}
|
||||
{network.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="col-span-4 flex justify-between items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.server}:{network.port} {network.tls && <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-300 text-green-800 dark:text-green-900">TLS</span>}</div>
|
||||
{network.nickserv && network.nickserv.account ? (
|
||||
<div className="col-span-4 items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.nickserv.account}</div>
|
||||
) : null}
|
||||
<div className="col-span-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}>
|
||||
<div className="col-span-4 flex justify-between items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.server}:{network.port} {network.tls && <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-300 text-green-800 dark:text-green-900">TLS</span>}</div>
|
||||
{network.nickserv && network.nickserv.account ? (
|
||||
<div className="col-span-4 items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.nickserv.account}</div>
|
||||
) : null}
|
||||
<div className="col-span-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}>
|
||||
Edit
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{edit && (
|
||||
<div className="px-4 py-4 flex border-b border-x-0 dark:border-gray-600 dark:bg-gray-700">
|
||||
<div className="min-w-full">
|
||||
{network.channels.length > 0 ? (
|
||||
<ol>
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Channel</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Monitoring since</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Last announce</div>
|
||||
</li>
|
||||
{network.channels.map(c => (
|
||||
<li key={c.id} className="text-gray-500 dark:text-gray-400">
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||
<span className="relative inline-flex items-center">
|
||||
{
|
||||
network.enabled ? (
|
||||
c.monitoring ? (
|
||||
<span className="mr-3 flex h-3 w-3 relative" title="monitoring">
|
||||
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/>
|
||||
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
||||
</span>
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
||||
}
|
||||
{c.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||
<span className="" title={simplifyDate(c.monitoring_since)}>{IsEmptyDate(c.monitoring_since)}</span>
|
||||
</div>
|
||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||
<span className="" title={simplifyDate(c.last_announce)}>{IsEmptyDate(c.last_announce)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : <div className="flex text-center justify-center py-4 dark:text-gray-500"><p>No channels!</p></div>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{edit && (
|
||||
<div className="px-4 py-4 flex border-b border-x-0 dark:border-gray-600 dark:bg-gray-700">
|
||||
<div className="min-w-full">
|
||||
{network.channels.length > 0 ? (
|
||||
<ol>
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Channel</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Monitoring since</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Last announce</div>
|
||||
</li>
|
||||
{network.channels.map(c => (
|
||||
<li key={c.id} className="text-gray-500 dark:text-gray-400">
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||
<span className="relative inline-flex items-center">
|
||||
{
|
||||
network.enabled ? (
|
||||
c.monitoring ? (
|
||||
<span className="mr-3 flex h-3 w-3 relative" title="monitoring">
|
||||
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/>
|
||||
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
||||
</span>
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
||||
}
|
||||
{c.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||
<span className="" title={simplifyDate(c.monitoring_since)}>{IsEmptyDate(c.monitoring_since)}</span>
|
||||
</div>
|
||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||
<span className="" title={simplifyDate(c.last_announce)}>{IsEmptyDate(c.last_announce)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : <div className="flex text-center justify-center py-4 dark:text-gray-500"><p>No channels!</p></div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,56 +7,56 @@ import { Switch } from "@headlessui/react";
|
|||
import { classNames } from "../../utils";
|
||||
|
||||
function NotificationSettings() {
|
||||
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false)
|
||||
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false);
|
||||
|
||||
const { data } = useQuery<Notification[], Error>('notifications', APIClient.notifications.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
const { data } = useQuery<Notification[], Error>("notifications", APIClient.notifications.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
<NotificationAddForm isOpen={addNotificationsIsOpen} toggle={toggleAddNotifications} />
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
<NotificationAddForm isOpen={addNotificationsIsOpen} toggle={toggleAddNotifications} />
|
||||
|
||||
<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 dark:text-white">Notifications</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<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 dark:text-white">Notifications</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Send notifications on events.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddNotifications}
|
||||
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"
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddNotifications}
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data && data.length > 0 ?
|
||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full">
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div>
|
||||
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</div>
|
||||
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>
|
||||
</li>
|
||||
|
||||
{data && data.map((n: Notification) => (
|
||||
<ListItem key={n.id} notification={n} />
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
: <EmptySimple title="No notifications setup" subtitle="Add a new notification" buttonText="New notification" buttonAction={toggleAddNotifications} />}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
{data && data.length > 0 ?
|
||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full">
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div>
|
||||
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</div>
|
||||
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>
|
||||
</li>
|
||||
|
||||
{data && data.map((n: Notification) => (
|
||||
<ListItem key={n.id} notification={n} />
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
: <EmptySimple title="No notifications setup" subtitle="Add a new notification" buttonText="New notification" buttonAction={toggleAddNotifications} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ListItemProps {
|
||||
|
@ -64,56 +64,56 @@ interface ListItemProps {
|
|||
}
|
||||
|
||||
function ListItem({ notification }: ListItemProps) {
|
||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false)
|
||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false);
|
||||
|
||||
return (
|
||||
<li key={notification.id} className="text-gray-500 dark:text-gray-400">
|
||||
<NotificationUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} notification={notification} />
|
||||
return (
|
||||
<li key={notification.id} className="text-gray-500 dark:text-gray-400">
|
||||
<NotificationUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} notification={notification} />
|
||||
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
<div className="col-span-1 flex items-center sm:px-6 ">
|
||||
<Switch
|
||||
checked={notification.enabled}
|
||||
onChange={toggleUpdateForm}
|
||||
className={classNames(
|
||||
notification.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-blue-500'
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
notification.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>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
{notification.name}
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
{notification.type}
|
||||
</div>
|
||||
<div className="col-span-5 flex items-center sm:px-6 ">
|
||||
{notification.events.map((n, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"
|
||||
>
|
||||
{n}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center sm:px-6 ">
|
||||
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateForm}>
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
<div className="col-span-1 flex items-center sm:px-6 ">
|
||||
<Switch
|
||||
checked={notification.enabled}
|
||||
onChange={toggleUpdateForm}
|
||||
className={classNames(
|
||||
notification.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-blue-500"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
notification.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>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
{notification.name}
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
{notification.type}
|
||||
</div>
|
||||
<div className="col-span-5 flex items-center sm:px-6 ">
|
||||
{notification.events.map((n, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"
|
||||
>
|
||||
{n}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center sm:px-6 ">
|
||||
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateForm}>
|
||||
Edit
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotificationSettings;
|
|
@ -1,106 +1,105 @@
|
|||
import { useRef, useState } from "react";
|
||||
|
||||
export const RegexPlayground = () => {
|
||||
const regexRef = useRef<HTMLInputElement>(null);
|
||||
const [output, setOutput] = useState<Array<React.ReactElement>>();
|
||||
const regexRef = useRef<HTMLInputElement>(null);
|
||||
const [output, setOutput] = useState<Array<React.ReactElement>>();
|
||||
|
||||
const onInput = (text: string) => {
|
||||
if (!regexRef || !regexRef.current)
|
||||
return;
|
||||
const onInput = (text: string) => {
|
||||
if (!regexRef || !regexRef.current)
|
||||
return;
|
||||
|
||||
const regexp = new RegExp(regexRef.current.value, "g");
|
||||
const regexp = new RegExp(regexRef.current.value, "g");
|
||||
|
||||
const results: Array<React.ReactElement> = [];
|
||||
text.split("\n").forEach((line, index) => {
|
||||
const matches = line.matchAll(regexp);
|
||||
const results: Array<React.ReactElement> = [];
|
||||
text.split("\n").forEach((line, index) => {
|
||||
const matches = line.matchAll(regexp);
|
||||
|
||||
let lastIndex = 0;
|
||||
// @ts-ignore
|
||||
for (const match of matches) {
|
||||
if (match.index === undefined)
|
||||
continue;
|
||||
let lastIndex = 0;
|
||||
for (const match of matches) {
|
||||
if (match.index === undefined)
|
||||
continue;
|
||||
|
||||
if (!match.length)
|
||||
continue;
|
||||
if (!match.length)
|
||||
continue;
|
||||
|
||||
const start = match.index;
|
||||
const start = match.index;
|
||||
|
||||
let length = 0;
|
||||
match.forEach((group) => length += group.length);
|
||||
let length = 0;
|
||||
match.forEach((group) => length += group.length);
|
||||
|
||||
results.push(
|
||||
<span key={`match-${start}`}>
|
||||
{line.substring(lastIndex, start)}
|
||||
<span className="bg-blue-300 text-black font-bold">
|
||||
{line.substring(start, start + length)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
lastIndex = start + length;
|
||||
}
|
||||
results.push(
|
||||
<span key={`match-${start}`}>
|
||||
{line.substring(lastIndex, start)}
|
||||
<span className="bg-blue-300 text-black font-bold">
|
||||
{line.substring(start, start + length)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
lastIndex = start + length;
|
||||
}
|
||||
|
||||
if (lastIndex < line.length) {
|
||||
results.push(
|
||||
<span key={`last-${lastIndex + 1}`}>
|
||||
{line.substring(lastIndex)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (lastIndex < line.length) {
|
||||
results.push(
|
||||
<span key={`last-${lastIndex + 1}`}>
|
||||
{line.substring(lastIndex)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (lastIndex > 0)
|
||||
results.push(<br key={`line-delim-${index}`}/>);
|
||||
});
|
||||
if (lastIndex > 0)
|
||||
results.push(<br key={`line-delim-${index}`}/>);
|
||||
});
|
||||
|
||||
setOutput(results);
|
||||
}
|
||||
setOutput(results);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Regex playground. Experiment with your filters here. WIP.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<label
|
||||
htmlFor="input-regex"
|
||||
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
RegExp filter
|
||||
</label>
|
||||
<input
|
||||
ref={regexRef}
|
||||
id="input-regex"
|
||||
type="text"
|
||||
autoComplete="true"
|
||||
className="mt-1 mb-4 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"
|
||||
/>
|
||||
<label
|
||||
htmlFor="input-lines"
|
||||
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
Lines to match
|
||||
</label>
|
||||
<div
|
||||
id="input-lines"
|
||||
className="mt-1 mb-4 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"
|
||||
onInput={(e) => onInput(e.currentTarget.innerText ?? "")}
|
||||
contentEditable
|
||||
></div>
|
||||
</div>
|
||||
<div className="py-4 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h3 className="text-md leading-6 font-medium text-gray-900 dark:text-white">
|
||||
Matches
|
||||
</h3>
|
||||
<p className="mt-1 text-lg text-gray-500 dark:text-gray-400">
|
||||
{output}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<label
|
||||
htmlFor="input-regex"
|
||||
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
RegExp filter
|
||||
</label>
|
||||
<input
|
||||
ref={regexRef}
|
||||
id="input-regex"
|
||||
type="text"
|
||||
autoComplete="true"
|
||||
className="mt-1 mb-4 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"
|
||||
/>
|
||||
<label
|
||||
htmlFor="input-lines"
|
||||
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
Lines to match
|
||||
</label>
|
||||
<div
|
||||
id="input-lines"
|
||||
className="mt-1 mb-4 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"
|
||||
onInput={(e) => onInput(e.currentTarget.innerText ?? "")}
|
||||
contentEditable
|
||||
></div>
|
||||
</div>
|
||||
<div className="py-4 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h3 className="text-md leading-6 font-medium text-gray-900 dark:text-white">
|
||||
Matches
|
||||
</h3>
|
||||
<p className="mt-1 text-lg text-gray-500 dark:text-gray-400">
|
||||
{output}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -9,73 +9,73 @@ import { useToggle } from "../../hooks/hooks";
|
|||
import { DeleteModal } from "../../components/modals";
|
||||
|
||||
function ReleaseSettings() {
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const deleteMutation = useMutation(() => APIClient.release.delete(), {
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => (
|
||||
<Toast type="success" body={`All releases was deleted`} t={t}/>
|
||||
));
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const deleteMutation = useMutation(() => APIClient.release.delete(), {
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => (
|
||||
<Toast type="success" body={"All releases was deleted"} t={t}/>
|
||||
));
|
||||
|
||||
// Invalidate filters just in case, most likely not necessary but can't hurt.
|
||||
queryClient.invalidateQueries("releases");
|
||||
// Invalidate filters just in case, most likely not necessary but can't hurt.
|
||||
queryClient.invalidateQueries("releases");
|
||||
|
||||
toggleDeleteModal()
|
||||
}
|
||||
})
|
||||
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate()
|
||||
toggleDeleteModal();
|
||||
}
|
||||
});
|
||||
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={deleteAction}
|
||||
title={`Delete all releases`}
|
||||
text="Are you sure you want to delete all releases? This action cannot be undone."
|
||||
/>
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Releases</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
return (
|
||||
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={deleteAction}
|
||||
title={"Delete all releases"}
|
||||
text="Are you sure you want to delete all releases? This action cannot be undone."
|
||||
/>
|
||||
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Releases</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Release settings. Reset state.
|
||||
</p>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 py-5 sm:p-0">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Danger Zone</h3>
|
||||
</div>
|
||||
|
||||
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 py-5 sm:p-0">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Danger Zone</h3>
|
||||
</div>
|
||||
|
||||
<ul className="p-4 mt-6 divide-y divide-gray-200 dark:divide-gray-700 border-red-500 border rounded-lg">
|
||||
<div className="flex justify-between items-center py-2">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<ul className="p-4 mt-6 divide-y divide-gray-200 dark:divide-gray-700 border-red-500 border rounded-lg">
|
||||
<div className="flex justify-between items-center py-2">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Delete all releases
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleDeleteModal}
|
||||
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-100 bg-red-100 dark:bg-red-500 hover:bg-red-200 dark:hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
||||
>
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleDeleteModal}
|
||||
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-100 bg-red-100 dark:bg-red-500 hover:bg-red-200 dark:hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
||||
>
|
||||
Delete all releases
|
||||
</button>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
</button>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReleaseSettings;
|
Loading…
Add table
Add a link
Reference in a new issue