11import { useState , useEffect , useCallback , useRef } from 'react'
22import { useNavigate , useOutletContext } from 'react-router-dom'
33import { backendsApi } from '../utils/api'
4+ import React from 'react'
45import { useOperations } from '../hooks/useOperations'
56import LoadingSpinner from '../components/LoadingSpinner'
67import { renderMarkdown } from '../utils/markdown'
7- import Modal from '../components/Modal'
88
99export default function Backends ( ) {
1010 const { addToast } = useOutletContext ( )
@@ -21,7 +21,7 @@ export default function Backends() {
2121 const [ manualUri , setManualUri ] = useState ( '' )
2222 const [ manualName , setManualName ] = useState ( '' )
2323 const [ manualAlias , setManualAlias ] = useState ( '' )
24- const [ selectedBackend , setSelectedBackend ] = useState ( null )
24+ const [ expandedRow , setExpandedRow ] = useState ( null )
2525 const debounceRef = useRef ( null )
2626
2727 const [ allBackends , setAllBackends ] = useState ( [ ] )
@@ -246,6 +246,7 @@ export default function Backends() {
246246 < table className = "table" >
247247 < thead >
248248 < tr >
249+ < th style = { { width : 30 } } > </ th >
249250 < th style = { { width : 40 } } > </ th >
250251 < SortHeader col = "name" > Backend</ SortHeader >
251252 < th > Description</ th >
@@ -256,12 +257,21 @@ export default function Backends() {
256257 </ tr >
257258 </ thead >
258259 < tbody >
259- { backends . map ( b => {
260+ { backends . map ( ( b , idx ) => {
260261 const op = getBackendOp ( b )
261262 const isProcessing = ! ! op
263+ const isExpanded = expandedRow === idx
262264
263265 return (
264- < tr key = { b . name || b . id } >
266+ < React . Fragment key = { b . name || b . id } >
267+ < tr
268+ onClick = { ( ) => setExpandedRow ( isExpanded ? null : idx ) }
269+ style = { { cursor : 'pointer' } }
270+ >
271+ { /* Chevron */ }
272+ < td style = { { width : 30 } } >
273+ < i className = { `fas fa-chevron-${ isExpanded ? 'down' : 'right' } ` } style = { { fontSize : '0.625rem' , color : 'var(--color-text-muted)' , transition : 'transform 150ms' } } />
274+ </ td >
265275 { /* Icon */ }
266276 < td >
267277 { b . icon ? (
@@ -279,12 +289,7 @@ export default function Backends() {
279289
280290 { /* Name */ }
281291 < td >
282- < span
283- style = { { fontWeight : 500 , cursor : 'pointer' , color : 'var(--color-primary)' } }
284- onClick = { ( ) => setSelectedBackend ( b ) }
285- >
286- { b . name || b . id }
287- </ span >
292+ < span style = { { fontWeight : 500 } } > { b . name || b . id } </ span >
288293 </ td >
289294
290295 { /* Description */ }
@@ -343,10 +348,7 @@ export default function Backends() {
343348
344349 { /* Actions */ }
345350 < td >
346- < div style = { { display : 'flex' , gap : 'var(--spacing-xs)' , justifyContent : 'flex-end' } } >
347- < button className = "btn btn-secondary btn-sm" onClick = { ( ) => setSelectedBackend ( b ) } title = "Details" >
348- < i className = "fas fa-info-circle" />
349- </ button >
351+ < div style = { { display : 'flex' , gap : 'var(--spacing-xs)' , justifyContent : 'flex-end' } } onClick = { e => e . stopPropagation ( ) } >
350352 { b . installed ? (
351353 < >
352354 < button className = "btn btn-secondary btn-sm" onClick = { ( ) => handleInstall ( b . name || b . id ) } title = "Reinstall" disabled = { isProcessing } >
@@ -364,6 +366,15 @@ export default function Backends() {
364366 </ div >
365367 </ td >
366368 </ tr >
369+ { /* Expanded detail row */ }
370+ { isExpanded && (
371+ < tr >
372+ < td colSpan = "8" style = { { padding : 0 } } >
373+ < BackendDetail backend = { b } />
374+ </ td >
375+ </ tr >
376+ ) }
377+ </ React . Fragment >
367378 )
368379 } ) }
369380 </ tbody >
@@ -389,106 +400,67 @@ export default function Backends() {
389400 </ div >
390401 ) }
391402
392- { /* Detail Modal */ }
393- { selectedBackend && (
394- < Modal onClose = { ( ) => setSelectedBackend ( null ) } >
395- < div style = { { padding : 'var(--spacing-md)' } } >
396- < div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'flex-start' , marginBottom : 'var(--spacing-md)' } } >
397- < div style = { { display : 'flex' , alignItems : 'center' , gap : 'var(--spacing-sm)' } } >
398- { selectedBackend . icon ? (
399- < img src = { selectedBackend . icon } alt = "" style = { { width : 48 , height : 48 , borderRadius : 'var(--radius-md)' } } />
400- ) : (
401- < div style = { {
402- width : 48 , height : 48 , borderRadius : 'var(--radius-md)' ,
403- background : 'var(--color-bg-tertiary)' , display : 'flex' ,
404- alignItems : 'center' , justifyContent : 'center' ,
405- } } >
406- < i className = "fas fa-cog" style = { { fontSize : '1.25rem' , color : 'var(--color-text-muted)' } } />
407- </ div >
408- ) }
409- < div >
410- < h3 style = { { fontWeight : 600 , fontSize : '1.125rem' } } > { selectedBackend . name || selectedBackend . id } </ h3 >
411- { selectedBackend . installed && < span className = "badge badge-success" > Installed</ span > }
412- </ div >
413- </ div >
414- < button className = "btn btn-secondary btn-sm" onClick = { ( ) => setSelectedBackend ( null ) } >
415- < i className = "fas fa-xmark" />
416- </ button >
417- </ div >
403+ </ div >
404+ )
405+ }
418406
419- { /* Description */ }
420- { selectedBackend . description && (
421- < div style = { { marginBottom : 'var(--spacing-md)' } } >
422- < div
423- style = { { fontSize : '0.875rem' , color : 'var(--color-text-secondary)' , lineHeight : 1.6 } }
424- dangerouslySetInnerHTML = { { __html : renderMarkdown ( selectedBackend . description ) } }
425- />
426- </ div >
427- ) }
407+ function BackendDetailRow ( { label, children } ) {
408+ if ( ! children ) return null
409+ return (
410+ < tr >
411+ < td style = { { fontWeight : 500 , fontSize : '0.8125rem' , color : 'var(--color-text-secondary)' , whiteSpace : 'nowrap' , verticalAlign : 'top' , padding : '6px 12px 6px 0' } } >
412+ { label }
413+ </ td >
414+ < td style = { { fontSize : '0.8125rem' , padding : '6px 0' } } > { children } </ td >
415+ </ tr >
416+ )
417+ }
428418
429- { /* Tags */ }
430- { selectedBackend . tags && selectedBackend . tags . length > 0 && (
431- < div style = { { marginBottom : 'var(--spacing-md)' } } >
432- < span className = "form-label" > Tags</ span >
433- < div style = { { display : 'flex' , gap : 4 , flexWrap : 'wrap' } } >
434- { selectedBackend . tags . map ( tag => (
435- < span key = { tag } className = "badge badge-info" style = { { fontSize : '0.6875rem' } } > { tag } </ span >
436- ) ) }
437- </ div >
419+ function BackendDetail ( { backend } ) {
420+ return (
421+ < div style = { { padding : 'var(--spacing-md) var(--spacing-lg)' , background : 'var(--color-bg-primary)' , borderTop : '1px solid var(--color-border-subtle)' } } >
422+ < table style = { { width : '100%' , borderCollapse : 'collapse' } } >
423+ < tbody >
424+ < BackendDetailRow label = "Description" >
425+ { backend . description && (
426+ < div
427+ style = { { color : 'var(--color-text-secondary)' , lineHeight : 1.6 } }
428+ dangerouslySetInnerHTML = { { __html : renderMarkdown ( backend . description ) } }
429+ />
430+ ) }
431+ </ BackendDetailRow >
432+ < BackendDetailRow label = "Repository" >
433+ { backend . gallery && (
434+ < span className = "badge badge-info" style = { { fontSize : '0.6875rem' } } >
435+ { typeof backend . gallery === 'string' ? backend . gallery : backend . gallery . name || '-' }
436+ </ span >
437+ ) }
438+ </ BackendDetailRow >
439+ < BackendDetailRow label = "License" >
440+ { backend . license && < span > { backend . license } </ span > }
441+ </ BackendDetailRow >
442+ < BackendDetailRow label = "Tags" >
443+ { backend . tags ?. length > 0 && (
444+ < div style = { { display : 'flex' , gap : '4px' , flexWrap : 'wrap' } } >
445+ { backend . tags . map ( tag => (
446+ < span key = { tag } className = "badge badge-info" style = { { fontSize : '0.6875rem' } } > { tag } </ span >
447+ ) ) }
438448 </ div >
439449 ) }
440-
441- { /* URLs */ }
442- { selectedBackend . urls && selectedBackend . urls . length > 0 && (
443- < div style = { { marginBottom : 'var(--spacing-md)' } } >
444- < span className = "form-label" > Links</ span >
445- < div style = { { display : 'flex' , flexDirection : 'column' , gap : 4 } } >
446- { selectedBackend . urls . map ( ( url , i ) => (
447- < a key = { i } href = { url } target = "_blank" rel = "noopener noreferrer" style = { { fontSize : '0.8125rem' , color : 'var(--color-primary)' , wordBreak : 'break-all' } } >
448- < i className = "fas fa-external-link-alt" style = { { marginRight : 4 } } /> { url }
449- </ a >
450- ) ) }
451- </ div >
450+ </ BackendDetailRow >
451+ < BackendDetailRow label = "Links" >
452+ { backend . urls ?. length > 0 && (
453+ < div style = { { display : 'flex' , flexDirection : 'column' , gap : '2px' } } >
454+ { backend . urls . map ( ( url , i ) => (
455+ < a key = { i } href = { url } target = "_blank" rel = "noopener noreferrer" style = { { fontSize : '0.8125rem' , color : 'var(--color-primary)' , wordBreak : 'break-all' } } >
456+ < i className = "fas fa-external-link-alt" style = { { marginRight : 4 , fontSize : '0.6875rem' } } /> { url }
457+ </ a >
458+ ) ) }
452459 </ div >
453460 ) }
454-
455- { /* Repository / License */ }
456- < div style = { { display : 'flex' , gap : 'var(--spacing-md)' , marginBottom : 'var(--spacing-md)' } } >
457- { selectedBackend . gallery && (
458- < div >
459- < span className = "form-label" > Repository</ span >
460- < p style = { { fontSize : '0.8125rem' } } > { typeof selectedBackend . gallery === 'string' ? selectedBackend . gallery : selectedBackend . gallery . name || '-' } </ p >
461- </ div >
462- ) }
463- { selectedBackend . license && (
464- < div >
465- < span className = "form-label" > License</ span >
466- < p style = { { fontSize : '0.8125rem' } } > { selectedBackend . license } </ p >
467- </ div >
468- ) }
469- </ div >
470-
471- { /* Actions */ }
472- < div style = { { display : 'flex' , gap : 'var(--spacing-sm)' , justifyContent : 'flex-end' , borderTop : '1px solid var(--color-border-subtle)' , paddingTop : 'var(--spacing-md)' } } >
473- { selectedBackend . installed ? (
474- < >
475- < button className = "btn btn-secondary btn-sm" onClick = { ( ) => { handleInstall ( selectedBackend . name || selectedBackend . id ) ; setSelectedBackend ( null ) } } >
476- < i className = "fas fa-rotate" /> Reinstall
477- </ button >
478- < button className = "btn btn-danger btn-sm" onClick = { ( ) => { handleDelete ( selectedBackend . name || selectedBackend . id ) ; setSelectedBackend ( null ) } } >
479- < i className = "fas fa-trash" /> Delete
480- </ button >
481- </ >
482- ) : (
483- < button className = "btn btn-primary btn-sm" onClick = { ( ) => { handleInstall ( selectedBackend . name || selectedBackend . id ) ; setSelectedBackend ( null ) } } >
484- < i className = "fas fa-download" /> Install
485- </ button >
486- ) }
487- < button className = "btn btn-secondary btn-sm" onClick = { ( ) => setSelectedBackend ( null ) } > Close</ button >
488- </ div >
489- </ div >
490- </ Modal >
491- ) }
461+ </ BackendDetailRow >
462+ </ tbody >
463+ </ table >
492464 </ div >
493465 )
494466}
0 commit comments