mirror of
https://github.com/idanoo/autobrr
synced 2025-07-25 17:59:14 +00:00
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:
parent
bea30cb0bd
commit
2c46993264
26 changed files with 464 additions and 280 deletions
87
web/src/components/alerts/ErrorPage.tsx
Normal file
87
web/src/components/alerts/ErrorPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -28,3 +28,5 @@ export function AlertWarning({ title, text }: props) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ErrorPage } from "./ErrorPage";
|
|
@ -15,14 +15,11 @@ export const AgeCell = ({ value }: CellProps) => (
|
|||
</div>
|
||||
);
|
||||
|
||||
export const ReleaseCell = ({ value }: CellProps) => (
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-300" title={value}>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const IndexerCell = ({ value }: CellProps) => (
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-500" title={value}>
|
||||
export const TitleCell = ({ value }: CellProps) => (
|
||||
<div
|
||||
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"
|
||||
title={value}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
|
@ -46,7 +43,6 @@ const StatusCellMap: Record<string, StatusCellMapEntry> = {
|
|||
"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",
|
||||
icon: <BanIcon className="h-5 w-5" aria-hidden="true" />
|
||||
|
||||
},
|
||||
"PUSH_APPROVED": {
|
||||
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) => (
|
||||
<div className="flex text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||
{value.map((v, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
title={`action: ${v.action}, type: ${v.type}, status: ${v.status}, time: ${simplifyDate(v.timestamp)}, rejections: ${v?.rejections}`}
|
||||
title={GetReleaseStatusString(v)}
|
||||
className={classNames(
|
||||
StatusCellMap[v.status].colors,
|
||||
"mr-1 inline-flex items-center rounded text-xs font-semibold uppercase cursor-pointer"
|
||||
|
|
|
@ -1,39 +1,33 @@
|
|||
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 {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
buttonText: string;
|
||||
buttonAction: any;
|
||||
buttonText?: string;
|
||||
buttonAction?: () => void;
|
||||
}
|
||||
|
||||
export const EmptySimple = ({ title, subtitle, buttonText, buttonAction }: EmptySimpleProps) => (
|
||||
export const EmptySimple = ({
|
||||
title,
|
||||
subtitle,
|
||||
buttonText,
|
||||
buttonAction
|
||||
}: EmptySimpleProps) => (
|
||||
<div className="text-center py-8">
|
||||
<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>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={buttonAction}
|
||||
className="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"
|
||||
>
|
||||
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
|
||||
{buttonText}
|
||||
</button>
|
||||
</div>
|
||||
{buttonText && buttonAction ? (
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={buttonAction}
|
||||
className="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"
|
||||
>
|
||||
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
|
||||
{buttonText}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
|
|
@ -32,7 +32,11 @@ export const TextFieldWide = ({
|
|||
</label>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Field name={name} value={defaultValue}>
|
||||
<Field
|
||||
name={name}
|
||||
value={defaultValue}
|
||||
required={required}
|
||||
>
|
||||
{({ field, meta }: FieldProps) => (
|
||||
<input
|
||||
{...field}
|
||||
|
|
|
@ -66,7 +66,12 @@ export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, d
|
|||
<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"
|
||||
onClick={deleteAction}
|
||||
onClick={() => {
|
||||
if (isOpen) {
|
||||
deleteAction();
|
||||
toggle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue