defmodule GitGudWeb.DashboardComponents do
@moduledoc """
The panels a dashboard is made of, and the small display helpers that
go with them.
Two dashboards use these: `GitGudWeb.PageLive.Home` (everything the
viewer can reach) and `GitGudWeb.OrgLive.Dashboard` (the same slice,
narrowed to one organization). They differ only in what they query;
they should never differ in how a row of work looks, which is why
this lives here rather than being written twice.
"""
use GitGudWeb, :html
alias GitGud.Repositories.Storage
@doc """
A titled, counted list panel. Renders the empty-state copy instead of
the list when `count` is zero, so callers don't each re-invent "or
else say nothing's here".
"""
attr :title, :string, required: true
attr :hint, :string, default: nil
attr :empty, :string, required: true
attr :count, :integer, required: true
attr :more_label, :string, default: nil
attr :more_path, :string, default: nil
slot :inner_block, required: true
def panel(assigns) do
~H"""
<section class="rounded-lg border border-base-300 p-4">
<header class="flex items-baseline justify-between gap-2 mb-2">
<div class="min-w-0">
<h2 class="font-semibold">
{@title}
<span :if={@count > 0} class="badge badge-sm badge-ghost ml-1">{@count}</span>
</h2>
<p :if={@hint} class="text-xs opacity-60">{@hint}</p>
</div>
<.link
:if={@more_path}
navigate={@more_path}
class="text-xs link link-hover opacity-60 shrink-0"
>
{@more_label} →
</.link>
</header>
<p :if={@count == 0} class="py-4 text-sm opacity-50">{@empty}</p>
<ul :if={@count > 0} class="divide-y divide-base-300">
{render_slot(@inner_block)}
</ul>
</section>
"""
end
@doc "One issue or pull request, as a row inside a `panel/1`."
attr :path, :string, required: true
attr :repo, :string, required: true
attr :number, :integer, required: true
attr :title, :string, required: true
attr :meta, :string, required: true
def work_row(assigns) do
~H"""
<li class="py-2 min-w-0">
<.link navigate={@path} class="text-sm link link-hover line-clamp-1">{@title}</.link>
<p class="text-xs opacity-60 mt-0.5 truncate">
<span class="font-mono">{@repo}#{@number}</span> · {@meta}
</p>
</li>
"""
end
@doc "One workflow run, as a row inside a `panel/1`."
attr :run, :map, required: true
def ci_row(assigns) do
~H"""
<li class="py-2 flex items-baseline gap-2 min-w-0">
<span class={["badge badge-xs shrink-0", ci_badge(@run.status)]}>{@run.status}</span>
<.link
navigate={
~p"/r/#{repo_handle(@run.repository)}/#{@run.repository.name}/actions/#{@run.number}"
}
class="text-sm link link-hover truncate"
>
{@run.workflow_name || "run ##{@run.number}"}
</.link>
<span class="text-xs opacity-60 font-mono truncate">{repo_path(@run.repository)}</span>
<span class="text-xs opacity-50 ml-auto shrink-0">{ago(@run.inserted_at)}</span>
</li>
"""
end
@doc "One repository, as a row in a recently-pushed list."
attr :repo, :map, required: true
def repo_row(assigns) do
~H"""
<li class="py-2 flex items-baseline gap-2 min-w-0">
<.link
navigate={~p"/r/#{repo_handle(@repo)}/#{@repo.name}"}
class="font-mono text-sm link link-hover truncate"
>
{repo_path(@repo)}
</.link>
<span :if={@repo.description} class="text-xs opacity-60 truncate">
{@repo.description}
</span>
<span class="text-xs opacity-50 ml-auto shrink-0">{ago(@repo.pushed_at)}</span>
</li>
"""
end
# ── display helpers ──────────────────────────────────────────────────
@doc "The namespace handle a repository lives under."
def repo_handle(%{organization: %{handle: h}}) when is_binary(h), do: h
def repo_handle(%{owner: user}), do: Storage.owner_handle(user)
@doc "`owner/name`, for the monospace crumbs on every row."
def repo_path(repo), do: "#{repo_handle(repo)}/#{repo.name}"
@doc "daisyUI badge class for a workflow run status."
def ci_badge("success"), do: "badge-success"
def ci_badge("failure"), do: "badge-error"
def ci_badge(s) when s in ~w(running queued blocked), do: "badge-warning"
def ci_badge(_), do: "badge-ghost"
@doc """
Who opened something. Federated rows carry a `source_actor` and no
local author, so this deliberately doesn't assume one.
"""
def author_name(%{author: %{handle: h}}) when is_binary(h), do: h
def author_name(_), do: "someone"
@doc "Coarse relative time. Precision past a day isn't worth a row of chrome."
def ago(nil), do: ""
def ago(%DateTime{} = dt) do
case DateTime.diff(DateTime.utc_now(), dt) do
s when s < 60 -> "just now"
s when s < 3600 -> "#{div(s, 60)}m ago"
s when s < 86_400 -> "#{div(s, 3600)}h ago"
s when s < 2_592_000 -> "#{div(s, 86_400)}d ago"
s when s < 31_536_000 -> "#{div(s, 2_592_000)}mo ago"
s -> "#{div(s, 31_536_000)}y ago"
end
end
@doc """
What to call the review panel's inference.
Once someone has actually been asked, say so; otherwise the panel is
still guessing from maintainer status and should admit it.
"""
def review_hint([]), do: "Open in repos you maintain, opened by someone else"
def review_hint(_requested), do: "Requested of you, plus repos you maintain"
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
5.5 KiB · text
History
8041c34