Skip to content

Commit 817d202

Browse files
committed
feat(gallery): Switch to expandable box instead of pop-over and display model files
Signed-off-by: Richard Palethorpe <io@richiejp.com>
1 parent 5c5e537 commit 817d202

2 files changed

Lines changed: 215 additions & 180 deletions

File tree

core/http/react-ui/src/pages/Backends.jsx

Lines changed: 80 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useState, useEffect, useCallback, useRef } from 'react'
22
import { useNavigate, useOutletContext } from 'react-router-dom'
33
import { backendsApi } from '../utils/api'
4+
import React from 'react'
45
import { useOperations } from '../hooks/useOperations'
56
import LoadingSpinner from '../components/LoadingSpinner'
67
import { renderMarkdown } from '../utils/markdown'
7-
import Modal from '../components/Modal'
88

99
export 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

Comments
 (0)