tarakan/lib/tarakan_web/components/infestation_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

219 lines
8.6 KiB
Elixir

defmodule TarakanWeb.InfestationComponents do
@moduledoc """
Functional infestation surfaces: ranked pattern table and per-pattern repo matrix.
"""
use Phoenix.Component
use Phoenix.VerifiedRoutes,
endpoint: TarakanWeb.Endpoint,
router: TarakanWeb.Router,
statics: TarakanWeb.static_paths()
alias TarakanWeb.RepositoryPaths
attr :infestations, :list, required: true
attr :id, :string, default: "infestation-constellation"
attr :compact, :boolean, default: false
@doc "Ranked multi-repo patterns (homepage / index)."
def constellation(assigns) do
max_repos =
assigns.infestations
|> Enum.map(& &1.repo_count)
|> Enum.max(fn -> 1 end)
assigns = assign(assigns, :max_repos, max(max_repos, 1))
~H"""
<div id={@id} class="border-2 border-strong bg-ground">
<div class="overflow-x-auto">
<table class="w-full min-w-[36rem] border-collapse text-left">
<thead>
<tr class="border-b border-rule font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
<th class="w-10 px-3 py-2 font-normal sm:px-4">#</th>
<th class="px-2 py-2 font-normal">Pattern</th>
<th class="w-[7rem] px-2 py-2 font-normal sm:w-[10rem]">Spread</th>
<th class="w-16 px-2 py-2 text-right font-normal tabular-nums">Repos</th>
<th class="hidden w-14 px-2 py-2 text-right font-normal sm:table-cell">Open</th>
<th class="hidden w-14 px-2 py-2 text-right font-normal md:table-cell">Ver</th>
<th class="hidden w-14 px-2 py-2 text-right font-normal md:table-cell">Fix</th>
</tr>
</thead>
<tbody class="divide-y divide-rule">
<tr
:for={{infestation, i} <- Enum.with_index(@infestations, 1)}
class="group transition-colors hover:bg-panel"
>
<td class="px-3 py-2.5 font-mono text-[11px] tabular-nums text-ink-faint sm:px-4">
{i}
</td>
<td class="min-w-0 px-2 py-2.5">
<.link
id={"#{@id}-hub-#{infestation.pattern_key}"}
navigate={~p"/infestations/#{infestation.pattern_key}"}
class="block min-w-0"
>
<span class="flex flex-wrap items-center gap-2">
<span
:if={infestation.severity}
class="shrink-0 font-mono text-[10px] uppercase tracking-[0.12em] text-signal"
>
{infestation.severity}
</span>
<span class="truncate text-sm font-semibold text-ink group-hover:text-signal">
{infestation.title}
</span>
</span>
<span
:if={!@compact}
class="mt-0.5 block truncate font-mono text-[10px] text-ink-faint"
>
{String.slice(infestation.pattern_key, 0, 12)}…
</span>
</.link>
</td>
<td class="px-2 py-2.5">
<div class="flex items-center gap-2">
<div class="h-1.5 min-w-0 flex-1 bg-rule">
<div
class="h-full bg-signal"
style={"width: #{bar_pct(infestation.repo_count, @max_repos)}%"}
>
</div>
</div>
</div>
</td>
<td class="px-2 py-2.5 text-right font-display text-lg tabular-nums text-ink">
{infestation.repo_count}
</td>
<td class="hidden px-2 py-2.5 text-right font-mono text-xs tabular-nums text-phosphor sm:table-cell">
{infestation.open_count}
</td>
<td class="hidden px-2 py-2.5 text-right font-mono text-xs tabular-nums text-ink md:table-cell">
{infestation.verified_count}
</td>
<td class="hidden px-2 py-2.5 text-right font-mono text-xs tabular-nums text-ink-muted md:table-cell">
{infestation.fixed_count}
</td>
</tr>
</tbody>
</table>
</div>
</div>
"""
end
attr :repos, :list, required: true, doc: "pattern_repos rows: one per repository"
attr :infestation, :map, required: true
attr :pattern_key, :string, required: true
attr :id, :string, default: "infestation-graph"
@doc """
Per-repo infestation matrix for one pattern.
One row per repository, rolled up from that repo's instances. The instance
grain (one row per finding) is the separate ledger on the same page.
"""
def infestation_graph(assigns) do
~H"""
<div id={@id} class="border-2 border-strong bg-ground">
<div
:if={@repos == []}
class="px-4 py-10 text-center font-mono text-xs text-ink-faint"
>
No listed public repositories.
</div>
<div :if={@repos != []} class="overflow-x-auto">
<table class="w-full min-w-[32rem] border-collapse text-left">
<thead>
<tr class="border-b border-rule font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
<th class="w-8 px-3 py-2 font-normal sm:px-4"></th>
<th class="px-2 py-2 font-normal">Repository</th>
<th class="w-24 px-2 py-2 font-normal">Status</th>
<th class="w-20 px-2 py-2 text-right font-normal tabular-nums">Findings</th>
<th class="hidden w-16 px-2 py-2 text-right font-normal tabular-nums sm:table-cell">
Open
</th>
<th class="w-20 px-3 py-2 text-right font-normal sm:px-4"></th>
</tr>
</thead>
<tbody class="divide-y divide-rule">
<tr
:for={repo <- @repos}
id={"#{@id}-node-#{repo.id}"}
class="group transition-colors hover:bg-panel"
>
<td class="px-3 py-2.5 sm:px-4">
<span class={[
"inline-block size-2 rounded-full",
repo.status == "open" && "bg-phosphor",
repo.status == "verified" && "bg-ink",
repo.status == "fixed" && "bg-ink-muted",
repo.status not in ["open", "verified", "fixed"] && "bg-ink-faint"
]}></span>
</td>
<td class="min-w-0 px-2 py-2.5">
<.link
:if={repo[:host]}
navigate={RepositoryPaths.repository_path(repo)}
class="block truncate font-mono text-xs font-semibold text-ink hover:text-signal hover:underline"
>
{repo.owner}/{repo.name}
</.link>
<span
:if={is_nil(repo[:host])}
class="block truncate font-mono text-xs font-semibold text-ink"
>
{repo.owner}/{repo.name}
</span>
</td>
<td class="px-2 py-2.5">
<span class={[
"font-mono text-[10px] uppercase tracking-[0.12em]",
repo.status == "open" && "text-phosphor",
repo.status == "verified" && "text-ink",
repo.status == "fixed" && "text-ink-muted",
repo.status not in ["open", "verified", "fixed"] && "text-ink-faint"
]}>
{repo.status}
</span>
</td>
<td class="px-2 py-2.5 text-right font-mono text-xs tabular-nums text-ink">
{repo[:instance_count] || 1}
</td>
<td class={[
"hidden px-2 py-2.5 text-right font-mono text-xs tabular-nums sm:table-cell",
(repo[:open_count] || 0) > 0 && "text-phosphor",
(repo[:open_count] || 0) == 0 && "text-ink-faint"
]}>
{repo[:open_count] || 0}
</td>
<td class="px-3 py-2.5 text-right sm:px-4">
<.link
:if={repo.occurrence_public_id}
navigate={~p"/findings/#{repo.occurrence_public_id}"}
class="font-mono text-[11px] text-signal hover:underline"
>
Finding →
</.link>
</td>
</tr>
</tbody>
</table>
</div>
</div>
"""
end
defp bar_pct(count, max) when max > 0 do
count
|> Kernel./(max)
|> Kernel.*(100)
|> Float.round(1)
|> max(4)
|> min(100)
end
defp bar_pct(_, _), do: 0
end