From efd874efac4b7ba1e9b22f678a66574efc4ae9a6 Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Fri, 5 Jun 2026 14:38:00 +0200 Subject: [PATCH] add cloud / selfhosted segment in profile creation --- client/ui/frontend/CLAUDE.md | 2 +- .../src/components/buttons/Button.tsx | 23 +- .../frontend/src/components/inputs/Input.tsx | 22 +- .../ui/frontend/src/hooks/useManagementUrl.ts | 29 ++- .../modules/profiles/ProfileCreationModal.tsx | 206 ++++++++++++++---- .../src/modules/profiles/ProfilesTab.tsx | 79 ++++--- .../src/modules/settings/SettingsGeneral.tsx | 18 +- .../modules/welcome/WelcomeStepManagement.tsx | 13 +- client/ui/i18n/locales/de/common.json | 10 +- client/ui/i18n/locales/en/common.json | 10 +- client/ui/i18n/locales/hu/common.json | 10 +- 11 files changed, 328 insertions(+), 94 deletions(-) diff --git a/client/ui/frontend/CLAUDE.md b/client/ui/frontend/CLAUDE.md index 5f5045387..9714df36c 100644 --- a/client/ui/frontend/CLAUDE.md +++ b/client/ui/frontend/CLAUDE.md @@ -54,7 +54,7 @@ Page-specific chrome lives next to the page, not in the layout: - `modules/login/` — `LoginWaitingForBrowserDialog.tsx` (the SSO browser-wait window). - `modules/session/` — `SessionExpiredDialog.tsx` and `SessionAboutToExpireDialog.tsx` (session lifecycle dialog windows). - `modules/auto-update/` — `UpdateInProgressDialog.tsx`, `UpdateBadge.tsx`, `UpdateVersionCard.tsx`. Context lives in `contexts/`. - - `modules/profiles/` — `ProfileAvatar.tsx`, `ProfileDropdown.tsx`, `ProfileCreationModal.tsx`, `ProfilesTab.tsx`. Context lives in `contexts/`. + - `modules/profiles/` — `ProfileAvatar.tsx`, `ProfileDropdown.tsx`, `ProfileCreationModal.tsx`, `ProfilesTab.tsx`. Context lives in `contexts/`. The creation modal collects both the profile name and a management target (Cloud vs self-hosted + URL, reusing `ManagementServerSwitch` + the `useManagementUrl` helpers like the onboarding step); `ProfilesTab.handleCreate` adds the profile, `Settings.SetConfig`s the chosen `managementUrl` onto it (keyed by profile name, before switching), then switches to it. Row actions (switch/deregister/delete) confirm via the shared `useConfirm()` modal. - `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. diff --git a/client/ui/frontend/src/components/buttons/Button.tsx b/client/ui/frontend/src/components/buttons/Button.tsx index 3976ce4fc..02b009203 100644 --- a/client/ui/frontend/src/components/buttons/Button.tsx +++ b/client/ui/frontend/src/components/buttons/Button.tsx @@ -1,5 +1,5 @@ import { cva, VariantProps } from "class-variance-authority"; -import { Check, Copy } from "lucide-react"; +import { Check, Copy, Loader2 } from "lucide-react"; import { ButtonHTMLAttributes, forwardRef, useState } from "react"; import { cn } from "@/lib/cn"; @@ -10,6 +10,10 @@ interface ButtonProps extends ButtonHTMLAttributes, ButtonVar disabled?: boolean; stopPropagation?: boolean; copy?: string; + // When true, the content is replaced by a centered spinner while keeping + // the button's rendered width/height (the content stays in the layout, + // just hidden). Also disables the button. + loading?: boolean; } const buttonVariants = cva( @@ -93,7 +97,7 @@ const buttonVariants = cva( }, size: { xs: "text-xs py-2.5 px-3.5", - xs2: "text-[0.78rem] py-[1.1rem] px-5 leading-[0]", + xs2: "text-[0.78rem] py-[1.1rem] px-4 leading-[0]", sm: "text-sm py-[9px] px-4", md: "py-[9px] px-4", lg: "text-lg py-[9px] px-4", @@ -124,6 +128,7 @@ export const Button = forwardRef(function Button onClick, disabled, copy, + loading = false, ...props }, ref, @@ -134,7 +139,7 @@ export const Button = forwardRef(function Button ); }); diff --git a/client/ui/frontend/src/components/inputs/Input.tsx b/client/ui/frontend/src/components/inputs/Input.tsx index 4dc834f2f..e3946ebc2 100644 --- a/client/ui/frontend/src/components/inputs/Input.tsx +++ b/client/ui/frontend/src/components/inputs/Input.tsx @@ -13,6 +13,10 @@ export interface InputProps extends InputHTMLAttributes, Input maxWidthClass?: string; icon?: ReactNode; error?: string; + // A soft, non-blocking caveat rendered in orange (vs. error's red). Used + // e.g. for "couldn't reach this server" where the value is syntactically + // fine and the user may still proceed. `error` takes precedence. + warning?: string; prefixClassName?: string; showPasswordToggle?: boolean; copy?: boolean; @@ -33,6 +37,10 @@ const inputVariants = cva("", { "dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-200 dark:border-red-500 text-red-500", "ring-offset-red-500/10 dark:ring-offset-red-500/10 dark:focus-visible:ring-red-500/10 focus-visible:ring-red-500/10", ], + warning: [ + "dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-200 dark:border-orange-400 text-orange-400", + "ring-offset-orange-400/10 dark:ring-offset-orange-400/10 dark:focus-visible:ring-orange-400/10 focus-visible:ring-orange-400/10", + ], }, prefixSuffixVariant: { default: [ @@ -53,6 +61,7 @@ export const Input = forwardRef(function Input( icon, maxWidthClass = "", error, + warning, variant = "default", prefixClassName, showPasswordToggle = false, @@ -174,7 +183,7 @@ export const Input = forwardRef(function Input( {...props} className={cn( inputVariants({ - variant: error ? "error" : variant, + variant: error ? "error" : warning ? "warning" : variant, }), "flex h-[40px] w-full rounded-md bg-white px-3 py-2 text-sm select-text", "file:bg-transparent file:text-sm file:font-medium file:border-0", @@ -238,9 +247,14 @@ export const Input = forwardRef(function Input( )} - {error && ( - - {error} + {(error || warning) && ( + + {error ?? warning} )} diff --git a/client/ui/frontend/src/hooks/useManagementUrl.ts b/client/ui/frontend/src/hooks/useManagementUrl.ts index 192783896..c764b4e30 100644 --- a/client/ui/frontend/src/hooks/useManagementUrl.ts +++ b/client/ui/frontend/src/hooks/useManagementUrl.ts @@ -86,6 +86,11 @@ export function useManagementUrl() { const [url, setUrl] = useState( config.managementUrl === CLOUD_MANAGEMENT_URL ? "" : config.managementUrl, ); + // Self-hosted reachability soft-check, mirrored from the onboarding / + // profile-creation flows: a failed probe is a non-blocking orange warning, + // and a second Save with the same URL goes through regardless. + const [checking, setChecking] = useState(false); + const [unreachable, setUnreachable] = useState(false); useEffect(() => { setModeState(modeFromUrl(config.managementUrl)); @@ -94,6 +99,11 @@ export function useManagementUrl() { } }, [config.managementUrl]); + // Clear the stale warning whenever the target changes. + useEffect(() => { + setUnreachable(false); + }, [url, mode]); + const setMode = async (next: ManagementMode) => { if ( next === ManagementMode.Cloud && @@ -125,7 +135,22 @@ export function useManagementUrl() { const canSave = dirty && (mode === ManagementMode.Cloud || urlValid); const displayUrl = mode === ManagementMode.Cloud ? CLOUD_MANAGEMENT_URL : url; - const save = () => saveField("managementUrl", targetUrl); + const save = async () => { + // Self-hosted: probe the server first. A failed probe surfaces a soft + // warning and bails; a second Save (unreachable already set) skips the + // re-check and saves anyway, so the user can override a false negative. + if (mode === ManagementMode.SelfHosted && !unreachable) { + setChecking(true); + const reachable = await checkManagementUrlReachable(targetUrl); + setChecking(false); + if (!reachable) { + setUnreachable(true); + return; + } + } + await saveField("managementUrl", targetUrl); + setUnreachable(false); + }; return { mode, @@ -136,5 +161,7 @@ export function useManagementUrl() { showError, canSave, save, + checking, + unreachable, }; } diff --git a/client/ui/frontend/src/modules/profiles/ProfileCreationModal.tsx b/client/ui/frontend/src/modules/profiles/ProfileCreationModal.tsx index 98085429e..49fbccb23 100644 --- a/client/ui/frontend/src/modules/profiles/ProfileCreationModal.tsx +++ b/client/ui/frontend/src/modules/profiles/ProfileCreationModal.tsx @@ -1,14 +1,27 @@ import { FormEvent, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { PlusCircle } from "lucide-react"; import * as Dialog from "@/components/dialog/Dialog"; import { Input } from "@/components/inputs/Input"; import { Button } from "@/components/buttons/Button"; +import { DialogActions } from "@/components/dialog/DialogActions"; +import { Label } from "@/components/typography/Label"; +import { HelpText } from "@/components/typography/HelpText"; +import { ManagementServerSwitch } from "@/components/ManagementServerSwitch"; +import { + CLOUD_MANAGEMENT_URL, + ManagementMode, + checkManagementUrlReachable, + isValidManagementUrl, + normalizeManagementUrl, +} from "@/hooks/useManagementUrl"; type Props = { open: boolean; onOpenChange: (open: boolean) => void; - onCreate: (name: string) => void; + // onCreate receives the sanitized profile name and the management URL the + // user picked (the cloud default for Cloud mode, the normalized self- + // hosted URL otherwise). + onCreate: (name: string, managementUrl: string) => void; }; // Mirror of the daemon's profilemanager.sanitizeProfileName rule @@ -27,70 +40,177 @@ const sanitizeProfileInput = (value: string): string => export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) => { const { t } = useTranslation(); const [name, setName] = useState(""); - const [error, setError] = useState(null); - const inputRef = useRef(null); + const [nameError, setNameError] = useState(null); + const nameRef = useRef(null); + + const [mode, setMode] = useState(ManagementMode.Cloud); + const [url, setUrl] = useState(""); + const [urlError, setUrlError] = useState(null); + // unreachable: soft warning. A second submit with the same URL proceeds + // anyway (matches the onboarding management step's behaviour for self- + // hosted servers behind internal DNS / VPN). + const [unreachable, setUnreachable] = useState(false); + const [checking, setChecking] = useState(false); + const urlRef = useRef(null); useEffect(() => { if (!open) { setName(""); - setError(null); + setNameError(null); + setMode(ManagementMode.Cloud); + setUrl(""); + setUrlError(null); + setUnreachable(false); + setChecking(false); } }, [open]); - const handleSubmit = (e: FormEvent) => { + // Reset the URL warnings whenever the user edits the URL or flips mode — + // otherwise a stale warning lingers next to a just-corrected value. + useEffect(() => { + setUrlError(null); + setUnreachable(false); + }, [url, mode]); + + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); + if (checking) return; + const sanitized = sanitizeProfileInput(name); if (sanitized.length === 0) { - setError(t("profile.dialog.required")); - inputRef.current?.focus(); + setNameError(t("profile.dialog.required")); + nameRef.current?.focus(); return; } - onCreate(sanitized); + + if (mode === ManagementMode.Cloud) { + onCreate(sanitized, CLOUD_MANAGEMENT_URL); + onOpenChange(false); + return; + } + + const trimmed = url.trim(); + if (!trimmed || !isValidManagementUrl(trimmed)) { + setUrlError(t("settings.general.management.urlError")); + urlRef.current?.focus(); + return; + } + + const target = normalizeManagementUrl(trimmed); + setChecking(true); + const reachable = await checkManagementUrlReachable(target); + setChecking(false); + // First failed check: soft warning + bail. A second submit with the + // same URL skips re-checking (unreachable still true) so the user can + // proceed if they're sure. + if (!reachable && !unreachable) { + setUnreachable(true); + return; + } + + onCreate(sanitized, target); onOpenChange(false); }; - const handleChange = (value: string) => { + const handleNameChange = (value: string) => { setName(sanitizeProfileInput(value)); - if (error) setError(null); + if (nameError) setNameError(null); }; + // Live syntactic feedback: flag a non-empty, malformed URL as the user + // types instead of waiting for submit. Empty is not an error yet (handled + // on submit); the unreachable soft-warning only applies once syntax is OK. + const trimmedUrl = url.trim(); + const showUrlSyntaxError = + mode === ManagementMode.SelfHosted && trimmedUrl !== "" && !isValidManagementUrl(trimmedUrl); + const urlInputError = showUrlSyntaxError + ? t("settings.general.management.urlError") + : (urlError ?? undefined); + // Soft, non-blocking caveat (orange) — only when the URL is otherwise OK. + const urlInputWarning = + !urlInputError && unreachable ? t("profile.dialog.urlUnreachable") : undefined; + return ( - e.preventDefault()}> + e.preventDefault()} + >
-
- {t("profile.dialog.title")} - - {t("profile.dialog.description")} - -
+
+
+
+ + + {t("profile.dialog.description")} + +
+ handleNameChange(e.target.value)} + error={nameError ?? undefined} + maxLength={64} + spellCheck={false} + autoComplete="off" + autoCapitalize="off" + /> +
-
- handleChange(e.target.value)} - error={error ?? undefined} - maxLength={64} - spellCheck={false} - autoComplete="off" - autoCapitalize="off" - /> -
+
+
+ + + {t("profile.dialog.managementHelp")} + +
+
+ + {mode === ManagementMode.SelfHosted && ( + setUrl(e.target.value)} + error={urlInputError} + warning={urlInputWarning} + spellCheck={false} + autoComplete="off" + autoCapitalize="off" + /> + )} +
+
- - - + + + + +
diff --git a/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx b/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx index 7a05a98ee..7385d39a0 100644 --- a/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx +++ b/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx @@ -1,4 +1,4 @@ -import { useLayoutEffect, useRef, useState } from "react"; +import { useLayoutEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { errorDialog } from "@/lib/dialogs.ts"; import { CircleMinus, LogIn, PlusCircle, Trash2, UserCircle } from "lucide-react"; @@ -12,6 +12,9 @@ import { Tooltip } from "@/components/Tooltip"; import i18next from "@/lib/i18n"; import { useProfile } from "@/contexts/ProfileContext"; import { useConfirm } from "@/contexts/DialogContext"; +import { Settings as SettingsSvc } from "@bindings/services"; +import { SetConfigParams } from "@bindings/services/models.js"; +import { CLOUD_MANAGEMENT_URL } from "@/hooks/useManagementUrl.ts"; import { SectionGroup, SettingsBottomBar } from "@/modules/settings/SettingsSection.tsx"; import { cn } from "@/lib/cn"; import { formatErrorMessage } from "@/lib/errors"; @@ -24,6 +27,7 @@ export function ProfilesTab() { profiles, activeProfile, loaded, + username, switchProfile, addProfile, removeProfile, @@ -33,26 +37,40 @@ export function ProfilesTab() { const confirm = useConfirm(); const [newOpen, setNewOpen] = useState(false); const [busy, setBusy] = useState(false); - const tabRootRef = useRef(null); - // After a successful switch we want to bring the user back to the top of - // the tab — the table re-sorts the new active profile to the row 0 and a - // user who scrolled to find a target down the list would otherwise lose - // visual anchoring. Settings is hosted inside a Radix ScrollArea so we - // walk up to the viewport (it owns the actual overflow) instead of - // `window.scrollTo`, which is a no-op here. - const scrollTabToTop = () => { - const el = tabRootRef.current?.closest( - "[data-radix-scroll-area-viewport]", - ); - el?.scrollTo({ top: 0, behavior: "smooth" }); - }; - - const sorted = [...profiles].sort((a, b) => { - if (a.name === activeProfile) return -1; - if (b.name === activeProfile) return 1; - return a.name.localeCompare(b.name); - }); + // The display order is established once — the active profile first, then + // the rest alphabetically — and then held stable for the lifetime of the + // window. Switching profiles must only flip the "active" badge, never + // reorder the rows (otherwise the row the user just clicked jumps to the + // top under their cursor). New profiles append at the end; removed ones + // drop out. `orderRef` is the source of truth for row order; the active + // badge is derived live from `activeProfile`. + const orderRef = useRef([]); + const ordered = useMemo(() => { + const present = new Set(profiles.map((p) => p.name)); + if (orderRef.current.length === 0) { + // First population: active-first, then alphabetical. + orderRef.current = [...profiles] + .sort((a, b) => { + if (a.name === activeProfile) return -1; + if (b.name === activeProfile) return 1; + return a.name.localeCompare(b.name); + }) + .map((p) => p.name); + } else { + // Preserve the established order; drop removed, append added. + const kept = orderRef.current.filter((n) => present.has(n)); + const added = profiles + .map((p) => p.name) + .filter((n) => !orderRef.current.includes(n)) + .sort((a, b) => a.localeCompare(b)); + orderRef.current = [...kept, ...added]; + } + const byName = new Map(profiles.map((p) => [p.name, p])); + return orderRef.current + .map((n) => byName.get(n)) + .filter((p): p is Profile => p !== undefined); + }, [profiles, activeProfile]); const guarded = async (title: string, fn: () => Promise) => { if (busy) return; @@ -77,7 +95,6 @@ export function ProfilesTab() { }); if (!ok) return; await guarded(i18next.t("profile.error.switchTitle"), () => switchProfile(name)); - scrollTabToTop(); }; const handleDeregister = async (name: string) => { @@ -102,9 +119,21 @@ export function ProfilesTab() { void guarded(i18next.t("profile.error.deleteTitle"), () => removeProfile(name)); }; - const handleCreate = async (name: string) => { + const handleCreate = async (name: string, managementUrl: string) => { try { await addProfile(name); + // Only persist a management URL for self-hosted; a fresh profile + // already defaults to NetBird Cloud, so writing the cloud URL + // would be a no-op. Do it before switching so any reconnect the + // switch triggers already targets the right deployment. SetConfig + // is keyed by profile name, so it writes the new profile even + // though it isn't active yet (adminUrl left empty — the daemon + // keeps its loaded value). + if (managementUrl !== CLOUD_MANAGEMENT_URL) { + await SettingsSvc.SetConfig( + new SetConfigParams({ profileName: name, username, managementUrl }), + ); + } await switchProfile(name); } catch (e) { await errorDialog({ @@ -115,7 +144,7 @@ export function ProfilesTab() { }; return ( -
+
{t("settings.profiles.intro")} @@ -126,7 +155,7 @@ export function ProfilesTab() { > - {sorted.map((profile) => ( + {ordered.map((profile) => (
- {loaded && sorted.length === 0 && ( + {loaded && ordered.length === 0 && (
(null); const prevMode = useRef(mode); @@ -79,11 +89,17 @@ export function SettingsGeneral() { ? t("settings.general.management.urlError") : undefined } + warning={ + unreachable + ? t("settings.general.management.urlUnreachable") + : undefined + } />
diff --git a/client/ui/i18n/locales/de/common.json b/client/ui/i18n/locales/de/common.json index 1d1e26a81..60af4f24b 100644 --- a/client/ui/i18n/locales/de/common.json +++ b/client/ui/i18n/locales/de/common.json @@ -107,12 +107,15 @@ "profile.selector.delete": "Profil löschen", "profile.dialog.title": "Neues Profil", - "profile.dialog.description": "Mit Profilen können Sie mehrere NetBird-Verbindungen nebeneinander verwalten. Geben Sie Ihrem Profil einen aussagekräftigen Namen.", + "profile.dialog.nameLabel": "Profilname", + "profile.dialog.description": "Legen Sie einen leicht erkennbaren Namen für Ihr Profil fest.", "profile.dialog.placeholder": "z. B. Arbeit", + "profile.dialog.managementHelp": "NetBird Cloud oder Ihr eigener Server.", + "profile.dialog.urlUnreachable": "Server nicht erreichbar. Überprüfen Sie die URL, oder fügen Sie das Profil trotzdem hinzu, wenn Sie sicher sind, dass sie korrekt ist.", "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.switch.confirm": "Bestätigen", "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", @@ -158,6 +161,7 @@ "settings.general.management.selfHosted": "Self-hosted", "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.urlUnreachable": "Server nicht erreichbar. Überprüfen Sie die URL, oder speichern Sie trotzdem, wenn Sie sicher sind, dass sie korrekt ist.", "settings.general.management.switchCloudTitle": "Zu NetBird Cloud wechseln?", "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", @@ -329,7 +333,7 @@ "welcome.management.urlLabel": "URL des Management-Servers", "welcome.management.urlPlaceholder": "https://netbird.selfhosted.com:443", "welcome.management.urlInvalid": "Bitte geben Sie eine gültige URL ein, z. B. https://netbird.selfhosted.com:443", - "welcome.management.urlUnreachable": "Wir konnten diesen Server nicht erreichen. Überprüfen Sie die URL oder Ihr Netzwerk — Sie können trotzdem fortfahren, wenn Sie sicher sind, dass sie korrekt ist.", + "welcome.management.urlUnreachable": "Server nicht erreichbar. Überprüfen Sie die URL oder Ihr Netzwerk und fahren Sie fort, wenn Sie sicher sind, dass sie korrekt ist.", "welcome.management.checking": "Wird geprüft …", "browserLogin.title": "Setzen Sie den Anmeldevorgang im Browser fort", diff --git a/client/ui/i18n/locales/en/common.json b/client/ui/i18n/locales/en/common.json index 87d784829..7240decd5 100644 --- a/client/ui/i18n/locales/en/common.json +++ b/client/ui/i18n/locales/en/common.json @@ -108,10 +108,13 @@ "profile.selector.switchTo": "Switch to this profile", "profile.dialog.title": "Enter Profile Name", - "profile.dialog.description": "Choose a memorable name.", + "profile.dialog.nameLabel": "Profile Name", + "profile.dialog.description": "Set an easily identifiable name for your profile.", "profile.dialog.placeholder": "e.g. work", "profile.dialog.submit": "Add Profile", "profile.dialog.required": "Please enter a profile name, e.g. work, home", + "profile.dialog.managementHelp": "Use NetBird Cloud or your own server.", + "profile.dialog.urlUnreachable": "Couldn't reach this server. Check the URL, or add the profile anyway if you're sure it's correct.", "header.menu.settings": "Settings...", "header.menu.defaultView": "Default View", @@ -120,7 +123,7 @@ "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.switch.confirm": "Confirm", "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", @@ -181,6 +184,7 @@ "settings.general.management.selfHosted": "Self-hosted", "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.urlUnreachable": "Couldn't reach this server. Check the URL, or save anyway if you're sure it's correct.", "settings.general.management.switchCloudTitle": "Switch to NetBird Cloud?", "settings.general.management.switchCloudMessage": "This disconnects your self-hosted server.\nYou may need to log in again.", "settings.general.management.switchCloudConfirm": "Switch to Cloud", @@ -350,7 +354,7 @@ "welcome.management.urlLabel": "Management server URL", "welcome.management.urlPlaceholder": "https://netbird.selfhosted.com:443", "welcome.management.urlInvalid": "Please enter a valid URL, e.g., https://netbird.selfhosted.com:443", - "welcome.management.urlUnreachable": "We couldn't reach this server. Check the URL or your network and try again — you can also continue if you're sure it's correct.", + "welcome.management.urlUnreachable": "Couldn't reach this server. Check the URL or your network, then continue if you're sure it's correct.", "welcome.management.checking": "Checking…", "browserLogin.title": "Continue in your browser to complete the login", diff --git a/client/ui/i18n/locales/hu/common.json b/client/ui/i18n/locales/hu/common.json index d652f4938..a63811b80 100644 --- a/client/ui/i18n/locales/hu/common.json +++ b/client/ui/i18n/locales/hu/common.json @@ -107,12 +107,15 @@ "profile.selector.delete": "Profil törlése", "profile.dialog.title": "Új profil", - "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.nameLabel": "Profilnév", + "profile.dialog.description": "Adjon profiljának egy könnyen azonosítható nevet.", "profile.dialog.placeholder": "pl. Munka", + "profile.dialog.managementHelp": "NetBird Cloud vagy saját kiszolgáló.", + "profile.dialog.urlUnreachable": "A szerver nem érhető el. Ellenőrizze az URL-t, vagy adja hozzá a profilt, ha biztos benne, hogy helyes.", "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.switch.confirm": "Megerősítés", "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", @@ -158,6 +161,7 @@ "settings.general.management.selfHosted": "Saját üzemeltetésű", "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.urlUnreachable": "A szerver nem érhető el. Ellenőrizze az URL-t, vagy mentse el, ha biztos benne, hogy helyes.", "settings.general.management.switchCloudTitle": "Átváltás a NetBird Cloudra?", "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", @@ -329,7 +333,7 @@ "welcome.management.urlLabel": "Menedzsmentszerver URL", "welcome.management.urlPlaceholder": "https://netbird.selfhosted.com:443", "welcome.management.urlInvalid": "Adjon meg egy érvényes URL-t, pl. https://netbird.selfhosted.com:443", - "welcome.management.urlUnreachable": "Nem sikerült elérni a szervert. Ellenőrizze az URL-t vagy a hálózatot — ha biztos benne, hogy helyes, folytathatja.", + "welcome.management.urlUnreachable": "A szerver nem érhető el. Ellenőrizze az URL-t vagy a hálózatot, majd folytassa, ha biztos benne, hogy helyes.", "welcome.management.checking": "Ellenőrzés…", "browserLogin.title": "Folytassa a böngészőben a bejelentkezés befejezéséhez",