Files
netbird/client/ui/frontend/src/modules/profile/ProfileContext.tsx
T

144 lines
4.2 KiB
TypeScript

import {
createContext,
useCallback,
useContext,
useEffect,
useState,
type ReactNode,
} from "react";
import { Dialogs, Events } from "@wailsio/runtime";
import {
Connection,
ProfileSwitcher,
Profiles as ProfilesSvc,
} from "@bindings/services";
import type { Profile } from "@bindings/services/models.js";
import i18next from "@/lib/i18n";
import { formatErrorMessage } from "@/lib/errors";
const EVENT_PROFILE_CHANGED = "netbird:profile:changed";
type ProfileContextValue = {
username: string;
activeProfile: string;
profiles: Profile[];
loaded: boolean;
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);
export const useProfile = () => {
const ctx = useContext(ProfileContext);
if (!ctx) {
throw new Error("useProfile must be used inside ProfileProvider");
}
return ctx;
};
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 refresh = useCallback(async () => {
try {
const u = await ProfilesSvc.Username();
const [active, list] = await Promise.all([
ProfilesSvc.GetActive(),
ProfilesSvc.List(u),
]);
setUsername(u);
setActiveProfile(active.profileName || "default");
setProfiles(list);
} catch (e) {
// Daemon-down is already surfaced globally by
// DaemonUnavailableOverlay; a second popup on top of it is
// pure noise. Every profile RPC routes through the same gRPC
// conn, so the Unavailable code is the reliable marker.
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes("code = Unavailable")) {
return;
}
await Dialogs.Error({
Title: i18next.t("profile.error.loadTitle"),
Message: formatErrorMessage(e),
});
} finally {
setLoaded(true);
}
}, []);
useEffect(() => {
void refresh();
}, [refresh]);
useEffect(() => {
// The tray and other windows drive switches through the same
// ProfileSwitcher.SwitchActive RPC, which emits this event on success.
// Without the subscription, a tray-initiated switch leaves this
// window painting the old activeProfile until the next mount.
const off = Events.On(EVENT_PROFILE_CHANGED, () => {
void refresh();
});
return () => {
off();
};
}, [refresh]);
const switchProfile = useCallback(
async (name: string) => {
await ProfileSwitcher.SwitchActive({ profileName: name, username });
await refresh();
},
[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 (
<ProfileContext.Provider
value={{
username,
activeProfile,
profiles,
loaded,
refresh,
switchProfile,
addProfile,
removeProfile,
logoutProfile,
}}
>
{children}
</ProfileContext.Provider>
);
};