tarakan/lib/tarakan_web/components/repository_components.ex
Maxfield Luke af6077b9c3
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s
Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
2026-07-29 04:43:40 -04:00

309 lines
12 KiB
Elixir

defmodule TarakanWeb.RepositoryComponents do
@moduledoc """
Shared repository page chrome.
Every repository-scoped page renders the same header at the same scale:
status badge row, display-face title, canonical source link, host metadata,
and the Code / Commits / Security tab rail. Page bodies differ; the frame does not.
"""
use Phoenix.Component
import TarakanWeb.CoreComponents,
only: [icon: 1, breadcrumbs: 1, handle_link: 1, notch_badge: 1]
alias TarakanWeb.RepositoryPaths
@doc ~S'Formats a star count compactly (`1200` -> `"1.2k"`); `nil` -> `"0"`.'
def compact_stars(count) when is_integer(count) and count >= 1000,
do: "#{Float.round(count / 1000, 1)}k"
def compact_stars(count) when is_integer(count), do: Integer.to_string(count)
def compact_stars(_count), do: "0"
attr :repository, Tarakan.Repositories.Repository, required: true
attr :active_tab, :atom, required: true, values: [:code, :commits, :security]
def repository_header(assigns) do
~H"""
<header>
<.breadcrumbs id="repository-breadcrumb">
<:crumb navigate="/">registry</:crumb>
<:crumb navigate={RepositoryPaths.repository_path(@repository)}>
{@repository.owner}
</:crumb>
<:crumb navigate={RepositoryPaths.repository_path(@repository)}>
{@repository.name}
</:crumb>
</.breadcrumbs>
<div class="flex flex-wrap items-center gap-3">
<.notch_badge
id="repository-status"
class={status_badge_class(@repository)}
title={repository_status_title(@repository)}
>
{repository_status_label(@repository)}
</.notch_badge>
<.notch_badge :if={@repository.archived} class="text-signal">
archived
</.notch_badge>
<.notch_badge
:if={@repository.managed_disclosure}
id="repository-managed-disclosure"
class="text-quote"
title="Disclosure to the vendor is handled by Tarakan staff. Handling only - the public record is unchanged."
>
Managed disclosure
</.notch_badge>
</div>
<div class="mt-3 flex flex-col justify-between gap-4 lg:flex-row lg:items-end">
<div class="min-w-0">
<h1
id="repository-name"
class="break-words font-display text-2xl font-medium uppercase leading-tight tracking-[0.02em] text-ink sm:text-3xl"
>
<span class="text-ink-faint">{@repository.owner}/</span>{@repository.name}
</h1>
<p :if={@repository.description} class="mt-2 max-w-3xl text-sm leading-6 text-ink-muted">
{@repository.description}
</p>
</div>
<a
:if={not Tarakan.Repositories.Repository.hosted?(@repository)}
id="repository-source-link"
href={@repository.canonical_url}
rel="noreferrer"
target="_blank"
class="inline-flex max-w-full min-w-0 items-center gap-2 font-mono text-xs text-ink-muted transition hover:text-signal"
>
<span class="min-w-0 break-all">{@repository.canonical_url}</span>
<.icon name="hero-arrow-up-right" class="size-4 shrink-0" />
</a>
</div>
<div
id="github-metadata"
class="mt-4 flex flex-wrap items-center gap-x-4 gap-y-2 font-mono text-[11px] text-ink-faint sm:gap-x-5 sm:text-xs"
>
<span :if={@repository.primary_language} class="inline-flex items-center gap-2">
<span class="size-2 bg-signal"></span>
{@repository.primary_language}
</span>
<span
:if={not Tarakan.Repositories.Repository.hosted?(@repository)}
class="inline-flex items-center gap-1.5"
>
<.icon name="hero-star" class="size-3.5" /> {@repository.stars_count} stars
</span>
<span
:if={not Tarakan.Repositories.Repository.hosted?(@repository)}
class="inline-flex items-center gap-1.5"
>
<.icon name="hero-code-bracket" class="size-3.5" /> {@repository.forks_count} forks
</span>
<span :if={Tarakan.Repositories.Repository.hosted?(@repository)}>hosted on tarakan</span>
<span :if={@repository.default_branch}>default: {@repository.default_branch}</span>
<span class="inline-flex items-center gap-1.5">
<.icon name="hero-clock" class="size-3.5" />
registered {Calendar.strftime(
@repository.inserted_at,
"%Y-%m-%d"
)}
</span>
<span :if={@repository.submitted_by} id="repository-submitter">
by <.handle_link handle={@repository.submitted_by.handle} class="text-ink-muted" />
</span>
</div>
<nav
id="repository-navigation"
class="mt-5 flex items-end gap-0 overflow-x-auto overscroll-x-contain border-b border-rule text-sm [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
aria-label="Repository"
>
<.repository_tab
id="repository-code-tab"
active={@active_tab == :code}
navigate={RepositoryPaths.repository_path(@repository)}
icon="hero-code-bracket"
label="Code"
/>
<.repository_tab
id="repository-commits-tab"
active={@active_tab == :commits}
navigate={RepositoryPaths.repository_commits_path(@repository)}
icon="hero-clock"
label="Commits"
/>
<.repository_tab
id="repository-overview-tab"
active={@active_tab == :security}
navigate={RepositoryPaths.repository_security_path(@repository)}
icon="hero-shield-check"
label="Security"
>
<span
:if={security_tab_count(@repository)}
class={[
"rounded-full px-1.5 py-0.5 font-mono text-[10px] leading-none",
security_tab_count_class(@repository)
]}
>
{security_tab_count(@repository)}
</span>
</.repository_tab>
</nav>
</header>
"""
end
attr :id, :string, required: true
attr :active, :boolean, required: true
attr :navigate, :string, required: true
attr :icon, :string, required: true
attr :label, :string, required: true
slot :inner_block
# The active tab stays a link. It used to render as a span, which made the
# most natural navigation on the page impossible: from a commit detail page
# the Commits tab points at the commit list, and from a nested file the Code
# tab points at the repository root, and neither could be clicked. A span is
# also not focusable, so keyboard users tabbed straight past the section they
# were in. aria-current carries the "you are here" meaning instead.
defp repository_tab(assigns) do
~H"""
<.link
id={@id}
navigate={@navigate}
aria-current={@active && "page"}
class={[
"-mb-px inline-flex shrink-0 items-center gap-2 border-b-2 px-3 py-3 transition sm:px-4",
@active && "border-signal font-semibold text-ink",
!@active && "border-transparent text-ink-muted hover:border-rule hover:text-ink"
]}
>
<.icon name={@icon} class="size-4" /> {@label} {render_slot(@inner_block)}
</.link>
"""
end
@doc """
The scan's submitted Tarakan Scan Format document, pretty-printed for the
raw-report block. Returns the stored text unchanged when it isn't JSON.
"""
def raw_report(%{raw_document: nil}), do: nil
def raw_report(%{raw_document: raw_document}) do
Jason.Formatter.pretty_print(raw_document)
rescue
_not_json -> raw_document
end
@doc """
Short label for who may do a job, or how a submission was made.
Hybrid is agent draft with a person in the loop - not a description of
Tarakan overall (the product is already human-owned).
"""
def provenance_label("agent"), do: "Agent"
def provenance_label("human"), do: "Human"
def provenance_label("hybrid"), do: "Agent + human"
def provenance_label(other), do: other
@doc "Human-readable label for a review or task kind."
def review_kind_label("code_review"), do: "Code review"
def review_kind_label("threat_model"), do: "Threat model"
def review_kind_label("privacy_review"), do: "Privacy review"
def review_kind_label("business_logic"), do: "Business logic"
def review_kind_label("verify_findings"), do: "Verify findings"
def review_kind_label("write_fix"), do: "Write a fix"
def review_kind_label("diff_review"), do: "Diff review"
def review_kind_label("refute_finding"), do: "Refute a finding"
def review_kind_label("reproduce_finding"), do: "Reproduce a finding"
def review_kind_label("calibrate_severity"), do: "Re-score severity"
def review_kind_label("synthesize_rule"), do: "Write a detector"
def review_kind_label(other), do: other
@doc """
Short status for repo headers and meta.
Avoids the useless lone label \"findings\" once every repo has some.
Prefers open/verified counts and clear/unreviewed states.
"""
def repository_status_label(%{status: "unscanned"}), do: "No report"
def repository_status_label(%{open_findings_count: open, verified_findings_count: verified})
when is_integer(open) and open > 0 do
open_label = if open == 1, do: "1 open", else: "#{open} open"
if is_integer(verified) and verified > 0 do
verified_label = if verified == 1, do: "1 verified", else: "#{verified} verified"
"#{open_label} · #{verified_label}"
else
open_label
end
end
def repository_status_label(%{status: status}) when status in ["reviewed", "clear"],
do: "Clear"
def repository_status_label(%{scan_count: n}) when is_integer(n) and n > 0, do: "Clear"
def repository_status_label(_repository), do: "No report"
defp repository_status_title(%{open_findings_count: open, verified_findings_count: verified})
when is_integer(open) and open > 0 do
open_part =
if open == 1,
do: "1 unique finding still open",
else: "#{open} unique findings still open"
if is_integer(verified) and verified > 0 do
verified_part =
if verified == 1,
do: "1 independently verified",
else: "#{verified} independently verified"
"#{open_part}; #{verified_part}"
else
open_part
end
end
defp repository_status_title(%{status: "unscanned"}),
do: "No public review on the record yet"
defp repository_status_title(%{status: status}) when status in ["reviewed", "clear"],
do: "Reported with no open findings"
defp repository_status_title(%{scan_count: n}) when is_integer(n) and n > 0,
do: "Reported with no open findings"
defp repository_status_title(_repository), do: "No public review on the record yet"
defp status_badge_class(%{open_findings_count: open}) when is_integer(open) and open > 0,
do: "text-signal"
defp status_badge_class(%{status: "unscanned"}), do: "text-ink-faint"
defp status_badge_class(%{status: status}) when status in ["reviewed", "clear"],
do: "text-quote"
defp status_badge_class(_repository), do: "text-ink-muted"
# Tab pill: open count when > 0, else verified count, else nothing for unscanned.
defp security_tab_count(%{open_findings_count: open}) when is_integer(open) and open > 0,
do: open
defp security_tab_count(%{verified_findings_count: verified})
when is_integer(verified) and verified > 0,
do: verified
defp security_tab_count(%{scan_count: scans}) when is_integer(scans) and scans > 0, do: "ok"
defp security_tab_count(_repository), do: nil
defp security_tab_count_class(%{open_findings_count: open}) when is_integer(open) and open > 0,
do: "bg-signal text-ground"
defp security_tab_count_class(_repository), do: "bg-panel text-ink-muted border border-rule"
end