From 29ff879dfd1036d44b7c4735a0b269a83173b4d1 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 3 Apr 2025 23:22:50 +0200 Subject: [PATCH] fix: update check blocks page loading (#2782) --- .../src/components/layout/header/update.tsx | 93 +++++++++++++++++++ .../src/components/layout/header/user.tsx | 16 ++-- .../src/components/user-avatar-menu.tsx | 25 +---- .../request-handler/src/update-checker.ts | 7 +- 4 files changed, 113 insertions(+), 28 deletions(-) create mode 100644 apps/nextjs/src/components/layout/header/update.tsx diff --git a/apps/nextjs/src/components/layout/header/update.tsx b/apps/nextjs/src/components/layout/header/update.tsx new file mode 100644 index 000000000..1989ecd22 --- /dev/null +++ b/apps/nextjs/src/components/layout/header/update.tsx @@ -0,0 +1,93 @@ +"use client"; + +import type { PropsWithChildren } from "react"; +import { Suspense, use } from "react"; +import { Indicator, Menu, Text } from "@mantine/core"; +import { IconBellRinging } from "@tabler/icons-react"; + +import type { RouterOutputs } from "@homarr/api"; +import { useScopedI18n } from "@homarr/translation/client"; + +interface UpdateIndicatorProps extends PropsWithChildren { + availableUpdatesPromise: Promise | undefined; + disabled: boolean; +} + +export const UpdateIndicator = ({ children, availableUpdatesPromise, disabled }: UpdateIndicatorProps) => { + if (disabled || availableUpdatesPromise === undefined) { + return children; + } + + return ( + + + {children} + + + ); +}; + +interface InnerUpdateIndicatorProps extends PropsWithChildren { + availableUpdatesPromise: Promise; + disabled: boolean; +} + +const InnerUpdateIndicator = ({ children, disabled, availableUpdatesPromise }: InnerUpdateIndicatorProps) => { + const availableUpdates = use(availableUpdatesPromise); + + return ( + + {children} + + ); +}; + +interface AvailableUpdatesMenuItemProps { + availableUpdatesPromise: Promise | undefined; +} + +export const AvailableUpdatesMenuItem = ({ availableUpdatesPromise }: AvailableUpdatesMenuItemProps) => { + if (availableUpdatesPromise === undefined) { + return null; + } + + return ( + + + + ); +}; + +interface InnerAvailableUpdatesMenuItemProps { + availableUpdatesPromise: Promise; +} + +const InnerAvailableUpdatesMenuItem = ({ availableUpdatesPromise }: InnerAvailableUpdatesMenuItemProps) => { + const t = useScopedI18n("common.userAvatar.menu"); + const availableUpdates = use(availableUpdatesPromise); + if (availableUpdates === undefined || availableUpdates.length === 0) { + return null; + } + + const latestUpdate = availableUpdates.at(0); + if (!latestUpdate) return null; + + return ( + <> + }> + + {t("updateAvailable", { + countUpdates: String(availableUpdates.length), + tag: latestUpdate.tagName, + })} + + + + + ); +}; diff --git a/apps/nextjs/src/components/layout/header/user.tsx b/apps/nextjs/src/components/layout/header/user.tsx index f4aabb767..6ec2cf306 100644 --- a/apps/nextjs/src/components/layout/header/user.tsx +++ b/apps/nextjs/src/components/layout/header/user.tsx @@ -1,21 +1,25 @@ -import { Indicator, UnstyledButton } from "@mantine/core"; +import { Suspense } from "react"; +import { UnstyledButton } from "@mantine/core"; import { api } from "@homarr/api/server"; import { auth } from "@homarr/auth/next"; import { CurrentUserAvatar } from "~/components/user-avatar"; import { UserAvatarMenu } from "~/components/user-avatar-menu"; +import { UpdateIndicator } from "./update"; export const UserButton = async () => { const session = await auth(); const isAdmin = session?.user.permissions.includes("admin"); - const data = isAdmin ? await api.updateChecker.getAvailableUpdates() : undefined; + const availableUpdatesPromise = isAdmin ? api.updateChecker.getAvailableUpdates() : undefined; return ( - + - - - + }> + + + + ); diff --git a/apps/nextjs/src/components/user-avatar-menu.tsx b/apps/nextjs/src/components/user-avatar-menu.tsx index cc676ee09..b0f9c6a10 100644 --- a/apps/nextjs/src/components/user-avatar-menu.tsx +++ b/apps/nextjs/src/components/user-avatar-menu.tsx @@ -7,7 +7,6 @@ import { useRouter } from "next/navigation"; import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core"; import { useHotkeys, useTimeout } from "@mantine/hooks"; import { - IconBellRinging, IconCheck, IconHome, IconLogin, @@ -25,13 +24,14 @@ import { useScopedI18n } from "@homarr/translation/client"; import { useAuthContext } from "~/app/[locale]/_client-providers/session"; import { CurrentLanguageCombobox } from "./language/current-language-combobox"; +import { AvailableUpdatesMenuItem } from "./layout/header/update"; interface UserAvatarMenuProps { children: ReactNode; - availableUpdates?: RouterOutputs["updateChecker"]["getAvailableUpdates"]; + availableUpdatesPromise?: Promise; } -export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuProps) => { +export const UserAvatarMenu = ({ children, availableUpdatesPromise }: UserAvatarMenuProps) => { const t = useScopedI18n("common.userAvatar.menu"); const { colorScheme, toggleColorScheme } = useMantineColorScheme(); useHotkeys([["mod+J", toggleColorScheme]]); @@ -65,24 +65,7 @@ export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuPro // We use keepMounted so we can add event listeners to prevent navigating away without saving the board - {availableUpdates && availableUpdates.length > 0 && availableUpdates[0] && ( - <> - } - > - - {t("updateAvailable", { - countUpdates: String(availableUpdates.length), - tag: availableUpdates[0].tagName, - })} - - - - - )} + }> {colorSchemeText} diff --git a/packages/request-handler/src/update-checker.ts b/packages/request-handler/src/update-checker.ts index 00a3e441d..bd9a52361 100644 --- a/packages/request-handler/src/update-checker.ts +++ b/packages/request-handler/src/update-checker.ts @@ -2,6 +2,7 @@ import dayjs from "dayjs"; import { Octokit } from "octokit"; import { compareSemVer, isValidSemVer } from "semver-parser"; +import { fetchWithTimeout } from "@homarr/common"; import { logger } from "@homarr/log"; import { createChannelWithLatestAndEvents } from "@homarr/redis"; import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler"; @@ -12,7 +13,11 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({ queryKey: "homarr-update-checker", cacheDuration: dayjs.duration(1, "hour"), async requestAsync(_) { - const octokit = new Octokit(); + const octokit = new Octokit({ + request: { + fetch: fetchWithTimeout, + }, + }); const releases = await octokit.rest.repos.listReleases({ owner: "homarr-labs", repo: "homarr",