Files
at-container-registry/pkg/appview/templates/pages/repository.html
T

290 lines
17 KiB
HTML

{{ define "repository" }}
<!DOCTYPE html>
<html lang="en">
<head>
{{ template "head" . }}
{{ template "meta" .Meta }}
</head>
<body>
{{ template "nav" . }}
<main class="container mx-auto px-4 py-8">
<div class="space-y-8">
<!-- Repository Header -->
<div class="card bg-base-100 shadow-sm p-6 space-y-6 w-full">
<div class="flex gap-4 items-start">
{{ template "repo-avatar" (dict "IconURL" .Repository.IconURL "RepositoryName" .Repository.Name "IsOwner" .IsOwner) }}
<div class="flex-1 min-w-0">
<h1 class="text-2xl font-bold">
<a href="/u/{{ .Owner.Handle }}" class="link link-primary">{{ .Owner.Handle }}</a>
<span class="text-base-content/60">/</span>
<span>{{ .Repository.Name }}</span>
</h1>
{{ if .Repository.Description }}
<p class="text-base-content/70 mt-2">{{ .Repository.Description }}</p>
{{ end }}
</div>
</div>
<!-- Star Button, Pull Count and Metadata Row -->
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-4">
{{ template "star" (dict "IsStarred" .IsStarred "StarCount" .StarCount "Interactive" true "Handle" .Owner.Handle "Repository" .Repository.Name) }}
{{ template "pull-count" (dict "PullCount" .PullCount) }}
</div>
<!-- Metadata Section -->
{{ if or .Repository.Licenses .Repository.SourceURL .Repository.DocumentationURL .Repository.Version }}
<div class="flex flex-wrap items-center gap-2">
{{ if .Repository.Version }}
<span class="badge badge-md badge-primary badge-outline" title="Version">
{{ .Repository.Version }}
</span>
{{ end }}
{{ if .Repository.Licenses }}
{{ range parseLicenses .Repository.Licenses }}
{{ if .IsValid }}
<a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" class="badge badge-md badge-soft badge-secondary" title="{{ .Name }}">
{{ .SPDXID }}
</a>
{{ else }}
<span class="badge badge-md badge-soft badge-secondary" title="Custom license: {{ .Name }}">
{{ .Name }}
</span>
{{ end }}
{{ end }}
{{ end }}
{{ if .Repository.SourceURL }}
<a href="{{ .Repository.SourceURL }}" target="_blank" rel="noopener noreferrer" class="link link-primary text-sm" aria-label="View source code (opens in new tab)">
Source
</a>
{{ end }}
{{ if .Repository.DocumentationURL }}
<a href="{{ .Repository.DocumentationURL }}" target="_blank" rel="noopener noreferrer" class="link link-primary text-sm" aria-label="View documentation (opens in new tab)">
Documentation
</a>
{{ end }}
</div>
{{ end }}
</div>
<div class="divider my-2"></div>
<!-- Pull Command -->
<div class="space-y-2">
{{ if eq .ArtifactType "helm-chart" }}
<p class="font-semibold">Pull this chart</p>
{{ if .Tags }}
{{ $firstTag := index .Tags 0 }}
{{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name " --version " $firstTag.Tag.Tag) }}
{{ else }}
{{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name) }}
{{ end }}
{{ else }}
<p class="font-semibold">Pull this image</p>
{{ if .Tags }}
{{ $firstTag := index .Tags 0 }}
{{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" $firstTag.Tag.Tag) }}
{{ else }}
{{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":latest") }}
{{ end }}
{{ end }}
</div>
</div>
<!-- README and Tags/Manifests Layout -->
{{ if .ReadmeHTML }}
<div class="grid grid-cols-1 lg:grid-cols-[3fr_2fr] gap-8">
<!-- README Section (Left) -->
<div class="card bg-base-100 shadow-sm p-6 space-y-4 min-w-0">
<h2 class="text-xl font-semibold">Overview</h2>
<div class="prose prose-sm max-w-none">
{{ .ReadmeHTML }}
</div>
</div>
<!-- Tags and Manifests (Right) -->
<div class="space-y-8 min-w-0">
{{ end }}
<!-- Tags Section -->
<div class="card bg-base-100 shadow-sm p-6 space-y-4">
<h2 class="text-xl font-semibold">Tags</h2>
{{ if .Tags }}
<div class="space-y-4">
{{ range .Tags }}
<div class="bg-base-200 rounded-lg p-4 space-y-3" id="tag-{{ sanitizeID .Tag.Tag }}">
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex flex-wrap items-center gap-2">
<span class="font-mono font-semibold text-lg">{{ .Tag.Tag }}</span>
{{ if eq .ArtifactType "helm-chart" }}
<span class="badge badge-md badge-soft badge-helm">{{ icon "helm" "size-3" }} Helm chart</span>
{{ else if .IsMultiArch }}
<span class="badge badge-md badge-soft badge-accent">Multi-arch</span>
{{ end }}
{{ if .HasAttestations }}
<span class="badge badge-md badge-soft badge-success">{{ icon "shield-check" "size-3" }} Attestations</span>
{{ end }}
</div>
<div class="flex items-center gap-2">
<time class="text-sm text-base-content/60" datetime="{{ .Tag.CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}">
{{ timeAgo .Tag.CreatedAt }}
</time>
{{ if $.IsOwner }}
<button class="btn btn-ghost btn-sm text-error"
hx-ext="json-enc"
hx-delete="/api/tags"
hx-vals='{"repo": "{{ $.Repository.Name }}", "tag": "{{ .Tag.Tag }}"}'
hx-confirm="Delete tag {{ .Tag.Tag }}?"
hx-target="#tag-{{ sanitizeID .Tag.Tag }}"
hx-swap="outerHTML"
aria-label="Delete tag {{ .Tag.Tag }}">
{{ icon "trash-2" "size-4" }}
</button>
{{ end }}
</div>
</div>
<div class="text-sm">
<div class="flex flex-wrap justify-between items-center gap-2">
<div class="flex items-center gap-2">
<code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Tag.Digest }}">{{ .Tag.Digest }}</code>
<button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Tag.Digest }}')" aria-label="Copy tag digest to clipboard">{{ icon "copy" "size-3" }}</button>
</div>
{{ if .Platforms }}
<div class="flex flex-wrap gap-1">
{{ range .Platforms }}
<span class="badge badge-sm badge-soft badge-secondary">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span>
{{ end }}
</div>
{{ end }}
</div>
</div>
{{ if eq .ArtifactType "helm-chart" }}
{{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name " --version " .Tag.Tag) }}
{{ else }}
{{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" .Tag.Tag) }}
{{ end }}
</div>
{{ end }}
</div>
{{ else }}
<p class="text-base-content/60">No tags available</p>
{{ end }}
</div>
<!-- Manifests Section -->
<div class="card bg-base-100 shadow-sm p-6 space-y-4">
<div class="flex flex-wrap justify-between items-center gap-4">
<h2 class="text-xl font-semibold">Manifests</h2>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" class="checkbox checkbox-sm" id="show-offline-toggle" onchange="toggleOfflineManifests()">
<span>Show offline images</span>
</label>
</div>
{{ if .Manifests }}
<div class="space-y-4">
{{ range .Manifests }}
<div class="bg-base-200 rounded-lg p-4 space-y-3" id="manifest-{{ sanitizeID .Manifest.Digest }}" data-reachable="{{ .Reachable }}">
<div class="flex flex-wrap items-start justify-between gap-2">
<div class="space-y-2">
<div class="flex flex-wrap items-center gap-2">
{{ if .IsManifestList }}
<span class="flex items-center gap-1 font-medium">{{ icon "package" "size-5" }} Multi-arch</span>
{{ else if eq .ArtifactType "helm-chart" }}
<span class="flex items-center gap-1 font-medium text-helm">{{ icon "helm" "size-5" }} Helm Chart</span>
{{ else }}
<span class="flex items-center gap-1 font-medium">{{ icon "box" "size-5" }} Image</span>
{{ end }}
{{ if .HasAttestations }}
<span class="badge badge-md badge-soft badge-success">{{ icon "shield-check" "size-3" }} Attestations</span>
{{ end }}
{{ if .Pending }}
<span class="badge badge-sm badge-info"
hx-get="/api/manifest-health?endpoint={{ .Manifest.HoldEndpoint | urlquery }}"
hx-trigger="load delay:2s"
hx-swap="outerHTML">
{{ icon "refresh-ccw" "size-3" }} Checking...
</span>
{{ else if not .Reachable }}
<span class="badge badge-sm badge-warning">{{ icon "alert-triangle" "size-3" }} Offline</span>
{{ end }}
</div>
<div class="flex items-center gap-2">
<code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Manifest.Digest }}">{{ .Manifest.Digest }}</code>
<button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Manifest.Digest }}')" aria-label="Copy manifest digest to clipboard">{{ icon "copy" "size-3" }}</button>
</div>
</div>
<div class="flex items-center gap-2">
<time class="text-sm text-base-content/60" datetime="{{ .Manifest.CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}">
{{ timeAgo .Manifest.CreatedAt }}
</time>
{{ if $.IsOwner }}
<button class="btn btn-ghost btn-sm text-error"
onclick="deleteManifest('{{ $.Repository.Name }}', '{{ .Manifest.Digest }}', '{{ sanitizeID .Manifest.Digest }}')"
aria-label="Delete manifest {{ truncateDigest .Manifest.Digest 16 }}">
{{ icon "trash-2" "size-4" }}
</button>
{{ end }}
</div>
</div>
<div class="text-sm">
<div class="flex flex-wrap justify-between items-center gap-2">
<div>
{{ if .Tags }}
<span class="text-base-content/60">Tags:</span>
{{ range $index, $tag := .Tags }}{{ if $index }}, {{ end }}{{ $tag }}{{ end }}
{{ else }}
<span class="text-base-content/50">(untagged)</span>
{{ end }}
</div>
{{ if .IsManifestList }}
{{ if .Platforms }}
<div class="flex flex-wrap gap-1">
{{ range .Platforms }}
<span class="badge badge-sm badge-soft badge-secondary">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span>
{{ end }}
</div>
{{ end }}
{{ end }}
</div>
</div>
</div>
{{ end }}
</div>
{{ else }}
<p class="text-base-content/60">No manifests available</p>
{{ end }}
</div>
{{ if .ReadmeHTML }}
</div><!-- Close sidebar -->
</div><!-- Close grid layout -->
{{ end }}
</div>
</main>
<!-- Modal container for HTMX -->
<div id="modal"></div>
<!-- Manifest Delete Confirmation Modal -->
<dialog id="manifest-delete-modal" class="modal">
<div class="modal-box">
<h3 class="text-lg font-bold">Confirm Deletion</h3>
<p id="manifest-delete-message" class="py-2">This manifest has associated tags that will also be deleted:</p>
<ul id="manifest-delete-tags" class="list-disc list-inside text-sm space-y-1"></ul>
<p class="font-bold py-2 text-error">This action cannot be undone.</p>
<div class="modal-action">
<button class="btn" onclick="closeManifestDeleteModal()">Cancel</button>
<button class="btn btn-error" id="confirm-manifest-delete-btn">Delete All</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick="closeManifestDeleteModal()">close</button>
</form>
</dialog>
{{ template "footer" . }}
</body>
</html>
{{ end }}