From 2fed48e0dd06f7a0b57ce517528d998fe11341a5 Mon Sep 17 00:00:00 2001
From: stacksmash76 <98354295+stacksmash76@users.noreply.github.com>
Date: Sun, 10 Sep 2023 12:35:43 +0200
Subject: [PATCH] enhancement(web): add react suspense and improve DX (#1089)
* add react suspense, fix broken stuff, clean up code, improve DX
enhancement: added react suspense + spinner to show loading (still can be added in certain places)
chore: cleaned up Header/NavBar code
chore: cleaned up DeleteModal code
chore: cleaned up other relevant code
enhancement: changed remove button style to be much more pleasant (see e.g. filter tabs)
fix: made active tab on filters page to be blue (as it should've been) when active
fix: fixed ghost delimiter which was only visible when DeleteModal was active in FormButtonGroup
chore: removed most of linter warnings/errors
fix: fixed incorrect/double modal transition in FilterExternalItem
fix: fixed incorrect z-height on Options popover in Settings/IRC (would've been visible when Add new was clicked)
enhancement: improved robustness of all Context classes to support seamless new-feature expansion (#866)
enhancement: improved expand logic (see #994 comments)
* reverted irc expand view to previous design
* forgot to propagate previous z-height fix
* jinxed it
* add license header to new files
---------
Co-authored-by: martylukyy <35452459+martylukyy@users.noreply.github.com>
Co-authored-by: Kyle Sanderson
---
web/src/App.tsx | 5 +-
web/src/components/SectionLoader.tsx | 32 +++
web/src/components/header/Header.tsx | 86 ++++++
web/src/components/header/LeftNav.tsx | 59 ++++
web/src/components/header/MobileNav.tsx | 45 ++++
web/src/components/header/RightNav.tsx | 98 +++++++
web/src/components/header/_shared.ts | 21 ++
web/src/components/header/index.tsx | 6 +
web/src/components/modals/index.tsx | 124 +++++----
web/src/components/panels/index.tsx | 1 +
web/src/domain/routes.tsx | 25 +-
.../forms/settings/DownloadClientForms.tsx | 1 +
web/src/screens/Base.tsx | 253 ------------------
web/src/screens/Settings.tsx | 12 +-
web/src/screens/filters/Action.tsx | 19 +-
web/src/screens/filters/Details.tsx | 210 ++++++++-------
web/src/screens/filters/External.tsx | 210 +++++++--------
web/src/screens/filters/List.tsx | 1 +
web/src/screens/settings/Api.tsx | 1 +
web/src/screens/settings/Feed.tsx | 113 ++++----
web/src/screens/settings/Irc.tsx | 47 +---
web/src/screens/settings/Releases.tsx | 1 +
web/src/utils/Context.ts | 212 +++++++--------
23 files changed, 845 insertions(+), 737 deletions(-)
create mode 100644 web/src/components/SectionLoader.tsx
create mode 100644 web/src/components/header/Header.tsx
create mode 100644 web/src/components/header/LeftNav.tsx
create mode 100644 web/src/components/header/MobileNav.tsx
create mode 100644 web/src/components/header/RightNav.tsx
create mode 100644 web/src/components/header/_shared.ts
create mode 100644 web/src/components/header/index.tsx
delete mode 100644 web/src/screens/Base.tsx
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 1ec977a..7df9afd 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -21,7 +21,8 @@ const queryClient = new QueryClient({
// See https://tanstack.com/query/v4/docs/guides/query-retries#retry-delay
// delay = Math.min(1000 * 2 ** attemptIndex, 30000)
retry: true,
- useErrorBoundary: true
+ useErrorBoundary: true,
+ suspense: true,
},
mutations: {
onError: (error) => {
@@ -59,4 +60,4 @@ export function App() {
);
-}
\ No newline at end of file
+}
diff --git a/web/src/components/SectionLoader.tsx b/web/src/components/SectionLoader.tsx
new file mode 100644
index 0000000..dee4d0a
--- /dev/null
+++ b/web/src/components/SectionLoader.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import { RingResizeSpinner } from "@components/Icons";
+import { classNames } from "@utils";
+
+const SIZE = {
+ small: "w-6 h-6",
+ medium: "w-8 h-8",
+ large: "w-12 h-12",
+ xlarge: "w-24 h-24"
+} as const;
+
+interface SectionLoaderProps {
+ $size: keyof typeof SIZE;
+}
+
+export const SectionLoader = ({ $size }: SectionLoaderProps) => {
+ if ($size === "xlarge") {
+ return (
+
+
+
+ );
+ } else {
+ return (
+
+ );
+ }
+}
diff --git a/web/src/components/header/Header.tsx b/web/src/components/header/Header.tsx
new file mode 100644
index 0000000..5ad3f2e
--- /dev/null
+++ b/web/src/components/header/Header.tsx
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import toast from "react-hot-toast";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import { Disclosure } from "@headlessui/react";
+import { Bars3Icon, XMarkIcon, MegaphoneIcon } from "@heroicons/react/24/outline";
+
+import { APIClient } from "@api/APIClient";
+import { AuthContext } from "@utils/Context";
+import Toast from "@components/notifications/Toast";
+
+import { LeftNav } from "./LeftNav";
+import { RightNav } from "./RightNav";
+import { MobileNav } from "./MobileNav";
+
+export const Header = () => {
+ const { data } = useQuery({
+ queryKey: ["updates"],
+ queryFn: () => APIClient.updates.getLatestRelease(),
+ retry: false,
+ refetchOnWindowFocus: false,
+ onError: err => console.log(err)
+ });
+
+ const logoutMutation = useMutation({
+ mutationFn: APIClient.auth.logout,
+ onSuccess: () => {
+ AuthContext.reset();
+ toast.custom((t) => (
+
+ ));
+ }
+ });
+
+ return (
+
+ {({ open }) => (
+ <>
+
+
+
+ >
+ )}
+
+ )
+}
diff --git a/web/src/components/header/LeftNav.tsx b/web/src/components/header/LeftNav.tsx
new file mode 100644
index 0000000..53b86c5
--- /dev/null
+++ b/web/src/components/header/LeftNav.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import { Link, NavLink } from "react-router-dom";
+import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/solid";
+
+import { classNames } from "@utils";
+import { ReactComponent as Logo } from "@app/logo.svg";
+
+import { NAV_ROUTES } from "./_shared";
+
+export const LeftNav = () => (
+
+
+
+
+
+
+
+
+ {NAV_ROUTES.map((item, itemIdx) => (
+
+ classNames(
+ "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",
+ isActive
+ ? "text-black dark:text-gray-50 font-bold"
+ : "text-gray-600 dark:text-gray-500"
+ )
+ }
+ end={item.path === "/"}
+ >
+ {item.name}
+
+ ))}
+
+ Docs
+
+
+
+
+
+);
diff --git a/web/src/components/header/MobileNav.tsx b/web/src/components/header/MobileNav.tsx
new file mode 100644
index 0000000..f6504a6
--- /dev/null
+++ b/web/src/components/header/MobileNav.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import { NavLink } from "react-router-dom";
+import { Disclosure } from "@headlessui/react";
+
+import { classNames } from "@utils";
+
+import { NAV_ROUTES } from "./_shared";
+import type { RightNavProps } from "./_shared";
+
+export const MobileNav = (props: RightNavProps) => (
+
+
+ {NAV_ROUTES.map((item) => (
+
+ classNames(
+ "shadow-sm border bg-gray-100 border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base",
+ isActive
+ ? "underline underline-offset-2 decoration-2 decoration-sky-500 font-bold text-black"
+ : "font-medium"
+ )
+ }
+ end={item.path === "/"}
+ >
+ {item.name}
+
+ ))}
+
+
+
+);
diff --git a/web/src/components/header/RightNav.tsx b/web/src/components/header/RightNav.tsx
new file mode 100644
index 0000000..ca8aaf7
--- /dev/null
+++ b/web/src/components/header/RightNav.tsx
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import { Fragment } from "react";
+import { Link } from "react-router-dom";
+import { UserIcon } from "@heroicons/react/24/solid";
+import { Menu, Transition } from "@headlessui/react";
+
+import { classNames } from "@utils";
+import { AuthContext } from "@utils/Context";
+
+import { RightNavProps } from "./_shared";
+
+export const RightNav = (props: RightNavProps) => {
+ const authContext = AuthContext.useValue();
+ return (
+
+
+
+
+
+ );
+}
diff --git a/web/src/components/header/_shared.ts b/web/src/components/header/_shared.ts
new file mode 100644
index 0000000..f679cd4
--- /dev/null
+++ b/web/src/components/header/_shared.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+interface NavItem {
+ name: string;
+ path: string;
+}
+
+export interface RightNavProps {
+ logoutMutation: () => void;
+}
+
+export const NAV_ROUTES: Array = [
+ { name: "Dashboard", path: "/" },
+ { name: "Filters", path: "/filters" },
+ { name: "Releases", path: "/releases" },
+ { name: "Settings", path: "/settings" },
+ { name: "Logs", path: "/logs" }
+];
diff --git a/web/src/components/header/index.tsx b/web/src/components/header/index.tsx
new file mode 100644
index 0000000..3316480
--- /dev/null
+++ b/web/src/components/header/index.tsx
@@ -0,0 +1,6 @@
+/*
+ * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+export { Header } from "./Header";
diff --git a/web/src/components/modals/index.tsx b/web/src/components/modals/index.tsx
index 16614b3..a55d827 100644
--- a/web/src/components/modals/index.tsx
+++ b/web/src/components/modals/index.tsx
@@ -7,24 +7,85 @@ import { FC, Fragment, MutableRefObject } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
-interface DeleteModalProps {
- isOpen: boolean;
- buttonRef: MutableRefObject | undefined;
- toggle: () => void;
- deleteAction: () => void;
- title: string;
- text: string;
+import { SectionLoader } from "@components/SectionLoader";
+
+interface ModalUpperProps {
+ title: string;
+ text: string;
}
-export const DeleteModal: FC = ({ isOpen, buttonRef, toggle, deleteAction, title, text }) => (
-
+interface ModalLowerProps {
+ isOpen: boolean;
+ isLoading: boolean;
+ toggle: () => void;
+ deleteAction: () => void;
+}
+
+interface DeleteModalProps extends ModalUpperProps, ModalLowerProps {
+ buttonRef: MutableRefObject | undefined;
+}
+
+const ModalUpper = ({ title, text }: ModalUpperProps) => (
+
+);
+
+const ModalLower = ({ isOpen, isLoading, toggle, deleteAction }: ModalLowerProps) => (
+
+ {isLoading ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+);
+
+export const DeleteModal: FC = (props: DeleteModalProps) => (
+
}
+ name={`actions.${idx}.external_download_client_id`}
+ label="Override download client id for arr"
+ tooltip={Override Download client Id from the one set in Clients. Useful if you have multiple clients inside the arr.
}
/>
);
@@ -648,7 +648,7 @@ function FilterActionsItem({ action, clients, idx, initialEdit, remove }: Filter
@@ -683,6 +683,7 @@ function FilterActionsItem({ action, clients, idx, initialEdit, remove }: Filter
>
removeAction(action.id)}
@@ -706,13 +707,13 @@ function FilterActionsItem({ action, clients, idx, initialEdit, remove }: Filter
-
+
} />
+
Number of seconds to wait before running actions.
https://autobrr.com/filters#rules } />
Filters are checked in order of priority. Higher number = higher priority.
https://autobrr.com/filters#rules } />
Number of max downloads as specified by the respective unit.
https://autobrr.com/filters#rules} />
-