update session expiration dialog texts, add monitor aware position

This commit is contained in:
Eduard Gert
2026-06-08 18:09:22 +02:00
parent b067544c8a
commit 6b2ae1c34c
19 changed files with 321 additions and 226 deletions
+2 -4
View File
@@ -2,8 +2,7 @@ import React from "react";
import ReactDOM from "react-dom/client";
import "./globals.css";
import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
import SessionExpiredDialog from "@/modules/session/SessionExpiredDialog.tsx";
import SessionAboutToExpireDialog from "@/modules/session/SessionAboutToExpireDialog.tsx";
import SessionExpirationDialog from "@/modules/session/SessionExpirationDialog.tsx";
import UpdateInProgressDialog from "@/modules/auto-update/UpdateInProgressDialog.tsx";
import WelcomeDialog from "@/modules/welcome/WelcomeDialog.tsx";
import ErrorDialog from "@/modules/error/ErrorDialog.tsx";
@@ -39,8 +38,7 @@ Promise.all([
<Route path="dialog">
<Route path="browser-login" element={<LoginWaitingForBrowserDialog />} />
<Route path="install-progress" element={<UpdateInProgressDialog />} />
<Route path="session-expired" element={<SessionExpiredDialog />} />
<Route path="session-about-to-expire" element={<SessionAboutToExpireDialog />} />
<Route path="session-expiration" element={<SessionExpirationDialog />} />
<Route path="welcome" element={<WelcomeDialog />} />
<Route path="error" element={<ErrorDialog />} />
</Route>
@@ -3,7 +3,7 @@ import { cn } from "@/lib/cn.ts";
import { isMacOS } from "@/lib/platform.ts";
// ConfirmDialog is the shared layout wrapper used by dialog-style window
// surfaces (SessionExpired, SessionAboutToExpire, …). Purely a layout
// surfaces (SessionExpiration, …). 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.
@@ -13,7 +13,7 @@ import i18next from "@/lib/i18n";
// 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
// SessionExpiration title swapping at countdown zero) keeps the chrome
// tight to the content with no scrollbar.
//
// Also re-measures on i18next `languageChanged`. The ResizeObserver in
@@ -1,20 +1,16 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "react-router-dom";
import { Events } from "@wailsio/runtime";
import { errorDialog } from "@/lib/dialogs.ts";
import { ClockIcon } from "lucide-react";
import { AlertCircleIcon, ClockIcon } from "lucide-react";
import { Button } from "@/components/buttons/Button";
import { ConfirmDialog } from "@/components/dialog/ConfirmDialog";
import { DialogActions } from "@/components/dialog/DialogActions";
import { DialogDescription } from "@/components/dialog/DialogDescription";
import { DialogHeading } from "@/components/dialog/DialogHeading";
import { SquareIcon } from "@/components/SquareIcon";
import {
Connection,
Profiles as ProfilesSvc,
Session,
WindowManager,
} from "@bindings/services";
import { Connection, Profiles as ProfilesSvc, Session, WindowManager } from "@bindings/services";
import { useAutoSizeWindow } from "@/hooks/useAutoSizeWindow";
import { formatErrorMessage } from "@/lib/errors.ts";
@@ -40,7 +36,7 @@ function formatRemaining(seconds: number): string {
return `${pad(minutes)}:${pad(secs)}`;
}
export default function SessionAboutToExpireDialog() {
export default function SessionExpirationDialog() {
const { t } = useTranslation();
const contentRef = useAutoSizeWindow<HTMLDivElement>(WINDOW_WIDTH);
const [params] = useSearchParams();
@@ -68,6 +64,20 @@ export default function SessionAboutToExpireDialog() {
return () => window.clearInterval(id);
}, [remaining]);
// Auto-close when the daemon flips back to Connected — covers extend
// flows started from outside this window (tray notification action,
// another UI surface) so the user isn't left staring at a stale dialog.
useEffect(() => {
const off = Events.On("netbird:status", (ev: { data: { status?: string } }) => {
if (ev?.data?.status === "Connected") {
WindowManager.CloseSessionExpiration().catch(console.error);
}
});
return () => {
off();
};
}, []);
// Mirrors tray.go::runExtendSession: starts the daemon SSO extend flow,
// opens the browser for the user to sign in, blocks on the daemon until
// the new deadline arrives. Tunnel stays up; success simply closes the
@@ -100,10 +110,10 @@ export default function SessionAboutToExpireDialog() {
// relevant.
return;
}
WindowManager.CloseSessionAboutToExpire().catch(console.error);
WindowManager.CloseSessionExpiration().catch(console.error);
} catch (e) {
await errorDialog({
Title: t("sessionAboutToExpire.extendFailedTitle"),
Title: t("sessionExpiration.extendFailedTitle"),
Message: formatErrorMessage(e),
});
} finally {
@@ -121,10 +131,10 @@ export default function SessionAboutToExpireDialog() {
profileName: active.profileName || "default",
username,
});
WindowManager.CloseSessionAboutToExpire().catch(console.error);
WindowManager.CloseSessionExpiration().catch(console.error);
} catch (e) {
await errorDialog({
Title: t("sessionAboutToExpire.logoutFailedTitle"),
Title: t("sessionExpiration.logoutFailedTitle"),
Message: formatErrorMessage(e),
});
} finally {
@@ -132,33 +142,41 @@ export default function SessionAboutToExpireDialog() {
}
}, [busy, t]);
const close = useCallback(() => {
WindowManager.CloseSessionExpiration().catch(console.error);
}, []);
return (
<ConfirmDialog ref={contentRef}>
<SquareIcon icon={ClockIcon} />
<SquareIcon icon={expired ? AlertCircleIcon : ClockIcon} />
<div className={"flex flex-col items-center gap-1"}>
<DialogHeading>
{expired
? t("sessionAboutToExpire.expired")
? t("sessionExpiration.expired")
: soon
? t("sessionAboutToExpire.title")
: t("sessionAboutToExpire.titleLater")}
? t("sessionExpiration.title")
: t("sessionExpiration.titleLater")}
</DialogHeading>
<DialogDescription>
{soon
? t("sessionAboutToExpire.description")
: t("sessionAboutToExpire.descriptionLater")}
{expired
? t("sessionExpiration.expiredDescription")
: soon
? t("sessionExpiration.description")
: t("sessionExpiration.descriptionLater")}
</DialogDescription>
</div>
<div
className={
"font-mono font-semibold text-2xl tabular-nums text-nb-gray-50 tracking-wider"
}
aria-live={"polite"}
>
{formatRemaining(remaining)}
</div>
{!expired && (
<div
className={
"font-mono font-semibold text-2xl tabular-nums text-nb-gray-50 tracking-wider"
}
aria-live={"polite"}
>
{formatRemaining(remaining)}
</div>
)}
<DialogActions>
<Button
@@ -167,18 +185,20 @@ export default function SessionAboutToExpireDialog() {
size={"md"}
className={"w-full"}
onClick={stay}
disabled={expired || busy}
disabled={busy}
>
{t("sessionAboutToExpire.stay")}
{expired
? t("sessionExpiration.authenticate")
: t("sessionExpiration.stay")}
</Button>
<Button
variant={"secondary"}
size={"md"}
className={"w-full"}
onClick={logout}
onClick={expired ? close : logout}
disabled={busy}
>
{t("sessionAboutToExpire.logout")}
{expired ? t("sessionExpiration.close") : t("sessionExpiration.logout")}
</Button>
</DialogActions>
</ConfirmDialog>
@@ -1,55 +0,0 @@
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Events } from "@wailsio/runtime";
import { AlertCircleIcon } from "lucide-react";
import { Button } from "@/components/buttons/Button";
import { ConfirmDialog } from "@/components/dialog/ConfirmDialog";
import { DialogActions } from "@/components/dialog/DialogActions";
import { DialogDescription } from "@/components/dialog/DialogDescription";
import { DialogHeading } from "@/components/dialog/DialogHeading";
import { SquareIcon } from "@/components/SquareIcon";
import { WindowManager } from "@bindings/services";
import { useAutoSizeWindow } from "@/hooks/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} />
<div className={"flex flex-col items-center gap-1"}>
<DialogHeading>{t("sessionExpired.title")}</DialogHeading>
<DialogDescription>{t("sessionExpired.description")}</DialogDescription>
</div>
<DialogActions>
<Button
autoFocus
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>
);
}