Files
artifactapi/ui/src/pages/Virtuals.tsx
T
unkin-agent 00edeb478b Show virtual parallel_fetch in UI and TUI (#173)
Virtuals carry a `parallel_fetch` flag in the v2 API, but the UI type omits it and neither the UI nor the TUI displays it, so the setting is invisible to operators.

- add `parallel_fetch` to the TS `Virtual` type
- add a Parallel column with a yellow badge to the UI virtuals table
- use one `selected` lookup for the virtual detail panel
- add a parallel column to the TUI virtuals list, padding the member count (`%3d members`) so columns align

Reviewed-on: #173
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-25 20:25:00 +11:00

115 lines
3.8 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../api/client';
import type { Remote, Virtual } from '../api/types';
import { Badge } from '../components/Badge';
import { DataTable } from '../components/DataTable';
import { UsageInstructions } from '../components/UsageInstructions';
import './Virtuals.css';
export function Virtuals() {
const [virtuals, setVirtuals] = useState<Virtual[]>([]);
const [remoteMap, setRemoteMap] = useState<Record<string, Remote>>({});
const [loading, setLoading] = useState(true);
const [expanded, setExpanded] = useState<string | null>(null);
useEffect(() => {
Promise.all([api.listVirtuals(), api.listRemotes()])
.then(([v, r]) => {
setVirtuals(v || []);
const map: Record<string, Remote> = {};
for (const remote of r || []) {
map[remote.name] = remote;
}
setRemoteMap(map);
})
.finally(() => setLoading(false));
}, []);
const selected = virtuals.find(x => x.name === expanded);
function memberLink(name: string) {
const remote = remoteMap[name];
if (remote?.repo_type === 'local') {
return `/locals/${name}`;
}
return `/remotes/${name}`;
}
return (
<div>
<h1 className="page-title">Virtual Repositories</h1>
{loading ? (
<div className="loading">Loading...</div>
) : (
<DataTable
columns={[
{
key: 'name',
header: 'Name',
render: (v: Virtual) => <span className="mono">{v.name}</span>,
},
{
key: 'type',
header: 'Type',
render: (v: Virtual) => <Badge variant="green">{v.package_type}</Badge>,
width: '110px',
},
{
key: 'members',
header: 'Members',
render: (v: Virtual) => (
<span className="member-count">{v.members?.length || 0} repos</span>
),
width: '110px',
},
{
key: 'description',
header: 'Description',
render: (v: Virtual) => v.description || <span className="text-muted">—</span>,
},
{
key: 'parallel',
header: 'Parallel',
render: (v: Virtual) =>
v.parallel_fetch ? <Badge variant="yellow">parallel</Badge> : <span className="text-muted">—</span>,
width: '100px',
},
{
key: 'managed',
header: 'Managed',
render: (v: Virtual) =>
v.managed_by ? <Badge variant="blue">{v.managed_by}</Badge> : <span className="text-muted">—</span>,
width: '100px',
},
]}
data={virtuals}
emptyMessage="No virtual repositories configured"
onRowClick={(v) => setExpanded(expanded === v.name ? null : v.name)}
/>
)}
{selected && (
<div className="virtual-detail-panel">
<h3 className="section-label">Members of {selected.name}</h3>
<ul className="member-list">
{selected.members?.map((m, i) => {
const remote = remoteMap[m];
const typeLabel = remote?.repo_type === 'local' ? 'local' : 'remote';
return (
<li key={m}>
<span className="member-priority">{i + 1}</span>
<Link to={memberLink(m)} className="mono">{m}</Link>
<Badge variant={typeLabel === 'local' ? 'yellow' : 'default'}>{typeLabel}</Badge>
</li>
);
})}
</ul>
<UsageInstructions packageType={selected.package_type} repoClass="virtual" name={selected.name} defaultOpen />
</div>
)}
</div>
);
}