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

@ -59,7 +59,7 @@ export default function Base() {
>
{({ 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="flex items-center justify-between h-16 px-4 sm:px-0">
<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",
"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)}
>
{item.name}

View file

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

View file

@ -1,11 +1,11 @@
import * as React from "react";
import { useQuery } from "react-query";
import {
useTable,
useFilters,
useGlobalFilter,
useSortBy,
usePagination
useTable,
useFilters,
useGlobalFilter,
useSortBy,
usePagination
} from "react-table";
import { APIClient } from "../../api/APIClient";
@ -17,185 +17,178 @@ 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 },
column: { filterValue, setFilter, preFilteredRows, id, render },
}: any) {
// 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])
// 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])
// Render a multi-select box
return (
<label className="flex items-baseline gap-x-2">
<span className="text-gray-700">{render("Header")}: </span>
<select
className="border-gray-300 rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
name={id}
id={id}
value={filterValue}
onChange={e => {
setFilter(e.target.value || undefined)
}}
>
<option value="">All</option>
{options.map((option, i) => (
<option key={i} value={option}>
{option}
</option>
))}
</select>
</label>
)
// Render a multi-select box
return (
<label className="flex items-baseline gap-x-2">
<span className="text-gray-700">{render("Header")}: </span>
<select
className="border-gray-300 rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
name={id}
id={id}
value={filterValue}
onChange={e => {
setFilter(e.target.value || undefined)
}}
>
<option value="">All</option>
{options.map((option, i) => (
<option key={i} value={option}>
{option}
</option>
))}
</select>
</label>
)
}
function Table({ columns, data }: any) {
// 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,
} = useTable(
{ columns, data },
useFilters,
useGlobalFilter,
useSortBy,
usePagination
);
// 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,
} = useTable(
{ columns, data },
useFilters,
useGlobalFilter,
useSortBy,
usePagination
);
if (!page.length)
return <EmptyListState text="No recent activity" />;
if (!page.length)
return <EmptyListState text="No recent activity" />;
// Render the UI for your table
return (
<div className="flex flex-col mt-4">
<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">
<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="px-6 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);
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="px-6 py-4 whitespace-nowrap"
role="cell"
{...cellRowRest}
>
{cell.column.Cell.name === "defaultRenderer"
? <div className="text-sm text-gray-500">{cell.render('Cell')}</div>
: cell.render('Cell')
}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
// Render the UI for your table
return (
<div className="inline-block min-w-full mt-4 mb-2 align-middle">
<div className="overflow-auto bg-white shadow 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);
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>
</div>
</div>
);
}
export const ActivityTable = () => {
const columns = React.useMemo(() => [
{
Header: "Age",
accessor: 'timestamp',
Cell: DataTable.AgeCell,
},
{
Header: "Release",
accessor: 'torrent_name',
Cell: DataTable.ReleaseCell,
},
{
Header: "Actions",
accessor: 'action_status',
Cell: DataTable.ReleaseStatusCell,
},
{
Header: "Indexer",
accessor: 'indexer',
Cell: DataTable.IndexerCell,
Filter: SelectColumnFilter,
filter: 'includes',
},
], [])
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,
},
{
Header: "Indexer",
accessor: 'indexer',
Cell: DataTable.TitleCell,
Filter: SelectColumnFilter,
filter: 'includes',
},
], [])
const { isLoading, data } = useQuery(
'dash_release',
() => APIClient.release.find("?limit=10"),
{ refetchOnWindowFocus: false }
);
const { isLoading, data } = useQuery(
'dash_release',
() => APIClient.release.find("?limit=10"),
{ refetchOnWindowFocus: false }
);
if (isLoading)
return null;
if (isLoading)
return null;
return (
<div className="flex flex-col mt-12">
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
Recent activity
</h3>
return (
<div className="flex flex-col mt-12">
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
Recent activity
</h3>
<Table columns={columns} data={data?.data} />
</div>
);
<Table columns={columns} data={data?.data} />
</div>
);
}

View file

@ -8,15 +8,15 @@ interface StatsItemProps {
const StatsItem = ({ name, value }: StatsItemProps) => (
<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"
>
<dt>
<p className="pb-1 text-sm font-medium text-gray-500 truncate">{name}</p>
</dt>
<dd className="flex items-baseline pb-6 sm:pb-7">
<p className="text-2xl font-semibold text-gray-900 dark:text-gray-200">{value}</p>
<dd className="flex items-baseline">
<p className="text-3xl font-extrabold text-gray-900 dark:text-gray-200">{value}</p>
</dd>
</div>
)

View file

@ -3,7 +3,7 @@ import { ActivityTable } from "./ActivityTable";
export const Dashboard = () => (
<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 />
<ActivityTable />
</div>

View file

@ -205,17 +205,17 @@ export default function FilterDetails() {
return (
<main>
<header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center">
<h1 className="text-3xl font-bold text-black dark:text-white capitalize">
<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">
<NavLink to="/filters" exact={true}>
Filters
</NavLink>
</h1>
<ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />
<h1 className="text-3xl font-bold text-black dark:text-white capitalize">{filter.name}</h1>
<h1 className="text-3xl font-bold text-black dark:text-white">{filter.name}</h1>
</div>
</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="relative mx-auto md:px-6 xl:px-4">
<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} />
<header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
<h1 className="text-3xl font-bold text-black dark:text-white capitalize">Filters</h1>
<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"
@ -54,7 +55,7 @@ export default function Filters() {
</div>
</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 ? (
<FilterList filters={data} />
) : (

View file

@ -62,7 +62,7 @@ export const ReleaseTable = () => {
{
Header: "Release",
accessor: 'torrent_name',
Cell: DataTable.ReleaseCell,
Cell: DataTable.TitleCell,
},
{
Header: "Actions",
@ -73,7 +73,7 @@ export const ReleaseTable = () => {
{
Header: "Indexer",
accessor: 'indexer',
Cell: DataTable.IndexerCell,
Cell: DataTable.TitleCell,
Filter: IndexerSelectColumnFilter,
filter: 'equal',
},
@ -177,8 +177,7 @@ export const ReleaseTable = () => {
))
)}
</div>
<div className="overflow-hidden bg-white shadow-lg dark:bg-gray-800 sm:rounded-lg">
<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) => {
@ -193,7 +192,7 @@ export const ReleaseTable = () => {
<th
key={`${rowKey}-${columnKey}`}
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}
>
<div className="flex items-center justify-between">
@ -233,14 +232,11 @@ export const ReleaseTable = () => {
return (
<td
key={cellRowKey}
className="px-6 py-4 whitespace-nowrap"
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
role="cell"
{...cellRowRest}
>
{cell.column.Cell.name === "defaultRenderer"
? <div className="text-sm text-gray-500">{cell.render('Cell')}</div>
: cell.render('Cell')
}
{cell.render('Cell')}
</td>
);
})}

View file

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

View file

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

View file

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