Files
artifactapi/ui/src/components/UsageInstructions.tsx
T
unkin-agent 6e6481e09a Split per-type registration points into per-type files (#150)
Parallel package-type PRs conflict because every type edits the same shared lines (README tables, provider.go, server.go imports, the package type list, UI maps, the e2e caching table). A new type now only adds files.

- move per-type README sections into docs/<type>/{remote,local,virtual}.md
- split provider.go capability and metadata types into cap_*.go and metadata_*.go
- import providers via internal/provider/all, one file per type
- declare each package type in package_type_<type>.go, registered via init()
- split the e2e caching cases and UI colours/snippets into per-type files

Reviewed-on: #150
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-17 15:54:47 +11:00

86 lines
2.7 KiB
TypeScript

import { useState } from 'react';
import './UsageInstructions.css';
import { packageTypes } from '../packagetypes';
import type { RepoClass, Snippet } from '../packagetypes/types';
// The UI is served on the same origin as the API, so this is the address clients use.
function baseURL(): string {
return window.location.origin;
}
// hostOnly is host[:port] for scheme-less docker/terraform addresses.
function hostOnly(): string {
return window.location.host;
}
// remoteProxyBase is where a remote (or virtual) repo's proxied artifacts live.
function remoteProxyBase(cls: RepoClass, name: string): string {
const seg = cls === 'virtual' ? 'virtual' : 'remote';
return `${baseURL()}/api/v1/${seg}/${name}`;
}
export function buildSnippets(packageType: string, repoClass: RepoClass, name: string): Snippet[] {
const url = baseURL();
const host = hostOnly();
const proxy = remoteProxyBase(repoClass, name);
const isLocal = repoClass === 'local';
const def = packageTypes[packageType] ?? packageTypes.generic;
return def.snippets({ packageType, name, url, host, proxy, isLocal });
}
function CodeBox({ snippet }: { snippet: Snippet }) {
const [copied, setCopied] = useState(false);
async function copy() {
try {
await navigator.clipboard.writeText(snippet.code);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
// Clipboard API unavailable (e.g. non-secure context); silently ignore.
}
}
return (
<div className="usage-snippet">
<div className="usage-snippet-title">{snippet.title}</div>
<div className="usage-codebox">
<button className="usage-copy-btn" onClick={copy} type="button">
{copied ? 'copied' : 'copy'}
</button>
<pre className="mono">{snippet.code}</pre>
</div>
{snippet.note && <div className="usage-note">{snippet.note}</div>}
</div>
);
}
interface UsageInstructionsProps {
packageType: string;
repoClass: RepoClass;
name: string;
defaultOpen?: boolean;
}
export function UsageInstructions({ packageType, repoClass, name, defaultOpen = false }: UsageInstructionsProps) {
const [open, setOpen] = useState(defaultOpen);
const snippets = buildSnippets(packageType, repoClass, name);
return (
<div className="usage-panel">
<button className="usage-toggle" onClick={() => setOpen(o => !o)} type="button" aria-expanded={open}>
<span className={`usage-caret ${open ? 'open' : ''}`}>&#9656;</span>
How do I use this?
</button>
{open && (
<div className="usage-body">
{snippets.map((s, i) => (
<CodeBox key={i} snippet={s} />
))}
</div>
)}
</div>
);
}