mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-20 15:49:55 +00:00
update dialogs, hide main window on browser login, keep state as disconnected when needslogin
This commit is contained in:
@@ -24,10 +24,10 @@ React 18 + TS 5.7 (`strict`, `noImplicitAny: false`) + Vite 6 + Tailwind 3 (`dar
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `/` | `Main` | `AppLayout` | Main window default route |
|
| `/` | `Main` | `AppLayout` | Main window default route |
|
||||||
| `/quick` | `QuickActions` | none | Standalone — **prototype**, not currently invoked by the Go side |
|
| `/quick` | `QuickActions` | none | Standalone — **prototype**, not currently invoked by the Go side |
|
||||||
| `/browser-login` | `BrowserLogin` | none | Auxiliary window (Go `WindowManager.OpenBrowserLogin`) |
|
| `/browser-login` | `WaitingForBrowserDialog` (modules/authentication) | none | Auxiliary window (Go `WindowManager.OpenBrowserLogin`) |
|
||||||
| `/update` | `Update` (pages) | none | Main window during enforced-update install |
|
| `/update` | `Update` (pages) | none | Main window during enforced-update install |
|
||||||
| `/session-expired` | `SessionExpired` (modules/session) | none | Auxiliary window (Go `WindowManager.OpenSessionExpired`, always-on-top) |
|
| `/session-expired` | `SessionExpiredDialog` (modules/authentication) | none | Auxiliary window (Go `WindowManager.OpenSessionExpired`, always-on-top) |
|
||||||
| `/session-about-to-expire` | `SessionAboutToExpire` (modules/session) | none | Auxiliary window (Go `WindowManager.OpenSessionAboutToExpire(seconds)`, always-on-top, mm:ss countdown via `?seconds=`) |
|
| `/session-about-to-expire` | `SessionAboutToExpireDialog` (modules/authentication) | none | Auxiliary window (Go `WindowManager.OpenSessionAboutToExpire(seconds)`, always-on-top, mm:ss countdown via `?seconds=`) |
|
||||||
| `/settings` | `Settings` | `SettingsLayout` | Auxiliary window (Go `WindowManager.OpenSettings`) |
|
| `/settings` | `Settings` | `SettingsLayout` | Auxiliary window (Go `WindowManager.OpenSettings`) |
|
||||||
| `*` | `<Navigate to="/">` | `AppLayout` | Catch-all |
|
| `*` | `<Navigate to="/">` | `AppLayout` | Catch-all |
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ Defined in `tailwind.config.ts`. `nb-gray` is the neutral palette (background =
|
|||||||
- **`screens/Profiles.tsx`** still imports bindings via the deep relative path. It's the example of the preferred `ProfileSwitcher.SwitchActive` flow but otherwise pre-AppLayout.
|
- **`screens/Profiles.tsx`** still imports bindings via the deep relative path. It's the example of the preferred `ProfileSwitcher.SwitchActive` flow but otherwise pre-AppLayout.
|
||||||
- **`pages/Debug.tsx`** is the legacy debug-bundle screen. The polished flow is in `modules/settings/SettingsTroubleshooting.tsx` (via `useDebugBundle`). `pages/Debug.tsx` isn't currently routed.
|
- **`pages/Debug.tsx`** is the legacy debug-bundle screen. The polished flow is in `modules/settings/SettingsTroubleshooting.tsx` (via `useDebugBundle`). `pages/Debug.tsx` isn't currently routed.
|
||||||
- **`pages/Update.tsx`** and **`screens/Update.tsx`** are two different update pages. The route table points at `pages/Update.tsx` (the production one with the 15-minute timeout, daemon-down-grace, and error-mapping). The `screens/Update.tsx` is an older simpler variant.
|
- **`pages/Update.tsx`** and **`screens/Update.tsx`** are two different update pages. The route table points at `pages/Update.tsx` (the production one with the 15-minute timeout, daemon-down-grace, and error-mapping). The `screens/Update.tsx` is an older simpler variant.
|
||||||
- **`modules/session/SessionExpired.tsx`** and **`modules/session/SessionAboutToExpire.tsx`** are the always-on-top auxiliary windows. Today they're only triggered via the DEV-only "Development" tab in Settings (`SettingsDevelopment.tsx`) — a daemon-status hook (status `SessionExpired`, plus a future "about-to-expire" signal) will drive them later. Sign-in / Stay-connected emit `EventTriggerLogin` so the main window's `startLogin()` orchestrator handles the SSO flow; Logout uses `Connection.Logout({profileName, username})`.
|
- **`modules/authentication/SessionExpiredDialog.tsx`** and **`modules/authentication/SessionAboutToExpireDialog.tsx`** are the always-on-top auxiliary windows. Today they're only triggered via the DEV-only "Development" tab in Settings (`SettingsDevelopment.tsx`) — a daemon-status hook (status `SessionExpired`, plus a future "about-to-expire" signal) will drive them later. Sign-in / Stay-connected emit `EventTriggerLogin` so the main window's `startLogin()` orchestrator handles the SSO flow; Logout uses `Connection.Logout({profileName, username})`.
|
||||||
- **`screens/QuickActions.tsx`** is wired to `/quick` in the route table but nothing on the Go side currently navigates there.
|
- **`screens/QuickActions.tsx`** is wired to `/quick` in the route table but nothing on the Go side currently navigates there.
|
||||||
- **`UpdateAvailableBanner`** is force-enabled via `FORCE_UPDATE_AVAILABLE = true` and additionally TODO-commented for the "only when management has auto updates enabled + force updates is disabled" case.
|
- **`UpdateAvailableBanner`** is force-enabled via `FORCE_UPDATE_AVAILABLE = true` and additionally TODO-commented for the "only when management has auto updates enabled + force updates is disabled" case.
|
||||||
- **`lib/MainModuleContext.tsx`** is exported but unused. Candidate for deletion.
|
- **`lib/MainModuleContext.tsx`** is exported but unused. Candidate for deletion.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"framer-motion": "^12.38.0",
|
"framer-motion": "^12.38.0",
|
||||||
"i18next": "^26.2.0",
|
"i18next": "^26.2.0",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.535.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-i18next": "^17.0.8",
|
"react-i18next": "^17.0.8",
|
||||||
|
|||||||
8
client/ui/frontend/pnpm-lock.yaml
generated
8
client/ui/frontend/pnpm-lock.yaml
generated
@@ -60,8 +60,8 @@ dependencies:
|
|||||||
specifier: ^26.2.0
|
specifier: ^26.2.0
|
||||||
version: 26.2.0(typescript@5.9.3)
|
version: 26.2.0(typescript@5.9.3)
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.469.0
|
specifier: ^0.535.0
|
||||||
version: 0.469.0(react@18.3.1)
|
version: 0.535.0(react@18.3.1)
|
||||||
react:
|
react:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
@@ -2096,8 +2096,8 @@ packages:
|
|||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/lucide-react@0.469.0(react@18.3.1):
|
/lucide-react@0.535.0(react@18.3.1):
|
||||||
resolution: {integrity: sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==}
|
resolution: {integrity: sha512-2E3+YWGLpjZ8ejIYrdqxVjWMSMiRQHmU6xZYE9xA2SC5j2m0NeB4/acjhRdhxbfniBKoNEukDDQnmShTxwOQ4g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import ReactDOM from "react-dom/client";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
|
import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||||
import QuickActions from "@/screens/QuickActions.tsx";
|
import QuickActions from "@/screens/QuickActions.tsx";
|
||||||
import SessionExpired from "@/modules/session/SessionExpired.tsx";
|
import SessionExpiredDialog from "@/modules/authentication/SessionExpiredDialog.tsx";
|
||||||
import SessionAboutToExpire from "@/modules/session/SessionAboutToExpire.tsx";
|
import SessionAboutToExpireDialog from "@/modules/authentication/SessionAboutToExpireDialog.tsx";
|
||||||
import Update from "@/screens/Update.tsx";
|
import Update from "@/screens/Update.tsx";
|
||||||
import { AppLayout } from "@/layouts/AppLayout.tsx";
|
import { AppLayout } from "@/layouts/AppLayout.tsx";
|
||||||
import { SettingsLayout } from "@/layouts/SettingsLayout.tsx";
|
import { SettingsLayout } from "@/layouts/SettingsLayout.tsx";
|
||||||
@@ -13,7 +13,7 @@ import { Settings } from "@/modules/settings/Settings.tsx";
|
|||||||
import { SkeletonTheme } from "react-loading-skeleton";
|
import { SkeletonTheme } from "react-loading-skeleton";
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
import { welcome } from "@/lib/welcome";
|
import { welcome } from "@/lib/welcome";
|
||||||
import BrowserLogin from "@/pages/BrowserLogin.tsx";
|
import WaitingForBrowserDialog from "@/modules/authentication/WaitingForBrowserDialog.tsx";
|
||||||
import { initI18n } from "@/lib/i18n";
|
import { initI18n } from "@/lib/i18n";
|
||||||
|
|
||||||
welcome();
|
welcome();
|
||||||
@@ -32,10 +32,10 @@ initI18n()
|
|||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/quick" element={<QuickActions />} />
|
<Route path="/quick" element={<QuickActions />} />
|
||||||
<Route path="/browser-login" element={<BrowserLogin />} />
|
<Route path="/browser-login" element={<WaitingForBrowserDialog />} />
|
||||||
<Route path="/update" element={<Update />} />
|
<Route path="/update" element={<Update />} />
|
||||||
<Route path="/session-expired" element={<SessionExpired />} />
|
<Route path="/session-expired" element={<SessionExpiredDialog />} />
|
||||||
<Route path="/session-about-to-expire" element={<SessionAboutToExpire />} />
|
<Route path="/session-about-to-expire" element={<SessionAboutToExpireDialog />} />
|
||||||
<Route element={<SettingsLayout />}>
|
<Route element={<SettingsLayout />}>
|
||||||
<Route path="settings" element={<Settings />} />
|
<Route path="settings" element={<Settings />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
29
client/ui/frontend/src/components/ConfirmDialog.tsx
Normal file
29
client/ui/frontend/src/components/ConfirmDialog.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { ReactNode, forwardRef } from "react";
|
||||||
|
|
||||||
|
// ConfirmDialog is the shared layout wrapper used by dialog-style window
|
||||||
|
// surfaces (SessionExpired, SessionAboutToExpire, …). Purely a layout
|
||||||
|
// primitive — callers compose the contents (SquareIcon, DialogHeading,
|
||||||
|
// DialogDescription, DialogActions) so each dialog can tweak its own
|
||||||
|
// internal structure without growing the ConfirmDialog API.
|
||||||
|
//
|
||||||
|
// Callers that mount the dialog inside its own Wails window pair this
|
||||||
|
// with useAutoSizeWindow by forwarding the returned ref onto the content
|
||||||
|
// wrapper so the window height tracks the rendered content.
|
||||||
|
type ConfirmDialogProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfirmDialog = forwardRef<HTMLDivElement, ConfirmDialogProps>(
|
||||||
|
function ConfirmDialog({ children }, ref) {
|
||||||
|
return (
|
||||||
|
<div className={"flex flex-col items-center justify-center"}>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={"flex flex-col items-center gap-5 p-8 text-center"}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
21
client/ui/frontend/src/components/DialogActions.tsx
Normal file
21
client/ui/frontend/src/components/DialogActions.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
|
// DialogActions wraps a vertical stack of Buttons inside a dialog surface.
|
||||||
|
// The wails-no-draggable class lets the user click the buttons even when
|
||||||
|
// the dialog window itself is draggable from any background region.
|
||||||
|
type DialogActionsProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DialogActions = ({ children, className }: DialogActionsProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"wails-no-draggable flex flex-col gap-3 w-full mx-auto",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
13
client/ui/frontend/src/components/DialogDescription.tsx
Normal file
13
client/ui/frontend/src/components/DialogDescription.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
|
// DialogDescription is the supporting description text rendered under a
|
||||||
|
// DialogHeading inside ConfirmDialog (and similar dialog surfaces).
|
||||||
|
type DialogDescriptionProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DialogDescription = ({ children, className }: DialogDescriptionProps) => (
|
||||||
|
<p className={cn("text-sm text-nb-gray-300", className)}>{children}</p>
|
||||||
|
);
|
||||||
16
client/ui/frontend/src/components/DialogHeading.tsx
Normal file
16
client/ui/frontend/src/components/DialogHeading.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
|
// DialogHeading is the title text used inside ConfirmDialog (and any other
|
||||||
|
// dialog-style surface with the same shape). Pair with DialogDescription
|
||||||
|
// for the standard title/description stack.
|
||||||
|
type DialogHeadingProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DialogHeading = ({ children, className }: DialogHeadingProps) => (
|
||||||
|
<p className={cn("text-base font-semibold text-nb-gray-50", className)}>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
23
client/ui/frontend/src/components/SquareIcon.tsx
Normal file
23
client/ui/frontend/src/components/SquareIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentType } from "react";
|
||||||
|
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.
|
||||||
|
type SquareIconProps = {
|
||||||
|
icon: ComponentType<LucideProps>;
|
||||||
|
iconSize?: number;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SquareIcon = ({ icon: Icon, iconSize = 20, className }: SquareIconProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-11 w-11 rounded-xl flex items-center justify-center bg-nb-gray-920 border border-nb-gray-900 text-white",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon size={iconSize} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -258,8 +258,6 @@
|
|||||||
"update.page.failed": "Update fehlgeschlagen",
|
"update.page.failed": "Update fehlgeschlagen",
|
||||||
|
|
||||||
"browserLogin.title": "Setzen Sie den Anmeldevorgang im Browser fort",
|
"browserLogin.title": "Setzen Sie den Anmeldevorgang im Browser fort",
|
||||||
"browserLogin.description": "Bitte schließen Sie die Kontoauthentifizierung im Browser-Tab ab und fahren Sie dort fort.",
|
|
||||||
"browserLogin.waiting": "Warten auf Anmeldung…",
|
|
||||||
"browserLogin.notSeeing": "Sehen Sie den Browser-Tab nicht?",
|
"browserLogin.notSeeing": "Sehen Sie den Browser-Tab nicht?",
|
||||||
"browserLogin.tryAgain": "Erneut versuchen",
|
"browserLogin.tryAgain": "Erneut versuchen",
|
||||||
|
|
||||||
|
|||||||
@@ -258,8 +258,6 @@
|
|||||||
"update.page.failed": "Update failed",
|
"update.page.failed": "Update failed",
|
||||||
|
|
||||||
"browserLogin.title": "Continue in your browser to complete the login",
|
"browserLogin.title": "Continue in your browser to complete the login",
|
||||||
"browserLogin.description": "Please complete the account authentication process in the browser tab and continue from there.",
|
|
||||||
"browserLogin.waiting": "Waiting for sign-in…",
|
|
||||||
"browserLogin.notSeeing": "Not seeing the browser tab?",
|
"browserLogin.notSeeing": "Not seeing the browser tab?",
|
||||||
"browserLogin.tryAgain": "Try again",
|
"browserLogin.tryAgain": "Try again",
|
||||||
|
|
||||||
|
|||||||
@@ -258,8 +258,6 @@
|
|||||||
"update.page.failed": "Frissítés sikertelen",
|
"update.page.failed": "Frissítés sikertelen",
|
||||||
|
|
||||||
"browserLogin.title": "Folytassa a böngészőben a bejelentkezés befejezéséhez",
|
"browserLogin.title": "Folytassa a böngészőben a bejelentkezés befejezéséhez",
|
||||||
"browserLogin.description": "Kérjük, fejezze be a fiókhitelesítést a böngésző fülén, és folytassa onnan.",
|
|
||||||
"browserLogin.waiting": "Várakozás a bejelentkezésre…",
|
|
||||||
"browserLogin.notSeeing": "Nem látja a böngésző fülét?",
|
"browserLogin.notSeeing": "Nem látja a böngésző fülét?",
|
||||||
"browserLogin.tryAgain": "Próbálja újra",
|
"browserLogin.tryAgain": "Próbálja újra",
|
||||||
|
|
||||||
|
|||||||
@@ -66,8 +66,16 @@ async function startLogin(): Promise<void> {
|
|||||||
if (result.needsSsoLogin) {
|
if (result.needsSsoLogin) {
|
||||||
const uri = result.verificationUriComplete || result.verificationUri;
|
const uri = result.verificationUriComplete || result.verificationUri;
|
||||||
if (uri) {
|
if (uri) {
|
||||||
|
// Open the in-app sign-in popup first so it's already on
|
||||||
|
// screen when the system browser steals focus; otherwise
|
||||||
|
// the browser lands on top and the user has to dig the
|
||||||
|
// NetBird window back out.
|
||||||
|
try {
|
||||||
|
await WindowManager.OpenBrowserLogin(uri);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
Connection.OpenURL(uri).catch(console.error);
|
Connection.OpenURL(uri).catch(console.error);
|
||||||
WindowManager.OpenBrowserLogin(uri).catch(console.error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelPromise = new Promise<void>((resolve) => {
|
const cancelPromise = new Promise<void>((resolve) => {
|
||||||
@@ -166,17 +174,17 @@ export const ConnectionStatusSwitch = () => {
|
|||||||
case "Connected":
|
case "Connected":
|
||||||
return ConnectionState.Connected;
|
return ConnectionState.Connected;
|
||||||
case "Connecting":
|
case "Connecting":
|
||||||
case "NeedsLogin":
|
|
||||||
// NeedsLogin is mid-SSO: the auto-chain in this component
|
|
||||||
// (and the tray's trigger-login emitter) flips the browser
|
|
||||||
// login window open as soon as the daemon reports it, so
|
|
||||||
// the switch should keep painting "Connecting" rather than
|
|
||||||
// dropping back to Disconnected while the user signs in.
|
|
||||||
return ConnectionState.Connecting;
|
return ConnectionState.Connecting;
|
||||||
case "Idle":
|
case "Idle":
|
||||||
|
case "NeedsLogin":
|
||||||
case "LoginFailed":
|
case "LoginFailed":
|
||||||
case "SessionExpired":
|
case "SessionExpired":
|
||||||
case "DaemonUnavailable":
|
case "DaemonUnavailable":
|
||||||
|
// NeedsLogin / SessionExpired without an in-flight user
|
||||||
|
// action read as Disconnected — the switch only flips to
|
||||||
|
// Connecting once the user (or the tray's trigger-login)
|
||||||
|
// kicks off the SSO flow, which sets action = "logging-in"
|
||||||
|
// and is handled by the guard above.
|
||||||
return ConnectionState.Disconnected;
|
return ConnectionState.Disconnected;
|
||||||
default:
|
default:
|
||||||
return ConnectionState.Disconnected;
|
return ConnectionState.Disconnected;
|
||||||
|
|||||||
41
client/ui/frontend/src/lib/useAutoSizeWindow.ts
Normal file
41
client/ui/frontend/src/lib/useAutoSizeWindow.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useLayoutEffect, useRef } from "react";
|
||||||
|
import { Window } from "@wailsio/runtime";
|
||||||
|
|
||||||
|
// useAutoSizeWindow resizes the current Wails window so its height matches
|
||||||
|
// the measured height of the content element the returned ref is attached
|
||||||
|
// to. Width stays fixed (Wails has no "fit-content-width" notion and the
|
||||||
|
// dialog-style session windows want a stable horizontal footprint).
|
||||||
|
//
|
||||||
|
// On first measurement the hook also calls Window.Show()/Focus() — the
|
||||||
|
// Go-side opens the window with Hidden: true so the user never sees the
|
||||||
|
// initial placeholder size snap to the measured size. Subsequent
|
||||||
|
// measurements (content changes after mount) only adjust the size.
|
||||||
|
//
|
||||||
|
// Re-measures via ResizeObserver so adding/removing content (e.g. the
|
||||||
|
// SessionAboutToExpire title swapping at countdown zero) keeps the chrome
|
||||||
|
// tight to the content with no scrollbar.
|
||||||
|
export function useAutoSizeWindow<T extends HTMLElement>(width: number) {
|
||||||
|
const ref = useRef<T | null>(null);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
let shown = false;
|
||||||
|
const apply = () => {
|
||||||
|
const h = Math.ceil(el.getBoundingClientRect().height);
|
||||||
|
if (h <= 0) return;
|
||||||
|
void Window.SetSize(width, h)
|
||||||
|
.then(() => {
|
||||||
|
if (shown) return;
|
||||||
|
shown = true;
|
||||||
|
void Window.Show().catch(() => {});
|
||||||
|
void Window.Focus().catch(() => {});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
apply();
|
||||||
|
const ro = new ResizeObserver(apply);
|
||||||
|
ro.observe(el);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}, [width]);
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
@@ -4,14 +4,17 @@ import { useSearchParams } from "react-router-dom";
|
|||||||
import { Events } from "@wailsio/runtime";
|
import { Events } from "@wailsio/runtime";
|
||||||
import { ClockIcon } from "lucide-react";
|
import { ClockIcon } from "lucide-react";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import {
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
Connection,
|
import { DialogActions } from "@/components/DialogActions";
|
||||||
Profiles as ProfilesSvc,
|
import { DialogDescription } from "@/components/DialogDescription";
|
||||||
WindowManager,
|
import { DialogHeading } from "@/components/DialogHeading";
|
||||||
} from "@bindings/services";
|
import { SquareIcon } from "@/components/SquareIcon";
|
||||||
|
import { Connection, Profiles as ProfilesSvc, WindowManager } from "@bindings/services";
|
||||||
|
import { useAutoSizeWindow } from "@/lib/useAutoSizeWindow";
|
||||||
|
|
||||||
const EVENT_TRIGGER_LOGIN = "trigger-login";
|
const EVENT_TRIGGER_LOGIN = "trigger-login";
|
||||||
const DEFAULT_SECONDS = 360;
|
const DEFAULT_SECONDS = 360;
|
||||||
|
const WINDOW_WIDTH = 360;
|
||||||
|
|
||||||
function formatMMSS(seconds: number): string {
|
function formatMMSS(seconds: number): string {
|
||||||
const s = Math.max(0, seconds | 0);
|
const s = Math.max(0, seconds | 0);
|
||||||
@@ -20,8 +23,9 @@ function formatMMSS(seconds: number): string {
|
|||||||
return `${String(m).padStart(2, "0")}:${String(r).padStart(2, "0")}`;
|
return `${String(m).padStart(2, "0")}:${String(r).padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SessionAboutToExpire() {
|
export default function SessionAboutToExpireDialog() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const contentRef = useAutoSizeWindow<HTMLDivElement>(WINDOW_WIDTH);
|
||||||
const [params] = useSearchParams();
|
const [params] = useSearchParams();
|
||||||
const initialSeconds = useMemo(() => {
|
const initialSeconds = useMemo(() => {
|
||||||
const raw = params.get("seconds");
|
const raw = params.get("seconds");
|
||||||
@@ -66,53 +70,48 @@ export default function SessionAboutToExpire() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<ConfirmDialog ref={contentRef}>
|
||||||
className={
|
<SquareIcon icon={ClockIcon} className={"mt-4"} />
|
||||||
"h-screen w-full flex flex-col items-center justify-center text-center px-6 py-8 bg-nb-gray-950"
|
|
||||||
}
|
<div className={"flex flex-col items-center gap-1"}>
|
||||||
>
|
<DialogHeading>
|
||||||
<div
|
|
||||||
className={
|
|
||||||
"h-12 w-12 rounded-full flex items-center justify-center bg-nb-gray-900 text-netbird mb-4"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ClockIcon size={22} />
|
|
||||||
</div>
|
|
||||||
<h1 className={"text-base font-semibold text-nb-gray-100"}>
|
|
||||||
{expired
|
{expired
|
||||||
? t("sessionAboutToExpire.expired")
|
? t("sessionAboutToExpire.expired")
|
||||||
: t("sessionAboutToExpire.title")}
|
: t("sessionAboutToExpire.title")}
|
||||||
</h1>
|
</DialogHeading>
|
||||||
<p className={"text-xs text-nb-gray-400 mt-1.5 max-w-[20rem] leading-snug"}>
|
<DialogDescription>
|
||||||
{t("sessionAboutToExpire.description")}
|
{t("sessionAboutToExpire.description")}
|
||||||
</p>
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
"mt-5 font-mono text-3xl tabular-nums text-nb-gray-100 tracking-wider"
|
"font-mono font-semibold text-2xl tabular-nums text-nb-gray-50 tracking-wider"
|
||||||
}
|
}
|
||||||
aria-live={"polite"}
|
aria-live={"polite"}
|
||||||
>
|
>
|
||||||
{formatMMSS(remaining)}
|
{formatMMSS(remaining)}
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex gap-2 mt-5 w-full max-w-[18rem]"}>
|
|
||||||
<Button
|
<DialogActions>
|
||||||
variant={"secondary"}
|
|
||||||
size={"xs"}
|
|
||||||
className={"flex-1"}
|
|
||||||
onClick={logout}
|
|
||||||
>
|
|
||||||
{t("sessionAboutToExpire.logout")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant={"primary"}
|
variant={"primary"}
|
||||||
size={"xs"}
|
size={"md"}
|
||||||
className={"flex-1"}
|
className={"w-full"}
|
||||||
onClick={stay}
|
onClick={stay}
|
||||||
disabled={expired}
|
disabled={expired}
|
||||||
>
|
>
|
||||||
{t("sessionAboutToExpire.stay")}
|
{t("sessionAboutToExpire.stay")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
<Button
|
||||||
</div>
|
variant={"secondary"}
|
||||||
|
size={"md"}
|
||||||
|
className={"w-full"}
|
||||||
|
onClick={logout}
|
||||||
|
>
|
||||||
|
{t("sessionAboutToExpire.logout")}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</ConfirmDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Events } from "@wailsio/runtime";
|
||||||
|
import { AlertCircleIcon } from "lucide-react";
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
|
import { DialogActions } from "@/components/DialogActions";
|
||||||
|
import { DialogDescription } from "@/components/DialogDescription";
|
||||||
|
import { DialogHeading } from "@/components/DialogHeading";
|
||||||
|
import { SquareIcon } from "@/components/SquareIcon";
|
||||||
|
import { WindowManager } from "@bindings/services";
|
||||||
|
import { useAutoSizeWindow } from "@/lib/useAutoSizeWindow";
|
||||||
|
|
||||||
|
const EVENT_TRIGGER_LOGIN = "trigger-login";
|
||||||
|
const WINDOW_WIDTH = 360;
|
||||||
|
|
||||||
|
export default function SessionExpiredDialog() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const contentRef = useAutoSizeWindow<HTMLDivElement>(WINDOW_WIDTH);
|
||||||
|
|
||||||
|
const signIn = useCallback(() => {
|
||||||
|
void Events.Emit(EVENT_TRIGGER_LOGIN);
|
||||||
|
WindowManager.CloseSessionExpired().catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const later = useCallback(() => {
|
||||||
|
WindowManager.CloseSessionExpired().catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmDialog ref={contentRef}>
|
||||||
|
<SquareIcon icon={AlertCircleIcon} className={"mt-4"} />
|
||||||
|
|
||||||
|
<div className={"flex flex-col items-center gap-1"}>
|
||||||
|
<DialogHeading>{t("sessionExpired.title")}</DialogHeading>
|
||||||
|
<DialogDescription>{t("sessionExpired.description")}</DialogDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant={"primary"} size={"md"} className={"w-full"} onClick={signIn}>
|
||||||
|
{t("sessionExpired.signIn")}
|
||||||
|
</Button>
|
||||||
|
<Button variant={"secondary"} size={"md"} className={"w-full"} onClick={later}>
|
||||||
|
{t("sessionExpired.later")}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</ConfirmDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { Events } from "@wailsio/runtime";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { Connection } from "@bindings/services";
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
|
import { DialogActions } from "@/components/DialogActions";
|
||||||
|
import { DialogDescription } from "@/components/DialogDescription";
|
||||||
|
import { DialogHeading } from "@/components/DialogHeading";
|
||||||
|
import { SquareIcon } from "@/components/SquareIcon";
|
||||||
|
import { useAutoSizeWindow } from "@/lib/useAutoSizeWindow";
|
||||||
|
|
||||||
|
const EVENT_CANCEL = "browser-login:cancel";
|
||||||
|
const WINDOW_WIDTH = 360;
|
||||||
|
|
||||||
|
export default function WaitingForBrowserDialog() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [params] = useSearchParams();
|
||||||
|
const uri = params.get("uri") ?? "";
|
||||||
|
const contentRef = useAutoSizeWindow<HTMLDivElement>(WINDOW_WIDTH);
|
||||||
|
|
||||||
|
const tryAgain = useCallback(() => {
|
||||||
|
if (!uri) return;
|
||||||
|
Connection.OpenURL(uri).catch(console.error);
|
||||||
|
}, [uri]);
|
||||||
|
|
||||||
|
const cancel = useCallback(() => {
|
||||||
|
void Events.Emit(EVENT_CANCEL);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmDialog ref={contentRef}>
|
||||||
|
<SquareIcon
|
||||||
|
icon={Loader2}
|
||||||
|
className={"mt-4 [&_svg]:animate-spin"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={"flex flex-col items-center gap-2"}>
|
||||||
|
<DialogHeading className={"text-balance"}>
|
||||||
|
{t("browserLogin.title")}
|
||||||
|
</DialogHeading>
|
||||||
|
<DialogDescription>
|
||||||
|
{t("browserLogin.notSeeing")}{" "}
|
||||||
|
<button
|
||||||
|
type={"button"}
|
||||||
|
onClick={tryAgain}
|
||||||
|
disabled={!uri}
|
||||||
|
className={
|
||||||
|
"wails-no-draggable text-netbird hover:underline disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("browserLogin.tryAgain")}
|
||||||
|
</button>
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
variant={"secondary"}
|
||||||
|
size={"md"}
|
||||||
|
className={"w-full"}
|
||||||
|
onClick={cancel}
|
||||||
|
>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</ConfirmDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Events } from "@wailsio/runtime";
|
|
||||||
import { ShieldAlertIcon } from "lucide-react";
|
|
||||||
import { Button } from "@/components/Button";
|
|
||||||
import { WindowManager } from "@bindings/services";
|
|
||||||
|
|
||||||
const EVENT_TRIGGER_LOGIN = "trigger-login";
|
|
||||||
|
|
||||||
export default function SessionExpired() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const signIn = useCallback(() => {
|
|
||||||
void Events.Emit(EVENT_TRIGGER_LOGIN);
|
|
||||||
WindowManager.CloseSessionExpired().catch(console.error);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const later = useCallback(() => {
|
|
||||||
WindowManager.CloseSessionExpired().catch(console.error);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
"h-screen w-full flex flex-col items-center justify-center text-center px-6 py-8 bg-nb-gray-950"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
"h-12 w-12 rounded-full flex items-center justify-center bg-nb-gray-900 text-netbird mb-4"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ShieldAlertIcon size={22} />
|
|
||||||
</div>
|
|
||||||
<h1 className={"text-base font-semibold text-nb-gray-100"}>
|
|
||||||
{t("sessionExpired.title")}
|
|
||||||
</h1>
|
|
||||||
<p className={"text-xs text-nb-gray-400 mt-1.5 max-w-[20rem] leading-snug"}>
|
|
||||||
{t("sessionExpired.description")}
|
|
||||||
</p>
|
|
||||||
<div className={"flex gap-2 mt-5 w-full max-w-[18rem]"}>
|
|
||||||
<Button
|
|
||||||
variant={"secondary"}
|
|
||||||
size={"xs"}
|
|
||||||
className={"flex-1"}
|
|
||||||
onClick={later}
|
|
||||||
>
|
|
||||||
{t("sessionExpired.later")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={"primary"}
|
|
||||||
size={"xs"}
|
|
||||||
className={"flex-1"}
|
|
||||||
onClick={signIn}
|
|
||||||
>
|
|
||||||
{t("sessionExpired.signIn")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useSearchParams } from "react-router-dom";
|
|
||||||
import { Events } from "@wailsio/runtime";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { Connection } from "@bindings/services";
|
|
||||||
import { Button } from "../components/Button";
|
|
||||||
import netbirdFull from "@/assets/logos/netbird-full.svg";
|
|
||||||
|
|
||||||
const EVENT_CANCEL = "browser-login:cancel";
|
|
||||||
|
|
||||||
export default function BrowserLogin() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [params] = useSearchParams();
|
|
||||||
const uri = params.get("uri") ?? "";
|
|
||||||
|
|
||||||
const tryAgain = useCallback(() => {
|
|
||||||
if (!uri) return;
|
|
||||||
Connection.OpenURL(uri).catch(console.error);
|
|
||||||
}, [uri]);
|
|
||||||
|
|
||||||
const cancel = useCallback(() => {
|
|
||||||
void Events.Emit(EVENT_CANCEL);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen flex-col items-center justify-center gap-3 p-8 text-center">
|
|
||||||
<img src={netbirdFull} alt="NetBird" className="mb-2 h-9" />
|
|
||||||
<h1 className="text-lg font-semibold text-white">{t("browserLogin.title")}</h1>
|
|
||||||
<p className="max-w-sm text-sm text-nb-gray-400">{t("browserLogin.description")}</p>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-nb-gray-400">
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" strokeWidth={1.5} />
|
|
||||||
{t("browserLogin.waiting")}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-nb-gray-400">
|
|
||||||
{t("browserLogin.notSeeing")}{" "}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={tryAgain}
|
|
||||||
disabled={!uri}
|
|
||||||
className="text-netbird hover:underline disabled:opacity-40 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{t("browserLogin.tryAgain")}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
<Button variant="secondary" onClick={cancel} className="mt-2">
|
|
||||||
{t("common.cancel")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -201,7 +201,7 @@ func main() {
|
|||||||
// The settings and browser-login windows are created lazily and
|
// The settings and browser-login windows are created lazily and
|
||||||
// destroyed on close, so they don't linger as hidden windows that
|
// destroyed on close, so they don't linger as hidden windows that
|
||||||
// Wails's macOS dock-reopen handler would pop back up.
|
// Wails's macOS dock-reopen handler would pop back up.
|
||||||
windowManager := services.NewWindowManager(app)
|
windowManager := services.NewWindowManager(app, window)
|
||||||
app.RegisterService(application.NewService(windowManager))
|
app.RegisterService(application.NewService(windowManager))
|
||||||
|
|
||||||
// Register an in-process StatusNotifierWatcher so the tray works on
|
// Register an in-process StatusNotifierWatcher so the tray works on
|
||||||
|
|||||||
@@ -31,15 +31,25 @@ const EventBrowserLoginCancel = "browser-login:cancel"
|
|||||||
// handler doesn't find a hidden window to resurrect.
|
// handler doesn't find a hidden window to resurrect.
|
||||||
type WindowManager struct {
|
type WindowManager struct {
|
||||||
app *application.App
|
app *application.App
|
||||||
|
mainWindow *application.WebviewWindow
|
||||||
settings *application.WebviewWindow
|
settings *application.WebviewWindow
|
||||||
browserLogin *application.WebviewWindow
|
browserLogin *application.WebviewWindow
|
||||||
sessionExpired *application.WebviewWindow
|
sessionExpired *application.WebviewWindow
|
||||||
sessionAboutToExpire *application.WebviewWindow
|
sessionAboutToExpire *application.WebviewWindow
|
||||||
|
// hiddenForLogin remembers windows that were visible when the
|
||||||
|
// BrowserLogin popup opened. They were Hide()n to keep focus on the
|
||||||
|
// SSO flow without resorting to AlwaysOnTop, and are restored when
|
||||||
|
// the BrowserLogin window closes (success or cancel).
|
||||||
|
hiddenForLogin []application.Window
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWindowManager(app *application.App) *WindowManager {
|
// NewWindowManager wires the manager to the main app. `mainWindow` is the
|
||||||
return &WindowManager{app: app}
|
// up-front-created webview the user interacts with from the tray — used to
|
||||||
|
// pick the BrowserLogin window's display so the sign-in popup follows the
|
||||||
|
// user onto the screen they're already looking at.
|
||||||
|
func NewWindowManager(app *application.App, mainWindow *application.WebviewWindow) *WindowManager {
|
||||||
|
return &WindowManager{app: app, mainWindow: mainWindow}
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenSettings shows the settings window, creating it on first use (and
|
// OpenSettings shows the settings window, creating it on first use (and
|
||||||
@@ -87,12 +97,32 @@ func (s *WindowManager) OpenBrowserLogin(uri string) {
|
|||||||
if uri != "" {
|
if uri != "" {
|
||||||
startURL = "/#/browser-login?uri=" + url.QueryEscape(uri)
|
startURL = "/#/browser-login?uri=" + url.QueryEscape(uri)
|
||||||
}
|
}
|
||||||
|
s.hideOtherWindowsLocked("browser-login")
|
||||||
|
// Prefer the screen the main window is on so the sign-in popup
|
||||||
|
// shows up where the user is already looking on multi-monitor
|
||||||
|
// setups. Falls back to OS-default centering if the main window
|
||||||
|
// has no resolvable screen yet.
|
||||||
|
var screen *application.Screen
|
||||||
|
if s.mainWindow != nil {
|
||||||
|
if sc, err := s.mainWindow.GetScreen(); err == nil {
|
||||||
|
screen = sc
|
||||||
|
}
|
||||||
|
}
|
||||||
s.browserLogin = s.app.Window.NewWithOptions(application.WebviewWindowOptions{
|
s.browserLogin = s.app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||||
Name: "browser-login",
|
Name: "browser-login",
|
||||||
Title: "NetBird Sign-in",
|
Title: "NetBird Sign-in",
|
||||||
Width: 460,
|
Width: 360,
|
||||||
Height: 440,
|
Height: 320,
|
||||||
DisableResize: true,
|
DisableResize: true,
|
||||||
|
// Hidden so the React side can measure its content via
|
||||||
|
// useAutoSizeWindow and call Window.SetSize + Show before the
|
||||||
|
// user sees the placeholder snapping to the measured height,
|
||||||
|
// matching the Session* windows.
|
||||||
|
Hidden: true,
|
||||||
|
// WindowCentered + Screen centers on the chosen display's
|
||||||
|
// WorkArea (see WebviewWindowOptions.Screen docs).
|
||||||
|
InitialPosition: application.WindowCentered,
|
||||||
|
Screen: screen,
|
||||||
MinimiseButtonState: application.ButtonHidden,
|
MinimiseButtonState: application.ButtonHidden,
|
||||||
MaximiseButtonState: application.ButtonHidden,
|
MaximiseButtonState: application.ButtonHidden,
|
||||||
CloseButtonState: application.ButtonEnabled,
|
CloseButtonState: application.ButtonEnabled,
|
||||||
@@ -113,15 +143,60 @@ func (s *WindowManager) OpenBrowserLogin(uri string) {
|
|||||||
s.app.Event.Emit(EventBrowserLoginCancel)
|
s.app.Event.Emit(EventBrowserLoginCancel)
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.browserLogin = nil
|
s.browserLogin = nil
|
||||||
|
s.restoreHiddenWindowsLocked()
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
})
|
})
|
||||||
} else if uri != "" {
|
// First open: window is Hidden, the React side auto-sizes via
|
||||||
|
// useAutoSizeWindow and calls Window.Show/Focus once content is
|
||||||
|
// measured. Returning here avoids the snap from placeholder to
|
||||||
|
// measured height.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if uri != "" {
|
||||||
s.browserLogin.SetURL("/#/browser-login?uri=" + url.QueryEscape(uri))
|
s.browserLogin.SetURL("/#/browser-login?uri=" + url.QueryEscape(uri))
|
||||||
}
|
}
|
||||||
s.browserLogin.Show()
|
s.browserLogin.Show()
|
||||||
s.browserLogin.Focus()
|
s.browserLogin.Focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hideOtherWindowsLocked hides every currently visible window except the one
|
||||||
|
// named `keepName` and remembers them in hiddenForLogin so they can be
|
||||||
|
// restored when the BrowserLogin flow ends. Caller must hold s.mu.
|
||||||
|
func (s *WindowManager) hideOtherWindowsLocked(keepName string) {
|
||||||
|
for _, w := range s.app.Window.GetAll() {
|
||||||
|
if w == nil || w.Name() == keepName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !w.IsVisible() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
w.Hide()
|
||||||
|
s.hiddenForLogin = append(s.hiddenForLogin, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// restoreHiddenWindowsLocked re-shows every window that was hidden by
|
||||||
|
// hideOtherWindowsLocked. Caller must hold s.mu.
|
||||||
|
func (s *WindowManager) restoreHiddenWindowsLocked() {
|
||||||
|
for _, w := range s.hiddenForLogin {
|
||||||
|
if w == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
w.Show()
|
||||||
|
}
|
||||||
|
s.hiddenForLogin = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BrowserLoginWindow returns the live SSO popup window, or nil if no SSO
|
||||||
|
// flow is in progress. While it is non-nil it should be treated as the
|
||||||
|
// app's focal window — tray "Open" and dock/taskbar activation hand off
|
||||||
|
// to it instead of the (currently hidden) main window.
|
||||||
|
func (s *WindowManager) BrowserLoginWindow() *application.WebviewWindow {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.browserLogin
|
||||||
|
}
|
||||||
|
|
||||||
// CloseBrowserLogin destroys the SSO popup window if it exists. Called from
|
// CloseBrowserLogin destroys the SSO popup window if it exists. Called from
|
||||||
// startLogin() when the flow completes or cancels programmatically.
|
// startLogin() when the flow completes or cancels programmatically.
|
||||||
func (s *WindowManager) CloseBrowserLogin() {
|
func (s *WindowManager) CloseBrowserLogin() {
|
||||||
@@ -136,6 +211,12 @@ func (s *WindowManager) CloseBrowserLogin() {
|
|||||||
|
|
||||||
// OpenSessionExpired shows the "session expired" prompt window above all
|
// OpenSessionExpired shows the "session expired" prompt window above all
|
||||||
// other application windows. Singleton — destroyed on close.
|
// other application windows. Singleton — destroyed on close.
|
||||||
|
//
|
||||||
|
// The window is created Hidden so the React side can measure its content
|
||||||
|
// and call Window.SetSize + Window.Show before the user sees the chrome —
|
||||||
|
// otherwise the user would briefly see the 360x320 placeholder snapping to
|
||||||
|
// the measured height. Re-opens (singleton already alive) Show/Focus
|
||||||
|
// directly here.
|
||||||
func (s *WindowManager) OpenSessionExpired() {
|
func (s *WindowManager) OpenSessionExpired() {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
@@ -143,10 +224,11 @@ func (s *WindowManager) OpenSessionExpired() {
|
|||||||
s.sessionExpired = s.app.Window.NewWithOptions(application.WebviewWindowOptions{
|
s.sessionExpired = s.app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||||
Name: "session-expired",
|
Name: "session-expired",
|
||||||
Title: "NetBird",
|
Title: "NetBird",
|
||||||
Width: 460,
|
Width: 360,
|
||||||
Height: 380,
|
Height: 320,
|
||||||
DisableResize: true,
|
DisableResize: true,
|
||||||
AlwaysOnTop: true,
|
AlwaysOnTop: true,
|
||||||
|
Hidden: true,
|
||||||
MinimiseButtonState: application.ButtonHidden,
|
MinimiseButtonState: application.ButtonHidden,
|
||||||
MaximiseButtonState: application.ButtonHidden,
|
MaximiseButtonState: application.ButtonHidden,
|
||||||
CloseButtonState: application.ButtonEnabled,
|
CloseButtonState: application.ButtonEnabled,
|
||||||
@@ -164,6 +246,7 @@ func (s *WindowManager) OpenSessionExpired() {
|
|||||||
s.sessionExpired = nil
|
s.sessionExpired = nil
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
s.sessionExpired.Show()
|
s.sessionExpired.Show()
|
||||||
s.sessionExpired.Focus()
|
s.sessionExpired.Focus()
|
||||||
@@ -183,6 +266,8 @@ func (s *WindowManager) CloseSessionExpired() {
|
|||||||
// OpenSessionAboutToExpire shows the countdown warning window above all
|
// OpenSessionAboutToExpire shows the countdown warning window above all
|
||||||
// other application windows. `seconds` seeds the initial countdown value
|
// other application windows. `seconds` seeds the initial countdown value
|
||||||
// rendered as mm:ss in the React layer. Singleton — destroyed on close.
|
// rendered as mm:ss in the React layer. Singleton — destroyed on close.
|
||||||
|
// Window is created Hidden so the React side can auto-size before paint
|
||||||
|
// (see OpenSessionExpired comment).
|
||||||
func (s *WindowManager) OpenSessionAboutToExpire(seconds int) {
|
func (s *WindowManager) OpenSessionAboutToExpire(seconds int) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
@@ -191,10 +276,11 @@ func (s *WindowManager) OpenSessionAboutToExpire(seconds int) {
|
|||||||
s.sessionAboutToExpire = s.app.Window.NewWithOptions(application.WebviewWindowOptions{
|
s.sessionAboutToExpire = s.app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||||
Name: "session-about-to-expire",
|
Name: "session-about-to-expire",
|
||||||
Title: "NetBird",
|
Title: "NetBird",
|
||||||
Width: 460,
|
Width: 360,
|
||||||
Height: 380,
|
Height: 320,
|
||||||
DisableResize: true,
|
DisableResize: true,
|
||||||
AlwaysOnTop: true,
|
AlwaysOnTop: true,
|
||||||
|
Hidden: true,
|
||||||
MinimiseButtonState: application.ButtonHidden,
|
MinimiseButtonState: application.ButtonHidden,
|
||||||
MaximiseButtonState: application.ButtonHidden,
|
MaximiseButtonState: application.ButtonHidden,
|
||||||
CloseButtonState: application.ButtonEnabled,
|
CloseButtonState: application.ButtonEnabled,
|
||||||
@@ -212,9 +298,9 @@ func (s *WindowManager) OpenSessionAboutToExpire(seconds int) {
|
|||||||
s.sessionAboutToExpire = nil
|
s.sessionAboutToExpire = nil
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
})
|
})
|
||||||
} else {
|
return
|
||||||
s.sessionAboutToExpire.SetURL(startURL)
|
|
||||||
}
|
}
|
||||||
|
s.sessionAboutToExpire.SetURL(startURL)
|
||||||
s.sessionAboutToExpire.Show()
|
s.sessionAboutToExpire.Show()
|
||||||
s.sessionAboutToExpire.Focus()
|
s.sessionAboutToExpire.Focus()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,6 +236,15 @@ func (t *Tray) reapplyMenuState() {
|
|||||||
// active app. Focus() additionally calls activateIgnoringOtherApps:YES on
|
// active app. Focus() additionally calls activateIgnoringOtherApps:YES on
|
||||||
// macOS and SetForegroundWindow on Windows.
|
// macOS and SetForegroundWindow on Windows.
|
||||||
func (t *Tray) ShowWindow() {
|
func (t *Tray) ShowWindow() {
|
||||||
|
// While an SSO flow is in progress the BrowserLogin popup is the focal
|
||||||
|
// window — the main window was hidden by WindowManager so the user
|
||||||
|
// stays on the sign-in surface. Tray "Open" / SIGUSR1 / dock-reopen
|
||||||
|
// should bring that window forward, not resurrect the main one mid-flow.
|
||||||
|
if w := t.svc.WindowManager.BrowserLoginWindow(); w != nil {
|
||||||
|
w.Show()
|
||||||
|
w.Focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
if t.window == nil {
|
if t.window == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user