mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-07 15:31:30 +02:00
Merge remote-tracking branch 'origin/ui-refactor' into ui-refactor
# Conflicts: # client/ui/frontend/src/screens/Update.tsx
This commit is contained in:
@@ -11,6 +11,7 @@ import { NewProfileModal } from "@/components/NewProfileModal";
|
||||
import { Tooltip } from "@/components/Tooltip";
|
||||
import { useProfile } from "@/modules/profile/ProfileContext";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { formatErrorMessage } from "@/lib/errors";
|
||||
|
||||
type ProfileDropdownProps = {
|
||||
onManageProfiles?: () => void;
|
||||
@@ -40,7 +41,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
|
||||
} catch (e) {
|
||||
await Dialogs.Error({
|
||||
Title: title,
|
||||
Message: e instanceof Error ? e.message : String(e),
|
||||
Message: formatErrorMessage(e),
|
||||
});
|
||||
} finally {
|
||||
setBusy(false);
|
||||
@@ -70,7 +71,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
|
||||
} catch (e) {
|
||||
await Dialogs.Error({
|
||||
Title: t("profile.error.createTitle"),
|
||||
Message: e instanceof Error ? e.message : String(e),
|
||||
Message: formatErrorMessage(e),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"tray.status.disconnected": "Getrennt",
|
||||
"tray.status.daemonUnavailable": "Nicht aktiv",
|
||||
"tray.status.error": "Fehler",
|
||||
"tray.session.expiresIn": "Sitzung: {remaining}",
|
||||
|
||||
"tray.menu.open": "NetBird öffnen",
|
||||
"tray.menu.connect": "Verbinden",
|
||||
@@ -31,6 +32,14 @@
|
||||
"notify.error.switchProfile": "Wechsel zu {profile} fehlgeschlagen",
|
||||
"notify.sessionExpired.title": "NetBird-Sitzung abgelaufen",
|
||||
"notify.sessionExpired.body": "Ihre NetBird-Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
|
||||
"notify.sessionWarning.title": "Sitzung läuft bald ab",
|
||||
"notify.sessionWarning.body": "Ihre NetBird-Sitzung läuft in {remaining} ab. Klicken Sie auf Jetzt verlängern, um zu erneuern.",
|
||||
"notify.sessionWarning.bodyGeneric": "Ihre NetBird-Sitzung läuft bald ab. Klicken Sie auf Jetzt verlängern, um zu erneuern.",
|
||||
"notify.sessionWarning.extend": "Jetzt verlängern",
|
||||
"notify.sessionWarning.dismiss": "Verwerfen",
|
||||
"notify.sessionWarning.failed": "NetBird-Sitzung konnte nicht verlängert werden",
|
||||
"notify.sessionWarning.successTitle": "NetBird-Sitzung verlängert",
|
||||
"notify.sessionWarning.successBody": "Ihre Sitzung wurde erneuert.",
|
||||
|
||||
"common.cancel": "Abbrechen",
|
||||
"common.save": "Speichern",
|
||||
@@ -272,6 +281,7 @@
|
||||
"sessionAboutToExpire.stay": "Verbunden bleiben",
|
||||
"sessionAboutToExpire.logout": "Abmelden",
|
||||
"sessionAboutToExpire.expired": "Sitzung abgelaufen",
|
||||
"sessionAboutToExpire.extendFailedTitle": "Sitzungsverlängerung fehlgeschlagen",
|
||||
|
||||
"peers.search.placeholder": "Nach Peer-Name, DNS oder IP-Adresse suchen",
|
||||
"peers.filter.all": "Alle",
|
||||
@@ -284,5 +294,14 @@
|
||||
|
||||
"daemon.unavailable.title": "NetBird-Dienst läuft nicht",
|
||||
"daemon.unavailable.description": "Die App stellt automatisch die Verbindung wieder her, sobald der Dienst läuft.",
|
||||
"daemon.unavailable.docsLink": "Dokumentation"
|
||||
"daemon.unavailable.docsLink": "Dokumentation",
|
||||
|
||||
"error.jwt_clock_skew": "Anmeldung fehlgeschlagen: Die Uhr dieses Geräts ist nicht mit dem Server synchron. Bitte synchronisieren Sie die Systemuhr und versuchen Sie es erneut.",
|
||||
"error.jwt_expired": "Ihr Anmeldetoken ist abgelaufen. Bitte melden Sie sich erneut an.",
|
||||
"error.jwt_signature_invalid": "Anmeldung fehlgeschlagen: Die Token-Signatur ist ungültig. Bitte wenden Sie sich an Ihren Administrator.",
|
||||
"error.session_expired": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
|
||||
"error.invalid_setup_key": "Der Setup-Schlüssel fehlt oder ist ungültig.",
|
||||
"error.permission_denied": "Die Anmeldung wurde vom Server abgelehnt.",
|
||||
"error.daemon_unreachable": "Der NetBird-Dienst antwortet nicht. Bitte prüfen Sie, ob der Dienst läuft.",
|
||||
"error.unknown": "Vorgang fehlgeschlagen. Technische Details siehe unten."
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"tray.status.disconnected": "Disconnected",
|
||||
"tray.status.daemonUnavailable": "Not running",
|
||||
"tray.status.error": "Error",
|
||||
"tray.session.expiresIn": "Session: {remaining}",
|
||||
|
||||
"tray.menu.open": "Open NetBird",
|
||||
"tray.menu.connect": "Connect",
|
||||
@@ -31,6 +32,14 @@
|
||||
"notify.error.switchProfile": "Failed to switch to {profile}",
|
||||
"notify.sessionExpired.title": "NetBird session expired",
|
||||
"notify.sessionExpired.body": "Your NetBird session has expired. Please log in again.",
|
||||
"notify.sessionWarning.title": "Session expires soon",
|
||||
"notify.sessionWarning.body": "Your NetBird session expires in {remaining}. Click Extend now to renew.",
|
||||
"notify.sessionWarning.bodyGeneric": "Your NetBird session is about to expire. Click Extend now to renew.",
|
||||
"notify.sessionWarning.extend": "Extend now",
|
||||
"notify.sessionWarning.dismiss": "Dismiss",
|
||||
"notify.sessionWarning.failed": "Failed to extend NetBird session",
|
||||
"notify.sessionWarning.successTitle": "NetBird session extended",
|
||||
"notify.sessionWarning.successBody": "Your session has been refreshed.",
|
||||
|
||||
"common.cancel": "Cancel",
|
||||
"common.save": "Save",
|
||||
@@ -293,6 +302,7 @@
|
||||
"sessionAboutToExpire.stay": "Stay connected",
|
||||
"sessionAboutToExpire.logout": "Logout",
|
||||
"sessionAboutToExpire.expired": "Session expired",
|
||||
"sessionAboutToExpire.extendFailedTitle": "Extend Session Failed",
|
||||
|
||||
"peers.search.placeholder": "Search by peer name, DNS or IP address",
|
||||
"peers.filter.all": "All",
|
||||
@@ -305,5 +315,14 @@
|
||||
|
||||
"daemon.unavailable.title": "NetBird Service Is Not Running",
|
||||
"daemon.unavailable.description": "The app will reconnect automatically once the service is running.",
|
||||
"daemon.unavailable.docsLink": "Documentation"
|
||||
"daemon.unavailable.docsLink": "Documentation",
|
||||
|
||||
"error.jwt_clock_skew": "Sign-in failed: this device's clock is out of sync with the server. Please sync your system clock and try again.",
|
||||
"error.jwt_expired": "Your sign-in token has expired. Please sign in again.",
|
||||
"error.jwt_signature_invalid": "Sign-in failed: the token signature is invalid. Please contact your administrator.",
|
||||
"error.session_expired": "Your session has expired. Please sign in again.",
|
||||
"error.invalid_setup_key": "The setup key is missing or invalid.",
|
||||
"error.permission_denied": "Sign-in was rejected by the server.",
|
||||
"error.daemon_unreachable": "The NetBird daemon is not responding. Please check that the service is running.",
|
||||
"error.unknown": "Operation failed. See details for the technical message."
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"tray.status.disconnected": "Lekapcsolva",
|
||||
"tray.status.daemonUnavailable": "Nem fut",
|
||||
"tray.status.error": "Hiba",
|
||||
"tray.session.expiresIn": "Munkamenet: {remaining}",
|
||||
|
||||
"tray.menu.open": "NetBird megnyitása",
|
||||
"tray.menu.connect": "Csatlakozás",
|
||||
@@ -31,6 +32,14 @@
|
||||
"notify.error.switchProfile": "Átváltás sikertelen erre: {profile}",
|
||||
"notify.sessionExpired.title": "NetBird munkamenet lejárt",
|
||||
"notify.sessionExpired.body": "A NetBird munkamenet lejárt. Kérjük, jelentkezzen be újra.",
|
||||
"notify.sessionWarning.title": "Munkamenet hamarosan lejár",
|
||||
"notify.sessionWarning.body": "A NetBird munkamenet {remaining} múlva lejár. Kattints a Meghosszabbítás gombra a megújításhoz.",
|
||||
"notify.sessionWarning.bodyGeneric": "A NetBird munkamenet hamarosan lejár. Kattints a Meghosszabbítás gombra a megújításhoz.",
|
||||
"notify.sessionWarning.extend": "Meghosszabbítás",
|
||||
"notify.sessionWarning.dismiss": "Elvetés",
|
||||
"notify.sessionWarning.failed": "A NetBird munkamenet meghosszabbítása sikertelen",
|
||||
"notify.sessionWarning.successTitle": "NetBird munkamenet meghosszabbítva",
|
||||
"notify.sessionWarning.successBody": "A munkamenet frissítve.",
|
||||
|
||||
"common.cancel": "Mégse",
|
||||
"common.save": "Mentés",
|
||||
@@ -272,6 +281,7 @@
|
||||
"sessionAboutToExpire.stay": "Maradjon csatlakoztatva",
|
||||
"sessionAboutToExpire.logout": "Kijelentkezés",
|
||||
"sessionAboutToExpire.expired": "Munkamenet lejárt",
|
||||
"sessionAboutToExpire.extendFailedTitle": "A munkamenet meghosszabbítása sikertelen",
|
||||
|
||||
"peers.search.placeholder": "Keresés társ neve, DNS vagy IP-cím alapján",
|
||||
"peers.filter.all": "Összes",
|
||||
@@ -284,5 +294,14 @@
|
||||
|
||||
"daemon.unavailable.title": "A NetBird szolgáltatás nem fut",
|
||||
"daemon.unavailable.description": "Az alkalmazás automatikusan újracsatlakozik, amint a szolgáltatás újra elérhető.",
|
||||
"daemon.unavailable.docsLink": "Dokumentáció"
|
||||
"daemon.unavailable.docsLink": "Dokumentáció",
|
||||
|
||||
"error.jwt_clock_skew": "A bejelentkezés sikertelen: az eszköz órája eltér a szerverétől. Kérjük, szinkronizálja a rendszer óráját, majd próbálja újra.",
|
||||
"error.jwt_expired": "A bejelentkezési token lejárt. Kérjük, jelentkezzen be újra.",
|
||||
"error.jwt_signature_invalid": "A bejelentkezés sikertelen: a token aláírása érvénytelen. Kérjük, lépjen kapcsolatba a rendszergazdával.",
|
||||
"error.session_expired": "A munkamenet lejárt. Kérjük, jelentkezzen be újra.",
|
||||
"error.invalid_setup_key": "A telepítési kulcs hiányzik vagy érvénytelen.",
|
||||
"error.permission_denied": "A szerver elutasította a bejelentkezést.",
|
||||
"error.daemon_unreachable": "A NetBird szolgáltatás nem válaszol. Kérjük, ellenőrizze, hogy fut-e a szolgáltatás.",
|
||||
"error.unknown": "A művelet meghiúsult. A technikai részleteket a Details mezőben találja."
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ToggleSwitch } from "@/components/ToggleSwitch.tsx";
|
||||
import { useStatus } from "@/modules/daemon-status/StatusContext.tsx";
|
||||
import { useProfile } from "@/modules/profile/ProfileContext.tsx";
|
||||
import { cn } from "@/lib/cn.ts";
|
||||
import { formatErrorMessage } from "@/lib/errors.ts";
|
||||
import netbirdFullLogo from "@/assets/logos/netbird-full.svg";
|
||||
|
||||
enum ConnectionState {
|
||||
@@ -38,8 +39,7 @@ const NEEDS_LOGIN_STATES = new Set([
|
||||
"LoginFailed",
|
||||
]);
|
||||
|
||||
const errorMessage = (e: unknown) =>
|
||||
e instanceof Error ? e.message : String(e);
|
||||
const errorMessage = formatErrorMessage;
|
||||
|
||||
// startLogin drives the daemon's SSO login end-to-end. The BrowserLogin
|
||||
// popup window is the only login UI; errors surface as a native
|
||||
@@ -230,6 +230,14 @@ export const ConnectionStatusSwitch = () => {
|
||||
// See connect() above — clear via the effect, not eagerly.
|
||||
};
|
||||
|
||||
// Tracks whether the daemon has entered Connecting during the
|
||||
// current "connect" action. Lets us distinguish "still waiting for
|
||||
// the daemon to start" (Idle → Idle) from "the connect flow was
|
||||
// cancelled externally" (Connecting → Idle, e.g. tray Disconnect
|
||||
// while the UI was Connecting). Reset whenever action returns to
|
||||
// null.
|
||||
const sawConnectingRef = useRef(false);
|
||||
|
||||
// Release the action latch when the daemon settles on a terminal
|
||||
// state for the user's intent — and, in the connect → NeedsLogin
|
||||
// case, hand off to driveLogin so the user doesn't have to click
|
||||
@@ -237,6 +245,13 @@ export const ConnectionStatusSwitch = () => {
|
||||
// .finally, not here: Login's internal Down makes the daemon flap
|
||||
// through Idle, which would otherwise look like a terminal state.
|
||||
useEffect(() => {
|
||||
if (action === null) {
|
||||
sawConnectingRef.current = false;
|
||||
return;
|
||||
}
|
||||
if (daemonState === "Connecting") {
|
||||
sawConnectingRef.current = true;
|
||||
}
|
||||
if (action === "connect") {
|
||||
if (needsLogin) {
|
||||
driveLogin();
|
||||
@@ -244,6 +259,14 @@ export const ConnectionStatusSwitch = () => {
|
||||
}
|
||||
if (daemonState === "Connected" || unreachable) {
|
||||
setAction(null);
|
||||
return;
|
||||
}
|
||||
// Cancelled externally (e.g. tray Disconnect during our
|
||||
// Connecting): the daemon went back to Idle after we'd
|
||||
// observed Connecting. Clear the latch so the UI stops
|
||||
// showing Connecting forever.
|
||||
if (sawConnectingRef.current && daemonState === "Idle") {
|
||||
setAction(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
52
client/ui/frontend/src/lib/errors.ts
Normal file
52
client/ui/frontend/src/lib/errors.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// Shared error formatter for native dialog bodies.
|
||||
//
|
||||
// The Go service layer (client/ui/services/connection.go classifyDaemonError)
|
||||
// wraps daemon errors in a ClientError struct exposed to the TS side as
|
||||
// {code, short, long}. Short is already localised (Go reads the current
|
||||
// preferences.Store language and resolves "error.<code>" via i18n.Bundle).
|
||||
// Long always carries the unwrapped raw daemon message so the operator can
|
||||
// see the JWT / mgm stack when the short text is too generic.
|
||||
//
|
||||
// Wails wraps Go-returned errors as Error({message, cause, kind}) where
|
||||
// .message holds the JSON-stringified payload and the structured object
|
||||
// lives on .cause — Object.keys(err) is empty in that case. We therefore
|
||||
// probe .cause first, then fall back to parsing .message as JSON, then
|
||||
// to plain .message text for callers that still hand us a raw Error.
|
||||
const extractClientError = (e: unknown): { short?: string; long?: string } | null => {
|
||||
if (!e || typeof e !== "object") return null;
|
||||
const withCause = e as { cause?: unknown; message?: unknown };
|
||||
if (withCause.cause && typeof withCause.cause === "object") {
|
||||
return withCause.cause as { short?: string; long?: string };
|
||||
}
|
||||
if (typeof withCause.message === "string") {
|
||||
const m = withCause.message.trim();
|
||||
if (m.startsWith("{") && m.endsWith("}")) {
|
||||
try {
|
||||
const parsed = JSON.parse(m);
|
||||
if (parsed && typeof parsed === "object") {
|
||||
if ("cause" in parsed && parsed.cause && typeof parsed.cause === "object") {
|
||||
return parsed.cause as { short?: string; long?: string };
|
||||
}
|
||||
return parsed as { short?: string; long?: string };
|
||||
}
|
||||
} catch {
|
||||
// not JSON — fall through to plain-message handling
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const formatErrorMessage = (e: unknown): string => {
|
||||
const ce = extractClientError(e);
|
||||
if (ce) {
|
||||
const short = typeof ce.short === "string" ? ce.short : "";
|
||||
const long = typeof ce.long === "string" ? ce.long : "";
|
||||
if (short && long && long !== short) {
|
||||
return `${short}\n\nDetails: ${long}`;
|
||||
}
|
||||
if (short) return short;
|
||||
}
|
||||
if (e instanceof Error) return e.message;
|
||||
return String(e);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { Events } from "@wailsio/runtime";
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
import { ClockIcon } from "lucide-react";
|
||||
import { Button } from "@/components/Button";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
@@ -9,10 +9,15 @@ import { DialogActions } from "@/components/DialogActions";
|
||||
import { DialogDescription } from "@/components/DialogDescription";
|
||||
import { DialogHeading } from "@/components/DialogHeading";
|
||||
import { SquareIcon } from "@/components/SquareIcon";
|
||||
import { Connection, Profiles as ProfilesSvc, WindowManager } from "@bindings/services";
|
||||
import {
|
||||
Connection,
|
||||
Profiles as ProfilesSvc,
|
||||
Session,
|
||||
WindowManager,
|
||||
} from "@bindings/services";
|
||||
import { useAutoSizeWindow } from "@/lib/useAutoSizeWindow";
|
||||
import { formatErrorMessage } from "@/lib/errors.ts";
|
||||
|
||||
const EVENT_TRIGGER_LOGIN = "trigger-login";
|
||||
const DEFAULT_SECONDS = 360;
|
||||
const WINDOW_WIDTH = 360;
|
||||
|
||||
@@ -35,6 +40,7 @@ export default function SessionAboutToExpireDialog() {
|
||||
}, [params]);
|
||||
|
||||
const [remaining, setRemaining] = useState(initialSeconds);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const expired = remaining <= 0;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -49,10 +55,38 @@ export default function SessionAboutToExpireDialog() {
|
||||
return () => window.clearInterval(id);
|
||||
}, [remaining]);
|
||||
|
||||
const stay = useCallback(() => {
|
||||
void Events.Emit(EVENT_TRIGGER_LOGIN);
|
||||
WindowManager.CloseSessionAboutToExpire().catch(console.error);
|
||||
}, []);
|
||||
// Mirrors tray.go::runExtendSession: starts the daemon SSO extend flow,
|
||||
// opens the browser for the user to sign in, blocks on the daemon until
|
||||
// the new deadline arrives. Tunnel stays up; success simply closes the
|
||||
// dialog, failure surfaces a native error dialog and leaves this one
|
||||
// open so the user can retry or logout.
|
||||
const stay = useCallback(async () => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const start = await Session.RequestExtend({ hint: "" });
|
||||
const uri = start.verificationUriComplete || start.verificationUri;
|
||||
if (uri) {
|
||||
try {
|
||||
await Connection.OpenURL(uri);
|
||||
} catch (e) {
|
||||
console.debug("OpenURL failed during extend", e);
|
||||
}
|
||||
}
|
||||
await Session.WaitExtend({
|
||||
deviceCode: start.deviceCode,
|
||||
userCode: start.userCode,
|
||||
});
|
||||
WindowManager.CloseSessionAboutToExpire().catch(console.error);
|
||||
} catch (e) {
|
||||
await Dialogs.Error({
|
||||
Title: t("sessionAboutToExpire.extendFailedTitle"),
|
||||
Message: formatErrorMessage(e),
|
||||
});
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [busy, t]);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
@@ -99,7 +133,7 @@ export default function SessionAboutToExpireDialog() {
|
||||
size={"md"}
|
||||
className={"w-full"}
|
||||
onClick={stay}
|
||||
disabled={expired}
|
||||
disabled={expired || busy}
|
||||
>
|
||||
{t("sessionAboutToExpire.stay")}
|
||||
</Button>
|
||||
@@ -108,6 +142,7 @@ export default function SessionAboutToExpireDialog() {
|
||||
size={"md"}
|
||||
className={"w-full"}
|
||||
onClick={logout}
|
||||
disabled={busy}
|
||||
>
|
||||
{t("sessionAboutToExpire.logout")}
|
||||
</Button>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@bindings/services";
|
||||
import type { DebugBundleResult } from "@bindings/services/models.js";
|
||||
import i18next from "@/lib/i18n";
|
||||
import { formatErrorMessage } from "@/lib/errors.ts";
|
||||
import { useProfile } from "@/modules/profile/ProfileContext.tsx";
|
||||
|
||||
const NETBIRD_UPLOAD_URL = "https://upload.debug.netbird.io/upload-url";
|
||||
@@ -158,7 +159,7 @@ export const useDebugBundle = () => {
|
||||
setStage({ kind: "idle" });
|
||||
await Dialogs.Error({
|
||||
Title: i18next.t("settings.error.debugBundleTitle"),
|
||||
Message: e instanceof Error ? e.message : String(e),
|
||||
Message: formatErrorMessage(e),
|
||||
});
|
||||
} finally {
|
||||
if (abortRef.current === ctrl) abortRef.current = null;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { HelpText } from "@/components/HelpText";
|
||||
import { Label } from "@/components/Label";
|
||||
import { loadLanguages } from "@/lib/i18n";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { formatErrorMessage } from "@/lib/errors";
|
||||
|
||||
// Flags live alongside the rest of the SVG flag library under
|
||||
// assets/flags/1x1 and are filename-matched to the language code
|
||||
@@ -91,7 +92,7 @@ export function LanguagePicker() {
|
||||
} catch (e) {
|
||||
await Dialogs.Error({
|
||||
Title: t("settings.error.saveTitle"),
|
||||
Message: e instanceof Error ? e.message : String(e),
|
||||
Message: formatErrorMessage(e),
|
||||
});
|
||||
} finally {
|
||||
setBusy(false);
|
||||
|
||||
@@ -13,9 +13,7 @@ import type { Config } from "@bindings/services/models.js";
|
||||
import i18next from "@/lib/i18n";
|
||||
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);
|
||||
import { formatErrorMessage as errorMessage } from "@/lib/errors.ts";
|
||||
|
||||
const SAVE_DEBOUNCE_MS = 400;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import i18next from "@/lib/i18n";
|
||||
import { useProfile } from "@/modules/profile/ProfileContext";
|
||||
import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { formatErrorMessage } from "@/lib/errors";
|
||||
|
||||
const DEFAULT_PROFILE = "default";
|
||||
|
||||
@@ -45,7 +46,7 @@ export function SettingsProfiles() {
|
||||
} catch (e) {
|
||||
await Dialogs.Error({
|
||||
Title: title,
|
||||
Message: e instanceof Error ? e.message : String(e),
|
||||
Message: formatErrorMessage(e),
|
||||
});
|
||||
} finally {
|
||||
setBusy(false);
|
||||
@@ -90,7 +91,7 @@ export function SettingsProfiles() {
|
||||
} catch (e) {
|
||||
await Dialogs.Error({
|
||||
Title: i18next.t("profile.error.createTitle"),
|
||||
Message: e instanceof Error ? e.message : String(e),
|
||||
Message: formatErrorMessage(e),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user