feat(web): error boundry and fixes (#270)

* web: Added error handling. Fixed table overflow issues. Improved release status titles. Fixed a few bugs where certain forms/modals didn't close properly.

feat(web): Added react-error-boundary and stacktracey for handling errors. In case of a recoverable/non-state updating error, a notification is thrown, otherwise the user is shown an error page with the appropriate stack trace and error type and cause with the possibility of resolving the error by reseting the page's state.
enhancement(web/data-table/cells): Improved cell display behavior in tables -- it's now scrollable on mobile. Improved release status string readability.
enhancement(web/settings/Irc): Made IRC refetch interval every 3 seconds for those who are not patient.
fix(web/modals/DeleteModal): Fixed modal bug where it didn't close on button click.
fix(web/forms/IndexerForms): Fixed bug where the form didn't close on button click.
enhancement(web/ReleaseTable): Made the table more compact by utilizing 3em padding between cells instead of 6.
enhancement(web/ActivityTable): Ditto as above.
enhancement(web): Changed width of every page section from max-w-7xl (1280px to where applicable)
chore(web/dashboard/ActivityTable): Reformatted the file to a saner syntax.
enhancement(web/dashboard/StatsItem): Enlarged font size, set font to extrabold instead of bold and made padding consistent.
fix(web/navbar): Fixed bold font not showing properly on Firefox due to an argument ordering issue.

* chore: update pkg and fix broken proxy
This commit is contained in:
stacksmash76 2022-05-12 16:26:41 +02:00 committed by GitHub
parent bea30cb0bd
commit 2c46993264
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 464 additions and 280 deletions

View file

@ -2,7 +2,6 @@
"name": "web", "name": "web",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"proxy": "http://127.0.0.1:7474",
"homepage": ".", "homepage": ".",
"dependencies": { "dependencies": {
"@fontsource/inter": "^4.5.4", "@fontsource/inter": "^4.5.4",
@ -13,6 +12,7 @@
"react": "^17.0.2", "react": "^17.0.2",
"react-cookie": "^4.1.1", "react-cookie": "^4.1.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-error-boundary": "^3.1.4",
"react-hot-toast": "^2.1.1", "react-hot-toast": "^2.1.1",
"react-multi-select-component": "4.2.5", "react-multi-select-component": "4.2.5",
"react-query": "^3.18.1", "react-query": "^3.18.1",
@ -21,6 +21,7 @@
"react-scripts": "^5.0.0", "react-scripts": "^5.0.0",
"react-select": "5.0.0-beta.0", "react-select": "5.0.0-beta.0",
"react-table": "^7.7.0", "react-table": "^7.7.0",
"stacktracey": "^2.1.8",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
}, },
"scripts": { "scripts": {
@ -66,6 +67,7 @@
"eslint-plugin-react": "^7.28.0", "eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.3.0",
"eslint-watch": "^8.0.0", "eslint-watch": "^8.0.0",
"http-proxy-middleware": "^2.0.6",
"postcss": "^8.4.6", "postcss": "^8.4.6",
"tailwindcss": "^3.0.18", "tailwindcss": "^3.0.18",
"typescript": "^4.1.2" "typescript": "^4.1.2"

View file

@ -26,7 +26,7 @@
</script> </script>
<!-- {{end}} --> <!-- {{end}} -->
</head> </head>
<body class="bg-gray-100 dark:bg-gray-900"> <body class="bg-color">
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="pattern"></div> <div id="root" class="pattern"></div>
</body> </body>

View file

@ -1,39 +1,53 @@
import { Fragment } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "react-query"; import { QueryClient, QueryClientProvider, useQueryErrorResetBoundary } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools"; import { ReactQueryDevtools } from "react-query/devtools";
import { Toaster } from "react-hot-toast"; import { ErrorBoundary } from "react-error-boundary";
import { toast, Toaster } from "react-hot-toast";
import Base from "./screens/Base"; import Base from "./screens/Base";
import { Login } from "./screens/auth/login"; import { Login } from "./screens/auth/login";
import { Logout } from "./screens/auth/logout"; import { Logout } from "./screens/auth/logout";
import { Onboarding } from "./screens/auth/onboarding"; import { Onboarding } from "./screens/auth/onboarding";
import { baseUrl } from "./utils"; import { baseUrl } from "./utils";
import { AuthContext, SettingsContext } from "./utils/Context"; import { AuthContext, SettingsContext } from "./utils/Context";
import { ErrorPage } from "./components/alerts";
import Toast from "./components/notifications/Toast";
function Protected() { export const queryClient = new QueryClient({
return ( defaultOptions: {
<Fragment> queries: { useErrorBoundary: true, },
<Toaster position="top-right" /> mutations: {
<Base /> onError: (error) => {
</Fragment> // Use a format string to convert the error object to a proper string without much hassle.
) const message = (
} typeof (error) === "object" && typeof ((error as Error).message) ?
(error as Error).message :
export const queryClient = new QueryClient(); `${error}`
);
toast.custom((t) => <Toast type="error" body={message} t={t} />);
}
},
},
});
export function App() { export function App() {
const { reset } = useQueryErrorResetBoundary();
const authContext = AuthContext.useValue(); const authContext = AuthContext.useValue();
const settings = SettingsContext.useValue(); const settings = SettingsContext.useValue();
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Toaster position="top-right" />
<ErrorBoundary
onReset={reset}
fallbackRender={ErrorPage}
>
<Router basename={baseUrl()}> <Router basename={baseUrl()}>
<Route exact path="/logout" component={Logout} /> <Route exact path="/logout" component={Logout} />
{authContext.isLoggedIn ? ( {authContext.isLoggedIn ? (
<Route component={Protected} /> <Route component={Base} />
) : ( ) : (
<Switch> <Switch>
<Route exact path="/onboard" component={Onboarding} /> <Route exact path="/onboard" component={Onboarding} />
@ -44,6 +58,7 @@ export function App() {
{settings.debug ? ( {settings.debug ? (
<ReactQueryDevtools initialIsOpen={false} /> <ReactQueryDevtools initialIsOpen={false} />
) : null} ) : null}
</ErrorBoundary>
</QueryClientProvider> </QueryClientProvider>
); );
} }

View file

@ -0,0 +1,87 @@
import StackTracey from "stacktracey";
import type { FallbackProps } from "react-error-boundary";
import { RefreshIcon } from "@heroicons/react/solid";
export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
const stack = new StackTracey(error).withSources();
const summary = stack.clean().asTable({
maxColumnWidths: {
callee: 48,
file: 48,
sourceLine: 256
}
});
const type = String(
(error && error.constructor && error.constructor.name)
|| typeof (error)
);
const msg = String(error && error.message);
return (
<div className="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-screen-md md:max-w-screen-lg lg:max-w-screen-xl">
<h1 className="text-3xl font-bold leading-6 text-gray-900 dark:text-gray-200 mt-4 mb-3">
We caught an unrecoverable error!
</h1>
<h3 className="text-xl leading-6 text-gray-700 dark:text-gray-400 mb-4">
Please consider reporting this error to our
{" "}
<a
rel="noopener noreferrer"
target="_blank"
href="https://github.com/autobrr/autobrr"
className="text-gray-700 dark:text-gray-200 underline font-semibold underline-offset-2 decoration-sky-500 hover:decoration-2 hover:text-black hover:dark:text-gray-100"
>
GitHub page
</a>
{" or to "}
<a
rel="noopener noreferrer"
target="_blank"
href="https://discord.gg/WQ2eUycxyT"
className="text-gray-700 dark:text-gray-200 underline font-semibold underline-offset-2 decoration-purple-500 hover:decoration-2 hover:text-black hover:dark:text-gray-100"
>
our official Discord channel
</a>
.
</h3>
<div
id="alert-additional-content-2"
className="px-4 pt-4 pb-3 m-auto bg-red-100 rounded-lg dark:bg-red-200 shadow-lg"
role="alert"
>
<div className="flex items-center">
<svg className="mr-2 w-5 h-5 text-red-700 dark:text-red-800" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
<h3 className="text-lg font-medium text-red-700 dark:text-red-800">{type}: {msg}</h3>
</div>
{summary ? (
<pre className="mt-2 mb-4 text-sm text-red-700 dark:text-red-800 overflow-x-auto">
{summary}
</pre>
) : null}
<span className="block text-gray-800 mb-2 text-md">
You can try resetting the page page using the button provided below.
However, this is not guaranteed to fix the error.
</span>
<button
type="button"
className="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-3 py-1.5 mr-2 text-center inline-flex items-center dark:bg-red-800 dark:hover:bg-red-900"
onClick={(event) => {
event.preventDefault();
resetErrorBoundary();
}}
>
<RefreshIcon className="-ml-0.5 mr-2 h-5 w-5" />
Reset page state
</button>
</div>
</div>
</div>
);
}

View file

@ -28,3 +28,5 @@ export function AlertWarning({ title, text }: props) {
</div> </div>
); );
} }
export { ErrorPage } from "./ErrorPage";

View file

@ -15,14 +15,11 @@ export const AgeCell = ({ value }: CellProps) => (
</div> </div>
); );
export const ReleaseCell = ({ value }: CellProps) => ( export const TitleCell = ({ value }: CellProps) => (
<div className="text-sm font-medium text-gray-900 dark:text-gray-300" title={value}> <div
{value} className="text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[128px] sm:max-w-none overflow-auto py-4"
</div> title={value}
); >
export const IndexerCell = ({ value }: CellProps) => (
<div className="text-sm font-medium text-gray-900 dark:text-gray-500" title={value}>
{value} {value}
</div> </div>
); );
@ -46,7 +43,6 @@ const StatusCellMap: Record<string, StatusCellMapEntry> = {
"PUSH_REJECTED": { "PUSH_REJECTED": {
colors: "bg-blue-200 dark:bg-blue-100 text-blue-400 dark:text-blue-800 hover:bg-blue-300 dark:hover:bg-blue-400", colors: "bg-blue-200 dark:bg-blue-100 text-blue-400 dark:text-blue-800 hover:bg-blue-300 dark:hover:bg-blue-400",
icon: <BanIcon className="h-5 w-5" aria-hidden="true" /> icon: <BanIcon className="h-5 w-5" aria-hidden="true" />
}, },
"PUSH_APPROVED": { "PUSH_APPROVED": {
colors: "bg-green-100 text-green-800 hover:bg-green-300", colors: "bg-green-100 text-green-800 hover:bg-green-300",
@ -58,12 +54,24 @@ const StatusCellMap: Record<string, StatusCellMapEntry> = {
} }
}; };
const GetReleaseStatusString = (releaseAction: ReleaseActionStatus) => {
const items: Array<string> = [
`action: ${releaseAction.action}`,
`type: ${releaseAction.type}`,
`status: ${releaseAction.status}`,
`time: ${simplifyDate(releaseAction.timestamp)}`
];
if (releaseAction.rejections.length)
items.push(`rejections: ${releaseAction.rejections}`);
return items.join(" | ");
};
export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => ( export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => (
<div className="flex text-sm font-medium text-gray-900 dark:text-gray-300"> <div className="flex text-sm font-medium text-gray-900 dark:text-gray-300">
{value.map((v, idx) => ( {value.map((v, idx) => (
<div <div
key={idx} key={idx}
title={`action: ${v.action}, type: ${v.type}, status: ${v.status}, time: ${simplifyDate(v.timestamp)}, rejections: ${v?.rejections}`} title={GetReleaseStatusString(v)}
className={classNames( className={classNames(
StatusCellMap[v.status].colors, StatusCellMap[v.status].colors,
"mr-1 inline-flex items-center rounded text-xs font-semibold uppercase cursor-pointer" "mr-1 inline-flex items-center rounded text-xs font-semibold uppercase cursor-pointer"

View file

@ -1,29 +1,22 @@
import { PlusIcon } from "@heroicons/react/solid"; import { PlusIcon } from "@heroicons/react/solid";
interface EmptyBasicProps {
title: string;
subtitle?: string;
}
export const EmptyBasic = ({ title, subtitle }: EmptyBasicProps) => (
<div className="text-center py-16">
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">{title}</h3>
{subtitle ?? <p className="mt-1 text-sm text-gray-500 dark:text-gray-200">{subtitle}</p>}
</div>
)
interface EmptySimpleProps { interface EmptySimpleProps {
title: string; title: string;
subtitle: string; subtitle: string;
buttonText: string; buttonText?: string;
buttonAction: any; buttonAction?: () => void;
} }
export const EmptySimple = ({ title, subtitle, buttonText, buttonAction }: EmptySimpleProps) => ( export const EmptySimple = ({
title,
subtitle,
buttonText,
buttonAction
}: EmptySimpleProps) => (
<div className="text-center py-8"> <div className="text-center py-8">
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">{title}</h3> <h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">{title}</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-200">{subtitle}</p> <p className="mt-1 text-sm text-gray-500 dark:text-gray-200">{subtitle}</p>
{buttonText && buttonAction ? (
<div className="mt-6"> <div className="mt-6">
<button <button
type="button" type="button"
@ -34,6 +27,7 @@ export const EmptySimple = ({ title, subtitle, buttonText, buttonAction }: Empty
{buttonText} {buttonText}
</button> </button>
</div> </div>
) : null}
</div> </div>
) )

View file

@ -32,7 +32,11 @@ export const TextFieldWide = ({
</label> </label>
</div> </div>
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<Field name={name} value={defaultValue}> <Field
name={name}
value={defaultValue}
required={required}
>
{({ field, meta }: FieldProps) => ( {({ field, meta }: FieldProps) => (
<input <input
{...field} {...field}

View file

@ -66,7 +66,12 @@ export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, d
<button <button
type="button" type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm" className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
onClick={deleteAction} onClick={() => {
if (isOpen) {
deleteAction();
toggle();
}
}}
> >
Remove Remove
</button> </button>

View file

@ -443,6 +443,9 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(['indexer']); queryClient.invalidateQueries(['indexer']);
toast.custom((t) => <Toast type="success" body={`${indexer.name} was deleted.`} t={t} />) toast.custom((t) => <Toast type="success" body={`${indexer.name} was deleted.`} t={t} />)
sleep(1500);
toggle();
} }
}) })

View file

@ -101,7 +101,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(['networks']); queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body="IRC Network added" t={t} />) toast.custom((t) => <Toast type="success" body="IRC Network added. Please allow up to 30 seconds for the network to come online." t={t} />)
toggle() toggle()
}, },
onError: () => { onError: () => {

File diff suppressed because one or more lines are too long

View file

@ -59,7 +59,7 @@ export default function Base() {
> >
{({ open }) => ( {({ open }) => (
<> <>
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8"> <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="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 justify-between h-16 px-4 sm:px-0">
<div className="flex items-center"> <div className="flex items-center">
@ -86,7 +86,7 @@ export default function Base() {
"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", "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" "transition-colors duration-200"
)} )}
activeClassName="text-black dark:text-gray-50 font-bold" activeClassName="text-black dark:text-gray-50 !font-bold"
isActive={(match, location) => isActiveMatcher(match, location, item)} isActive={(match, location) => isActiveMatcher(match, location, item)}
> >
{item.name} {item.name}

View file

@ -56,8 +56,8 @@ export const Logs = () => {
return ( return (
<main> <main>
<header className="py-10"> <header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <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 capitalize">Logs</h1> <h1 className="text-3xl font-bold text-black dark:text-white">Logs</h1>
<div className="flex mt-4 justify-center"> <div className="flex mt-4 justify-center">
<ExclamationIcon <ExclamationIcon
className="h-5 w-5 text-yellow-400" className="h-5 w-5 text-yellow-400"
@ -67,7 +67,7 @@ export const Logs = () => {
</div> </div>
</div> </div>
</header> </header>
<div className="max-w-7xl mx-auto pb-12 px-2 sm:px-4 lg:px-8"> <div className="max-w-screen-xl mx-auto pb-12 px-2 sm:px-4 lg:px-8">
<div <div
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg px-2 sm:px-4 pb-3 sm:pb-4" className="bg-white dark:bg-gray-800 rounded-lg shadow-lg px-2 sm:px-4 pb-3 sm:pb-4"
> >

View file

@ -68,8 +68,8 @@ export default function Settings() {
return ( return (
<main> <main>
<header className="py-10"> <header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <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 capitalize">Settings</h1> <h1 className="text-3xl font-bold text-black dark:text-white">Settings</h1>
</div> </div>
</header> </header>

View file

@ -74,10 +74,8 @@ function Table({ columns, data }: any) {
// Render the UI for your table // Render the UI for your table
return ( return (
<div className="flex flex-col mt-4"> <div className="inline-block min-w-full mt-4 mb-2 align-middle">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="overflow-auto bg-white shadow dark:bg-gray-800 rounded-lg">
<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">
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800"> <thead className="bg-gray-50 dark:bg-gray-800">
{headerGroups.map((headerGroup) => { {headerGroups.map((headerGroup) => {
@ -92,7 +90,7 @@ function Table({ columns, data }: any) {
<th <th
key={`${rowKey}-${columnKey}`} key={`${rowKey}-${columnKey}`}
scope="col" scope="col"
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group" className="first:pl-5 pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group"
{...columnRest} {...columnRest}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -131,14 +129,11 @@ function Table({ columns, data }: any) {
return ( return (
<td <td
key={cellRowKey} key={cellRowKey}
className="px-6 py-4 whitespace-nowrap" className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
role="cell" role="cell"
{...cellRowRest} {...cellRowRest}
> >
{cell.column.Cell.name === "defaultRenderer" {cell.render('Cell')}
? <div className="text-sm text-gray-500">{cell.render('Cell')}</div>
: cell.render('Cell')
}
</td> </td>
) )
})} })}
@ -149,8 +144,6 @@ function Table({ columns, data }: any) {
</table> </table>
</div> </div>
</div> </div>
</div>
</div>
); );
} }
@ -164,7 +157,7 @@ export const ActivityTable = () => {
{ {
Header: "Release", Header: "Release",
accessor: 'torrent_name', accessor: 'torrent_name',
Cell: DataTable.ReleaseCell, Cell: DataTable.TitleCell,
}, },
{ {
Header: "Actions", Header: "Actions",
@ -174,7 +167,7 @@ export const ActivityTable = () => {
{ {
Header: "Indexer", Header: "Indexer",
accessor: 'indexer', accessor: 'indexer',
Cell: DataTable.IndexerCell, Cell: DataTable.TitleCell,
Filter: SelectColumnFilter, Filter: SelectColumnFilter,
filter: 'includes', filter: 'includes',
}, },

View file

@ -8,15 +8,15 @@ interface StatsItemProps {
const StatsItem = ({ name, value }: StatsItemProps) => ( const StatsItem = ({ name, value }: StatsItemProps) => (
<div <div
className="relative px-4 pt-5 pb-2 overflow-hidden bg-white rounded-lg shadow-lg dark:bg-gray-800 sm:pt-6 sm:px-6" className="relative px-4 py-5 overflow-hidden bg-white rounded-lg shadow-lg dark:bg-gray-800"
title="All time" title="All time"
> >
<dt> <dt>
<p className="pb-1 text-sm font-medium text-gray-500 truncate">{name}</p> <p className="pb-1 text-sm font-medium text-gray-500 truncate">{name}</p>
</dt> </dt>
<dd className="flex items-baseline pb-6 sm:pb-7"> <dd className="flex items-baseline">
<p className="text-2xl font-semibold text-gray-900 dark:text-gray-200">{value}</p> <p className="text-3xl font-extrabold text-gray-900 dark:text-gray-200">{value}</p>
</dd> </dd>
</div> </div>
) )

View file

@ -3,7 +3,7 @@ import { ActivityTable } from "./ActivityTable";
export const Dashboard = () => ( export const Dashboard = () => (
<main className="py-10"> <main className="py-10">
<div className="px-4 pb-8 mx-auto max-w-7xl sm:px-6 lg:px-8"> <div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
<Stats /> <Stats />
<ActivityTable /> <ActivityTable />
</div> </div>

View file

@ -205,17 +205,17 @@ export default function FilterDetails() {
return ( return (
<main> <main>
<header className="py-10"> <header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center"> <div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center">
<h1 className="text-3xl font-bold text-black dark:text-white capitalize"> <h1 className="text-3xl font-bold text-black dark:text-white">
<NavLink to="/filters" exact={true}> <NavLink to="/filters" exact={true}>
Filters Filters
</NavLink> </NavLink>
</h1> </h1>
<ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" /> <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />
<h1 className="text-3xl font-bold text-black dark:text-white capitalize">{filter.name}</h1> <h1 className="text-3xl font-bold text-black dark:text-white">{filter.name}</h1>
</div> </div>
</header> </header>
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8"> <div className="max-w-screen-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="relative mx-auto md:px-6 xl:px-4"> <div className="relative mx-auto md:px-6 xl:px-4">
<div className="px-4 sm:px-6 md:px-0"> <div className="px-4 sm:px-6 md:px-0">

View file

@ -39,9 +39,10 @@ export default function Filters() {
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} /> <FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} />
<header className="py-10"> <header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between"> <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 capitalize">Filters</h1> <h1 className="text-3xl font-bold text-black dark:text-white">
Filters
</h1>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<button <button
type="button" type="button"
@ -54,7 +55,7 @@ export default function Filters() {
</div> </div>
</header> </header>
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8 relative"> <div className="max-w-screen-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8 relative">
{data && data.length > 0 ? ( {data && data.length > 0 ? (
<FilterList filters={data} /> <FilterList filters={data} />
) : ( ) : (

View file

@ -62,7 +62,7 @@ export const ReleaseTable = () => {
{ {
Header: "Release", Header: "Release",
accessor: 'torrent_name', accessor: 'torrent_name',
Cell: DataTable.ReleaseCell, Cell: DataTable.TitleCell,
}, },
{ {
Header: "Actions", Header: "Actions",
@ -73,7 +73,7 @@ export const ReleaseTable = () => {
{ {
Header: "Indexer", Header: "Indexer",
accessor: 'indexer', accessor: 'indexer',
Cell: DataTable.IndexerCell, Cell: DataTable.TitleCell,
Filter: IndexerSelectColumnFilter, Filter: IndexerSelectColumnFilter,
filter: 'equal', filter: 'equal',
}, },
@ -177,8 +177,7 @@ export const ReleaseTable = () => {
)) ))
)} )}
</div> </div>
<div className="overflow-auto bg-white shadow-lg dark:bg-gray-800 rounded-lg">
<div className="overflow-hidden bg-white shadow-lg dark:bg-gray-800 sm:rounded-lg">
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800"> <thead className="bg-gray-50 dark:bg-gray-800">
{headerGroups.map((headerGroup) => { {headerGroups.map((headerGroup) => {
@ -193,7 +192,7 @@ export const ReleaseTable = () => {
<th <th
key={`${rowKey}-${columnKey}`} key={`${rowKey}-${columnKey}`}
scope="col" scope="col"
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group" className="first:pl-5 pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group"
{...columnRest} {...columnRest}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -233,14 +232,11 @@ export const ReleaseTable = () => {
return ( return (
<td <td
key={cellRowKey} key={cellRowKey}
className="px-6 py-4 whitespace-nowrap" className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
role="cell" role="cell"
{...cellRowRest} {...cellRowRest}
> >
{cell.column.Cell.name === "defaultRenderer" {cell.render('Cell')}
? <div className="text-sm text-gray-500">{cell.render('Cell')}</div>
: cell.render('Cell')
}
</td> </td>
); );
})} })}

View file

@ -3,11 +3,11 @@ import { ReleaseTable } from "./ReleaseTable";
export const Releases = () => ( export const Releases = () => (
<main> <main>
<header className="py-10"> <header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between"> <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 capitalize">Releases</h1> <h1 className="text-3xl font-bold text-black dark:text-white">Releases</h1>
</div> </div>
</header> </header>
<div className="px-4 pb-8 mx-auto max-w-7xl sm:px-6 lg:px-8"> <div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
<ReleaseTable /> <ReleaseTable />
</div> </div>
</main> </main>

View file

@ -3,7 +3,6 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { Menu, Switch, Transition } from "@headlessui/react"; import { Menu, Switch, Transition } from "@headlessui/react";
import type {FieldProps} from "formik";
import {classNames} from "../../utils"; import {classNames} from "../../utils";
import {Fragment, useRef, useState} from "react"; import {Fragment, useRef, useState} from "react";
import {toast} from "react-hot-toast"; import {toast} from "react-hot-toast";
@ -17,7 +16,7 @@ import {
TrashIcon TrashIcon
} from "@heroicons/react/outline"; } from "@heroicons/react/outline";
import {FeedUpdateForm} from "../../forms/settings/FeedForms"; import {FeedUpdateForm} from "../../forms/settings/FeedForms";
import {EmptyBasic} from "../../components/emptystates"; import { EmptySimple } from "../../components/emptystates";
function FeedSettings() { function FeedSettings() {
const {data} = useQuery<Feed[], Error>('feeds', APIClient.feeds.find, const {data} = useQuery<Feed[], Error>('feeds', APIClient.feeds.find,
@ -33,7 +32,7 @@ function FeedSettings() {
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Feeds</h3> <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"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage torznab feeds. Manage Torznab feeds.
</p> </p>
</div> </div>
</div> </div>
@ -59,7 +58,7 @@ function FeedSettings() {
))} ))}
</ol> </ol>
</section> </section>
: <EmptyBasic title="No feeds" subtitle="Setup via indexers" />} : <EmptySimple title="No feeds" subtitle="Setup via indexers" />}
</div> </div>
</div> </div>
) )

View file

@ -18,7 +18,11 @@ export const IrcSettings = () => {
const { data } = useQuery( const { data } = useQuery(
"networks", "networks",
APIClient.irc.getNetworks, APIClient.irc.getNetworks,
{ refetchOnWindowFocus: false } {
refetchOnWindowFocus: false,
// Refetch every 3 seconds
refetchInterval: 3000
}
); );
return ( return (

11
web/src/setupProxy.js Normal file
View file

@ -0,0 +1,11 @@
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://127.0.0.1:7474',
changeOrigin: true,
})
);
};

View file

@ -2543,6 +2543,13 @@ array.prototype.flatmap@^1.2.5:
define-properties "^1.1.3" define-properties "^1.1.3"
es-abstract "^1.19.0" es-abstract "^1.19.0"
as-table@^1.0.36:
version "1.0.55"
resolved "https://registry.yarnpkg.com/as-table/-/as-table-1.0.55.tgz#dc984da3937745de902cea1d45843c01bdbbec4f"
integrity sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==
dependencies:
printable-characters "^1.0.42"
asap@~2.0.6: asap@~2.0.6:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
@ -3466,6 +3473,11 @@ damerau-levenshtein@^1.0.7:
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
data-uri-to-buffer@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz#d296973d5a4897a5dbe31716d118211921f04770"
integrity sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==
data-urls@^2.0.0: data-urls@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
@ -4532,6 +4544,14 @@ get-package-type@^0.1.0:
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
get-source@^2.0.12:
version "2.0.12"
resolved "https://registry.yarnpkg.com/get-source/-/get-source-2.0.12.tgz#0b47d57ea1e53ce0d3a69f4f3d277eb8047da944"
integrity sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==
dependencies:
data-uri-to-buffer "^2.0.0"
source-map "^0.6.1"
get-stream@^6.0.0: get-stream@^6.0.0:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
@ -4818,6 +4838,17 @@ http-proxy-middleware@^2.0.3:
is-plain-obj "^3.0.0" is-plain-obj "^3.0.0"
micromatch "^4.0.2" micromatch "^4.0.2"
http-proxy-middleware@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f"
integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==
dependencies:
"@types/http-proxy" "^1.17.8"
http-proxy "^1.18.1"
is-glob "^4.0.1"
is-plain-obj "^3.0.0"
micromatch "^4.0.2"
http-proxy@^1.18.1: http-proxy@^1.18.1:
version "1.18.1" version "1.18.1"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
@ -7151,6 +7182,11 @@ pretty-format@^27.5.1:
ansi-styles "^5.0.0" ansi-styles "^5.0.0"
react-is "^17.0.1" react-is "^17.0.1"
printable-characters@^1.0.42:
version "1.0.42"
resolved "https://registry.yarnpkg.com/printable-characters/-/printable-characters-1.0.42.tgz#3f18e977a9bd8eb37fcc4ff5659d7be90868b3d8"
integrity sha1-Pxjpd6m9jrN/zE/1ZZ176Qhos9g=
process-nextick-args@~2.0.0: process-nextick-args@~2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@ -7307,6 +7343,13 @@ react-dom@^17.0.2:
object-assign "^4.1.1" object-assign "^4.1.1"
scheduler "^0.20.2" scheduler "^0.20.2"
react-error-boundary@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
dependencies:
"@babel/runtime" "^7.12.5"
react-error-overlay@^6.0.10: react-error-overlay@^6.0.10:
version "6.0.10" version "6.0.10"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
@ -8062,6 +8105,14 @@ stackframe@^1.1.1:
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.1.tgz#1033a3473ee67f08e2f2fc8eba6aef4f845124e1" resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.1.tgz#1033a3473ee67f08e2f2fc8eba6aef4f845124e1"
integrity sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg== integrity sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==
stacktracey@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/stacktracey/-/stacktracey-2.1.8.tgz#bf9916020738ce3700d1323b32bd2c91ea71199d"
integrity sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==
dependencies:
as-table "^1.0.36"
get-source "^2.0.12"
"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: "statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"