5.6 KiB · text 5af55d9
defmodule GitGudWeb.RepoLive.Packages do
@moduledoc """
Lists container/package versions mirrored from zot for a repository.
This is a UI over the data zot pushes through `/_webhooks/zot`.
Click a package row to see all of its versions inline.
"""
use GitGudWeb, :live_view
alias GitGud.Packages
alias GitGud.Repositories
alias GitGud.Repositories.Storage
@impl true
def mount(%{"owner" => owner, "name" => name}, _session, socket) do
repo =
Repositories.get_repository_by_path!(owner, name)
|> GitGud.Repo.preload([:owner, :organization])
packages = Packages.list_packages(repo)
{:ok,
socket
|> assign(:repo, repo)
|> assign(:handle, Storage.repo_handle(repo))
|> GitGudWeb.RepoLive.Header.assign_chrome(repo)
|> assign(:packages, packages)
|> assign(:expanded_id, nil)
|> assign(:versions_by_id, %{})
|> assign(:registry_host, Application.get_env(:git_gud, :registry_host))
|> assign(:page_title, "Packages — #{owner}/#{name}")}
end
# Reconstruct the zot repository path the webhook deflated: a package
# named after the repo was pushed as `<owner>/<repo>`; a sub-image as
# `<owner>/<repo>/<image>`. (See PackagesWebhookController.)
defp pull_ref(handle, repo, pkg, version, host) do
base =
if pkg.name == repo.name,
do: "#{handle}/#{repo.name}",
else: "#{handle}/#{repo.name}/#{pkg.name}"
"#{host}/#{base}:#{version}"
end
@impl true
def handle_event("toggle", %{"id" => id}, socket) do
id = String.to_integer(id)
cond do
socket.assigns.expanded_id == id ->
{:noreply, assign(socket, :expanded_id, nil)}
true ->
package = Enum.find(socket.assigns.packages, &(&1.id == id))
versions =
case Map.get(socket.assigns.versions_by_id, id) do
nil -> Packages.list_versions(package)
cached -> cached
end
{:noreply,
socket
|> assign(:expanded_id, id)
|> assign(:versions_by_id, Map.put(socket.assigns.versions_by_id, id, versions))}
end
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="space-y-4">
<GitGudWeb.RepoLive.Header.header
repo={@repo}
handle={@handle}
current_scope={@current_scope}
has_packages?={@has_packages?}
can_admin?={@can_admin?}
latest_run={@latest_run}
/>
<header>
<h1 class="text-xl font-semibold">Packages</h1>
<p class="text-xs opacity-70 mt-1">
Mirrored from the OCI registry. Authoritative storage lives in zot;
this view tracks pushes via webhook so you can see what's been
published without leaving the forge.
</p>
</header>
<ul
id="packages"
class="divide-y divide-base-300 border border-base-300 rounded"
>
<li :if={@packages == []} class="px-3 py-6 text-sm opacity-60 text-center">
No packages published yet.
</li>
<li :for={pkg <- @packages} class="px-3 py-2">
<button
type="button"
phx-click="toggle"
phx-value-id={pkg.id}
class="w-full flex items-baseline justify-between gap-3 text-left"
>
<div>
<p class="font-mono text-sm">{pkg.name}</p>
<p class="text-xs opacity-60">
<span class="badge badge-xs">{pkg.kind}</span>
<span :if={pkg.latest_version} class="ml-1">
latest: <span class="font-mono">{pkg.latest_version}</span>
</span>
</p>
</div>
<.icon
name={if @expanded_id == pkg.id, do: "hero-chevron-up", else: "hero-chevron-down"}
class="size-4 opacity-50"
/>
</button>
<ul
:if={@expanded_id == pkg.id}
class="mt-2 ml-4 border-l border-base-300 pl-3 space-y-1 text-xs"
>
<li :for={v <- Map.get(@versions_by_id, pkg.id, [])} class="space-y-1">
<div class="flex items-baseline justify-between gap-2">
<span class="font-mono">{v.version}</span>
<span class="opacity-60 truncate">{short_digest(v.digest)}</span>
<span class="opacity-50">{format_size(v.size)}</span>
<span class="opacity-50">{format_dt(v.pushed_at)}</span>
</div>
<code
:if={@registry_host}
class="block bg-base-200 rounded px-2 py-1 select-all text-[11px]"
>podman pull {pull_ref(@handle, @repo, pkg, v.version, @registry_host)}</code>
</li>
<li :if={Map.get(@versions_by_id, pkg.id, []) == []} class="opacity-60">
No versions.
</li>
</ul>
</li>
</ul>
</div>
</Layouts.app>
"""
end
defp short_digest(nil), do: ""
defp short_digest("sha256:" <> rest), do: "sha256:" <> binary_part(rest, 0, min(12, byte_size(rest)))
defp short_digest(d) when is_binary(d), do: binary_part(d, 0, min(20, byte_size(d)))
defp short_digest(_), do: ""
defp format_size(nil), do: ""
defp format_size(0), do: ""
defp format_size(b) when b < 1024, do: "#{b} B"
defp format_size(b) when b < 1_048_576, do: "#{Float.round(b / 1024, 1)} KB"
defp format_size(b), do: "#{Float.round(b / 1_048_576, 1)} MB"
defp format_dt(nil), do: ""
defp format_dt(dt), do: Calendar.strftime(dt, "%Y-%m-%d %H:%M")
end