From 58778807890fcf21f6ade4cdaae3bc176eeac780 Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Fri, 5 Jun 2026 13:16:39 +0200 Subject: [PATCH] replace native confirm dialog with modals in settings --- client/ui/frontend/CLAUDE.md | 8 +- .../ui/frontend/src/components/SquareIcon.tsx | 26 ++++- .../src/components/buttons/Button.tsx | 2 +- .../src/components/dialog/ConfirmModal.tsx | 110 ++++++++++++++++++ .../frontend/src/components/dialog/Dialog.tsx | 2 +- .../frontend/src/contexts/DialogContext.tsx | 74 ++++++++++++ .../ui/frontend/src/hooks/useManagementUrl.ts | 45 +++---- client/ui/frontend/src/layouts/AppLayout.tsx | 21 ++-- .../src/modules/profiles/ProfilesTab.tsx | 50 +++----- client/ui/i18n/locales/de/common.json | 14 +-- client/ui/i18n/locales/en/common.json | 14 +-- client/ui/i18n/locales/hu/common.json | 14 +-- 12 files changed, 279 insertions(+), 101 deletions(-) create mode 100644 client/ui/frontend/src/components/dialog/ConfirmModal.tsx create mode 100644 client/ui/frontend/src/contexts/DialogContext.tsx diff --git a/client/ui/frontend/CLAUDE.md b/client/ui/frontend/CLAUDE.md index 6bc517d10..5f5045387 100644 --- a/client/ui/frontend/CLAUDE.md +++ b/client/ui/frontend/CLAUDE.md @@ -33,7 +33,7 @@ React 18 + TS 5.7 (`strict`, `noImplicitAny: false`) + Vite 6 + Tailwind 3 (`dar In `app.tsx` the four dialog routes are nested under a parent `` so the table reads as a tree, not a flat list. The Go side mirrors the prefix — `WindowManager` opens windows at `/#/dialog/`. The `dialog` group has no shared layout component; it's purely a URL grouping. -`AppLayout` is the only in-window layout. It mounts the shared provider stack (`StatusProvider → ProfileProvider → DebugBundleProvider → ClientVersionProvider`) inside a `relative flex h-full flex-col` shell and renders ``. Both `Main` (route `/`) and `Settings` (route `/settings`) sit under it. Order matters: `SettingsContext` depends on `ProfileContext`, `ClientVersionContext` reads `StatusContext` events. `StatusProvider` (in `contexts/StatusContext.tsx`) owns the single `Peers.Get` + `netbird:status` subscription, exposes `{ status, error, refresh, isReady, isDaemonAvailable, isDaemonUnavailable }`, **and only renders its children when the daemon is reachable** — until the first `Peers.Get` resolves and on `DaemonUnavailable` it short-circuits to just the `` (also owned by the provider). The consequence: every context downstream (`ProfileProvider`, `DebugBundleProvider`, `ClientVersionProvider`) can assume the daemon is reachable at mount time — no per-context `useStatus` gating. When the daemon flips back to unavailable the whole downstream subtree unmounts and remounts fresh once it returns. `ClientVersionProvider` no longer paints any inline overlay; install progress lives in its own auxiliary window (see `/install-progress` route). +`AppLayout` is the only in-window layout. It mounts the shared provider stack (`DialogProvider → StatusProvider → ProfileProvider → DebugBundleProvider → ClientVersionProvider`) inside a `relative flex h-full flex-col` shell and renders ``. `DialogProvider` is outermost (and outside the daemon-availability gate) so `useConfirm()` works everywhere regardless of daemon state. Both `Main` (route `/`) and `Settings` (route `/settings`) sit under it. Order matters: `SettingsContext` depends on `ProfileContext`, `ClientVersionContext` reads `StatusContext` events. `StatusProvider` (in `contexts/StatusContext.tsx`) owns the single `Peers.Get` + `netbird:status` subscription, exposes `{ status, error, refresh, isReady, isDaemonAvailable, isDaemonUnavailable }`, **and only renders its children when the daemon is reachable** — until the first `Peers.Get` resolves and on `DaemonUnavailable` it short-circuits to just the `` (also owned by the provider). The consequence: every context downstream (`ProfileProvider`, `DebugBundleProvider`, `ClientVersionProvider`) can assume the daemon is reachable at mount time — no per-context `useStatus` gating. When the daemon flips back to unavailable the whole downstream subtree unmounts and remounts fresh once it returns. `ClientVersionProvider` no longer paints any inline overlay; install progress lives in its own auxiliary window (see `/install-progress` route). Page-specific chrome lives next to the page, not in the layout: - **`pages/main/Main.tsx`** owns the `Header`, `ViewModeProvider`, and `NavSectionProvider`. All three are main-window-only: @@ -58,11 +58,11 @@ Page-specific chrome lives next to the page, not in the layout: - `modules/welcome/` — first-launch onboarding dialog window. `WelcomeDialog.tsx` is the orchestrator (state machine over `tray → management → finish`); each step has its own file (`WelcomeStepTray`, `WelcomeStepManagement`). The `management` step is conditionally rendered: only when active profile is `"default"`, the profile email is empty, and the current management URL is cloud-default-or-empty (`shouldShowManagementStep` in the orchestrator). Reachability of self-hosted URLs is a soft warning via `hooks/useManagementUrl.ts checkManagementUrlReachable`; the user can re-click Continue to proceed despite a failed check. No login step — once the dialog closes, the user lands in the main window and clicks Connect there, which runs the connect toggle's local `startLogin` orchestrator. Note: there's no `modules/daemon-status/` or `modules/debug-bundle/` folder. The daemon-status overlay is a generic presentational component (`components/empty-state/DaemonUnavailableOverlay.tsx`) and `useDebugBundle` is inlined into `contexts/DebugBundleContext.tsx` — both folders would be empty otherwise. -- `contexts/` — every React context in the app lives here as a flat file (`StatusContext`, `ProfileContext`, `DebugBundleContext`, `ClientVersionContext`, `SettingsContext`, `NetworksContext`, `PeerDetailContext`, `ViewModeContext`, `NavSectionContext`). Single mental model: "where is the X context? `contexts/XContext.tsx`." +- `contexts/` — every React context in the app lives here as a flat file (`StatusContext`, `ProfileContext`, `DebugBundleContext`, `ClientVersionContext`, `SettingsContext`, `NetworksContext`, `PeerDetailContext`, `ViewModeContext`, `NavSectionContext`, `DialogContext`). Single mental model: "where is the X context? `contexts/XContext.tsx`." - `components/` — presentational primitives, no domain coupling. Grouped by family: - `components/buttons/` — `Button`, `IconButton`. - `components/inputs/` — `Input`, `SearchInput`. - - `components/dialog/` — `Dialog`, `DialogActions`, `DialogDescription`, `DialogHeading`, `ConfirmDialog`. + - `components/dialog/` — `Dialog`, `DialogActions`, `DialogDescription`, `DialogHeading`, `ConfirmDialog` (window-based dialog layout primitive), `ConfirmModal` (in-app Radix confirmation modal; usually driven via `useConfirm()` rather than rendered directly). - `components/switches/` — `SwitchItem`, `SwitchItemGroup`, `ToggleSwitch`, `FancyToggleSwitch`. - `components/typography/` — `Label`, `HelpText`. - `components/empty-state/` — `EmptyState`, `NoResults`, `NotConnectedState`. @@ -182,7 +182,7 @@ This is the only SSO entry point used by the polished Main UI. There is no `/log **Always go through `src/lib/dialogs.ts`** — `errorDialog` / `warningDialog` / `infoDialog` / `questionDialog`, not `Dialogs.*` from `@wailsio/runtime` directly. These thin wrappers force `Detached: true` on Windows (no-op elsewhere, and any caller-supplied `Detached` wins). A native Windows `MessageBox` attached to a parent window sets that window `WS_DISABLED` for its lifetime and re-enables it on dismissal; when the parent is the main window — whose `WindowClosing` hook hides instead of closes (`main.go`) — the enable/hide sequence races and leaves the window unable to process its close (X) button afterwards. Detaching gives the box a NULL owner so no window is ever disabled. macOS keeps the attached sheet-style presentation. The wrappers re-export the same option shape, so call sites are otherwise unchanged. -Errors → `errorDialog` with action-named title ("Save Settings Failed", not "Error"). Confirmations → `warningDialog` with explicit `Buttons` — compare against the **Label string**, not an index. **Skip** native dialogs for inline form validation, transient link errors on the dashboard, and "partial success" notes inside an otherwise-OK flow. Full API + per-OS notes in `../WAILS-DIALOGS.md`; full convention rationale in `../CLAUDE.md`. +Errors → `errorDialog` with action-named title ("Save Settings Failed", not "Error"). For **confirmations inside an app window** (the polished surfaces), prefer the in-app `useConfirm()` from `contexts/DialogContext.tsx` over the native `warningDialog` — `const ok = await confirm({ title, description, confirmLabel, danger? })` resolves to a boolean. It renders a single shared `ConfirmModal` (left-aligned title + multi-line description, Cancel/confirm footer) mounted at the provider level, so call sites don't each wire up their own modal + open state. Used by the Profiles tab (switch/deregister/delete) and the management-server cloud switch (`useManagementUrl`). Reserve the native `warningDialog` (compare against the **Label string**, not an index) for confirmations raised outside a normal app window (tray-driven flows, etc.). **Skip** native dialogs for inline form validation, transient link errors on the dashboard, and "partial success" notes inside an otherwise-OK flow. Full API + per-OS notes in `../WAILS-DIALOGS.md`; full convention rationale in `../CLAUDE.md`. ## Tailwind tokens diff --git a/client/ui/frontend/src/components/SquareIcon.tsx b/client/ui/frontend/src/components/SquareIcon.tsx index 469655eec..86db78830 100644 --- a/client/ui/frontend/src/components/SquareIcon.tsx +++ b/client/ui/frontend/src/components/SquareIcon.tsx @@ -3,18 +3,36 @@ import { LucideProps } from "lucide-react"; import { cn } from "@/lib/cn"; // SquareIcon is the rounded-square icon tile used by dialog-style surfaces -// (ConfirmDialog, etc.). Renders a bordered dark tile with the provided -// lucide icon centered inside. +// (ConfirmDialog, etc.). Renders a bordered tile with the provided lucide +// icon centered inside. The `tone` selects the semantic colour scheme — +// `default` keeps the neutral dark tile; info/warning/danger tint the tile, +// border and icon to match the action's severity. +export type SquareIconTone = "default" | "info" | "warning" | "danger"; + +const toneClass: Record = { + default: "bg-nb-gray-920 border-nb-gray-900 text-white", + info: "bg-sky-950 border-sky-500 text-sky-100", + warning: "bg-netbird-950 border-netbird text-netbird", + danger: "bg-red-950 border-red-500 text-red-500", +}; + type SquareIconProps = { icon: ComponentType; iconSize?: number; + tone?: SquareIconTone; className?: string; }; -export const SquareIcon = ({ icon: Icon, iconSize = 20, className }: SquareIconProps) => ( +export const SquareIcon = ({ + icon: Icon, + iconSize = 20, + tone = "default", + className, +}: SquareIconProps) => (
diff --git a/client/ui/frontend/src/components/buttons/Button.tsx b/client/ui/frontend/src/components/buttons/Button.tsx index 138540619..3976ce4fc 100644 --- a/client/ui/frontend/src/components/buttons/Button.tsx +++ b/client/ui/frontend/src/components/buttons/Button.tsx @@ -93,7 +93,7 @@ const buttonVariants = cva( }, size: { xs: "text-xs py-2.5 px-3.5", - xs2: "text-[0.78rem] py-2 px-4", + xs2: "text-[0.78rem] py-[1.1rem] px-5 leading-[0]", sm: "text-sm py-[9px] px-4", md: "py-[9px] px-4", lg: "text-lg py-[9px] px-4", diff --git a/client/ui/frontend/src/components/dialog/ConfirmModal.tsx b/client/ui/frontend/src/components/dialog/ConfirmModal.tsx new file mode 100644 index 000000000..0f33ba67d --- /dev/null +++ b/client/ui/frontend/src/components/dialog/ConfirmModal.tsx @@ -0,0 +1,110 @@ +import { ReactNode, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import * as Dialog from "@/components/dialog/Dialog"; +import { Button } from "@/components/buttons/Button"; +import { DialogHeading } from "@/components/dialog/DialogHeading"; +import { DialogDescription } from "@/components/dialog/DialogDescription"; +import { DialogActions } from "@/components/dialog/DialogActions"; + +// ConfirmModal is the shared in-app confirmation modal — a left-aligned +// title + (optionally multi-line) description with Cancel / confirm buttons +// in the footer. It's the in-window counterpart to the native warningDialog. +// +// Most call sites should not render this directly: use the imperative +// `useConfirm()` from DialogContext (`await confirm({...})`), which mounts a +// single instance at the provider level. Render ConfirmModal yourself only +// when you need bespoke control over its open/busy lifecycle. +type ConfirmModalProps = { + open: boolean; + title: ReactNode; + description: ReactNode; + /** Confirm button label. */ + confirmLabel: string; + /** Cancel button label; defaults to the shared "Cancel" string. */ + cancelLabel?: string; + /** Use the destructive (red) confirm button variant. */ + danger?: boolean; + /** Disable the buttons (and ignore dismiss) while an action runs. */ + busy?: boolean; + onConfirm: () => void; + onCancel: () => void; +}; + +export const ConfirmModal = ({ + open, + title, + description, + confirmLabel, + cancelLabel, + danger = false, + busy = false, + onConfirm, + onCancel, +}: ConfirmModalProps) => { + const { t } = useTranslation(); + + // Retain the last shown content so it stays rendered through Radix's + // close animation instead of blanking out the instant the caller clears + // its state on close. + type Snapshot = Pick & { + cancelLabel: string; + }; + const [snapshot, setSnapshot] = useState(null); + const resolvedCancel = cancelLabel ?? t("common.cancel"); + useEffect(() => { + if (open) { + setSnapshot({ title, description, confirmLabel, cancelLabel: resolvedCancel, danger }); + } + }, [open, title, description, confirmLabel, resolvedCancel, danger]); + + const view = open + ? { title, description, confirmLabel, cancelLabel: resolvedCancel, danger } + : snapshot; + + return ( + { + if (!next && !busy) onCancel(); + }} + > + e.preventDefault()} + > + {view && ( +
+
+ {view.title} + + {view.description} + +
+ + + + + +
+ )} +
+
+ ); +}; diff --git a/client/ui/frontend/src/components/dialog/Dialog.tsx b/client/ui/frontend/src/components/dialog/Dialog.tsx index 744ac3c4e..77d176b41 100644 --- a/client/ui/frontend/src/components/dialog/Dialog.tsx +++ b/client/ui/frontend/src/components/dialog/Dialog.tsx @@ -15,7 +15,7 @@ const Overlay = forwardRef< ref={ref} className={cn( "fixed inset-0 z-50 grid items-center justify-items-center overflow-y-auto px-10 py-16", - "bg-black/40 backdrop-blur-sm", + "bg-black/60", "data-[state=open]:animate-in data-[state=closed]:animate-out", "data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0", "duration-150 ease-out", diff --git a/client/ui/frontend/src/contexts/DialogContext.tsx b/client/ui/frontend/src/contexts/DialogContext.tsx new file mode 100644 index 000000000..bf421fc29 --- /dev/null +++ b/client/ui/frontend/src/contexts/DialogContext.tsx @@ -0,0 +1,74 @@ +import { createContext, ReactNode, useCallback, useContext, useRef, useState } from "react"; +import { ConfirmModal } from "@/components/dialog/ConfirmModal"; + +// DialogContext exposes an imperative `confirm(...)` that resolves to a +// boolean — the in-app equivalent of the native warningDialog promise. The +// single lives here at the provider level, so call sites +// just `await confirm({...})` instead of each wiring up their own modal +// component + open/busy state. +// +// const confirm = useConfirm(); +// if (await confirm({ title, description, confirmLabel })) { …do it… } +// +// Mounted once (outermost in AppLayout) so it's available in every in-window +// route across both the main and settings windows. +export type ConfirmOptions = { + title: ReactNode; + description: ReactNode; + confirmLabel: string; + /** Defaults to the shared "Cancel" string inside ConfirmModal. */ + cancelLabel?: string; + /** Use the destructive (red) confirm button variant. */ + danger?: boolean; +}; + +type DialogContextValue = { + confirm: (options: ConfirmOptions) => Promise; +}; + +const DialogContext = createContext(null); + +export function DialogProvider({ children }: { children: ReactNode }) { + const [open, setOpen] = useState(false); + const [options, setOptions] = useState(null); + const resolverRef = useRef<((result: boolean) => void) | null>(null); + + const confirm = useCallback((opts: ConfirmOptions) => { + setOptions(opts); + setOpen(true); + return new Promise((resolve) => { + resolverRef.current = resolve; + }); + }, []); + + // Resolve the pending promise and start the close animation. The options + // stay in state so ConfirmModal still has content to render while it + // animates out. + const settle = (result: boolean) => { + resolverRef.current?.(result); + resolverRef.current = null; + setOpen(false); + }; + + return ( + + {children} + settle(true)} + onCancel={() => settle(false)} + /> + + ); +} + +export const useConfirm = () => { + const ctx = useContext(DialogContext); + if (!ctx) throw new Error("useConfirm must be used within a DialogProvider"); + return ctx.confirm; +}; diff --git a/client/ui/frontend/src/hooks/useManagementUrl.ts b/client/ui/frontend/src/hooks/useManagementUrl.ts index 405b7aa05..192783896 100644 --- a/client/ui/frontend/src/hooks/useManagementUrl.ts +++ b/client/ui/frontend/src/hooks/useManagementUrl.ts @@ -1,7 +1,7 @@ -import { useEffect, useRef, useState } from "react"; -import { warningDialog } from "@/lib/dialogs.ts"; -import i18next from "@/lib/i18n"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useSettings } from "@/contexts/SettingsContext.tsx"; +import { useConfirm } from "@/contexts/DialogContext.tsx"; export const CLOUD_MANAGEMENT_URL = "https://api.netbird.io:443"; @@ -77,6 +77,8 @@ function modeFromUrl(url: string): ManagementMode { } export function useManagementUrl() { + const { t } = useTranslation(); + const confirm = useConfirm(); const { config, saveField } = useSettings(); const [mode, setModeState] = useState( modeFromUrl(config.managementUrl), @@ -84,11 +86,6 @@ export function useManagementUrl() { const [url, setUrl] = useState( config.managementUrl === CLOUD_MANAGEMENT_URL ? "" : config.managementUrl, ); - // Guard against double-showing the cloud-switch confirmation when the - // user toggles the segmented control multiple times before the prior - // Dialogs.Warning promise resolves. Without it each click queues a - // fresh native dialog and the user sees them stack up. - const switchConfirmOpenRef = useRef(false); useEffect(() => { setModeState(modeFromUrl(config.managementUrl)); @@ -97,34 +94,22 @@ export function useManagementUrl() { } }, [config.managementUrl]); - const setMode = (next: ManagementMode) => { + const setMode = async (next: ManagementMode) => { if ( next === ManagementMode.Cloud && config.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. - if (switchConfirmOpenRef.current) return; - switchConfirmOpenRef.current = true; - const cancelLabel = i18next.t("common.cancel"); - const confirmLabel = i18next.t("settings.general.management.switchCloudConfirm"); - void warningDialog({ - Title: i18next.t("settings.general.management.switchCloudTitle"), - Message: i18next.t("settings.general.management.switchCloudMessage"), - Buttons: [ - { Label: cancelLabel, IsCancel: true, IsDefault: true }, - { Label: confirmLabel }, - ], - }) - .then((result) => { - if (result !== confirmLabel) return; - setModeState(ManagementMode.Cloud); - void saveField("managementUrl", CLOUD_MANAGEMENT_URL); - }) - .finally(() => { - switchConfirmOpenRef.current = false; - }); + // reconnect/re-login. Confirm via the in-app modal before applying. + const ok = await confirm({ + title: t("settings.general.management.switchCloudTitle"), + description: t("settings.general.management.switchCloudMessage"), + confirmLabel: t("settings.general.management.switchCloudConfirm"), + }); + if (!ok) return; + setModeState(ManagementMode.Cloud); + void saveField("managementUrl", CLOUD_MANAGEMENT_URL); return; } setModeState(next); diff --git a/client/ui/frontend/src/layouts/AppLayout.tsx b/client/ui/frontend/src/layouts/AppLayout.tsx index 0dcdebd42..e4711d5e2 100644 --- a/client/ui/frontend/src/layouts/AppLayout.tsx +++ b/client/ui/frontend/src/layouts/AppLayout.tsx @@ -3,6 +3,7 @@ import { ClientVersionProvider } from "@/contexts/ClientVersionContext.tsx"; import { StatusProvider } from "@/contexts/StatusContext.tsx"; import { DebugBundleProvider } from "@/contexts/DebugBundleContext.tsx"; import { ProfileProvider } from "@/contexts/ProfileContext.tsx"; +import { DialogProvider } from "@/contexts/DialogContext.tsx"; // Shared shell for every in-window route (main + settings). Owns the daemon- // availability gate (via StatusProvider) and the providers every page needs. @@ -14,15 +15,17 @@ import { ProfileProvider } from "@/contexts/ProfileContext.tsx"; export const AppLayout = () => { return (
- - - - - - - - - + + + + + + + + + + +
); }; diff --git a/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx b/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx index 20d6d4c66..7a05a98ee 100644 --- a/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx +++ b/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx @@ -1,6 +1,6 @@ import { useLayoutEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { errorDialog, warningDialog } from "@/lib/dialogs.ts"; +import { errorDialog } from "@/lib/dialogs.ts"; import { CircleMinus, LogIn, PlusCircle, Trash2, UserCircle } from "lucide-react"; import type { Profile } from "@bindings/services/models.js"; import { Badge } from "@/components/Badge"; @@ -11,6 +11,7 @@ import { pickProfileIcon } from "@/modules/profiles/ProfileAvatar"; import { Tooltip } from "@/components/Tooltip"; import i18next from "@/lib/i18n"; import { useProfile } from "@/contexts/ProfileContext"; +import { useConfirm } from "@/contexts/DialogContext"; import { SectionGroup, SettingsBottomBar } from "@/modules/settings/SettingsSection.tsx"; import { cn } from "@/lib/cn"; import { formatErrorMessage } from "@/lib/errors"; @@ -29,6 +30,7 @@ export function ProfilesTab() { logoutProfile, } = useProfile(); + const confirm = useConfirm(); const [newOpen, setNewOpen] = useState(false); const [busy, setBusy] = useState(false); const tabRootRef = useRef(null); @@ -68,49 +70,35 @@ export function ProfilesTab() { }; const handleSwitch = async (name: string) => { - const cancelLabel = i18next.t("common.cancel"); - const confirmLabel = i18next.t("profile.switch.confirm"); - const result = await warningDialog({ - Title: i18next.t("profile.switch.title"), - Message: i18next.t("profile.switch.message", { name }), - Buttons: [ - { Label: cancelLabel, IsCancel: true }, - { Label: confirmLabel, IsDefault: true }, - ], + const ok = await confirm({ + title: t("profile.switch.title", { name }), + description: t("profile.switch.message", { name }), + confirmLabel: t("profile.switch.confirm"), }); - if (result !== confirmLabel) return; + if (!ok) return; await guarded(i18next.t("profile.error.switchTitle"), () => switchProfile(name)); scrollTabToTop(); }; const handleDeregister = async (name: string) => { - const cancelLabel = i18next.t("common.cancel"); - const confirmLabel = i18next.t("profile.deregister.confirm"); - const result = await warningDialog({ - Title: i18next.t("profile.deregister.title"), - Message: i18next.t("profile.deregister.message", { name }), - Buttons: [ - { Label: cancelLabel, IsCancel: true }, - { Label: confirmLabel, IsDefault: true }, - ], + const ok = await confirm({ + title: t("profile.deregister.title", { name }), + description: t("profile.deregister.message", { name }), + confirmLabel: t("profile.deregister.confirm"), }); - if (result !== confirmLabel) return; + if (!ok) return; void guarded(i18next.t("profile.error.deregisterTitle"), () => logoutProfile(name)); }; const handleDelete = async (name: string) => { if (name === DEFAULT_PROFILE) return; - const cancelLabel = i18next.t("common.cancel"); - const confirmLabel = i18next.t("common.delete"); - const result = await warningDialog({ - Title: i18next.t("profile.delete.title"), - Message: i18next.t("profile.delete.message", { name }), - Buttons: [ - { Label: cancelLabel, IsCancel: true }, - { Label: confirmLabel, IsDefault: true }, - ], + const ok = await confirm({ + title: t("profile.delete.title", { name }), + description: t("profile.delete.message", { name }), + confirmLabel: t("common.delete"), + danger: true, }); - if (result !== confirmLabel) return; + if (!ok) return; void guarded(i18next.t("profile.error.deleteTitle"), () => removeProfile(name)); }; diff --git a/client/ui/i18n/locales/de/common.json b/client/ui/i18n/locales/de/common.json index ab9bc7971..1d1e26a81 100644 --- a/client/ui/i18n/locales/de/common.json +++ b/client/ui/i18n/locales/de/common.json @@ -110,14 +110,14 @@ "profile.dialog.description": "Mit Profilen können Sie mehrere NetBird-Verbindungen nebeneinander verwalten. Geben Sie Ihrem Profil einen aussagekräftigen Namen.", "profile.dialog.placeholder": "z. B. Arbeit", - "profile.switch.title": "Profil wechseln", - "profile.switch.message": "Zu \"{name}\" wechseln? Ihr aktuelles Profil wird getrennt.", + "profile.switch.title": "Zu Profil \"{name}\" wechseln?", + "profile.switch.message": "Sind Sie sicher, dass Sie das Profil wechseln möchten?\nIhr aktuelles Profil wird getrennt.", "profile.switch.confirm": "Wechseln", - "profile.deregister.title": "Profil abmelden", - "profile.deregister.message": "Sind Sie sicher, dass Sie \"{name}\" abmelden möchten? Sie müssen sich erneut anmelden, um es zu nutzen.", + "profile.deregister.title": "Profil \"{name}\" abmelden?", + "profile.deregister.message": "Sind Sie sicher, dass Sie dieses Profil abmelden möchten?\nSie müssen sich erneut anmelden, um es zu nutzen.", "profile.deregister.confirm": "Abmelden", - "profile.delete.title": "Profil löschen", - "profile.delete.message": "Sind Sie sicher, dass Sie \"{name}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "profile.delete.title": "Profil \"{name}\" löschen?", + "profile.delete.message": "Sind Sie sicher, dass Sie dieses Profil löschen möchten?\nDiese Aktion kann nicht rückgängig gemacht werden.", "profile.delete.disabledActive": "Aktive Profile können nicht gelöscht werden. Wechseln Sie zu einem anderen Profil, bevor Sie dieses löschen.", "profile.delete.disabledDefault": "Das Standardprofil kann nicht gelöscht werden.", "profile.error.switchTitle": "Profilwechsel fehlgeschlagen", @@ -159,7 +159,7 @@ "settings.general.management.urlPlaceholder": "https://netbird.selfhosted.com:443", "settings.general.management.urlError": "Bitte geben Sie eine gültige URL ein, z. B. https://netbird.selfhosted.com:443", "settings.general.management.switchCloudTitle": "Zu NetBird Cloud wechseln?", - "settings.general.management.switchCloudMessage": "Dadurch wird die Verbindung zu Ihrem self-hosted Management-Server getrennt und eine neue Verbindung zu NetBird Cloud hergestellt. Möglicherweise müssen Sie sich erneut anmelden.", + "settings.general.management.switchCloudMessage": "Dies trennt die Verbindung zu Ihrem self-hosted Server.\nMöglicherweise müssen Sie sich erneut anmelden.", "settings.general.management.switchCloudConfirm": "Zu Cloud wechseln", "settings.network.section.connectivity": "Konnektivität", diff --git a/client/ui/i18n/locales/en/common.json b/client/ui/i18n/locales/en/common.json index b3f693d5a..87d784829 100644 --- a/client/ui/i18n/locales/en/common.json +++ b/client/ui/i18n/locales/en/common.json @@ -118,14 +118,14 @@ "header.menu.advancedView": "Advanced View", "header.menu.updateAvailable": "Update Available", - "profile.switch.title": "Switch Profile", - "profile.switch.message": "Switch to \"{name}\"? Your current profile will be disconnected.", + "profile.switch.title": "Switch Profile to \"{name}\"?", + "profile.switch.message": "Are you sure you want to switch profiles?\nYour current profile will be disconnected.", "profile.switch.confirm": "Switch", - "profile.deregister.title": "Deregister Profile", - "profile.deregister.message": "Are you sure you want to deregister \"{name}\"? You will need to log in again to use it.", + "profile.deregister.title": "Deregister Profile \"{name}\"?", + "profile.deregister.message": "Are you sure you want to deregister this profile?\nYou will need to log in again to use it.", "profile.deregister.confirm": "Deregister", - "profile.delete.title": "Delete Profile", - "profile.delete.message": "Are you sure you want to delete \"{name}\"? This action cannot be undone.", + "profile.delete.title": "Delete Profile \"{name}\"?", + "profile.delete.message": "Are you sure you want to delete this profile?\nThis action cannot be undone.", "profile.delete.disabledActive": "Active profiles cannot be deleted. Switch to a different one before deleting this profile.", "profile.delete.disabledDefault": "The default profile cannot be deleted.", "profile.error.switchTitle": "Switch Profile Failed", @@ -182,7 +182,7 @@ "settings.general.management.urlPlaceholder": "https://netbird.selfhosted.com:443", "settings.general.management.urlError": "Please enter a valid URL, e.g., https://netbird.selfhosted.com:443", "settings.general.management.switchCloudTitle": "Switch to NetBird Cloud?", - "settings.general.management.switchCloudMessage": "This will disconnect from your self-hosted management server and reconnect to NetBird Cloud. You may need to log in again.", + "settings.general.management.switchCloudMessage": "This disconnects your self-hosted server.\nYou may need to log in again.", "settings.general.management.switchCloudConfirm": "Switch to Cloud", "settings.network.section.connectivity": "Connectivity", diff --git a/client/ui/i18n/locales/hu/common.json b/client/ui/i18n/locales/hu/common.json index 38e2b6434..d652f4938 100644 --- a/client/ui/i18n/locales/hu/common.json +++ b/client/ui/i18n/locales/hu/common.json @@ -110,14 +110,14 @@ "profile.dialog.description": "A profilok lehetővé teszik, hogy különálló NetBird-kapcsolatokat tartson egymás mellett. Adjon profiljának egy könnyen megjegyezhető nevet.", "profile.dialog.placeholder": "pl. Munka", - "profile.switch.title": "Profilváltás", - "profile.switch.message": "Átvált erre: \"{name}\"? Az aktuális profilja le lesz választva.", + "profile.switch.title": "Váltás a(z) \"{name}\" profilra?", + "profile.switch.message": "Biztosan profilt szeretne váltani?\nAz aktuális profilja le lesz választva.", "profile.switch.confirm": "Váltás", - "profile.deregister.title": "Profil leválasztása", - "profile.deregister.message": "Biztosan le szeretné választani a következőt: \"{name}\"? Újra be kell jelentkeznie a használatához.", + "profile.deregister.title": "\"{name}\" profil leválasztása?", + "profile.deregister.message": "Biztosan le szeretné választani ezt a profilt?\nÚjra be kell jelentkeznie a használatához.", "profile.deregister.confirm": "Leválasztás", - "profile.delete.title": "Profil törlése", - "profile.delete.message": "Biztosan törölni szeretné a következőt: \"{name}\"? Ez a művelet nem vonható vissza.", + "profile.delete.title": "\"{name}\" profil törlése?", + "profile.delete.message": "Biztosan törölni szeretné ezt a profilt?\nEz a művelet nem vonható vissza.", "profile.delete.disabledActive": "Aktív profilok nem törölhetők. Váltson másik profilra, mielőtt törölné ezt.", "profile.delete.disabledDefault": "Az alapértelmezett profil nem törölhető.", "profile.error.switchTitle": "Profilváltás sikertelen", @@ -159,7 +159,7 @@ "settings.general.management.urlPlaceholder": "https://netbird.selfhosted.com:443", "settings.general.management.urlError": "Adjon meg egy érvényes URL-t, pl. https://netbird.selfhosted.com:443", "settings.general.management.switchCloudTitle": "Átváltás a NetBird Cloudra?", - "settings.general.management.switchCloudMessage": "Ez megszünteti a kapcsolatot a saját üzemeltetésű kezelőszerverrel, és újra csatlakozik a NetBird Cloudhoz. Lehet, hogy újra be kell jelentkeznie.", + "settings.general.management.switchCloudMessage": "Ez leválasztja a saját üzemeltetésű kiszolgálót.\nLehet, hogy újra be kell jelentkeznie.", "settings.general.management.switchCloudConfirm": "Váltás a felhőre", "settings.network.section.connectivity": "Kapcsolódás",