@@ -10,63 +10,108 @@ import { Link } from 'react-router-dom'
1010
1111import { navToLogin , useApiMutation } from '@oxide/api'
1212import {
13- DirectionDownIcon ,
14- PrevArrow12Icon ,
13+ Organization16Icon ,
1514 Profile16Icon ,
15+ SelectArrows6Icon ,
16+ Servers16Icon ,
17+ Success12Icon ,
1618} from '@oxide/design-system/icons/react'
1719
18- import { SiloSystemPicker } from '~/components/TopBarPicker'
1920import { useCrumbs } from '~/hooks/use-crumbs'
2021import { useCurrentUser } from '~/layouts/AuthenticatedLayout'
2122import { buttonStyle } from '~/ui/lib/Button'
2223import * as DropdownMenu from '~/ui/lib/DropdownMenu'
24+ import { Identicon } from '~/ui/lib/Identicon'
2325import { Slash } from '~/ui/lib/Slash'
2426import { intersperse } from '~/util/array'
2527import { pb } from '~/util/path-builder'
2628
2729export function TopBar ( { systemOrSilo } : { systemOrSilo : 'system' | 'silo' } ) {
30+ const { isFleetViewer } = useCurrentUser ( )
2831 // The height of this component is governed by the `PageContainer`
2932 // It's important that this component returns two distinct elements (wrapped in a fragment).
3033 // Each element will occupy one of the top column slots provided by `PageContainer`.
3134 return (
3235 < >
33- < div className = "flex items-center border-b border-r px-3 border-secondary" >
34- < SiloSystemPicker value = { systemOrSilo } />
36+ < div className = "flex items-center border-b border-r px-2 border-secondary" >
37+ < HomeButton level = { systemOrSilo } />
3538 </ div >
3639 { /* Height is governed by PageContainer grid */ }
37- { /* shrink-0 is needed to prevent getting squished by body content */ }
38- < div className = "z-topBar border-b bg-default border-secondary" >
39- < div className = "mx-3 flex h-[--top-bar-height] shrink-0 items-center justify-between" >
40+ < div className = "flex items-center justify-between gap-4 border-b px-3 bg-default border-secondary" >
41+ < div className = "flex flex-1 gap-2.5" >
4042 < Breadcrumbs />
41- < div className = "flex items-center gap-2" >
42- < UserMenu />
43- </ div >
43+ </ div >
44+ < div className = "flex items-center gap-2" >
45+ { isFleetViewer && < SiloSystemPicker level = { systemOrSilo } /> }
46+ < UserMenu />
4447 </ div >
4548 </ div >
4649 </ >
4750 )
4851}
4952
53+ const bigIconBox = 'flex h-[34px] w-[34px] items-center justify-center rounded'
54+
55+ const BigIdenticon = ( { name } : { name : string } ) => (
56+ < Identicon
57+ className = { cn ( bigIconBox , 'text-accent bg-accent-secondary-hover' ) }
58+ name = { name }
59+ />
60+ )
61+
62+ const SystemIcon = ( ) => (
63+ < div className = { cn ( bigIconBox , 'text-quinary bg-tertiary' ) } >
64+ < Servers16Icon />
65+ </ div >
66+ )
67+
68+ function HomeButton ( { level } : { level : 'system' | 'silo' } ) {
69+ const { me } = useCurrentUser ( )
70+
71+ const config =
72+ level === 'silo'
73+ ? {
74+ to : pb . projects ( ) ,
75+ icon : < BigIdenticon name = { me . siloName } /> ,
76+ heading : 'Silo' ,
77+ label : me . siloName ,
78+ }
79+ : {
80+ to : pb . silos ( ) ,
81+ icon : < SystemIcon /> ,
82+ heading : 'Oxide' ,
83+ label : 'System' ,
84+ }
85+
86+ return (
87+ < Link to = { config . to } className = "w-full grow rounded-lg p-1 hover:bg-hover" >
88+ < div className = "flex w-full items-center" >
89+ < div className = "mr-2" > { config . icon } </ div >
90+ < div className = "min-w-0 flex-1" >
91+ < div className = "text-mono-xs text-quaternary" > { config . heading } </ div >
92+ < div className = "overflow-hidden text-ellipsis whitespace-nowrap text-sans-md text-secondary" >
93+ { config . label }
94+ </ div >
95+ </ div >
96+ </ div >
97+ </ Link >
98+ )
99+ }
100+
50101function Breadcrumbs ( ) {
51102 const crumbs = useCrumbs ( ) . filter ( ( c ) => ! c . titleOnly )
52- const isTopLevel = crumbs . length <= 1
53103 return (
54104 < nav
55- className = "flex items-center gap-0.5 overflow-clip pr-4 text-sans-md"
105+ className = "flex items-center gap-0.5 overflow-clip text-sans-md"
56106 aria-label = "Breadcrumbs"
57107 >
58- < PrevArrow12Icon
59- className = { cn ( 'mx-1.5 flex-shrink-0 text-quinary' , isTopLevel && 'opacity-40' ) }
60- />
61-
62108 { intersperse (
63109 crumbs . map ( ( { label, path } , i ) => (
64110 < Link
65111 to = { path }
66112 className = { cn (
67113 'whitespace-nowrap text-sans-md hover:text-secondary' ,
68- // make the last breadcrumb brighter, but only if we're below the top level
69- ! isTopLevel && i === crumbs . length - 1 ? 'text-secondary' : 'text-tertiary'
114+ i === crumbs . length - 1 ? 'text-secondary' : 'text-tertiary'
70115 ) }
71116 key = { `${ label } |${ path } ` }
72117 >
@@ -89,16 +134,15 @@ function UserMenu() {
89134 < DropdownMenu . Root >
90135 < DropdownMenu . Trigger
91136 className = { cn (
92- buttonStyle ( { size : 'sm' , variant : 'secondary ' } ) ,
93- 'flex items-center gap-2 '
137+ buttonStyle ( { size : 'sm' , variant : 'ghost ' } ) ,
138+ 'flex items-center gap-1.5 !px-2 !border-secondary '
94139 ) }
95140 aria-label = "User menu"
96141 >
97142 < Profile16Icon className = "text-quaternary" />
98143 < span className = "normal-case text-sans-md text-secondary" >
99144 { me . displayName || 'User' }
100145 </ span >
101- < DirectionDownIcon className = "!w-2.5" />
102146 </ DropdownMenu . Trigger >
103147 < DropdownMenu . Content gap = { 8 } >
104148 < DropdownMenu . LinkItem to = { pb . profile ( ) } > Settings</ DropdownMenu . LinkItem >
@@ -107,3 +151,44 @@ function UserMenu() {
107151 </ DropdownMenu . Root >
108152 )
109153}
154+
155+ /**
156+ * Choose between System and Silo-scoped route trees, or if the user doesn't
157+ * have access to system routes (i.e., if systemPolicyView 403s) show the
158+ * current silo.
159+ */
160+ function SiloSystemPicker ( { level } : { level : 'silo' | 'system' } ) {
161+ return (
162+ < DropdownMenu . Root >
163+ < DropdownMenu . Trigger
164+ className = "flex items-center rounded border px-2 py-1.5 text-sans-md text-secondary border-secondary hover:bg-hover"
165+ aria-label = "Switch between system and silo"
166+ >
167+ < div className = "flex items-center text-quaternary" >
168+ { level === 'system' ? < Servers16Icon /> : < Organization16Icon /> }
169+ </ div >
170+ < div className = "ml-1.5 mr-3" > { level === 'system' ? 'System' : 'Silo' } </ div >
171+ { /* aria-hidden is a tip from the Reach docs */ }
172+ < SelectArrows6Icon className = "text-quinary" aria-hidden />
173+ </ DropdownMenu . Trigger >
174+ < DropdownMenu . Content className = "mt-2 max-h-80 overflow-y-auto" anchor = "bottom start" >
175+ < SystemSiloItem to = { pb . silos ( ) } label = "System" isSelected = { level === 'system' } />
176+ < SystemSiloItem to = { pb . projects ( ) } label = "Silo" isSelected = { level === 'silo' } />
177+ </ DropdownMenu . Content >
178+ </ DropdownMenu . Root >
179+ )
180+ }
181+
182+ function SystemSiloItem ( props : { label : string ; to : string ; isSelected : boolean } ) {
183+ return (
184+ < DropdownMenu . LinkItem
185+ to = { props . to }
186+ className = { cn ( '!pr-3' , { 'is-selected' : props . isSelected } ) }
187+ >
188+ < div className = "flex w-full items-center gap-2" >
189+ < div className = "flex-grow" > { props . label } </ div >
190+ { props . isSelected && < Success12Icon className = "block" /> }
191+ </ div >
192+ </ DropdownMenu . LinkItem >
193+ )
194+ }
0 commit comments