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:
stacksmash76 2022-05-17 06:44:07 +02:00 committed by GitHub
parent 7f06a4c707
commit cb8f280e86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 6797 additions and 6541 deletions

View file

@ -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>
);
}

View file

@ -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>
)
}
);
};

View file

@ -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>
);
}

View file

@ -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>
);
}
};

View file

@ -29,4 +29,4 @@ export const Logout = () => {
<p>Logged out</p>
</div>
);
}
};

View file

@ -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>
);
}
};

View file

@ -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>
);
}
};

View file

@ -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>
);
};

View file

@ -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

View file

@ -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>
);
}

View file

@ -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>
);
);};

View file

@ -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>
);
};

View file

@ -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>
);

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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;