mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-28 09:39:05 +02:00
update stuff
This commit is contained in:
@@ -8,11 +8,9 @@ import {
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
|
||||
export type AppearanceView = "default" | "advanced";
|
||||
export type ConnectionLayout = "default" | "switch";
|
||||
|
||||
export type AppearanceState = {
|
||||
view: AppearanceView;
|
||||
connectionLayout: ConnectionLayout;
|
||||
expanded: boolean;
|
||||
showPeersNav: boolean;
|
||||
@@ -25,7 +23,6 @@ export type AppearanceState = {
|
||||
const STORAGE_KEY = "netbird:appearance";
|
||||
|
||||
const DEFAULTS: AppearanceState = {
|
||||
view: "default",
|
||||
connectionLayout: "default",
|
||||
expanded: true,
|
||||
showPeersNav: true,
|
||||
@@ -47,7 +44,6 @@ const readStored = (): AppearanceState => {
|
||||
};
|
||||
|
||||
type AppearanceContextValue = AppearanceState & {
|
||||
setView: (v: AppearanceView) => void;
|
||||
setField: <K extends keyof AppearanceState>(k: K, v: AppearanceState[K]) => void;
|
||||
};
|
||||
|
||||
@@ -79,13 +75,9 @@ export const AppearanceProvider = ({ children }: { children: ReactNode }) => {
|
||||
[],
|
||||
);
|
||||
|
||||
const setView = useCallback((v: AppearanceView) => {
|
||||
setState((s) => ({ ...s, view: v }));
|
||||
}, []);
|
||||
|
||||
const value = useMemo<AppearanceContextValue>(
|
||||
() => ({ ...state, setView, setField }),
|
||||
[state, setView, setField],
|
||||
() => ({ ...state, setField }),
|
||||
[state, setField],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
import {
|
||||
Connection as ConnectionSvc,
|
||||
Debug as DebugSvc,
|
||||
@@ -19,8 +20,7 @@ export type DebugStage =
|
||||
| { kind: "bundling" }
|
||||
| { kind: "uploading" }
|
||||
| { kind: "cancelling" }
|
||||
| { kind: "done"; result: DebugBundleResult; uploadAttempted: boolean }
|
||||
| { kind: "error"; message: string };
|
||||
| { kind: "done"; result: DebugBundleResult; uploadAttempted: boolean };
|
||||
|
||||
const sleep = (ms: number, signal: AbortSignal) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
@@ -53,10 +53,7 @@ export const useDebugBundle = () => {
|
||||
const [lastBundlePath, setLastBundlePath] = useState<string>("");
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const isRunning =
|
||||
stage.kind !== "idle" &&
|
||||
stage.kind !== "done" &&
|
||||
stage.kind !== "error";
|
||||
const isRunning = stage.kind !== "idle" && stage.kind !== "done";
|
||||
|
||||
const reset = () => setStage({ kind: "idle" });
|
||||
|
||||
@@ -157,7 +154,11 @@ export const useDebugBundle = () => {
|
||||
setStage({ kind: "idle" });
|
||||
return;
|
||||
}
|
||||
setStage({ kind: "error", message: String(e) });
|
||||
setStage({ kind: "idle" });
|
||||
await Dialogs.Error({
|
||||
Title: "Debug Bundle Failed",
|
||||
Message: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
} finally {
|
||||
if (abortRef.current === ctrl) abortRef.current = null;
|
||||
}
|
||||
|
||||
@@ -6,15 +6,20 @@ import {
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { Profiles as ProfilesSvc } from "@bindings/services";
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
import { Connection, Peers, Profiles as ProfilesSvc } from "@bindings/services";
|
||||
import type { Profile } from "@bindings/services/models.js";
|
||||
|
||||
type ProfileContextValue = {
|
||||
username: string;
|
||||
activeProfile: string;
|
||||
profiles: Profile[];
|
||||
loaded: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
switchProfile: (name: string) => Promise<void>;
|
||||
addProfile: (name: string) => Promise<void>;
|
||||
removeProfile: (name: string) => Promise<void>;
|
||||
logoutProfile: (name: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const ProfileContext = createContext<ProfileContextValue | null>(null);
|
||||
@@ -30,18 +35,24 @@ export const useProfile = () => {
|
||||
export const ProfileProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [activeProfile, setActiveProfile] = useState("");
|
||||
const [profiles, setProfiles] = useState<Profile[]>([]);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const u = await ProfilesSvc.Username();
|
||||
const active = await ProfilesSvc.GetActive();
|
||||
const [active, list] = await Promise.all([
|
||||
ProfilesSvc.GetActive(),
|
||||
ProfilesSvc.List(u),
|
||||
]);
|
||||
setUsername(u);
|
||||
setActiveProfile(active.profileName || "default");
|
||||
setError(null);
|
||||
setProfiles(list);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
await Dialogs.Error({
|
||||
Title: "Load Profiles Failed",
|
||||
Message: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
} finally {
|
||||
setLoaded(true);
|
||||
}
|
||||
@@ -53,10 +64,53 @@ export const ProfileProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
const switchProfile = useCallback(
|
||||
async (name: string) => {
|
||||
// Mirror tray.go switchProfile: only reconnect when the daemon was
|
||||
// actively online. Calling Up on an Idle/NeedsLogin daemon makes
|
||||
// the daemon wait 50s on its internal waitForUp and return
|
||||
// DeadlineExceeded.
|
||||
let wasActive = false;
|
||||
try {
|
||||
const prev = await Peers.Get();
|
||||
const s = (prev?.status ?? "").toLowerCase();
|
||||
wasActive = s === "connected" || s === "connecting";
|
||||
} catch {
|
||||
wasActive = false;
|
||||
}
|
||||
|
||||
await ProfilesSvc.Switch({ profileName: name, username });
|
||||
setActiveProfile(name);
|
||||
|
||||
if (wasActive) {
|
||||
await Connection.Down();
|
||||
await Connection.Up({ profileName: name, username });
|
||||
}
|
||||
|
||||
await refresh();
|
||||
},
|
||||
[username],
|
||||
[username, refresh],
|
||||
);
|
||||
|
||||
const addProfile = useCallback(
|
||||
async (name: string) => {
|
||||
await ProfilesSvc.Add({ profileName: name, username });
|
||||
await refresh();
|
||||
},
|
||||
[username, refresh],
|
||||
);
|
||||
|
||||
const removeProfile = useCallback(
|
||||
async (name: string) => {
|
||||
await ProfilesSvc.Remove({ profileName: name, username });
|
||||
await refresh();
|
||||
},
|
||||
[username, refresh],
|
||||
);
|
||||
|
||||
const logoutProfile = useCallback(
|
||||
async (name: string) => {
|
||||
await Connection.Logout({ profileName: name, username });
|
||||
await refresh();
|
||||
},
|
||||
[username, refresh],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -64,10 +118,13 @@ export const ProfileProvider = ({ children }: { children: ReactNode }) => {
|
||||
value={{
|
||||
username,
|
||||
activeProfile,
|
||||
profiles,
|
||||
loaded,
|
||||
error,
|
||||
refresh,
|
||||
switchProfile,
|
||||
addProfile,
|
||||
removeProfile,
|
||||
logoutProfile,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,26 +1,9 @@
|
||||
import FancyToggleSwitch from "@/components/FancyToggleSwitch";
|
||||
import { CardSelect } from "@/components/CardSelect.tsx";
|
||||
import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
|
||||
import {
|
||||
useAppearance,
|
||||
type AppearanceView,
|
||||
} from "@/modules/appearance/AppearanceContext.tsx";
|
||||
import simpleScreen from "@/assets/screens/simple.png";
|
||||
import advancedScreen from "@/assets/screens/advanced.png";
|
||||
|
||||
const ScreenPreview = ({ src, alt }: { src: string; alt: string }) => (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
draggable={false}
|
||||
className={"h-full w-full object-contain select-none"}
|
||||
/>
|
||||
);
|
||||
import { useAppearance } from "@/modules/appearance/AppearanceContext.tsx";
|
||||
|
||||
export function SettingsAppearance() {
|
||||
const {
|
||||
view,
|
||||
setView,
|
||||
showPeersNav,
|
||||
showResourcesNav,
|
||||
showExitNodeNav,
|
||||
@@ -30,59 +13,37 @@ export function SettingsAppearance() {
|
||||
} = useAppearance();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionGroup title={"View"}>
|
||||
<CardSelect
|
||||
value={view}
|
||||
onChange={(v) => setView(v as AppearanceView)}
|
||||
>
|
||||
<CardSelect.Option
|
||||
value={"default"}
|
||||
title={"Simple"}
|
||||
description={"Streamlined view with essential controls."}
|
||||
preview={<ScreenPreview src={simpleScreen} alt={"Simple view"} />}
|
||||
/>
|
||||
<CardSelect.Option
|
||||
value={"advanced"}
|
||||
title={"Advanced"}
|
||||
description={"All details and power-user options visible."}
|
||||
preview={<ScreenPreview src={advancedScreen} alt={"Advanced view"} />}
|
||||
/>
|
||||
</CardSelect>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={"Interface"}>
|
||||
<FancyToggleSwitch
|
||||
value={showPeersNav}
|
||||
onChange={(v) => setField("showPeersNav", v)}
|
||||
label={"Peers"}
|
||||
helpText={"Show the Peers item in the side navigation."}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={showResourcesNav}
|
||||
onChange={(v) => setField("showResourcesNav", v)}
|
||||
label={"Resources"}
|
||||
helpText={"Show the Resources item in the side navigation."}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={showExitNodeNav}
|
||||
onChange={(v) => setField("showExitNodeNav", v)}
|
||||
label={"Exit Node"}
|
||||
helpText={"Show the active exit node in the side navigation."}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={showProfileSelector}
|
||||
onChange={(v) => setField("showProfileSelector", v)}
|
||||
label={"Profile Selector"}
|
||||
helpText={"Show the profile selector in the header."}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={showSettingsButton}
|
||||
onChange={(v) => setField("showSettingsButton", v)}
|
||||
label={"Settings Button"}
|
||||
helpText={"Show the settings button in the header."}
|
||||
/>
|
||||
</SectionGroup>
|
||||
</>
|
||||
<SectionGroup title={"Interface"}>
|
||||
<FancyToggleSwitch
|
||||
value={showPeersNav}
|
||||
onChange={(v) => setField("showPeersNav", v)}
|
||||
label={"Peers"}
|
||||
helpText={"Show the Peers item in the side navigation."}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={showResourcesNav}
|
||||
onChange={(v) => setField("showResourcesNav", v)}
|
||||
label={"Resources"}
|
||||
helpText={"Show the Resources item in the side navigation."}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={showExitNodeNav}
|
||||
onChange={(v) => setField("showExitNodeNav", v)}
|
||||
label={"Exit Node"}
|
||||
helpText={"Show the active exit node in the side navigation."}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={showProfileSelector}
|
||||
onChange={(v) => setField("showProfileSelector", v)}
|
||||
label={"Profile Selector"}
|
||||
helpText={"Show the profile selector in the header."}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={showSettingsButton}
|
||||
onChange={(v) => setField("showSettingsButton", v)}
|
||||
label={"Settings Button"}
|
||||
helpText={"Show the settings button in the header."}
|
||||
/>
|
||||
</SectionGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,11 +7,15 @@ import {
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
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 errorMessage = (e: unknown) =>
|
||||
e instanceof Error ? e.message : String(e);
|
||||
|
||||
const SAVE_DEBOUNCE_MS = 400;
|
||||
|
||||
type SettingsContextValue = {
|
||||
@@ -35,7 +39,6 @@ export const useSettings = () => {
|
||||
const useSettingsState = () => {
|
||||
const { username, activeProfile, loaded: profileLoaded } = useProfile();
|
||||
const [config, setConfig] = useState<Config | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -47,9 +50,11 @@ const useSettingsState = () => {
|
||||
username,
|
||||
});
|
||||
setConfig(c);
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
await Dialogs.Error({
|
||||
Title: "Load Settings Failed",
|
||||
Message: errorMessage(e),
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, [profileLoaded, activeProfile, username]);
|
||||
@@ -75,9 +80,11 @@ const useSettingsState = () => {
|
||||
profileName: activeProfile,
|
||||
username,
|
||||
});
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
await Dialogs.Error({
|
||||
Title: "Save Settings Failed",
|
||||
Message: errorMessage(e),
|
||||
});
|
||||
}
|
||||
},
|
||||
[activeProfile, username],
|
||||
@@ -135,33 +142,29 @@ const useSettingsState = () => {
|
||||
[config, save],
|
||||
);
|
||||
|
||||
return { config, error, setField, saveField, saveFields, saveNow };
|
||||
return { config, setField, saveField, saveFields, saveNow };
|
||||
};
|
||||
|
||||
export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { config, error, setField, saveField, saveFields, saveNow } = useSettingsState();
|
||||
const { config, 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 ? (
|
||||
<SkeletonSettings />
|
||||
) : (
|
||||
<SettingsContext.Provider
|
||||
value={{
|
||||
config,
|
||||
setField,
|
||||
saveField,
|
||||
saveFields,
|
||||
saveNow,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
<div className={"flex-1 min-h-0 overflow-y-auto"}>
|
||||
{!config ? (
|
||||
<SkeletonSettings />
|
||||
) : (
|
||||
<SettingsContext.Provider
|
||||
value={{
|
||||
config,
|
||||
setField,
|
||||
saveField,
|
||||
saveFields,
|
||||
saveNow,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -31,8 +31,14 @@ export function SettingsTroubleshooting() {
|
||||
reset,
|
||||
} = useDebugBundleContext();
|
||||
|
||||
if (stage.kind === "done" || stage.kind === "error") {
|
||||
return <ResultSection stage={stage} onClose={reset} />;
|
||||
if (stage.kind === "done") {
|
||||
return (
|
||||
<DoneResult
|
||||
result={stage.result}
|
||||
uploaded={stage.uploadAttempted}
|
||||
onClose={reset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (stage.kind !== "idle") {
|
||||
return <ProgressSection stage={stage} onCancel={cancel} />;
|
||||
@@ -127,30 +133,6 @@ function ProgressSection({ stage, onCancel }: { stage: DebugStage; onCancel: ()
|
||||
);
|
||||
}
|
||||
|
||||
function ResultSection({
|
||||
stage,
|
||||
onClose,
|
||||
}: {
|
||||
stage: Extract<DebugStage, { kind: "done" } | { kind: "error" }>;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
if (stage.kind === "error") {
|
||||
return (
|
||||
<StatusPanel
|
||||
variant={"error"}
|
||||
title={"Bundle failed"}
|
||||
description={stage.message}
|
||||
actions={
|
||||
<Button variant={"secondary"} size={"xs"} onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <DoneResult result={stage.result} uploaded={stage.uploadAttempted} onClose={onClose} />;
|
||||
}
|
||||
|
||||
function DoneResult({
|
||||
result,
|
||||
uploaded,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
import { useSettings } from "@/modules/settings/SettingsContext.tsx";
|
||||
|
||||
export enum ManagementMode {
|
||||
@@ -52,13 +53,29 @@ export function useManagementUrl() {
|
||||
}, [config.managementUrl]);
|
||||
|
||||
const setMode = (next: ManagementMode) => {
|
||||
setModeState(next);
|
||||
if (
|
||||
next === ManagementMode.Cloud &&
|
||||
config.managementUrl !== CLOUD_MANAGEMENT_URL
|
||||
) {
|
||||
void saveField("managementUrl", CLOUD_MANAGEMENT_URL);
|
||||
// Switching from a self-hosted management server to NetBird Cloud
|
||||
// re-points the client at a different deployment and forces a
|
||||
// reconnect/re-login. Confirm before applying.
|
||||
void Dialogs.Warning({
|
||||
Title: "Switch to NetBird Cloud?",
|
||||
Message:
|
||||
"This will disconnect from your self-hosted management server and reconnect to NetBird Cloud. You may need to log in again.",
|
||||
Buttons: [
|
||||
{ Label: "Cancel", IsCancel: true, IsDefault: true },
|
||||
{ Label: "Switch to Cloud" },
|
||||
],
|
||||
}).then((result) => {
|
||||
if (result !== "Switch to Cloud") return;
|
||||
setModeState(ManagementMode.Cloud);
|
||||
void saveField("managementUrl", CLOUD_MANAGEMENT_URL);
|
||||
});
|
||||
return;
|
||||
}
|
||||
setModeState(next);
|
||||
};
|
||||
|
||||
const normalizedUrl = normalizeManagementUrl(url);
|
||||
|
||||
Reference in New Issue
Block a user