eee8ee1c31
ci/woodpecker/tag/docker Pipeline was successful
## Why A repository detail page in the ArtifactAPI UI showed configuration and stats, but nothing that told a user how to actually *consume* the repo. You had to already know the per-package-type URL scheme (yum baseurl, pip index-url, docker registry host, terraform source address, ...) by hand. This adds an in-page, copy-pasteable "How do I use this?" panel so each repo page tells you exactly how to point a Linux host at it. ## Changes - Add a `UsageInstructions` component: a collapsible "How do I use this?" panel with monospace code boxes and copy-to-clipboard buttons, styled to match the existing detail-section / badge theme. - Generate instructions per package type (rpm, pypi, npm, docker, terraform, helm, alpine, goproxy, puppet, generic) and per class: - **remote** — consume via the caching proxy (`/api/v1/remote/<name>/...`). - **local** — consume via the real registry endpoint, plus a publish/push example (rpm `PUT .../files/`, docker Registry V2 push, terraform provider upload). - **virtual** — consume the merged index via `/api/v1/virtual/<name>/...` using the same per-type client config. - Interpolate the repository's real name into every snippet so it is genuinely copy-pasteable. - Resolve the instance base URL from `window.location.origin` (the UI is served on the API origin, client `BASE=''`) instead of hardcoding a hostname; falls back to the public host only when `window` is unavailable. - Render the panel on `RemoteDetail`, `LocalDetail`, and the `Virtuals` member-expand panel. ## Verification - `npm run build` (tsc typecheck + vite build) passes. - Rendered the component in headless Chromium against real repo data for rpm remote, rpm local (with publish), docker local (with push), terraform local (HCL `required_providers` + signing note), and a pypi virtual — all snippets render with the correct URLs and theme. Reviewed-on: #105 Co-authored-by: Ben Vincent <ben@unkin.net> Co-committed-by: Ben Vincent <ben@unkin.net>
113 lines
3.7 KiB
TypeScript
113 lines
3.7 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));
|
|
}, []);
|
|
|
|
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: '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)}
|
|
/>
|
|
)}
|
|
|
|
{expanded && (
|
|
<div className="virtual-detail-panel">
|
|
<h3 className="section-label">Members of {expanded}</h3>
|
|
<ul className="member-list">
|
|
{virtuals
|
|
.find(v => v.name === expanded)
|
|
?.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>
|
|
{(() => {
|
|
const v = virtuals.find(x => x.name === expanded);
|
|
return v ? (
|
|
<UsageInstructions packageType={v.package_type} repoClass="virtual" name={v.name} defaultOpen />
|
|
) : null;
|
|
})()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|