Skip to content

Commit

Permalink
feat: add update indicator
Browse files Browse the repository at this point in the history
  • Loading branch information
manuel-rw committed Dec 13, 2024
1 parent 732f4d4 commit 8a088db
Show file tree
Hide file tree
Showing 11 changed files with 433 additions and 7 deletions.
16 changes: 12 additions & 4 deletions apps/nextjs/src/components/layout/header/user.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { UnstyledButton } from "@mantine/core";
import { Indicator, 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";

export const UserButton = () => {
export const UserButton = async () => {
const data = await api.updateChecker.getAvailableUpdates();
const session = await auth();
const isAdmin = session?.user.permissions.includes("admin");
return (
<UserAvatarMenu>
<UserAvatarMenu availableUpdates={isAdmin ? data : undefined}>
<UnstyledButton>
<CurrentUserAvatar size="md" />
<Indicator disabled={!data || data.length === 0 || !isAdmin} size={15} processing withBorder>
<CurrentUserAvatar size="md" />
</Indicator>
</UnstyledButton>
</UserAvatarMenu>
);
Expand Down
21 changes: 20 additions & 1 deletion apps/nextjs/src/components/user-avatar-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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,
Expand All @@ -23,14 +24,17 @@ import { useScopedI18n } from "@homarr/translation/client";

import "flag-icons/css/flag-icons.min.css";

import type { RouterOutputs } from "@homarr/api";

import { useAuthContext } from "~/app/[locale]/_client-providers/session";
import { CurrentLanguageCombobox } from "./language/current-language-combobox";

interface UserAvatarMenuProps {
children: ReactNode;
availableUpdates: RouterOutputs["updateChecker"]["getAvailableUpdates"];
}

export const UserAvatarMenu = ({ children }: UserAvatarMenuProps) => {
export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuProps) => {
const t = useScopedI18n("common.userAvatar.menu");
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
useHotkeys([["mod+J", toggleColorScheme]]);
Expand Down Expand Up @@ -64,6 +68,21 @@ export const UserAvatarMenu = ({ children }: UserAvatarMenuProps) => {
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board
<Menu width={300} withArrow withinPortal keepMounted>
<Menu.Dropdown>
{availableUpdates && availableUpdates.length > 0 && availableUpdates[0] && (
<>
<Menu.Item
component={"a"}
href={availableUpdates[0].url}
target="_blank"
leftSection={<IconBellRinging size="1rem" />}
>
<Text fw="bold" size="sm">
{t("updateAvailable", { countUpdates: availableUpdates.length, tag: availableUpdates[0].tag_name })}
</Text>
</Menu.Item>
<Menu.Divider />
</>
)}
<Menu.Item onClick={toggleColorScheme} leftSection={<ColorSchemeIcon size="1rem" />}>
{colorSchemeText}
</Menu.Item>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "homarr",
"private": true,
"version": "1.0.0",
"scripts": {
"build": "cross-env CI=true turbo build",
"clean": "git clean -xdf node_modules",
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { logRouter } from "./router/log";
import { mediaRouter } from "./router/medias/media-router";
import { searchEngineRouter } from "./router/search-engine/search-engine-router";
import { serverSettingsRouter } from "./router/serverSettings";
import { updateCheckerRouter } from "./router/update-checker";
import { userRouter } from "./router/user";
import { widgetRouter } from "./router/widgets";
import { createTRPCRouter } from "./trpc";
Expand All @@ -35,6 +36,7 @@ export const appRouter = createTRPCRouter({
cronJobs: cronJobsRouter,
apiKeys: apiKeysRouter,
media: mediaRouter,
updateChecker: updateCheckerRouter,
});

// export type definition of API
Expand Down
17 changes: 17 additions & 0 deletions packages/api/src/router/update-checker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createSubPubChannel } from "@homarr/redis";

import { createTRPCRouter, protectedProcedure } from "../trpc";

export const updateCheckerRouter = createTRPCRouter({
getAvailableUpdates: protectedProcedure.query(async () => {
const channel = createSubPubChannel<{
availableUpdates: { name: string | null; contentHtml?: string; url: string; tag_name: string }[];
}>("homarr:update", {
persist: true,
});

const data = await channel.getLastDataAsync();

return data?.availableUpdates;
}),
});
4 changes: 3 additions & 1 deletion packages/cron-jobs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
"@homarr/request-handler": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0"
"@homarr/validation": "workspace:^0.1.0",
"octokit": "^4.0.2",
"semver-parser": "^4.1.7"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/cron-jobs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { pingJob } from "./jobs/ping";
import type { RssFeed } from "./jobs/rss-feeds";
import { rssFeedsJob } from "./jobs/rss-feeds";
import { sessionCleanupJob } from "./jobs/session-cleanup";
import { updateCheckerJob } from "./jobs/update-checker";
import { createCronJobGroup } from "./lib";

export const jobGroup = createCronJobGroup({
Expand All @@ -29,6 +30,7 @@ export const jobGroup = createCronJobGroup({
indexerManager: indexerManagerJob,
healthMonitoring: healthMonitoringJob,
sessionCleanup: sessionCleanupJob,
updateChecker: updateCheckerJob,
});

export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
Expand Down
58 changes: 58 additions & 0 deletions packages/cron-jobs/src/jobs/update-checker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Octokit } from "octokit";
import { compareSemVer, isValidSemVer } from "semver-parser";

import { EVERY_HOUR } from "@homarr/cron-jobs-core/expressions";
import { logger } from "@homarr/log";
import { createSubPubChannel } from "@homarr/redis";

import packageJson from "../../../../package.json";
import { createCronJob } from "../lib";

export const updateCheckerJob = createCronJob("updateChecker", EVERY_HOUR, {
runOnStart: true,
}).withCallback(async () => {
const octokit = new Octokit();
const releases = await octokit.rest.repos.listReleases({
owner: "homarr-labs",
repo: "homarr",
});

const currentVersion = (packageJson as { version: string }).version;
const availableReleases = [];

for (const release of releases.data) {
if (!isValidSemVer(release.tag_name)) {
logger.warn(`Unable to parse semantic tag '${release.tag_name}'. Update check might not work.`);
continue;
}

availableReleases.push(release);
}

const availableNewerReleases = availableReleases
.filter((release) => compareSemVer(release.tag_name, currentVersion) > 0)
.sort((releaseA, releaseB) => compareSemVer(releaseB.tag_name, releaseA.tag_name));
if (availableReleases.length > 0) {
logger.info(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
`Update checker found a new available version: ${availableReleases[0]!.tag_name}. Current version is ${currentVersion}`,
);
} else {
logger.debug(`Update checker did not find any available updates. Current version is ${currentVersion}`);
}

const channel = createSubPubChannel<{
availableUpdates: { name: string | null; contentHtml?: string; url: string; tag_name: string }[];
}>("homarr:update", {
persist: true,
});

await channel.publishAsync({
availableUpdates: availableNewerReleases.map((release) => ({
name: release.name,
contentHtml: release.body_html,
url: release.html_url,
tag_name: release.tag_name,
})),
});
});
1 change: 1 addition & 0 deletions packages/redis/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export {
createIntegrationOptionsChannel,
createChannelWithLatestAndEvents,
handshakeAsync,
createSubPubChannel,
} from "./lib/channel";

export const exampleChannel = createSubPubChannel<{ message: string }>("example");
Expand Down
6 changes: 5 additions & 1 deletion packages/translation/src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,8 @@
"logout": "Logout",
"login": "Login",
"homeBoard": "Your home board",
"loggedOut": "Logged out"
"loggedOut": "Logged out",
"updateAvailable": "{countUpdates} updates available: {tag}"
}
},
"dangerZone": "Danger zone",
Expand Down Expand Up @@ -2153,6 +2154,9 @@
},
"sessionCleanup": {
"label": "Session Cleanup"
},
"updateChecker": {
"label": "Update checker"
}
}
},
Expand Down
Loading

0 comments on commit 8a088db

Please sign in to comment.