Skip to content

Commit 3099a3f

Browse files
Merge pull request #429 from microsoft/psl-add-User-Profile
feat: Add User Profile Menu and Logout Option
2 parents 0bdec21 + 6cf5d4b commit 3099a3f

3 files changed

Lines changed: 177 additions & 3 deletions

File tree

src/frontend/declarations.d.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,16 @@ declare module "*.png" {
22
const value: string;
33
export default value;
44
}
5-
5+
6+
interface AppConfig {
7+
API_URL?: string;
8+
REACT_APP_MSAL_AUTH_CLIENTID?: string;
9+
REACT_APP_MSAL_AUTH_AUTHORITY?: string;
10+
REACT_APP_MSAL_REDIRECT_URL?: string;
11+
REACT_APP_MSAL_POST_REDIRECT_URL?: string;
12+
ENABLE_AUTH?: boolean;
13+
}
14+
15+
interface Window {
16+
appConfig?: AppConfig;
17+
}

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

Lines changed: 28 additions & 2 deletions
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,7 +18,19 @@ 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+
// window.appConfig is set in main.jsx after fetching /config; falls back to
24+
// false when the config has not loaded or auth is disabled.
25+
const isAuthEnabled = (): boolean => {
26+
if (typeof window === "undefined") return false;
27+
return Boolean(window.appConfig && window.appConfig.ENABLE_AUTH);
28+
};
29+
1930
const Header: React.FC<HeaderProps> = ({ title = "Contoso", subtitle, children }) => {
31+
const authEnabled = isAuthEnabled();
32+
const hasToolbarContent = React.Children.count(children) > 0 || authEnabled;
33+
2034
return (
2135
<header
2236
style={{
@@ -56,8 +70,20 @@ const Header: React.FC<HeaderProps> = ({ title = "Contoso", subtitle, children }
5670
</Subtitle2>
5771
</div>
5872

59-
{/* HEADER TOOLBAR (rendered only if passed as a child) */}
60-
{children}
73+
{/* HEADER TOOLBAR (rendered only when there is toolbar content
74+
or the auth-enabled user profile menu to display) */}
75+
{hasToolbarContent && (
76+
<div
77+
style={{
78+
display: "flex",
79+
alignItems: "center",
80+
gap: "8px",
81+
}}
82+
>
83+
{children}
84+
{authEnabled && <UserProfile />}
85+
</div>
86+
)}
6187
</header>
6288
);
6389
};
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={`Signed in as ${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)