Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
309 lines
12 KiB
Elixir
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
|