fix: update check blocks page loading (#2782)
This commit is contained in:
93
apps/nextjs/src/components/layout/header/update.tsx
Normal file
93
apps/nextjs/src/components/layout/header/update.tsx
Normal file
@@ -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<RouterOutputs["updateChecker"]["getAvailableUpdates"]> | undefined;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateIndicator = ({ children, availableUpdatesPromise, disabled }: UpdateIndicatorProps) => {
|
||||||
|
if (disabled || availableUpdatesPromise === undefined) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={children}>
|
||||||
|
<InnerUpdateIndicator availableUpdatesPromise={availableUpdatesPromise} disabled={disabled}>
|
||||||
|
{children}
|
||||||
|
</InnerUpdateIndicator>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface InnerUpdateIndicatorProps extends PropsWithChildren {
|
||||||
|
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InnerUpdateIndicator = ({ children, disabled, availableUpdatesPromise }: InnerUpdateIndicatorProps) => {
|
||||||
|
const availableUpdates = use(availableUpdatesPromise);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Indicator
|
||||||
|
disabled={!availableUpdates || availableUpdates.length === 0 || disabled}
|
||||||
|
size={15}
|
||||||
|
processing
|
||||||
|
withBorder
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Indicator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AvailableUpdatesMenuItemProps {
|
||||||
|
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AvailableUpdatesMenuItem = ({ availableUpdatesPromise }: AvailableUpdatesMenuItemProps) => {
|
||||||
|
if (availableUpdatesPromise === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<InnerAvailableUpdatesMenuItem availableUpdatesPromise={availableUpdatesPromise} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface InnerAvailableUpdatesMenuItemProps {
|
||||||
|
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Menu.Item component={"a"} href={latestUpdate.url} target="_blank" leftSection={<IconBellRinging size="1rem" />}>
|
||||||
|
<Text fw="bold" size="sm">
|
||||||
|
{t("updateAvailable", {
|
||||||
|
countUpdates: String(availableUpdates.length),
|
||||||
|
tag: latestUpdate.tagName,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Divider />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { api } from "@homarr/api/server";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
|
|
||||||
import { CurrentUserAvatar } from "~/components/user-avatar";
|
import { CurrentUserAvatar } from "~/components/user-avatar";
|
||||||
import { UserAvatarMenu } from "~/components/user-avatar-menu";
|
import { UserAvatarMenu } from "~/components/user-avatar-menu";
|
||||||
|
import { UpdateIndicator } from "./update";
|
||||||
|
|
||||||
export const UserButton = async () => {
|
export const UserButton = async () => {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
const isAdmin = session?.user.permissions.includes("admin");
|
const isAdmin = session?.user.permissions.includes("admin");
|
||||||
const data = isAdmin ? await api.updateChecker.getAvailableUpdates() : undefined;
|
const availableUpdatesPromise = isAdmin ? api.updateChecker.getAvailableUpdates() : undefined;
|
||||||
return (
|
return (
|
||||||
<UserAvatarMenu availableUpdates={data}>
|
<UserAvatarMenu availableUpdatesPromise={availableUpdatesPromise}>
|
||||||
<UnstyledButton>
|
<UnstyledButton>
|
||||||
<Indicator disabled={data?.length === 0 || !isAdmin} size={15} processing withBorder>
|
<Suspense fallback={<CurrentUserAvatar size="md" />}>
|
||||||
<CurrentUserAvatar size="md" />
|
<UpdateIndicator availableUpdatesPromise={availableUpdatesPromise} disabled={!isAdmin}>
|
||||||
</Indicator>
|
<CurrentUserAvatar size="md" />
|
||||||
|
</UpdateIndicator>
|
||||||
|
</Suspense>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</UserAvatarMenu>
|
</UserAvatarMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useRouter } from "next/navigation";
|
|||||||
import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core";
|
import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core";
|
||||||
import { useHotkeys, useTimeout } from "@mantine/hooks";
|
import { useHotkeys, useTimeout } from "@mantine/hooks";
|
||||||
import {
|
import {
|
||||||
IconBellRinging,
|
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconHome,
|
IconHome,
|
||||||
IconLogin,
|
IconLogin,
|
||||||
@@ -25,13 +24,14 @@ import { useScopedI18n } from "@homarr/translation/client";
|
|||||||
|
|
||||||
import { useAuthContext } from "~/app/[locale]/_client-providers/session";
|
import { useAuthContext } from "~/app/[locale]/_client-providers/session";
|
||||||
import { CurrentLanguageCombobox } from "./language/current-language-combobox";
|
import { CurrentLanguageCombobox } from "./language/current-language-combobox";
|
||||||
|
import { AvailableUpdatesMenuItem } from "./layout/header/update";
|
||||||
|
|
||||||
interface UserAvatarMenuProps {
|
interface UserAvatarMenuProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
availableUpdates?: RouterOutputs["updateChecker"]["getAvailableUpdates"];
|
availableUpdatesPromise?: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuProps) => {
|
export const UserAvatarMenu = ({ children, availableUpdatesPromise }: UserAvatarMenuProps) => {
|
||||||
const t = useScopedI18n("common.userAvatar.menu");
|
const t = useScopedI18n("common.userAvatar.menu");
|
||||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||||
useHotkeys([["mod+J", toggleColorScheme]]);
|
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
|
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board
|
||||||
<Menu width={300} withArrow withinPortal keepMounted>
|
<Menu width={300} withArrow withinPortal keepMounted>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
{availableUpdates && availableUpdates.length > 0 && availableUpdates[0] && (
|
<AvailableUpdatesMenuItem availableUpdatesPromise={availableUpdatesPromise} />
|
||||||
<>
|
|
||||||
<Menu.Item
|
|
||||||
component={"a"}
|
|
||||||
href={availableUpdates[0].url}
|
|
||||||
target="_blank"
|
|
||||||
leftSection={<IconBellRinging size="1rem" />}
|
|
||||||
>
|
|
||||||
<Text fw="bold" size="sm">
|
|
||||||
{t("updateAvailable", {
|
|
||||||
countUpdates: String(availableUpdates.length),
|
|
||||||
tag: availableUpdates[0].tagName,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Divider />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Menu.Item onClick={toggleColorScheme} leftSection={<ColorSchemeIcon size="1rem" />}>
|
<Menu.Item onClick={toggleColorScheme} leftSection={<ColorSchemeIcon size="1rem" />}>
|
||||||
{colorSchemeText}
|
{colorSchemeText}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import dayjs from "dayjs";
|
|||||||
import { Octokit } from "octokit";
|
import { Octokit } from "octokit";
|
||||||
import { compareSemVer, isValidSemVer } from "semver-parser";
|
import { compareSemVer, isValidSemVer } from "semver-parser";
|
||||||
|
|
||||||
|
import { fetchWithTimeout } from "@homarr/common";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import { createChannelWithLatestAndEvents } from "@homarr/redis";
|
import { createChannelWithLatestAndEvents } from "@homarr/redis";
|
||||||
import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler";
|
import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler";
|
||||||
@@ -12,7 +13,11 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({
|
|||||||
queryKey: "homarr-update-checker",
|
queryKey: "homarr-update-checker",
|
||||||
cacheDuration: dayjs.duration(1, "hour"),
|
cacheDuration: dayjs.duration(1, "hour"),
|
||||||
async requestAsync(_) {
|
async requestAsync(_) {
|
||||||
const octokit = new Octokit();
|
const octokit = new Octokit({
|
||||||
|
request: {
|
||||||
|
fetch: fetchWithTimeout,
|
||||||
|
},
|
||||||
|
});
|
||||||
const releases = await octokit.rest.repos.listReleases({
|
const releases = await octokit.rest.repos.listReleases({
|
||||||
owner: "homarr-labs",
|
owner: "homarr-labs",
|
||||||
repo: "homarr",
|
repo: "homarr",
|
||||||
|
|||||||
Reference in New Issue
Block a user