Skip to content

Commit 360a7bd

Browse files
refactor: relocate View as control from header to sidebar
Move ProfileSwitch from the top header (Home.jsx) to the left sidebar (AppSidebar.jsx) where it lives permanently above the Connections section. This addresses product feedback that the top-row View as control was distracting across all pages. Changes: - Remove ProfileSwitch from Home.jsx entirely - Add ProfileSwitch to AppSidebar.jsx bottom section - Update ProfileSwitch.jsx with sidebar-compatible styling (collapsed prop) - Add comprehensive sidebar CSS variants to ProfileSwitch.module.css The View as dropdown now appears for admins in the sidebar, integrated with the connection switcher and user profile area. Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
1 parent 6c3d500 commit 360a7bd

4 files changed

Lines changed: 282 additions & 63 deletions

File tree

‎src/components/layout/AppSidebar.jsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AGENTS_ENABLED, canAccessHomeSection, getConnectionAccessBadge, getConn
77
import { PERMISSIONS } from '@/lib/permissions'
88
import ManageConnectionsModal from '@/components/ManageConnectionsModal'
99
import SettingsModal from '@/components/SettingsModal'
10+
import ProfileSwitch from './ProfileSwitch'
1011
import { useAuth } from '@/hooks/useAuth'
1112
import styles from './AppSidebar.module.css'
1213

@@ -30,7 +31,7 @@ export default function AppSidebar() {
3031
const [showConnectionDropdown, setShowConnectionDropdown] = useState(false)
3132
const userMenuRef = useRef(null)
3233
const connectionDropdownRef = useRef(null)
33-
const { logout, role, username, impersonating, permissions, hasPermission } = useAuth()
34+
const { logout, role, username, impersonating, permissions, hasPermission, canSwitchProfile } = useAuth()
3435
// Connections (add/edit/delete a database) is administrative: the backend already
3536
// refuses it to Developer and Data Engineer with 403, so hiding the button stops the
3637
// UI offering a door that only leads to an error. Settings is likewise administrative
@@ -137,6 +138,7 @@ export default function AppSidebar() {
137138
{/* Bottom: connections + profile */}
138139
<div className={styles.divider} />
139140
<div className={styles.bottom}>
141+
{canSwitchProfile && <ProfileSwitch collapsed={collapsed} />}
140142
<div className={styles.connectionSwitcher} ref={connectionDropdownRef}>
141143
<button
142144
className={styles.bottomItem}

‎src/components/layout/ProfileSwitch.jsx‎

Lines changed: 83 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { adminAPI } from '@/lib/api/client'
66
import { useSetActiveSection } from '@/lib/stores/useNavStore'
77
import styles from './ProfileSwitch.module.css'
88

9-
export default function ProfileSwitch() {
9+
/**
10+
* Profile switch / "View as" control for admins to impersonate sub-users.
11+
* @param {Object} props
12+
* @param {boolean} [props.collapsed] - When true (sidebar collapsed mode), hide labels
13+
*/
14+
export default function ProfileSwitch({ collapsed = false }) {
1015
const {
1116
isAdmin,
1217
impersonating,
@@ -86,82 +91,104 @@ export default function ProfileSwitch() {
8691
}
8792
}
8893

94+
// When impersonating, show an indicator with exit option
8995
if (impersonating) {
9096
return (
91-
<div className={styles.banner} ref={rootRef} data-testid="profile-switch-banner">
92-
<div className={styles.bannerCopy}>
93-
<span className={styles.bannerLabel}>Viewing as</span>
94-
<strong className={styles.bannerName}>{username}</strong>
95-
<span className={styles.roleBadge}>{role}</span>
96-
{impersonatorUsername && (
97-
<span className={styles.bannerMeta}>signed in as {impersonatorUsername}</span>
98-
)}
99-
</div>
100-
<div className={styles.bannerActions}>
101-
{error && <span className={styles.error}>{error}</span>}
102-
<button
103-
type="button"
104-
className={styles.secondaryBtn}
105-
onClick={() => setOpen((value) => !value)}
106-
disabled={switching}
107-
>
108-
Switch user
109-
<ChevronDown size={14} />
110-
</button>
111-
<button
112-
type="button"
113-
className={styles.exitBtn}
114-
onClick={handleStop}
115-
disabled={switching}
116-
data-testid="profile-switch-exit"
117-
>
118-
<X size={14} />
119-
Exit
120-
</button>
121-
</div>
122-
{open && (
123-
<CandidateMenu
124-
candidates={candidates}
125-
loading={loading}
126-
switching={switching}
127-
activeUsername={username}
128-
onSelect={handleSelect}
129-
/>
130-
)}
131-
</div>
132-
)
133-
}
134-
135-
return (
136-
<div className={styles.switchWrap} ref={rootRef} data-testid="profile-switch">
97+
<div className={styles.sidebarWrap} ref={rootRef} data-testid="profile-switch-banner">
13798
<button
13899
type="button"
139-
className={styles.trigger}
100+
className={styles.sidebarTriggerActive}
140101
onClick={() => setOpen((value) => !value)}
141102
disabled={switching}
142-
aria-label="View as another user"
143-
data-testid="profile-switch-trigger"
103+
title={collapsed ? `Viewing as ${username}` : undefined}
144104
>
145-
<ArrowLeftRight size={14} />
146-
<span>View as</span>
147-
<ChevronDown size={14} />
105+
<ArrowLeftRight size={15} className={styles.sidebarIcon} />
106+
<span className={`${styles.sidebarLabel} ${collapsed ? styles.sidebarLabelHidden : ''}`}>
107+
Viewing as {username}
108+
</span>
109+
{!collapsed && <ChevronDown size={14} className={styles.chevronIcon} />}
148110
</button>
149-
{open && (
111+
{open && !collapsed && (
112+
<div className={styles.sidebarDropdown}>
113+
<div className={styles.sidebarDropdownInfo}>
114+
<span className={styles.sidebarDropdownLabel}>Viewing as</span>
115+
<strong className={styles.sidebarDropdownName}>{username}</strong>
116+
<span className={styles.roleBadge}>{role}</span>
117+
</div>
118+
{impersonatorUsername && (
119+
<div className={styles.sidebarDropdownMeta}>Signed in as {impersonatorUsername}</div>
120+
)}
121+
<div className={styles.sidebarDropdownDivider} />
122+
{error && <div className={styles.sidebarDropdownError}>{error}</div>}
123+
<button
124+
type="button"
125+
className={styles.sidebarDropdownAction}
126+
onClick={() => setOpen((value) => !value)}
127+
disabled={switching}
128+
>
129+
Switch to another user
130+
</button>
131+
<button
132+
type="button"
133+
className={styles.sidebarDropdownActionPrimary}
134+
onClick={handleStop}
135+
disabled={switching}
136+
data-testid="profile-switch-exit"
137+
>
138+
<X size={14} />
139+
Exit View as
140+
</button>
141+
{open && (
142+
<CandidateMenu
143+
candidates={candidates}
144+
loading={loading}
145+
switching={switching}
146+
activeUsername={username}
147+
onSelect={handleSelect}
148+
isSidebar
149+
/>
150+
)}
151+
</div>
152+
)}
153+
</div>
154+
)
155+
}
156+
157+
// Normal state: show "View as" trigger
158+
return (
159+
<div className={styles.sidebarWrap} ref={rootRef} data-testid="profile-switch">
160+
<button
161+
type="button"
162+
className={styles.sidebarTrigger}
163+
onClick={() => setOpen((value) => !value)}
164+
disabled={switching}
165+
aria-label="View as another user"
166+
title={collapsed ? 'View as' : undefined}
167+
data-testid="profile-switch-trigger"
168+
>
169+
<ArrowLeftRight size={15} className={styles.sidebarIcon} />
170+
<span className={`${styles.sidebarLabel} ${collapsed ? styles.sidebarLabelHidden : ''}`}>
171+
View as
172+
</span>
173+
{!collapsed && <ChevronDown size={14} className={styles.chevronIcon} />}
174+
</button>
175+
{open && !collapsed && (
150176
<CandidateMenu
151177
candidates={candidates}
152178
loading={loading}
153179
switching={switching}
154180
error={error}
155181
onSelect={handleSelect}
182+
isSidebar
156183
/>
157184
)}
158185
</div>
159186
)
160187
}
161188

162-
function CandidateMenu({ candidates, loading, switching, error, activeUsername, onSelect }) {
189+
function CandidateMenu({ candidates, loading, switching, error, activeUsername, onSelect, isSidebar }) {
163190
return (
164-
<div className={styles.menu} role="listbox" aria-label="Users to view as">
191+
<div className={isSidebar ? styles.sidebarMenu : styles.menu} role="listbox" aria-label="Users to view as">
165192
<div className={styles.menuLabel}>Switch into a user</div>
166193
{loading && <div className={styles.menuEmpty}>Loading users…</div>}
167194
{!loading && error && <div className={styles.menuError}>{error}</div>}

‎src/components/layout/ProfileSwitch.module.css‎

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,197 @@
227227
background: #374151;
228228
color: #e5e7eb;
229229
}
230+
231+
/* ── Sidebar variant ── */
232+
.sidebarWrap {
233+
position: relative;
234+
width: 100%;
235+
}
236+
237+
.sidebarTrigger,
238+
.sidebarTriggerActive {
239+
display: flex;
240+
align-items: center;
241+
justify-content: flex-start;
242+
gap: 10px;
243+
padding: 8px 10px;
244+
border-radius: 8px;
245+
border: none;
246+
background: transparent;
247+
color: #6b7280;
248+
font-size: 14px;
249+
font-weight: 400;
250+
font-family: var(--font-family-sans);
251+
cursor: pointer;
252+
width: 100%;
253+
text-align: left;
254+
transition: background 0.1s, color 0.1s;
255+
white-space: nowrap;
256+
overflow: hidden;
257+
}
258+
259+
.sidebarTrigger:hover:not(:disabled) {
260+
background: #f3f4f6;
261+
color: #111827;
262+
}
263+
264+
.sidebarTrigger:disabled {
265+
opacity: 0.6;
266+
cursor: default;
267+
}
268+
269+
.sidebarTriggerActive {
270+
background: #fef3c7;
271+
color: #92400e;
272+
}
273+
274+
.sidebarTriggerActive:hover:not(:disabled) {
275+
background: #fde68a;
276+
}
277+
278+
.sidebarIcon {
279+
flex-shrink: 0;
280+
opacity: 0.6;
281+
}
282+
283+
.sidebarTriggerActive .sidebarIcon {
284+
opacity: 0.9;
285+
}
286+
287+
.sidebarLabel {
288+
overflow: hidden;
289+
opacity: 1;
290+
transition: opacity 0.12s ease;
291+
flex: 1;
292+
min-width: 0;
293+
text-overflow: ellipsis;
294+
}
295+
296+
.sidebarLabelHidden {
297+
opacity: 0;
298+
width: 0;
299+
pointer-events: none;
300+
}
301+
302+
.chevronIcon {
303+
opacity: 0.6;
304+
flex-shrink: 0;
305+
}
306+
307+
.sidebarDropdown {
308+
position: absolute;
309+
bottom: calc(100% + 6px);
310+
left: 0;
311+
right: 0;
312+
background: #fff;
313+
border: 1px solid #e5e7eb;
314+
border-radius: 10px;
315+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
316+
z-index: 220;
317+
overflow: hidden;
318+
}
319+
320+
.sidebarDropdownInfo {
321+
display: flex;
322+
align-items: center;
323+
gap: 8px;
324+
padding: 10px 12px;
325+
flex-wrap: wrap;
326+
}
327+
328+
.sidebarDropdownLabel {
329+
font-size: 11px;
330+
font-weight: 500;
331+
letter-spacing: 0.04em;
332+
text-transform: uppercase;
333+
color: #9ca3af;
334+
}
335+
336+
.sidebarDropdownName {
337+
font-size: 13px;
338+
font-weight: 600;
339+
color: #111827;
340+
}
341+
342+
.sidebarDropdownMeta {
343+
padding: 0 12px 8px;
344+
font-size: 11px;
345+
color: #6b7280;
346+
}
347+
348+
.sidebarDropdownDivider {
349+
height: 1px;
350+
background: #f3f4f6;
351+
}
352+
353+
.sidebarDropdownError {
354+
padding: 8px 12px;
355+
font-size: 12px;
356+
color: #b91c1c;
357+
}
358+
359+
.sidebarDropdownAction {
360+
display: flex;
361+
align-items: center;
362+
gap: 8px;
363+
width: 100%;
364+
padding: 9px 12px;
365+
background: transparent;
366+
border: none;
367+
font-size: 13px;
368+
color: #374151;
369+
cursor: pointer;
370+
text-align: left;
371+
transition: background 0.1s;
372+
}
373+
374+
.sidebarDropdownAction:hover:not(:disabled) {
375+
background: #f9fafb;
376+
color: #111827;
377+
}
378+
379+
.sidebarDropdownAction:disabled {
380+
opacity: 0.6;
381+
cursor: default;
382+
}
383+
384+
.sidebarDropdownActionPrimary {
385+
display: flex;
386+
align-items: center;
387+
justify-content: center;
388+
gap: 6px;
389+
width: calc(100% - 16px);
390+
margin: 8px;
391+
padding: 8px 12px;
392+
background: #111827;
393+
border: none;
394+
border-radius: 6px;
395+
font-size: 13px;
396+
font-weight: 500;
397+
color: #fff;
398+
cursor: pointer;
399+
transition: background 0.1s;
400+
}
401+
402+
.sidebarDropdownActionPrimary:hover:not(:disabled) {
403+
background: #1f2937;
404+
}
405+
406+
.sidebarDropdownActionPrimary:disabled {
407+
opacity: 0.6;
408+
cursor: default;
409+
}
410+
411+
.sidebarMenu {
412+
position: absolute;
413+
bottom: calc(100% + 6px);
414+
left: 0;
415+
right: 0;
416+
max-height: 320px;
417+
overflow: auto;
418+
background: #fff;
419+
border: 1px solid #e5e7eb;
420+
border-radius: 10px;
421+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
422+
z-index: 220;
423+
}

0 commit comments

Comments
 (0)