add settings skeleton

This commit is contained in:
Eduard Gert
2026-05-11 13:58:41 +02:00
parent 0c2702c0d7
commit 1aae067aaa
5 changed files with 126 additions and 64 deletions

View File

@@ -14,7 +14,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, Bu
export const buttonVariants = cva(
[
"relative",
"text-sm focus:z-10 focus:ring-2 font-medium focus:outline-none whitespace-nowrap shadow-sm",
"text-sm focus:z-10 focus:ring-2 font-semibold focus:outline-none whitespace-nowrap shadow-sm",
"inline-flex gap-2 items-center justify-center transition-colors focus:ring-offset-1",
"disabled:opacity-40 disabled:cursor-not-allowed disabled:dark:text-nb-gray-300 dark:ring-offset-neutral-950/50",
],
@@ -153,8 +153,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
}}
{...props}
>
{copy !== undefined &&
(copied ? <Check size={iconSize} /> : <Copy size={iconSize} />)}
{copy !== undefined && (copied ? <Check size={iconSize} /> : <Copy size={iconSize} />)}
{children}
</button>
);

View File

@@ -0,0 +1,91 @@
import { ReactNode } from "react";
import { Browser } from "@wailsio/runtime";
import { Update as UpdateSvc } from "@bindings/services";
import { Button } from "@/components/Button";
import { useStatus } from "@/hooks/useStatus";
import { cn } from "@/lib/cn";
function openUrl(url: string) {
void Browser.OpenURL(url).catch(() => window.open(url, "_blank"));
}
function formatLastChecked(date: Date) {
return date.toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
function triggerUpdate() {
UpdateSvc.Trigger().catch(() => {});
}
export function NetBirdVersionCard() {
const { status } = useStatus();
const updateVersion = (status?.events ?? [])
.map((e) => e.metadata?.["new_version_available"])
.find((v): v is string => Boolean(v));
if (updateVersion) {
return (
<Card>
<div>
<Title>Version {updateVersion} is available.</Title>
<Link
url={`https://github.com/netbirdio/netbird/releases/tag/v${updateVersion}`}
>
What's new?
</Link>
</div>
<Button variant={"primary"} size={"xs"} onClick={triggerUpdate}>
Restart Now
</Button>
</Card>
);
}
return (
<Card className={"max-w-md"}>
<div>
<Title>Last checked on {formatLastChecked(new Date())}</Title>
<Link url={"https://github.com/netbirdio/netbird/releases/latest"}>Changelog</Link>
</div>
<Button variant={"primary"} size={"xs"} onClick={triggerUpdate}>
Check for updates
</Button>
</Card>
);
}
function Card({ children, className }: { children: ReactNode; className?: string }) {
return (
<div
className={cn(
"w-full max-w-md flex items-center justify-between gap-4 rounded-md border border-nb-gray-800 bg-nb-gray-910 px-4 py-3",
className,
)}
>
{children}
</div>
);
}
function Title({ children }: { children: ReactNode }) {
return <p className={"text-sm font-semibold"}>{children}</p>;
}
function Link({ url, children }: { url: string; children: ReactNode }) {
return (
<button
type={"button"}
onClick={() => openUrl(url)}
className={
"text-sm text-netbird hover:underline hover:underline-offset-4 hover:decoration-[0.5px] font-medium"
}
>
{children}
</button>
);
}

View File

@@ -1,9 +1,8 @@
import { Browser } from "@wailsio/runtime";
import { Update as UpdateSvc } from "@bindings/services";
import netbirdFull from "@/assets/logos/netbird-full.svg";
import pkg from "../../../package.json";
import { useStatus } from "@/hooks/useStatus";
import { Button } from "@/components/Button";
import { NetBirdVersionCard } from "@/components/NetBirdVersionCard";
import { useAccentTrigger } from "@/modules/settings/SettingsAccent";
const LEGAL_LINKS: { label: string; url: string }[] = [
@@ -17,29 +16,11 @@ function openUrl(url: string) {
void Browser.OpenURL(url).catch(() => window.open(url, "_blank"));
}
function formatLastChecked(date: Date) {
return date.toLocaleString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
export function SettingsAbout() {
const { status } = useStatus();
const guiVersion = pkg.version;
const daemonVersion = status?.daemonVersion ?? "—";
const updateVersion = (status?.events ?? [])
.map((e) => e.metadata?.["new_version_available"])
.find((v): v is string => Boolean(v));
const triggerUpdate = () => {
UpdateSvc.Trigger().catch(() => {});
};
const handleVersionClick = useAccentTrigger();
return (
@@ -59,44 +40,7 @@ export function SettingsAbout() {
<p className={"text-sm text-nb-gray-300"}>GUI v{guiVersion}</p>
</div>
{updateVersion ? (
<div
className={
"w-full max-w-md flex items-center justify-between gap-4 rounded-md border border-nb-gray-800 bg-nb-gray-910 px-4 py-3"
}
>
<div>
<p className={"text-sm font-semibold"}>
Version {updateVersion} is available.
</p>
<button
type={"button"}
onClick={() =>
openUrl(
`https://github.com/netbirdio/netbird/releases/tag/v${updateVersion}`,
)
}
className={
"text-sm text-netbird hover:underline hover:underline-offset-4 hover:decoration-[0.5px] font-medium"
}
>
What's new?
</button>
</div>
<Button variant={"primary"} size={"xs"} onClick={triggerUpdate}>
Restart Now
</Button>
</div>
) : (
<div className={"flex flex-col items-center gap-2"}>
<p className={"text-xs text-nb-gray-400 text-center"}>
Last checked for updates on {formatLastChecked(new Date())}
</p>
<Button variant={"secondary"} size={"sm"} onClick={triggerUpdate}>
Check for updates
</Button>
</div>
)}
<NetBirdVersionCard />
<p className={"text-sm text-nb-gray-300 text-center"}>
© {new Date().getFullYear()} NetBird. All Rights Reserved.

View File

@@ -10,6 +10,7 @@ import {
import { Settings as SettingsSvc } from "@bindings/services";
import type { Config } from "@bindings/services/models.js";
import { useProfile } from "@/modules/profile/ProfileContext.tsx";
import { SkeletonSettings } from "@/modules/skeletons/SkeletonSettings.tsx";
const SAVE_DEBOUNCE_MS = 400;
@@ -138,15 +139,15 @@ const useSettingsState = () => {
};
export const SettingsProvider = ({ children }: { children: ReactNode }) => {
const { config, error, setField, saveField, saveFields, saveNow } =
useSettingsState();
const { config, error, setField, saveField, saveFields, saveNow } = useSettingsState();
// TODO: Better displaying of errors
return (
<>
{error && <p className={"pb-6 text-sm text-red-500"}>{error}</p>}
<div className={"flex-1 min-h-0 overflow-y-auto"}>
{!config ? (
<div className={"p-6 text-sm text-nb-gray-500"}>Loading</div>
<SkeletonSettings />
) : (
<SettingsContext.Provider
value={{

View File

@@ -0,0 +1,27 @@
import Skeleton from "react-loading-skeleton";
export const SkeletonSettings = () => {
return (
<div className={"gap-6 flex flex-col"}>
<div>
<Skeleton width={100} height={16} className={"mb-4"} />
<div>
<Skeleton width={100} height={14} />
<Skeleton width={400} height={10} />
</div>
<div className={"mt-3"}>
<Skeleton width={100} height={14} />
<Skeleton width={400} height={10} />
</div>
</div>
<div>
<Skeleton width={100} height={16} className={"mb-4"} />
<div>
<Skeleton width={100} height={14} />
<Skeleton width={400} height={10} />
<Skeleton width={300} height={10} />
</div>
</div>
</div>
);
};