@@ -26,6 +26,11 @@ export default function NamespaceHeader({
2626 const [ existingPR , setExistingPR ] = useState ( null ) ;
2727 const [ prLoading , setPrLoading ] = useState ( false ) ;
2828
29+ // Branch switcher state
30+ const [ branches , setBranches ] = useState ( [ ] ) ;
31+ const [ branchDropdownOpen , setBranchDropdownOpen ] = useState ( false ) ;
32+ const branchDropdownRef = useRef ( null ) ;
33+
2934 // Modal states
3035 const [ showGitSettings , setShowGitSettings ] = useState ( false ) ;
3136 const [ showCreateBranch , setShowCreateBranch ] = useState ( false ) ;
@@ -65,7 +70,7 @@ export default function NamespaceHeader({
6570 onGitConfigLoaded ( config ) ;
6671 }
6772
68- // If this is a branch namespace, fetch parent's git config and check for existing PR
73+ // If this is a branch namespace, fetch parent's git config, branches, and check for existing PR
6974 if ( config ?. parent_namespace ) {
7075 try {
7176 const parentConfig = await djClient . getNamespaceGitConfig (
@@ -76,6 +81,15 @@ export default function NamespaceHeader({
7681 console . error ( 'Failed to fetch parent git config:' , e ) ;
7782 }
7883
84+ try {
85+ const branchList = await djClient . getNamespaceBranches (
86+ config . parent_namespace ,
87+ ) ;
88+ setBranches ( branchList || [ ] ) ;
89+ } catch ( e ) {
90+ console . error ( 'Failed to fetch branches:' , e ) ;
91+ }
92+
7993 // Check for existing PR
8094 setPrLoading ( true ) ;
8195 try {
@@ -102,12 +116,18 @@ export default function NamespaceHeader({
102116 fetchData ( ) ;
103117 } , [ djClient , namespace ] ) ;
104118
105- // Close dropdown when clicking outside
119+ // Close dropdowns when clicking outside
106120 useEffect ( ( ) => {
107121 const handleClickOutside = event => {
108122 if ( dropdownRef . current && ! dropdownRef . current . contains ( event . target ) ) {
109123 setDeploymentsDropdownOpen ( false ) ;
110124 }
125+ if (
126+ branchDropdownRef . current &&
127+ ! branchDropdownRef . current . contains ( event . target )
128+ ) {
129+ setBranchDropdownOpen ( false ) ;
130+ }
111131 } ;
112132 document . addEventListener ( 'mousedown' , handleClickOutside ) ;
113133 return ( ) => document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
@@ -243,88 +263,215 @@ export default function NamespaceHeader({
243263 />
244264 </ svg >
245265 { namespace ? (
246- namespaceParts . map ( ( part , index , arr ) => (
247- < span
248- key = { index }
249- style = { {
250- display : 'flex' ,
251- alignItems : 'center' ,
252- gap : '8px' ,
253- } }
254- >
255- < a
256- href = { `/namespaces/${ arr . slice ( 0 , index + 1 ) . join ( '.' ) } ` }
257- style = { {
258- fontWeight : '400' ,
259- color : '#1e293b' ,
260- textDecoration : 'none' ,
261- } }
266+ namespaceParts . map ( ( part , index , arr ) => {
267+ const isLast = index === arr . length - 1 ;
268+ const href = `/namespaces/${ arr . slice ( 0 , index + 1 ) . join ( '.' ) } ` ;
269+ return (
270+ < span
271+ key = { index }
272+ style = { { display : 'flex' , alignItems : 'center' , gap : '8px' } }
262273 >
263- { part }
264- </ a >
265- { index < arr . length - 1 && (
266- < svg
267- xmlns = "http://www.w3.org/2000/svg"
268- width = "12"
269- height = "12"
270- fill = "#94a3b8"
271- viewBox = "0 0 16 16"
272- >
273- < path
274- fillRule = "evenodd"
275- d = "M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"
276- />
277- </ svg >
278- ) }
279- </ span >
280- ) )
274+ { /* Last segment of a branch namespace becomes the branch switcher */ }
275+ { isLast && isBranchNamespace ? (
276+ < div
277+ ref = { branchDropdownRef }
278+ style = { { position : 'relative' } }
279+ >
280+ < button
281+ onClick = { ( ) => setBranchDropdownOpen ( o => ! o ) }
282+ style = { {
283+ display : 'flex' ,
284+ alignItems : 'center' ,
285+ gap : '4px' ,
286+ padding : '0' ,
287+ background : 'none' ,
288+ border : 'none' ,
289+ fontWeight : '400' ,
290+ fontSize : 'inherit' ,
291+ color : '#1e293b' ,
292+ cursor : 'pointer' ,
293+ } }
294+ >
295+ < svg
296+ xmlns = "http://www.w3.org/2000/svg"
297+ width = "12"
298+ height = "12"
299+ viewBox = "0 0 24 24"
300+ fill = "none"
301+ stroke = "#64748b"
302+ strokeWidth = "2"
303+ strokeLinecap = "round"
304+ strokeLinejoin = "round"
305+ >
306+ < line x1 = "6" y1 = "3" x2 = "6" y2 = "15" />
307+ < circle cx = "18" cy = "6" r = "3" />
308+ < circle cx = "6" cy = "18" r = "3" />
309+ < path d = "M18 9a9 9 0 0 1-9 9" />
310+ </ svg >
311+ { part }
312+ < span style = { { fontSize : '8px' , color : '#94a3b8' } } >
313+ { branchDropdownOpen ? '▲' : '▼' }
314+ </ span >
315+ </ button >
316+
317+ { branchDropdownOpen && (
318+ < div
319+ style = { {
320+ position : 'absolute' ,
321+ top : '100%' ,
322+ left : 0 ,
323+ marginTop : '4px' ,
324+ backgroundColor : 'white' ,
325+ border : '1px solid #e2e8f0' ,
326+ borderRadius : '8px' ,
327+ boxShadow : '0 4px 12px rgba(0,0,0,0.12)' ,
328+ zIndex : 1000 ,
329+ minWidth : '180px' ,
330+ overflow : 'hidden' ,
331+ } }
332+ >
333+ < div
334+ style = { {
335+ padding : '8px 12px 6px' ,
336+ fontSize : '10px' ,
337+ fontWeight : 600 ,
338+ textTransform : 'uppercase' ,
339+ letterSpacing : '0.05em' ,
340+ color : '#94a3b8' ,
341+ borderBottom : '1px solid #f1f5f9' ,
342+ } }
343+ >
344+ < a
345+ href = { `/namespaces/${ gitConfig . parent_namespace } ` }
346+ style = { {
347+ color : '#94a3b8' ,
348+ textDecoration : 'none' ,
349+ } }
350+ onClick = { ( ) => setBranchDropdownOpen ( false ) }
351+ >
352+ { gitConfig . parent_namespace }
353+ </ a >
354+ </ div >
355+ { branches . length === 0 ? (
356+ < div
357+ style = { {
358+ padding : '10px 12px' ,
359+ fontSize : '12px' ,
360+ color : '#94a3b8' ,
361+ } }
362+ >
363+ No branches found
364+ </ div >
365+ ) : (
366+ branches . map ( b => {
367+ const isCurrent = b . namespace === namespace ;
368+ return (
369+ < a
370+ key = { b . namespace }
371+ href = { `/namespaces/${ b . namespace } ` }
372+ onClick = { ( ) => setBranchDropdownOpen ( false ) }
373+ style = { {
374+ display : 'flex' ,
375+ alignItems : 'center' ,
376+ justifyContent : 'space-between' ,
377+ padding : '8px 12px' ,
378+ fontSize : '13px' ,
379+ color : isCurrent ? '#1e40af' : '#1e293b' ,
380+ backgroundColor : isCurrent
381+ ? '#eff6ff'
382+ : 'white' ,
383+ textDecoration : 'none' ,
384+ borderBottom : '1px solid #f8fafc' ,
385+ } }
386+ >
387+ < span
388+ style = { {
389+ display : 'flex' ,
390+ alignItems : 'center' ,
391+ gap : '6px' ,
392+ minWidth : 0 ,
393+ } }
394+ >
395+ { isCurrent && (
396+ < svg
397+ xmlns = "http://www.w3.org/2000/svg"
398+ width = "10"
399+ height = "10"
400+ viewBox = "0 0 24 24"
401+ fill = "none"
402+ stroke = "currentColor"
403+ strokeWidth = "3"
404+ strokeLinecap = "round"
405+ strokeLinejoin = "round"
406+ style = { { flexShrink : 0 } }
407+ >
408+ < polyline points = "20 6 9 17 4 12" />
409+ </ svg >
410+ ) }
411+ < span
412+ style = { {
413+ overflow : 'hidden' ,
414+ textOverflow : 'ellipsis' ,
415+ whiteSpace : 'nowrap' ,
416+ maxWidth : '180px' ,
417+ } }
418+ title = { b . git_branch || b . namespace }
419+ >
420+ { b . git_branch || b . namespace }
421+ </ span >
422+ </ span >
423+ < span
424+ style = { {
425+ fontSize : '11px' ,
426+ color : '#94a3b8' ,
427+ flexShrink : 0 ,
428+ marginLeft : '8px' ,
429+ } }
430+ >
431+ { b . num_nodes } nodes
432+ </ span >
433+ </ a >
434+ ) ;
435+ } )
436+ ) }
437+ </ div >
438+ ) }
439+ </ div >
440+ ) : (
441+ < a
442+ href = { href }
443+ style = { {
444+ fontWeight : '400' ,
445+ color : '#1e293b' ,
446+ textDecoration : 'none' ,
447+ } }
448+ >
449+ { part }
450+ </ a >
451+ ) }
452+ { ! isLast && (
453+ < svg
454+ xmlns = "http://www.w3.org/2000/svg"
455+ width = "12"
456+ height = "12"
457+ fill = "#94a3b8"
458+ viewBox = "0 0 16 16"
459+ >
460+ < path
461+ fillRule = "evenodd"
462+ d = "M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"
463+ />
464+ </ svg >
465+ ) }
466+ </ span >
467+ ) ;
468+ } )
281469 ) : (
282470 < span style = { { fontWeight : '600' , color : '#1e293b' } } >
283471 All Namespaces
284472 </ span >
285473 ) }
286474
287- { /* Branch indicator */ }
288- { isBranchNamespace && (
289- < span
290- style = { {
291- display : 'flex' ,
292- alignItems : 'center' ,
293- gap : '4px' ,
294- padding : '2px 8px' ,
295- backgroundColor : '#dbeafe' ,
296- borderRadius : '12px' ,
297- fontSize : '11px' ,
298- color : '#1e40af' ,
299- marginLeft : '4px' ,
300- } }
301- >
302- < svg
303- xmlns = "http://www.w3.org/2000/svg"
304- width = "12"
305- height = "12"
306- viewBox = "0 0 24 24"
307- fill = "none"
308- stroke = "currentColor"
309- strokeWidth = "2"
310- strokeLinecap = "round"
311- strokeLinejoin = "round"
312- >
313- < line x1 = "6" y1 = "3" x2 = "6" y2 = "15" />
314- < circle cx = "18" cy = "6" r = "3" />
315- < circle cx = "6" cy = "18" r = "3" />
316- < path d = "M18 9a9 9 0 0 1-9 9" />
317- </ svg >
318- Branch of{ ' ' }
319- < a
320- href = { `/namespaces/${ gitConfig . parent_namespace } ` }
321- style = { { color : '#1e40af' , textDecoration : 'underline' } }
322- >
323- { gitConfig . parent_namespace }
324- </ a >
325- </ span >
326- ) }
327-
328475 { /* Git-only (read-only) indicator */ }
329476 { gitConfig ?. git_only && (
330477 < span
0 commit comments