This commit is contained in:
Fred KISSIE
2025-11-05 08:38:23 +01:00
parent a247ef7564
commit b9ce316574
3 changed files with 98 additions and 46 deletions

View File

@@ -1279,6 +1279,8 @@
"settingsErrorUpdateDescription": "An error occurred while updating settings", "settingsErrorUpdateDescription": "An error occurred while updating settings",
"sidebarCollapse": "Collapse", "sidebarCollapse": "Collapse",
"sidebarExpand": "Expand", "sidebarExpand": "Expand",
"productUpdateMoreInfo": "{noOfUpdates} more updates",
"productUpdateInfo": "{noOfUpdates} updates",
"pangolinUpdateAvailable": "New version available", "pangolinUpdateAvailable": "New version available",
"pangolinUpdateAvailableInfo": "Version {version} is ready to install", "pangolinUpdateAvailableInfo": "Version {version} is ready to install",
"pangolinUpdateAvailableReleaseNotes": "View release notes", "pangolinUpdateAvailableReleaseNotes": "View release notes",

View File

@@ -3,29 +3,59 @@
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useLocalStorage } from "@app/hooks/useLocalStorage"; import { useLocalStorage } from "@app/hooks/useLocalStorage";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { versionsQueries } from "@app/lib/queries"; import { productUpdatesQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query"; import { useQueries } from "@tanstack/react-query";
import { ArrowRight, BellIcon, XIcon } from "lucide-react"; import { ArrowRight, BellIcon, XIcon } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
interface ProductUpdatesProps {} export default function ProductUpdates() {
const data = useQueries({
queries: [
productUpdatesQueries.list,
productUpdatesQueries.latestVersion
],
combine(result) {
return {
updates: result[0].data?.data ?? [],
latestVersion: result[1].data
};
}
});
const t = useTranslations();
export default function ProductUpdates({}: ProductUpdatesProps) {
return ( return (
<div className="flex flex-col gap-1 relative z-1 overflow-clip"> <div className="flex flex-col gap-1 overflow-clip">
{/* <small className="text-xs text-muted-foreground flex items-center gap-1"> {data.updates.length > 0 && (
<BellIcon className="flex-none size-3" /> <small className="text-xs text-muted-foreground flex items-center gap-1">
<span>3 more updates</span> <BellIcon className="flex-none size-3" />
</small> */} <span>
<NewVersionAvailable /> {t("productUpdateMoreInfo", {
noOfUpdates: data.updates.length
})}
</span>
</small>
)}
<NewVersionAvailable version={data.latestVersion} />
</div> </div>
); );
} }
function NewVersionAvailable() { type NewVersionAvailableProps = {
version:
| Awaited<
ReturnType<
NonNullable<
typeof productUpdatesQueries.latestVersion.queryFn
>
>
>
| undefined;
};
function NewVersionAvailable({ version }: NewVersionAvailableProps) {
const { env } = useEnvContext(); const { env } = useEnvContext();
const t = useTranslations(); const t = useTranslations();
const { data: version } = useQuery(versionsQueries.latestVersion());
const [ignoredVersionUpdate, setIgnoredVersionUpdate] = useLocalStorage< const [ignoredVersionUpdate, setIgnoredVersionUpdate] = useLocalStorage<
string | null string | null
@@ -33,8 +63,8 @@ function NewVersionAvailable() {
const showNewVersionPopup = const showNewVersionPopup =
version?.data && version?.data &&
ignoredVersionUpdate !== version.data.pangolin.latestVersion && ignoredVersionUpdate !== version.data?.pangolin.latestVersion &&
env.app.version !== version.data.pangolin.latestVersion; env.app.version !== version.data?.pangolin.latestVersion;
if (!showNewVersionPopup) return null; if (!showNewVersionPopup) return null;

View File

@@ -1,38 +1,58 @@
import { import { keepPreviousData, queryOptions } from "@tanstack/react-query";
type InfiniteData,
type QueryClient,
keepPreviousData,
queryOptions,
type skipToken
} from "@tanstack/react-query";
import { durationToMs } from "./durationToMs"; import { durationToMs } from "./durationToMs";
import { build } from "@server/build"; import { build } from "@server/build";
import { remote } from "./api"; import { remote } from "./api";
import type ResponseT from "@server/types/Response"; import type ResponseT from "@server/types/Response";
export const versionsQueries = { type ProductUpdate = {
latestVersion: () => link: string | null;
queryOptions({ edition: "enterprise" | "community" | "cloud" | null;
queryKey: ["LATEST_VERSION"] as const, id: number;
queryFn: async ({ signal }) => { priority: "CRITICAL" | "IMPORTANT" | "NORMAL" | null;
const data = await remote.get< title: string;
ResponseT<{ contents: string;
pangolin: { publishedAt: Date;
latestVersion: string; showUntil: Date;
releaseNotes: string; };
};
}> export const productUpdatesQueries = {
>("/latest-version"); list: queryOptions({
return data.data; queryKey: ["PRODUCT_UPDATES"] as const,
}, queryFn: async ({ signal }) => {
placeholderData: keepPreviousData, const data = await remote.get<ResponseT<ProductUpdate[]>>(
refetchInterval: (query) => { "/product-updates",
if (query.state.data) { { signal }
return durationToMs(30, "minutes"); );
} return data.data;
return false; },
}, refetchInterval: (query) => {
enabled: build === "oss" || build === "enterprise" // disabled in cloud version if (query.state.data) {
// because we don't need to listen for new versions there return durationToMs(5, "minutes");
}) }
return false;
}
}),
latestVersion: queryOptions({
queryKey: ["LATEST_VERSION"] as const,
queryFn: async ({ signal }) => {
const data = await remote.get<
ResponseT<{
pangolin: {
latestVersion: string;
releaseNotes: string;
};
}>
>("/versions", { signal });
return data.data;
},
placeholderData: keepPreviousData,
refetchInterval: (query) => {
if (query.state.data) {
return durationToMs(30, "minutes");
}
return false;
},
enabled: build === "oss" || build === "enterprise" // disabled in cloud version
// because we don't need to listen for new versions there
})
}; };