mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-30 08:19:09 +01:00
wip
This commit is contained in:
@@ -1,12 +1,45 @@
|
||||
import { createContext, useContext, useMemo, type ReactNode } from "react";
|
||||
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react";
|
||||
import { Update as UpdateSvc } from "@bindings/services";
|
||||
import { useStatus } from "@/hooks/useStatus";
|
||||
import { UpdateAvailableBanner } from "@/modules/auto-update/UpdateAvailableBanner";
|
||||
import { UpdatingOverlay } from "@/modules/auto-update/UpdatingOverlay";
|
||||
|
||||
type ClientVersionContextValue = {
|
||||
updateAvailable: boolean;
|
||||
updateVersion: string | null;
|
||||
triggerUpdate: () => void;
|
||||
updating: boolean;
|
||||
updateError: string | null;
|
||||
dismissUpdateError: () => void;
|
||||
};
|
||||
|
||||
// Dev toggles — flip to preview UI states without triggering real flows.
|
||||
const FORCE_UPDATE_AVAILABLE = true;
|
||||
const FORCE_UPDATING = false;
|
||||
const FORCE_VERSION = "0.65.0";
|
||||
// Hide all "update available" UI (header trigger, settings badge, banner)
|
||||
// regardless of what the daemon reports.
|
||||
const HIDE_UPDATE_AVAILABLE = false;
|
||||
// FORCE_ERROR options:
|
||||
// null → no error (loading state)
|
||||
// "timeout" → "Update timed out" state
|
||||
// "cancel" → "Update canceled" state
|
||||
// "fail" → generic "Update failed" state (uses FORCE_ERROR_MSG)
|
||||
type ForceError = "timeout" | "cancel" | "fail" | null;
|
||||
const FORCE_ERROR = null as ForceError;
|
||||
const FORCE_ERROR_MSG = "installer exited with code 1";
|
||||
|
||||
const forcedErrorMessage = (): string | null => {
|
||||
switch (FORCE_ERROR) {
|
||||
case "timeout":
|
||||
return "update timed out after 15m";
|
||||
case "cancel":
|
||||
return "update canceled by user";
|
||||
case "fail":
|
||||
return FORCE_ERROR_MSG;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const ClientVersionContext = createContext<ClientVersionContextValue | null>(null);
|
||||
@@ -21,25 +54,60 @@ export const useClientVersion = () => {
|
||||
|
||||
export const ClientVersionProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { status } = useStatus();
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const [updateError, setUpdateError] = useState<string | null>(null);
|
||||
|
||||
const value = useMemo<ClientVersionContextValue>(() => {
|
||||
const version = (status?.events ?? [])
|
||||
.map((e) => e.metadata?.["new_version_available"])
|
||||
.find((v): v is string => Boolean(v));
|
||||
|
||||
return {
|
||||
updateAvailable: Boolean(version),
|
||||
updateVersion: version ?? null,
|
||||
triggerUpdate: () => {
|
||||
UpdateSvc.Trigger().catch(() => {});
|
||||
},
|
||||
};
|
||||
const updateVersion = useMemo(() => {
|
||||
if (HIDE_UPDATE_AVAILABLE) return null;
|
||||
if (FORCE_UPDATE_AVAILABLE || FORCE_UPDATING) return FORCE_VERSION;
|
||||
return (
|
||||
(status?.events ?? [])
|
||||
.map((e) => e.metadata?.["new_version_available"])
|
||||
.find((v): v is string => Boolean(v)) ?? null
|
||||
);
|
||||
}, [status]);
|
||||
|
||||
const triggerUpdate = useCallback(() => {
|
||||
setUpdateError(null);
|
||||
setUpdating(true);
|
||||
UpdateSvc.Trigger()
|
||||
.then((result) => {
|
||||
if (!result?.success) {
|
||||
setUpdateError(result?.errorMsg || "Update failed");
|
||||
setUpdating(false);
|
||||
}
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
setUpdateError(String(e));
|
||||
setUpdating(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const dismissUpdateError = useCallback(() => setUpdateError(null), []);
|
||||
|
||||
const value = useMemo<ClientVersionContextValue>(
|
||||
() => ({
|
||||
updateAvailable: Boolean(updateVersion),
|
||||
updateVersion,
|
||||
triggerUpdate,
|
||||
updating,
|
||||
updateError,
|
||||
dismissUpdateError,
|
||||
}),
|
||||
[updateVersion, triggerUpdate, updating, updateError, dismissUpdateError],
|
||||
);
|
||||
|
||||
return (
|
||||
<ClientVersionContext.Provider value={value}>
|
||||
{children}
|
||||
<UpdateAvailableBanner />
|
||||
{(updating || updateError || FORCE_UPDATING || FORCE_ERROR) && (
|
||||
<UpdatingOverlay
|
||||
version={updateVersion}
|
||||
error={updateError ?? forcedErrorMessage()}
|
||||
onDismiss={dismissUpdateError}
|
||||
/>
|
||||
)}
|
||||
</ClientVersionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { forwardRef, type HTMLAttributes } from "react";
|
||||
import { ArrowUpCircleIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
type Props = {
|
||||
type Props = HTMLAttributes<HTMLDivElement> & {
|
||||
size?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const UpdateBadge = ({ size = 15, className }: Props) => {
|
||||
export const UpdateBadge = forwardRef<HTMLDivElement, Props>(function UpdateBadge(
|
||||
{ size = 15, className, ...rest },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<div className={cn("relative flex items-center justify-center", className)}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("relative flex items-center justify-center", className)}
|
||||
{...rest}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
"animate-ping absolute inline-flex h-[15px] w-[15px] rounded-full bg-netbird opacity-20 pointer-events-none"
|
||||
@@ -17,4 +24,4 @@ export const UpdateBadge = ({ size = 15, className }: Props) => {
|
||||
<ArrowUpCircleIcon size={size} className={"text-netbird"} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Loader2, XCircle } from "lucide-react";
|
||||
import { Button } from "@/components/Button";
|
||||
|
||||
type Props = {
|
||||
version: string | null;
|
||||
error: string | null;
|
||||
onDismiss: () => void;
|
||||
};
|
||||
|
||||
type Variant = {
|
||||
title: string;
|
||||
description: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
function classifyError(msg: string, version: string | null): Variant {
|
||||
const lower = msg.toLowerCase();
|
||||
const target = version ? `v${version}` : "the new version";
|
||||
if (lower.includes("timeout") || lower.includes("timed out")) {
|
||||
return {
|
||||
title: "Update Is Taking Too Long",
|
||||
description: `Installing ${target} took too long and didn't finish.`,
|
||||
};
|
||||
}
|
||||
if (lower.includes("cancel")) {
|
||||
return {
|
||||
title: "Update Was Stopped",
|
||||
description: `The update to ${target} was canceled before it finished.`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: "Couldn't Install the Update",
|
||||
description: `${target} couldn't be installed.`,
|
||||
message: msg || "unknown error",
|
||||
};
|
||||
}
|
||||
|
||||
export const UpdatingOverlay = ({ version, error, onDismiss }: Props) => {
|
||||
const isError = Boolean(error);
|
||||
const errorInfo = error ? classifyError(error, version) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"fixed inset-0 z-[100] flex items-center justify-center bg-nb-gray-950/85 backdrop-blur-sm cursor-default select-none wails-draggable"
|
||||
}
|
||||
onPointerDown={(e) => {
|
||||
if (isError) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (isError) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className={"flex flex-col items-center gap-5 px-8 max-w-lg text-center"}>
|
||||
{isError ? (
|
||||
<div
|
||||
className={"h-9 w-9 rounded-md flex items-center justify-center bg-red-500"}
|
||||
>
|
||||
<XCircle className={"text-white"} size={18} />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={"h-9 w-9 rounded-md flex items-center justify-center bg-nb-gray-100"}
|
||||
>
|
||||
<Loader2 className={"animate-spin text-nb-gray-950"} size={16} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={"flex flex-col items-center gap-1"}>
|
||||
<p className={"text-base font-medium text-nb-gray-50"}>
|
||||
{isError
|
||||
? errorInfo!.title
|
||||
: version
|
||||
? `Updating NetBird to v${version}`
|
||||
: "Updating NetBird"}
|
||||
</p>
|
||||
<p className={"text-sm text-nb-gray-300"}>
|
||||
{isError ? (
|
||||
<>
|
||||
{errorInfo!.description}
|
||||
{errorInfo!.message && (
|
||||
<>
|
||||
<br />
|
||||
<span className={"first-letter:uppercase"}>
|
||||
{errorInfo!.message}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
"A newer version is available and is being installed. NetBird will restart automatically once the update is finished."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isError && (
|
||||
<div className={"wails-no-draggable"}>
|
||||
<Button variant={"secondary"} size={"xs"} onClick={onDismiss}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user