Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/frontend/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,16 @@ declare module "*.png" {
const value: string;
export default value;
}


interface AppConfig {
API_URL?: string;
REACT_APP_MSAL_AUTH_CLIENTID?: string;
REACT_APP_MSAL_AUTH_AUTHORITY?: string;
REACT_APP_MSAL_REDIRECT_URL?: string;
REACT_APP_MSAL_POST_REDIRECT_URL?: string;
ENABLE_AUTH?: boolean;
}

interface Window {
appConfig?: AppConfig;
}
30 changes: 28 additions & 2 deletions src/frontend/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from "react";
import { Subtitle2 } from "@fluentui/react-components";
import UserProfile from "./UserProfile";

/**
* @component
* @name Header
Expand All @@ -16,7 +18,19 @@ type HeaderProps = {
children?: React.ReactNode;
};

// Determine once whether MSAL authentication is enabled, so the hooks inside
// UserProfile (which require MsalProvider in the tree) are only mounted when safe.
// window.appConfig is set in main.jsx after fetching /config; falls back to
// false when the config has not loaded or auth is disabled.
const isAuthEnabled = (): boolean => {
if (typeof window === "undefined") return false;
return Boolean(window.appConfig && window.appConfig.ENABLE_AUTH);
};

const Header: React.FC<HeaderProps> = ({ title = "Contoso", subtitle, children }) => {
const authEnabled = isAuthEnabled();
const hasToolbarContent = React.Children.count(children) > 0 || authEnabled;

return (
<header
style={{
Expand Down Expand Up @@ -56,8 +70,20 @@ const Header: React.FC<HeaderProps> = ({ title = "Contoso", subtitle, children }
</Subtitle2>
</div>

{/* HEADER TOOLBAR (rendered only if passed as a child) */}
{children}
{/* HEADER TOOLBAR (rendered only when there is toolbar content
or the auth-enabled user profile menu to display) */}
{hasToolbarContent && (
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
{children}
{authEnabled && <UserProfile />}
</div>
)}
</header>
);
};
Expand Down
136 changes: 136 additions & 0 deletions src/frontend/src/components/Header/UserProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React from "react";
import {
Avatar,
Button,
Menu,
MenuTrigger,
MenuPopover,
MenuList,
MenuItem,
MenuDivider,
Tooltip,
} from "@fluentui/react-components";
import {
Person20Regular,
SignOut20Regular,
} from "@fluentui/react-icons";
import useAuth from "../../msal-auth/useAuth";

/**
* @component UserProfile
* @description Renders an avatar in the header. Clicking opens a menu showing
* the signed-in user's name and email along with a Logout option.
* Designed to be rendered only when MSAL authentication is enabled.
*/
const getInitials = (name?: string, username?: string): string => {
const source = name && name.trim().length > 0 ? name : username || "";
if (!source) return "U";

if (source.includes("@")) {
const prefix = source.split("@")[0];
const parts = prefix.split(/[._-]/).filter(Boolean);
if (parts.length >= 2) {
return (parts[0][0] + parts[1][0]).toUpperCase();
}
return prefix.substring(0, 2).toUpperCase();
}

return source
.split(" ")
.filter(Boolean)
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2);
};

const UserProfile: React.FC = () => {
const { isAuthenticated, accounts, logout } = useAuth();

if (!isAuthenticated || !accounts || accounts.length === 0) {
return null;
}

const account = accounts[0];
const name = account?.name || account?.username || "User";
const email = account?.username || "";
const initials = getInitials(account?.name, account?.username);

const handleLogout = (e: React.MouseEvent) => {
e.stopPropagation();
logout();
};

return (
<Menu positioning="below-end">
<MenuTrigger disableButtonEnhancement>
<Tooltip content={`Signed in as ${name}`} relationship="label">
<Button
appearance="subtle"
shape="circular"
aria-label={`Signed in as ${name}`}
style={{ padding: 0, minWidth: "auto" }}
onClick={(e) => e.stopPropagation()}
>
Comment thread
Vamshi-Microsoft marked this conversation as resolved.
<Avatar
name={name}
initials={initials}
color="colorful"
size={32}
/>
</Button>
</Tooltip>
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem
icon={<Person20Regular />}
disabled
style={{ cursor: "default", opacity: 1 }}
>
Comment thread
Vamshi-Microsoft marked this conversation as resolved.
<div
style={{
display: "flex",
flexDirection: "column",
minWidth: 0,
maxWidth: 240,
}}
>
<span
style={{
fontWeight: 600,
fontSize: 14,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
color: "var(--colorNeutralForeground1)",
}}
>
{name}
</span>
{email && (
<span
style={{
fontSize: 12,
color: "var(--colorNeutralForeground3)",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{email}
</span>
)}
</div>
</MenuItem>
<MenuDivider />
<MenuItem icon={<SignOut20Regular />} onClick={handleLogout}>
Logout
</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
);
};

export default UserProfile;
Loading