refactor: various frontend improvements (#101)

* Removed recoil and replaced it with react-ridge-state, a 0.4kb alternative.

* Added AuthContext and SettingsContext persistent localStorage states.

* Fixed tailwind.config.js incorrect key directive. See https://tailwindcss.com/docs/content-configuration#safelisting-classes.

* Changed darkMode in Tailwind to "class" and started manually adjusting the theme according to the appropriate media query.

* Added possibility of changing the theme manually via the Settings tab.

* Changed Releases.tsx behavior to show the UI only when the HTTP request succeeded and there is some data (i.e. table is non-empty).

* Changed the table color of screens/filters/list.tsx to a one notch lighter shade of gray for eye-comfort.

* Replaced "User" in the header, with the users real username.

* Made data version, commit and date fields optional in settings/Application.tsx.

* Started working on a RegExp playground, which works fine, but JS won't cooperate and return the right match length. Either way, the RegExp must be implemented on backend and then must be communicated with the frontend. Otherwise a potential for incorrect results exists.

* Removed Layout.tsx, since it was redundant.

* Created a Checkbox component class for easier and consistent future use.

* Rewritten App.tsx, Login.tsx, Logout.tsx to accomodate for new changes.

* Fixed previous mistake regarding tailwind.config.js purge key, since we're still using old postcss7 from October last year

* Removed package-lock.json from both root and web directories.

* Refresh TypeScript configuration to support a types/ directory containing d.ts. The effect of this is that types don't have to be imported anymore and are at all times available globally. This also unifies them into a single source of truth, which will be a lot easier to manage in the future. Note: Only certain interop types have been moved at the time of writing.

* Fixed minor Checkbox argument mistake.

* fix: remove length from data check

* chore: lock files are annoying

* fix: select

* fix: wip release filtering
This commit is contained in:
stacksmash76 2022-01-26 23:54:29 +01:00 committed by GitHub
parent 53d75ef4d5
commit 20138030e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 2596 additions and 2453 deletions

View file

@ -1,12 +1,16 @@
import { Fragment } from 'react'
import { Disclosure, Menu, Transition } from '@headlessui/react'
import { ChevronDownIcon, MenuIcon, XIcon } from '@heroicons/react/outline'
import { Fragment } from "react";
import { NavLink, Link, Route, Switch } from "react-router-dom";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
import Logs from "./Logs";
import Settings from "./Settings";
import { Releases } from "./Releases";
import { Dashboard } from "./Dashboard";
import { FilterDetails, Filters } from "./filters";
import Logs from './Logs';
import { Releases } from "./Releases";
import { AuthContext } from '../utils/Context';
import logo from '../logo.png';
function classNames(...classes: string[]) {
@ -14,10 +18,17 @@ function classNames(...classes: string[]) {
}
export default function Base() {
const nav = [{ 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 = [
{ name: 'Dashboard', path: "/" },
{ name: 'Filters', path: "/filters" },
{ name: 'Releases', path: "/releases" },
{ name: "Settings", path: "/settings" },
{ name: "Logs", path: "/logs" }
];
return (
<div className="">
<div>
<Disclosure as="nav" className="bg-gray-900 pb-48">
{({ open }) => (
<>
@ -49,12 +60,6 @@ export default function Base() {
return true
}
// if (item.path ==="/" && location.pathname ==="/") {
// console.log("match base");
// return true
// }
if (!match) {
return false;
}
@ -86,7 +91,7 @@ export default function Base() {
className="max-w-xs rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
<span
className="hidden text-gray-300 text-sm font-medium sm:block">
<span className="sr-only">Open user menu for </span>User
<span className="sr-only">Open user menu for </span>{authContext.username}
</span>
<ChevronDownIcon
className="hidden flex-shrink-0 ml-1 h-5 w-5 text-gray-400 sm:block"
@ -111,7 +116,7 @@ export default function Base() {
<Menu.Item>
{({ active }) => (
<Link
to="settings"
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'
@ -186,7 +191,7 @@ export default function Base() {
</div>
<div className="mt-3 px-2 space-y-1">
<Link
to="settings"
to="/settings"
className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700"
>
Settings

View file

@ -4,7 +4,6 @@ import App from '../App'
import { useTable, useFilters, useGlobalFilter, useSortBy, usePagination } from 'react-table'
import APIClient from '../api/APIClient'
import { useQuery } from 'react-query'
import { ReleaseFindResponse, ReleaseStats } from '../domain/interfaces'
import { EmptyListState } from '../components/emptystates'
import { ReleaseStatusCell } from './Releases'

View file

@ -7,7 +7,6 @@ import { useQuery } from "react-query"
import { useTable, useSortBy, usePagination } from "react-table"
import APIClient from "../api/APIClient"
import { EmptyListState } from "../components/emptystates"
import { ReleaseActionStatus } from "../domain/interfaces"
import { classNames } from "../utils"
export function Releases() {
@ -294,24 +293,22 @@ function Table() {
// Render the UI for your table
return (
<>
<div className="sm:flex sm:gap-x-2">
{/* <GlobalFilter
preGlobalFilteredRows={preGlobalFilteredRows}
globalFilter={state.globalFilter}
setGlobalFilter={setGlobalFilter}
/> */}
{/* {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>
{isSuccess ?
{isSuccess && data ? (
<div className="flex flex-col mt-4">
{/* <GlobalFilter
preGlobalFilteredRows={preGlobalFilteredRows}
globalFilter={state.globalFilter}
setGlobalFilter={setGlobalFilter}
/> */}
{/* {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 className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div className="overflow-hidden bg-white shadow dark:bg-gray-800 sm:rounded-lg">
@ -373,7 +370,6 @@ function Table() {
</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">
@ -421,8 +417,7 @@ function Table() {
</PageButton>
<PageButton
onClick={() => nextPage()}
disabled={!canNextPage
}>
disabled={!canNextPage}>
<span className="sr-only">Next</span>
<ChevronRightIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
</PageButton>
@ -438,13 +433,11 @@ function Table() {
</div>
</div>
</div>
</div>
</div>
</div>
</div>
: <EmptyListState text="No recent activity" />}
) : <EmptyListState text="No recent activity" />}
</>
)
}

View file

@ -1,17 +1,19 @@
import {CogIcon, DownloadIcon, KeyIcon} from '@heroicons/react/outline'
import {NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch} from "react-router-dom";
import IndexerSettings from "./settings/Indexer";
import IrcSettings from "./settings/Irc";
import ApplicationSettings from "./settings/Application";
import DownloadClientSettings from "./settings/DownloadClient";
import {classNames} from "../utils";
import ActionSettings from "./settings/Action";
import { RegexPlayground } from './settings/RegexPlayground';
const subNavigation = [
{name: 'Application', href: '', icon: CogIcon, current: true},
{name: 'Indexers', href: 'indexers', icon: KeyIcon, current: false},
{name: 'IRC', href: 'irc', icon: KeyIcon, current: false},
{name: 'Clients', href: 'clients', icon: DownloadIcon, current: false},
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
// {name: 'Actions', href: 'actions', icon: PlayIcon, current: false},
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
// {name: 'Notifications', href: 'notifications', icon: BellIcon, current: false},
@ -97,10 +99,13 @@ export default function Settings() {
<DownloadClientSettings/>
</Route>
<Route path={`${url}/actions`}>
{/*<Route path={`${url}/actions`}>
<ActionSettings/>
</Route>
</Route>*/}
<Route path={`${url}/regex-playground`}>
<RegexPlayground />
</Route>
</RouteSwitch>
</div>
</div>

View file

@ -1,40 +1,36 @@
import { useMutation } from "react-query";
import APIClient from "../../api/APIClient";
import { Form, Formik } from "formik";
import { useRecoilState } from "recoil";
import { isLoggedIn } from "../../state/state";
import { useHistory } from "react-router-dom";
import { useEffect } from "react";
import logo from "../../logo.png"
import { useMutation } from "react-query";
import { Form, Formik } from "formik";
import APIClient from "../../api/APIClient";
import { TextField, PasswordField } from "../../components/inputs";
interface loginData {
import logo from "../../logo.png";
import { AuthContext } from "../../utils/Context";
interface LoginData {
username: string;
password: string;
}
function Login() {
const [loggedIn, setLoggedIn] = useRecoilState(isLoggedIn);
let history = useHistory();
const history = useHistory();
const [, setAuthContext] = AuthContext.use();
useEffect(() => {
if (loggedIn) {
// setLoading(false);
history.push('/');
} else {
// setLoading(false);
const mutation = useMutation(
(data: LoginData) => APIClient.auth.login(data.username, data.password),
{
onSuccess: (_, variables: LoginData) => {
setAuthContext({
username: variables.username,
isLoggedIn: true
});
history.push("/");
},
}
}, [loggedIn, history])
);
const mutation = useMutation((data: loginData) => APIClient.auth.login(data.username, data.password), {
onSuccess: () => {
setLoggedIn(true);
},
})
const handleSubmit = (data: any) => {
mutation.mutate(data)
}
const handleSubmit = (data: any) => mutation.mutate(data);
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
@ -45,27 +41,19 @@ function Login() {
alt="logo"
/>
</div>
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<Formik
initialValues={{
username: "",
password: "",
}}
initialValues={{ username: "", password: "" }}
onSubmit={handleSubmit}
>
{() => (
<Form>
<div className="space-y-6">
<TextField name="username" label="Username" columns={6} autoComplete="username" />
<PasswordField name="password" label="Password" columns={6} autoComplete="current-password" />
</div>
<div className="mt-6">
<button
type="submit"

View file

@ -1,23 +1,26 @@
import APIClient from "../../api/APIClient";
import {useRecoilState} from "recoil";
import {isLoggedIn} from "../../state/state";
import {useEffect} from "react";
import {useCookies} from "react-cookie";
import {useHistory} from "react-router-dom";
function Logout() {
const [loggedIn, setLoggedIn] = useRecoilState(isLoggedIn);
let history = useHistory();
import APIClient from "../../api/APIClient";
import { AuthContext } from "../../utils/Context";
function Logout() {
const history = useHistory();
const [, setAuthContext] = AuthContext.use();
const [,, removeCookie] = useCookies(['user_session']);
useEffect(() => {
APIClient.auth.logout().then(r => {
removeCookie("user_session")
setLoggedIn(false);
history.push('/login');
})
}, [loggedIn, history, removeCookie, setLoggedIn])
useEffect(
() => {
APIClient.auth.logout().then(r => {
setAuthContext({ username: "", isLoggedIn: false });
removeCookie("user_session");
history.push('/login');
})
},
[history, removeCookie, setAuthContext]
);
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-800 flex flex-col justify-center py-12 sm:px-6 lg:px-8">

View file

@ -12,7 +12,6 @@ import {
useParams,
useRouteMatch
} from "react-router-dom";
import { Action, ActionType, DownloadClient, Filter, Indexer } from "../../domain/interfaces";
import { useToggle } from "../../hooks/hooks";
import { useMutation, useQuery } from "react-query";
import { queryClient } from "../../App";
@ -21,7 +20,6 @@ import { CONTAINER_OPTIONS, CODECS_OPTIONS, RESOLUTION_OPTIONS, SOURCES_OPTIONS,
import DEBUG from "../../components/debug";
import { TitleSubtitle } from "../../components/headings";
import { buildPath, classNames } from "../../utils";
import SelectM from "react-select";
import APIClient from "../../api/APIClient";
import { toast } from 'react-hot-toast'
@ -30,7 +28,7 @@ import Toast from '../../components/notifications/Toast';
import { Field, FieldArray, Form, Formik } from "formik";
import { AlertWarning } from "../../components/alerts";
import { DeleteModal } from "../../components/modals";
import { NumberField, TextField, SwitchGroup, Select, MultiSelect, DownloadClientSelect, CheckboxField } from "../../components/inputs";
import { NumberField, TextField, SwitchGroup, Select, MultiSelect, DownloadClientSelect, IndexerMultiSelect, CheckboxField } from "../../components/inputs";
const tabs = [
{ name: 'General', href: '', current: true },
@ -312,7 +310,12 @@ function General({ indexers }: GeneralProps) {
let opts = indexers ? indexers.map(v => ({
label: v.name,
value: v
value: {
id: v.id,
name: v.name,
identifier: v.identifier,
enabled: v.enabled
}
})) : [];
return (
@ -323,36 +326,7 @@ function General({ indexers }: GeneralProps) {
<TextField name="name" label="Filter name" columns={6} placeholder="eg. Filter 1" />
<div className="col-span-6">
<label htmlFor="indexers" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
Indexers
</label>
<Field name="indexers" type="select" multiple={true}>
{({
field,
form: { setFieldValue },
}: any) => {
return (
<SelectM
{...field}
value={field.value && field.value.map((v: any) => ({
label: v.name,
value: v
}))}
onChange={(values: any) => {
let am = values && values.map((i: any) => i.value)
setFieldValue(field.name, am)
}}
isClearable={true}
isMulti={true}
placeholder="Choose indexers"
className="mt-2 block w-full focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
options={opts}
/>
)
}}
</Field>
<IndexerMultiSelect name="indexers" options={opts} label="Indexers" columns={6} />
</div>
</div>
</div>
@ -375,12 +349,7 @@ function General({ indexers }: GeneralProps) {
);
}
// interface FilterTabGeneralProps {
// filter: Filter;
// }
function MoviesTv() {
return (
<div>
<div className="mt-6 grid grid-cols-12 gap-6">
@ -480,7 +449,7 @@ function Advanced() {
return (
<div>
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center cursor-pointer" onClick={toggleReleases}>
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Releases</h3>
@ -503,7 +472,7 @@ function Advanced() {
)}
</div>
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center cursor-pointer" onClick={toggleGroups}>
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Groups</h3>
@ -526,7 +495,7 @@ function Advanced() {
)}
</div>
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center cursor-pointer" onClick={toggleCategories}>
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Categories and tags</h3>
@ -552,7 +521,7 @@ function Advanced() {
)}
</div>
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center cursor-pointer" onClick={toggleUploaders}>
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Uploaders</h3>
@ -575,7 +544,7 @@ function Advanced() {
)}
</div>
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center cursor-pointer" onClick={toggleFreeleech}>
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Freeleech</h3>
@ -893,7 +862,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
)}
</Field>
<button className="px-4 py-4 w-full flex block" type="button" onClick={toggleEdit}>
<button className="px-4 py-4 w-full flex" type="button" onClick={toggleEdit}>
<div className="min-w-0 flex-1 sm:flex sm:items-center sm:justify-between">
<div className="truncate">
<div className="flex text-sm">

View file

@ -5,7 +5,6 @@ import { EmptyListState } from "../../components/emptystates";
import {
Link,
} from "react-router-dom";
import { Filter } from "../../domain/interfaces";
import { useToggle } from "../../hooks/hooks";
import { useMutation, useQuery } from "react-query";
import { classNames } from "../../utils";
@ -51,7 +50,7 @@ export default function Filters() {
</header>
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
<div className="bg-white dark:bg-gray-900 light:rounded-lg light:shadow">
<div className="bg-white dark:bg-gray-800 light:rounded-lg light:shadow">
<div className="relative inset-0 light:py-3 light:px-3 light:sm:px-3 light:lg:px-3 h-full">
{data && data.length > 0 ? <FilterList filters={data} /> :
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />}
@ -72,8 +71,8 @@ function FilterList({ filters }: FilterListProps) {
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="light:py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 dark:border-gray-800 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
<thead className="bg-gray-50 dark:bg-gray-900 text-gray-500 dark:text-gray-400">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
<tr>
<th
scope="col"
@ -98,7 +97,7 @@ function FilterList({ filters }: FilterListProps) {
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-800">
{filters.map((filter: Filter, idx) => (
<FilterListItem filter={filter} key={idx} idx={idx} />
))}
@ -135,7 +134,7 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
return (
<tr key={filter.name}
className={idx % 2 === 0 ? 'bg-white dark:bg-gray-900' : 'bg-gray-50 dark:bg-gray-900'}>
className={idx % 2 === 0 ? 'bg-white dark:bg-gray-800' : 'bg-gray-50 dark:bg-gray-800'}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100">
<Switch
checked={enabled}
@ -161,7 +160,7 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
</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-100 dark:bg-gray-800 text-gray-800 dark:text-gray-400">{t.name}</span>)}</td>
<span key={t.id} className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-100 dark:bg-gray-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">
<Link to={`filters/${filter.id.toString()}`} className="text-indigo-600 dark:text-gray-200 hover:text-indigo-900 dark:hover:text-gray-400">
Edit

View file

@ -70,32 +70,15 @@ function ActionSettings() {
<tr>
<td>empty</td>
</tr>
{/*{downloadclients.map((client, personIdx) => (*/}
{/* <tr key={client.name}*/}
{/* className={personIdx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>*/}
{/* <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{client.name}</td>*/}
{/* <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{client.type}</td>*/}
{/* <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{client.port}</td>*/}
{/* <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{client.enabled}</td>*/}
{/* <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">*/}
{/* <Link to="edit" className="text-indigo-600 hover:text-indigo-900">*/}
{/* Edit*/}
{/* </Link>*/}
{/* </td>*/}
{/* </tr>*/}
{/*))}*/}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
)
);
}
export default ActionSettings;

View file

@ -1,25 +1,22 @@
import React, { useState } from "react";
import { Switch } from "@headlessui/react";
import { classNames } from "../../utils";
// import {useRecoilState} from "recoil";
// import {configState} from "../../state/state";
import { useQuery } from "react-query";
import { Config } from "../../domain/interfaces";
import APIClient from "../../api/APIClient";
import { Checkbox } from "../../components/Checkbox";
import { SettingsContext } from "../../utils/Context";
function ApplicationSettings() {
const [isDebug, setIsDebug] = useState(true)
// const [config] = useRecoilState(configState)
const [settings, setSettings] = SettingsContext.use();
const { isLoading, data } = useQuery<Config, Error>(['config'], () => APIClient.config.get(),
const { isLoading, data } = useQuery<Config, Error>(
['config'],
() => APIClient.config.get(),
{
retry: false,
refetchOnWindowFocus: false,
onError: err => {
console.log(err)
}
},
)
onError: err => console.log(err)
}
);
return (
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
@ -32,7 +29,6 @@ function ApplicationSettings() {
</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">
@ -79,69 +75,53 @@ function ApplicationSettings() {
)}
</div>
<div className="pt-6 pb-6 divide-y divide-gray-200 dark:divide-gray-700">
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
<div className="px-4 py-5 sm:p-0">
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
<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 font-semibold text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.version}</dd>
</div>
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6 dark:bg-gray-700">
<dt className="font-medium text-gray-500 dark:text-white">Commit:</dt>
<dd className="mt-1 font-semibold text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.commit}</dd>
</div>
<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 font-semibold text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.date}</dd>
</div>
{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>
<div className="px-4 sm:px-6">
<ul className="mt-2 divide-y divide-gray-200">
<Switch.Group as="li" className="py-4 flex items-center justify-between">
<div className="flex flex-col">
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white" passive>
Debug
</Switch.Label>
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
Enable debug mode to get more logs.
</Switch.Description>
</div>
<Switch
checked={isDebug}
disabled={true}
onChange={setIsDebug}
className={classNames(
isDebug ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700',
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)}
>
<span className="sr-only">Use setting</span>
<span
aria-hidden="true"
className={classNames(
isDebug ? 'translate-x-5' : 'translate-x-0',
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
)}
/>
</Switch>
</Switch.Group>
</ul>
</div>
{/*<div className="mt-4 py-4 px-4 flex justify-end sm:px-6">*/}
{/* <button*/}
{/* type="button"*/}
{/* className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"*/}
{/* >*/}
{/* Cancel*/}
{/* </button>*/}
{/* <button*/}
{/* type="submit"*/}
{/* className="ml-5 bg-indigo-700 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-indigo-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"*/}
{/* >*/}
{/* Save*/}
{/* </button>*/}
{/*</div>*/}
<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>

View file

@ -1,4 +1,3 @@
import { DownloadClient } from "../../domain/interfaces";
import { useToggle } from "../../hooks/hooks";
import { Switch } from "@headlessui/react";
import { useQuery } from "react-query";

View file

@ -2,7 +2,6 @@ import { useEffect } from "react";
import { useToggle } from "../../hooks/hooks";
import { useQuery } from "react-query";
import { IndexerAddForm, IndexerUpdateForm } from "../../forms";
import { Indexer } from "../../domain/interfaces";
import { Switch } from "@headlessui/react";
import { classNames } from "../../utils";
import { EmptySimple } from "../../components/emptystates";

View file

@ -1,41 +1,11 @@
import { useEffect } from "react";
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
import { useToggle } from "../../hooks/hooks";
import { useQuery } from "react-query";
import { EmptySimple } from "../../components/emptystates";
import APIClient from "../../api/APIClient";
import { formatDistanceToNowStrict, formatISO9075 } from "date-fns";
interface IrcNetwork {
id: number;
name: string;
enabled: boolean;
addr: string;
server: string;
port: string;
nick: string;
username: string;
realname: string;
pass: string;
connected: boolean;
connected_since: string;
tls: boolean;
nickserv: {
account: string;
}
channels: Channel[]
}
import APIClient from "../../api/APIClient";
import { useToggle } from "../../hooks/hooks";
import { EmptySimple } from "../../components/emptystates";
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
interface Channel {
id: number;
enabled: boolean;
name: string;
password: string;
detached: boolean;
monitoring: boolean;
monitoring_since: string;
last_announce: string;
}
function IsEmptyDate(date: string) {
if (date !== "0001-01-01T00:00:00Z") {
@ -57,9 +27,6 @@ function simplifyDate(date: string) {
function IrcSettings() {
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false)
useEffect(() => {
}, []);
const { data } = useQuery<IrcNetwork[], Error>('networks', APIClient.irc.getNetworks,
{
refetchOnWindowFocus: false
@ -167,13 +134,11 @@ const LiItem = ({ idx, network }: LiItemProps) => {
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">
<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>
@ -181,9 +146,7 @@ const LiItem = ({ idx, network }: LiItemProps) => {
</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">
{

View file

@ -0,0 +1,99 @@
import React, { useRef, useState } from "react";
export const RegexPlayground = () => {
const regexRef = useRef<HTMLInputElement>(null);
const [output, setOutput] = useState<Array<React.ReactElement>>();
const onInput = (text: string) => {
if (!regexRef || !regexRef.current)
return;
const regexp = new RegExp(regexRef.current.value, "g");
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;
const start = match.index;
results.push(
<span key={`match=${start}`}>
{line.substring(lastIndex, start)}
<span className="bg-green-200 text-black font-bold">
{line.substring(start, start + match.length)}
</span>
</span>
);
lastIndex = start + match.length;
}
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}`}/>);
});
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">
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-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-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>
);
}