mirror of
https://github.com/idanoo/autobrr
synced 2025-07-25 17:59:14 +00:00
feat(web): move from react-router to @tanstack/router (#1338)
* fix(auth): invalid cookie handling and wrongful basic auth invalidation * fix(auth): fix test to reflect new HTTP status code * fix(auth/web): do not throw on error * fix(http): replace http codes in middleware to prevent basic auth invalidation fix typo in comment * fix test * fix(web): api client handle 403 * refactor(http): auth_test use testify.assert * refactor(http): set session opts after valid login * refactor(http): send more client headers * fix(http): test * refactor(web): move router to tanstack/router * refactor(web): use route loaders and suspense * refactor(web): useSuspense for settings * refactor(web): invalidate cookie in middleware * fix: loclfile * fix: load filter/id * fix(web): login, onboard, types, imports * fix(web): filter load * fix(web): build errors * fix(web): ts-expect-error * fix(tests): filter_test.go * fix(filters): tests * refactor: remove duplicate spinner components refactor: ReleaseTable.tsx loading animation refactor: remove dedicated `pendingComponent` for `settingsRoute` * fix: refactor missed SectionLoader to RingResizeSpinner * fix: substitute divides with borders to account for unloaded elements * fix(api): action status URL param * revert: action status URL param add comment * fix(routing): notfound handling and split files * fix(filters): notfound get params * fix(queries): colon * fix(queries): comments ts-ignore * fix(queries): extract queryKeys * fix(queries): remove err * fix(routes): move zob schema inline * fix(auth): middleware and redirect to login * fix(auth): failing test * fix(logs): invalidate correct key * fix(logs): invalidate correct key * fix(logs): invalidate correct key * fix: JSX element stealing focus from searchbar * reimplement empty release table state text * fix(context): use deep-copy * fix(releases): empty state and filter input warnings * fix(releases): empty states * fix(auth): onboarding * fix(cache): invalidate queries --------- Co-authored-by: ze0s <43699394+zze0s@users.noreply.github.com>
This commit is contained in:
parent
cc9656cd41
commit
1a23b69bcf
64 changed files with 2543 additions and 2091 deletions
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 - 2024, 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 (
|
||||
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
||||
<RingResizeSpinner className={classNames(SIZE[$size], "mx-auto my-36 text-blue-500")} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<RingResizeSpinner className={classNames(SIZE[$size], "text-blue-500")} />
|
||||
);
|
||||
}
|
||||
};
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { ExternalLink } from "@components/ExternalLink";
|
||||
|
||||
import Logo from "@app/logo.svg?react";
|
||||
|
@ -12,8 +12,11 @@ export const NotFound = () => {
|
|||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center ">
|
||||
<div className="flex justify-center">
|
||||
<Logo className="h-24 sm:h-48" />
|
||||
<Logo className="h-24 sm:h-48"/>
|
||||
</div>
|
||||
<h2 className="text-2xl text-center font-bold text-gray-900 dark:text-gray-200 my-8 px-2">
|
||||
404 Page not found
|
||||
</h2>
|
||||
<h1 className="text-3xl text-center font-bold text-gray-900 dark:text-gray-200 my-8 px-2">
|
||||
Oops, looks like there was a little too much brr!
|
||||
</h1>
|
||||
|
|
|
@ -9,7 +9,6 @@ import { formatDistanceToNowStrict } from "date-fns";
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { CellProps } from "react-table";
|
||||
import { ArrowPathIcon, CheckIcon } from "@heroicons/react/24/solid";
|
||||
import { ExternalLink } from "../ExternalLink";
|
||||
import {
|
||||
ClockIcon,
|
||||
XMarkIcon,
|
||||
|
@ -19,8 +18,9 @@ import {
|
|||
} from "@heroicons/react/24/outline";
|
||||
|
||||
import { APIClient } from "@api/APIClient";
|
||||
import {classNames, humanFileSize, simplifyDate} from "@utils";
|
||||
import { filterKeys } from "@screens/filters/List";
|
||||
import { FilterKeys } from "@api/query_keys";
|
||||
import { classNames, humanFileSize, simplifyDate } from "@utils";
|
||||
import { ExternalLink } from "../ExternalLink";
|
||||
import Toast from "@components/notifications/Toast";
|
||||
import { RingResizeSpinner } from "@components/Icons";
|
||||
import { Tooltip } from "@components/tooltips/Tooltip";
|
||||
|
@ -164,7 +164,7 @@ const RetryActionButton = ({ status }: RetryActionButtonProps) => {
|
|||
mutationFn: (vars: RetryAction) => APIClient.release.replayAction(vars.releaseId, vars.actionId),
|
||||
onSuccess: () => {
|
||||
// Invalidate filters just in case, most likely not necessary but can't hurt.
|
||||
queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: FilterKeys.lists() });
|
||||
|
||||
toast.custom((t) => (
|
||||
<Toast type="success" body={`${status?.action} replayed`} t={t} />
|
||||
|
|
|
@ -23,3 +23,11 @@ export const DEBUG: FC<DebugProps> = ({ values }) => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function LogDebug(...data: any[]): void {
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(...data)
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
|
||||
import toast from "react-hot-toast";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
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";
|
||||
|
@ -17,37 +17,35 @@ import { RightNav } from "./RightNav";
|
|||
import { MobileNav } from "./MobileNav";
|
||||
import { ExternalLink } from "@components/ExternalLink";
|
||||
|
||||
export const Header = () => {
|
||||
const { isError:isConfigError, error: configError, data: config } = useQuery({
|
||||
queryKey: ["config"],
|
||||
queryFn: () => APIClient.config.get(),
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
import { AuthIndexRoute } from "@app/routes";
|
||||
import { ConfigQueryOptions, UpdatesQueryOptions } from "@api/queries";
|
||||
|
||||
export const Header = () => {
|
||||
const router = useRouter()
|
||||
const { auth } = AuthIndexRoute.useRouteContext()
|
||||
|
||||
const { isError:isConfigError, error: configError, data: config } = useQuery(ConfigQueryOptions(true));
|
||||
if (isConfigError) {
|
||||
console.log(configError);
|
||||
}
|
||||
|
||||
const { isError, error, data } = useQuery({
|
||||
queryKey: ["updates"],
|
||||
queryFn: () => APIClient.updates.getLatestRelease(),
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: config?.check_for_updates === true
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
console.log(error);
|
||||
const { isError: isUpdateError, error, data } = useQuery(UpdatesQueryOptions(config?.check_for_updates === true));
|
||||
if (isUpdateError) {
|
||||
console.log("update error", error);
|
||||
}
|
||||
|
||||
const logoutMutation = useMutation({
|
||||
mutationFn: APIClient.auth.logout,
|
||||
onSuccess: () => {
|
||||
AuthContext.reset();
|
||||
toast.custom((t) => (
|
||||
<Toast type="success" body="You have been logged out. Goodbye!" t={t} />
|
||||
));
|
||||
auth.logout()
|
||||
|
||||
router.history.push("/")
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error("logout error", err)
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -62,7 +60,7 @@ export const Header = () => {
|
|||
<div className="border-b border-gray-300 dark:border-gray-775">
|
||||
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
|
||||
<LeftNav />
|
||||
<RightNav logoutMutation={logoutMutation.mutate} />
|
||||
<RightNav logoutMutation={logoutMutation.mutate} auth={auth} />
|
||||
<div className="-mr-2 flex sm:hidden">
|
||||
{/* Mobile menu button */}
|
||||
<Disclosure.Button className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
|
||||
|
@ -94,7 +92,7 @@ export const Header = () => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<MobileNav logoutMutation={logoutMutation.mutate} />
|
||||
<MobileNav logoutMutation={logoutMutation.mutate} auth={auth} />
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import { Link, NavLink } from "react-router-dom";
|
||||
// import { Link, NavLink } from "react-router-dom";
|
||||
|
||||
import { Link } from '@tanstack/react-router'
|
||||
|
||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
import { classNames } from "@utils";
|
||||
|
@ -23,22 +26,27 @@ export const LeftNav = () => (
|
|||
<div className="sm:ml-3 hidden sm:block">
|
||||
<div className="flex items-baseline space-x-4">
|
||||
{NAV_ROUTES.map((item, itemIdx) => (
|
||||
<NavLink
|
||||
<Link
|
||||
key={item.name + itemIdx}
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
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 === "/"}
|
||||
params={{}}
|
||||
>
|
||||
{item.name}
|
||||
</NavLink>
|
||||
{({ isActive }) => {
|
||||
return (
|
||||
<>
|
||||
<span className={
|
||||
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"
|
||||
)
|
||||
}>{item.name}</span>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Link>
|
||||
))}
|
||||
<ExternalLink
|
||||
href="https://autobrr.com"
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import { NavLink } from "react-router-dom";
|
||||
import {Link} from "@tanstack/react-router";
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
|
||||
import { classNames } from "@utils";
|
||||
|
@ -15,21 +15,28 @@ export const MobileNav = (props: RightNavProps) => (
|
|||
<Disclosure.Panel className="border-b border-gray-300 dark:border-gray-700 md:hidden">
|
||||
<div className="px-2 py-3 space-y-1 sm:px-3">
|
||||
{NAV_ROUTES.map((item) => (
|
||||
<NavLink
|
||||
<Link
|
||||
key={item.path}
|
||||
activeOptions={{ exact: item.exact }}
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
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
|
||||
search={{}}
|
||||
params={{}}
|
||||
>
|
||||
{({ isActive }) => {
|
||||
return (
|
||||
<span className={
|
||||
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}
|
||||
</NavLink>
|
||||
)
|
||||
}>
|
||||
{item.name}
|
||||
</span>
|
||||
)
|
||||
}}
|
||||
</Link>
|
||||
))}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
|
|
|
@ -4,18 +4,16 @@
|
|||
*/
|
||||
|
||||
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";
|
||||
import { Cog6ToothIcon, ArrowLeftOnRectangleIcon } from "@heroicons/react/24/outline";
|
||||
import {Link} from "@tanstack/react-router";
|
||||
|
||||
export const RightNav = (props: RightNavProps) => {
|
||||
const authContext = AuthContext.useValue();
|
||||
return (
|
||||
<div className="hidden sm:block">
|
||||
<div className="ml-4 flex items-center sm:ml-6">
|
||||
|
@ -34,7 +32,7 @@ export const RightNav = (props: RightNavProps) => {
|
|||
<span className="sr-only">
|
||||
Open user menu for{" "}
|
||||
</span>
|
||||
{authContext.username}
|
||||
{props.auth.username}
|
||||
</span>
|
||||
<UserIcon
|
||||
className="inline ml-1 h-5 w-5"
|
||||
|
|
|
@ -3,17 +3,21 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import { AuthCtx } from "@utils/Context";
|
||||
|
||||
interface NavItem {
|
||||
name: string;
|
||||
path: string;
|
||||
exact?: boolean;
|
||||
}
|
||||
|
||||
export interface RightNavProps {
|
||||
logoutMutation: () => void;
|
||||
auth: AuthCtx
|
||||
}
|
||||
|
||||
export const NAV_ROUTES: Array<NavItem> = [
|
||||
{ name: "Dashboard", path: "/" },
|
||||
{ name: "Dashboard", path: "/", exact: true },
|
||||
{ name: "Filters", path: "/filters" },
|
||||
{ name: "Releases", path: "/releases" },
|
||||
{ name: "Settings", path: "/settings" },
|
||||
|
|
|
@ -8,7 +8,7 @@ import { FC, Fragment, MutableRefObject, useState } from "react";
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
import { SectionLoader } from "@components/SectionLoader";
|
||||
import { RingResizeSpinner } from "@components/Icons";
|
||||
|
||||
interface ModalUpperProps {
|
||||
title: string;
|
||||
|
@ -58,7 +58,7 @@ const ModalUpper = ({ title, text }: ModalUpperProps) => (
|
|||
const ModalLower = ({ isOpen, isLoading, toggle, deleteAction }: ModalLowerProps) => (
|
||||
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
{isLoading ? (
|
||||
<SectionLoader $size="small" />
|
||||
<RingResizeSpinner className="text-blue-500 size-6" />
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
|
@ -221,7 +221,7 @@ export const ForceRunModal: FC<ForceRunModalProps> = (props: ForceRunModalProps)
|
|||
|
||||
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
{props.isLoading ? (
|
||||
<SectionLoader $size="small" />
|
||||
<RingResizeSpinner className="text-blue-500 size-6" />
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue