import { useQuery } from '@tanstack/react-query'; import { CheckIcon, ContainerIcon, CopyIcon } from 'lucide-react'; import { useState } from 'react'; import { Badge } from '#/components/atoms/badge.jsx'; import { Notice } from '#/components/atoms/notice.jsx'; import { RowIcon } from '#/components/atoms/row-icon.jsx'; import { EmptyState } from '#/components/molecules/empty-state.jsx'; import { ListRow } from '#/components/molecules/list-row.jsx'; import { TextLink } from '#/components/molecules/text-link.jsx'; import { get } from '#/lib/api.js'; import { bytes, timeAgo } from '#/lib/format.js'; import { queryKeys } from '#/lib/query.js'; /** One line summarizing an image repository's shape. */ function repoSummary(repo) { if (repo.manifests === 0) return 'nothing pushed yet'; const parts = [ `${repo.tags.length} ${repo.tags.length === 1 ? 'tag' : 'tags'}`, ]; parts.push(bytes(repo.size)); if (repo.updatedAt) parts.push(`pushed ${timeAgo(repo.updatedAt)}`); return parts.join(' ยท '); } /** A command with a copy button beside it, shown in full where room allows. */ function CopyCommand({ command, compact }) { const [copied, setCopied] = useState(false); const copy = (event) => { event.preventDefault(); event.stopPropagation(); navigator.clipboard.writeText(command).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); }; return ( ); } /** Every tag of one image: name, manifest digest, and its pull command. */ function TagList({ repo }) { return (
{repo.tags.map((tag) => (
{tag.tag} {tag.digest}
))}
); } /** * @param {{data: Object}} props */ export function RegistryPage({ data: account }) { const [openTags, setOpenTags] = useState(/** @type {string|null} */ (null)); const { data, error } = useQuery({ queryKey: queryKeys.ociRepos, queryFn: () => get('/oci/repos'), }); if (error) { return {error.message}; } if (!data) return null; if (!data.repos.length) { return ( } title="No images yet" description={ <> Your account is a container registry. Sign in with{' '} docker login {account.hostname} -u {account.handle} {' '} and an app password, then push to{' '} {account.hostname}/image-name:tag . Layers are stored in your repo as blobs, so images ride along in backups and migrate with your account. Anyone can pull. } /> ); } return (
{data.repos.map((repo) => { const latest = repo.tags.find((t) => t.tag === 'latest') ?? repo.tags[0]; return (
} title={
{repo.name} {repo.tags.slice(0, 4).map((tag) => ( {tag.tag} ))} {repo.tags.length > 4 && ( +{repo.tags.length - 4} )}
} description={repoSummary(repo)} actions={
{repo.tags.length > 0 && ( setOpenTags(openTags === repo.name ? null : repo.name) } > {openTags === repo.name ? 'Hide tags' : 'Tags'} )} {latest && ( )}
} /> {openTags === repo.name && }
); })}
); }