usePolled takes a cache key: it saves each answer in localStorage and starts from the saved one, so opening the agents, alerts, builds, review runs or hives view shows the last data at once and the refresh spinner marks the update. A saved answer is dropped after a day, when it was saved by another build of the UI, at log out (a new onLogout of the shared user menu) and when the gateway ends the login (a new onSessionEnded of the session guard). Pages with an editor (schedules, notification flows, broadcast) do not use it.
129 lines
4.1 KiB
TypeScript
129 lines
4.1 KiB
TypeScript
// <UserMenu> — the profile affordance in a header's top-right corner: an
|
|
// initials avatar button opening a popover with the signed-in name, a link
|
|
// to authelia's own account settings, and a sign-out link. The swarm UI, the
|
|
// hive dashboard and the per-agent page all mount this one, so they look and
|
|
// behave the same. Same icon-button + popover chrome as `SettingsMenu`.
|
|
//
|
|
// Who: `auth_request` only answers yes or no, never *who*. The name comes from
|
|
// authelia's `GET /api/user/info`, which each UI's nginx vhost proxies at
|
|
// `/api/whoami` on its own origin, so the browser sends the session cookie
|
|
// and no CORS is involved. A fetch that fails leaves the avatar as a "?".
|
|
//
|
|
// Avatar: authelia has no photo field, so it is generated from the initials
|
|
// (no third-party call: gravatar would leak a hashed e-mail on every load).
|
|
//
|
|
// Styles live in `@hive/shared/user-menu.css`, which the mounting page
|
|
// imports or `@import`s itself: the dashboard's JS bundle loads `.css`
|
|
// imports as text, so a component that imported it would not style there.
|
|
import { useEffect, useRef, useState } from "preact/hooks";
|
|
import {
|
|
accountLinks,
|
|
initialOf,
|
|
labelOf,
|
|
parseWhoAmI,
|
|
pickAccent,
|
|
type WhoAmI,
|
|
} from "./identity.js";
|
|
|
|
export function UserMenu({
|
|
whoamiUrl,
|
|
autheliaUrl,
|
|
onLogout,
|
|
}: {
|
|
// Where the signed-in user is asked for, on this page's own origin.
|
|
whoamiUrl: string;
|
|
// Authelia's base URL, for the settings and sign-out links; none when it
|
|
// is not known, and the menu then shows only the name.
|
|
autheliaUrl: string | null;
|
|
// Runs when "log out" is chosen, for a page to forget what it kept of this
|
|
// session.
|
|
onLogout?: () => void;
|
|
}) {
|
|
const [who, setWho] = useState<WhoAmI | null>(null);
|
|
const [open, setOpen] = useState(false);
|
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
fetch(whoamiUrl)
|
|
.then((r) =>
|
|
r.ok ? r.json() : Promise.reject(new Error(`http ${r.status}`)),
|
|
)
|
|
.then((body) => {
|
|
if (!cancelled) setWho(parseWhoAmI(body));
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setWho(null);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [whoamiUrl]);
|
|
|
|
// Close on an outside click or Escape, as `SettingsMenu` does.
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
function onPointerDown(e: MouseEvent) {
|
|
if (rootRef.current && !rootRef.current.contains(e.target as Node))
|
|
setOpen(false);
|
|
}
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
if (e.key === "Escape") setOpen(false);
|
|
}
|
|
document.addEventListener("pointerdown", onPointerDown);
|
|
document.addEventListener("keydown", onKeyDown);
|
|
return () => {
|
|
document.removeEventListener("pointerdown", onPointerDown);
|
|
document.removeEventListener("keydown", onKeyDown);
|
|
};
|
|
}, [open]);
|
|
|
|
const label = labelOf(who);
|
|
const links = accountLinks(autheliaUrl);
|
|
|
|
return (
|
|
<div class="user-menu" ref={rootRef}>
|
|
<button
|
|
type="button"
|
|
class="user-menu-button"
|
|
style={{ background: pickAccent(label ?? "operator") }}
|
|
aria-haspopup="true"
|
|
aria-expanded={open}
|
|
aria-label={label ? `signed in as ${label}` : "account"}
|
|
onClick={() => setOpen((v) => !v)}
|
|
>
|
|
{initialOf(label)}
|
|
</button>
|
|
{open ? (
|
|
<div class="user-menu-popover" role="menu">
|
|
{label ? <div class="user-menu-name">{label}</div> : null}
|
|
{links ? (
|
|
<>
|
|
<a
|
|
class="user-menu-item"
|
|
href={links.settings}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
role="menuitem"
|
|
onClick={() => setOpen(false)}
|
|
>
|
|
authelia settings
|
|
</a>
|
|
<a
|
|
class="user-menu-item"
|
|
href={links.logout}
|
|
role="menuitem"
|
|
onClick={() => {
|
|
onLogout?.();
|
|
setOpen(false);
|
|
}}
|
|
>
|
|
log out
|
|
</a>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|