Skip to content

Commit 4f5fbad

Browse files
feat: add UserProfile component to display authenticated user information in header
1 parent b22ea8d commit 4f5fbad

2 files changed

Lines changed: 158 additions & 1 deletion

File tree

src/frontend/src/components/Header/Header.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React from "react";
22
import { Subtitle2 } from "@fluentui/react-components";
3+
import UserProfile from "./UserProfile";
4+
35
/**
46
* @component
57
* @name Header
@@ -16,6 +18,16 @@ type HeaderProps = {
1618
children?: React.ReactNode;
1719
};
1820

21+
// Determine once whether MSAL authentication is enabled, so the hooks inside
22+
// UserProfile (which require MsalProvider in the tree) are only mounted when safe.
23+
const isAuthEnabled = (): boolean => {
24+
// window.appConfig is set in main.jsx after fetching /config
25+
// Falls back to false when the config has not loaded or auth is disabled.
26+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
27+
const cfg = (typeof window !== "undefined" ? (window as any).appConfig : null);
28+
return Boolean(cfg && cfg.ENABLE_AUTH);
29+
};
30+
1931
const Header: React.FC<HeaderProps> = ({ title = "Contoso", subtitle, children }) => {
2032
return (
2133
<header
@@ -57,7 +69,16 @@ const Header: React.FC<HeaderProps> = ({ title = "Contoso", subtitle, children }
5769
</div>
5870

5971
{/* HEADER TOOLBAR (rendered only if passed as a child) */}
60-
{children}
72+
<div
73+
style={{
74+
display: "flex",
75+
alignItems: "center",
76+
gap: "8px",
77+
}}
78+
>
79+
{children}
80+
{isAuthEnabled() && <UserProfile />}
81+
</div>
6182
</header>
6283
);
6384
};
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import React from "react";
2+
import {
3+
Avatar,
4+
Button,
5+
Menu,
6+
MenuTrigger,
7+
MenuPopover,
8+
MenuList,
9+
MenuItem,
10+
MenuDivider,
11+
Tooltip,
12+
} from "@fluentui/react-components";
13+
import {
14+
Person20Regular,
15+
SignOut20Regular,
16+
} from "@fluentui/react-icons";
17+
import useAuth from "../../msal-auth/useAuth";
18+
19+
/**
20+
* @component UserProfile
21+
* @description Renders an avatar in the header. Clicking opens a menu showing
22+
* the signed-in user's name and email along with a Logout option.
23+
* Designed to be rendered only when MSAL authentication is enabled.
24+
*/
25+
const getInitials = (name?: string, username?: string): string => {
26+
const source = name && name.trim().length > 0 ? name : username || "";
27+
if (!source) return "U";
28+
29+
if (source.includes("@")) {
30+
const prefix = source.split("@")[0];
31+
const parts = prefix.split(/[._-]/).filter(Boolean);
32+
if (parts.length >= 2) {
33+
return (parts[0][0] + parts[1][0]).toUpperCase();
34+
}
35+
return prefix.substring(0, 2).toUpperCase();
36+
}
37+
38+
return source
39+
.split(" ")
40+
.filter(Boolean)
41+
.map((n) => n[0])
42+
.join("")
43+
.toUpperCase()
44+
.slice(0, 2);
45+
};
46+
47+
const UserProfile: React.FC = () => {
48+
const { isAuthenticated, accounts, logout } = useAuth();
49+
50+
if (!isAuthenticated || !accounts || accounts.length === 0) {
51+
return null;
52+
}
53+
54+
const account = accounts[0];
55+
const name = account?.name || account?.username || "User";
56+
const email = account?.username || "";
57+
const initials = getInitials(account?.name, account?.username);
58+
59+
const handleLogout = (e: React.MouseEvent) => {
60+
e.stopPropagation();
61+
logout();
62+
};
63+
64+
return (
65+
<Menu positioning="below-end">
66+
<MenuTrigger disableButtonEnhancement>
67+
<Tooltip content={email || name} relationship="label">
68+
<Button
69+
appearance="subtle"
70+
shape="circular"
71+
aria-label={`Signed in as ${name}`}
72+
style={{ padding: 0, minWidth: "auto" }}
73+
onClick={(e) => e.stopPropagation()}
74+
>
75+
<Avatar
76+
name={name}
77+
initials={initials}
78+
color="colorful"
79+
size={32}
80+
/>
81+
</Button>
82+
</Tooltip>
83+
</MenuTrigger>
84+
<MenuPopover>
85+
<MenuList>
86+
<MenuItem
87+
icon={<Person20Regular />}
88+
disabled
89+
style={{ cursor: "default", opacity: 1 }}
90+
>
91+
<div
92+
style={{
93+
display: "flex",
94+
flexDirection: "column",
95+
minWidth: 0,
96+
maxWidth: 240,
97+
}}
98+
>
99+
<span
100+
style={{
101+
fontWeight: 600,
102+
fontSize: 14,
103+
whiteSpace: "nowrap",
104+
overflow: "hidden",
105+
textOverflow: "ellipsis",
106+
color: "var(--colorNeutralForeground1)",
107+
}}
108+
>
109+
{name}
110+
</span>
111+
{email && (
112+
<span
113+
style={{
114+
fontSize: 12,
115+
color: "var(--colorNeutralForeground3)",
116+
whiteSpace: "nowrap",
117+
overflow: "hidden",
118+
textOverflow: "ellipsis",
119+
}}
120+
>
121+
{email}
122+
</span>
123+
)}
124+
</div>
125+
</MenuItem>
126+
<MenuDivider />
127+
<MenuItem icon={<SignOut20Regular />} onClick={handleLogout}>
128+
Logout
129+
</MenuItem>
130+
</MenuList>
131+
</MenuPopover>
132+
</Menu>
133+
);
134+
};
135+
136+
export default UserProfile;

0 commit comments

Comments
 (0)