defmodule GitGudWeb.AuditComponents do
@moduledoc """
One audit entry as a row: when, who, what, and where.
`describe/1` has a clause per `GitGud.Audit.Catalog` action; one it
doesn't know yet shows as the raw action rather than breaking the
page, so recording can land before its wording does.
"""
use GitGudWeb, :html
import GitGudWeb.DashboardComponents, only: [ago: 1]
alias GitGud.Audit.Entry
@doc "The action as a phrase, without its actor."
@spec describe(Entry.t()) :: String.t()
def describe(%Entry{action: action} = entry) do
case action do
"repo_secret.set" ->
"set the secret"
"repo_secret.deleted" ->
"deleted the secret"
"repo_variable.set" ->
"set the variable"
"repo_variable.deleted" ->
"deleted the variable"
"org_secret.set" ->
"set the organization secret"
"org_secret.deleted" ->
"deleted the organization secret"
"org_variable.set" ->
"set the organization variable"
"org_variable.deleted" ->
"deleted the organization variable"
"org.invitation_sent" ->
"invited #{entry.metadata["role"] || "member"}"
"org.invitation_withdrawn" ->
"withdrew the invitation for"
"org.invitation_accepted" ->
"accepted the invitation as #{entry.metadata["role"]}"
"org.invitation_declined" ->
"declined the invitation"
"org.member_role_changed" ->
role_change(entry.metadata)
"org.member_removed" ->
"removed #{entry.metadata["role"] || "member"}"
"team.created" ->
"created the team"
"org.created" ->
"created the organization (#{entry.metadata["visibility"]})"
"org.profile_updated" ->
profile_change(entry.metadata)
"org.visibility_changed" ->
"changed visibility #{from_to(entry.metadata)}"
"repo.created" ->
repo_created(entry.metadata)
"repo.forked" ->
"forked it to #{entry.metadata["destination"]}"
"repo.deleted" ->
"deleted the repository"
"repo.branch_overwritten" ->
"overwrote from upstream the branch"
"repo.settings_updated" ->
"updated the settings (#{keys(entry.metadata)})"
"repo.visibility_changed" ->
"changed visibility #{from_to(entry.metadata)}"
"repo.archived" ->
"archived the repository"
"repo.unarchived" ->
"unarchived the repository"
"repo.upstream_settings_changed" ->
upstream_change(entry.metadata)
"repo.interaction_policy_changed" ->
"changed the interaction policy #{from_to(entry.metadata)}"
"branch_protection.created" ->
"protected branches matching"
"branch_protection.deleted" ->
"removed the protection on"
"deploy_key.added" ->
deploy_key_added(entry.metadata)
"deploy_key.removed" ->
"removed the deploy key"
"webhook.created" ->
"added a webhook to"
"webhook.deleted" ->
"deleted the webhook to"
"webhook.enabled" ->
"enabled the webhook to"
"webhook.disabled" ->
"disabled the webhook to"
"registry_token.created" ->
"created the registry token (#{joined(entry.metadata["scopes"])})"
"registry_token.revoked" ->
"revoked the registry token"
"job_token_grant.created" ->
grant_created(entry.metadata)
"job_token_grant.deleted" ->
"removed job-token access for"
"audit.pruned" ->
"pruned #{entry.metadata["count"]} audit entries"
other ->
other
end
end
defp role_change(metadata), do: "changed the role #{from_to(metadata)} of"
defp profile_change(%{"fields" => [_ | _] = fields}),
do: "updated the profile (#{Enum.join(fields, ", ")})"
defp profile_change(_), do: "updated the profile"
defp repo_created(%{"forked_from" => from}) when is_binary(from),
do: "forked the repository from #{from}"
defp repo_created(%{"imported_from_host" => host}) when is_binary(host),
do: "imported the repository from #{host}"
defp repo_created(metadata), do: "created the repository (#{metadata["visibility"]})"
defp upstream_change(metadata) do
auto = if metadata["auto"], do: ", syncing automatically", else: ""
"set upstream sync to #{metadata["mode"]}#{auto}"
end
defp deploy_key_added(%{"read_write" => true}), do: "added the read-write deploy key"
defp deploy_key_added(_), do: "added the read-only deploy key"
defp grant_created(metadata) do
can =
[metadata["read"] && "read", metadata["dispatch"] && "dispatch"]
|> Enum.filter(& &1)
|> Enum.join(", ")
"granted job-token access (#{can}) to"
end
defp joined(values) when is_list(values), do: Enum.join(values, ", ")
defp joined(value), do: to_string(value)
defp keys(metadata), do: metadata |> Map.keys() |> Enum.sort() |> Enum.join(", ")
defp from_to(metadata), do: "(#{metadata["from"]} → #{metadata["to"]})"
@doc "Category tab label."
def category_label("secrets"), do: "Secrets & variables"
def category_label("members"), do: "Members"
def category_label("settings"), do: "Settings"
def category_label("repositories"), do: "Repositories"
def category_label("branch_protection"), do: "Branch protection"
def category_label("deploy_keys"), do: "Deploy keys"
def category_label("webhooks"), do: "Webhooks"
def category_label("credentials"), do: "Tokens"
def category_label("runners"), do: "Runners"
def category_label("ci"), do: "CI"
def category_label("moderation"), do: "Moderation"
def category_label("instance"), do: "Instance"
def category_label("account"), do: "Account"
def category_label(other), do: other
@doc """
An entry's row. `show_request` adds the client address and user
agent, which only the instance view shows.
"""
attr :id, :string, required: true
attr :entry, Entry, required: true
attr :show_request, :boolean, default: false
def entry_row(assigns) do
~H"""
<li id={@id} class="flex items-start gap-3 py-3 text-sm">
<span
class="w-24 shrink-0 text-xs opacity-60 tabular-nums"
title={DateTime.to_iso8601(@entry.inserted_at)}
>
{ago(@entry.inserted_at)}
</span>
<div class="min-w-0 flex-1 space-y-1">
<p>
<.actor entry={@entry} />
<span class="opacity-80">{describe(@entry)}</span>
<code :if={@entry.target_label} class="font-mono text-xs bg-base-200 rounded px-1">
{@entry.target_label}
</code>
<span :if={@entry.repository_label} class="opacity-70">
in <.place entry={@entry} />
</span>
</p>
<details :if={@entry.metadata != %{} or @show_request} class="text-xs opacity-70">
<summary class="cursor-pointer select-none">Details</summary>
<dl class="mt-1 grid grid-cols-[max-content_1fr] gap-x-3 gap-y-0.5 font-mono">
<%= for {key, value} <- Enum.sort(@entry.metadata) do %>
<dt>{key}</dt>
<dd class="break-all">{render_value(value)}</dd>
<% end %>
<%= if @show_request do %>
<dt>ip</dt>
<dd>{@entry.ip || "—"}</dd>
<dt>user agent</dt>
<dd class="break-all">{@entry.user_agent || "—"}</dd>
<dt>via</dt>
<dd>{@entry.via || "—"}</dd>
<% end %>
</dl>
</details>
</div>
</li>
"""
end
attr :entry, Entry, required: true
defp actor(%{entry: %Entry{actor_type: "user", actor_id: id}} = assigns) when not is_nil(id) do
~H"""
<.link navigate={~p"/u/#{@entry.actor_label}"} class="font-medium link link-hover">
{@entry.actor_label}
</.link>
"""
end
defp actor(assigns) do
~H"""
<span class="font-medium">{@entry.actor_label}</span>
"""
end
attr :entry, Entry, required: true
# A link while the repository still exists; its label once it's gone.
defp place(%{entry: %Entry{repository_id: id}} = assigns) when not is_nil(id) do
~H"""
<.link navigate={"/r/" <> @entry.repository_label} class="link link-hover font-mono">
{@entry.repository_label}
</.link>
"""
end
defp place(assigns) do
~H"""
<span class="font-mono line-through decoration-1">{@entry.repository_label}</span>
"""
end
defp render_value(v) when is_list(v), do: Enum.map_join(v, ", ", &to_string/1)
defp render_value(nil), do: "—"
defp render_value(v), do: to_string(v)
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
8.4 KiB · text
History
99ed362