6.4 KiB · text 5af55d9
defmodule GitGudWeb.WebhookLive.Index do
use GitGudWeb, :live_view
alias GitGud.Repositories
alias GitGud.Repositories.Storage
alias GitGud.Webhooks
alias GitGud.Webhooks.Webhook
@impl true
def mount(%{"owner" => owner, "name" => name}, _session, socket) do
repo =
Repositories.get_repository_by_path!(owner, name)
|> GitGud.Repo.preload([:owner, :organization])
{:ok,
socket
|> assign(:repo, repo)
|> assign(:handle, Storage.repo_handle(repo))
|> GitGudWeb.RepoLive.Header.assign_chrome(repo)
|> stream(:hooks, Webhooks.list_webhooks(repo))
|> assign_deliveries()
|> assign_form(Webhook.changeset(%Webhook{}, %{}))
|> assign(:page_title, "Webhooks — #{owner}/#{name}")}
end
defp assign_form(socket, cs), do: assign(socket, :form, to_form(cs))
# Recompute the per-hook recent-delivery log. Cheap enough to rerun on
# each mutation so the inline log stays in step with the hook list.
defp assign_deliveries(socket) do
deliveries =
socket.assigns.repo
|> Webhooks.list_webhooks()
|> Webhooks.recent_deliveries_by_hook()
assign(socket, :deliveries, deliveries)
end
@impl true
def handle_event("validate", %{"webhook" => attrs}, socket) do
cs = %Webhook{} |> Webhook.changeset(attrs) |> Map.put(:action, :validate)
{:noreply, assign_form(socket, cs)}
end
def handle_event("save", %{"webhook" => attrs}, socket) do
events = attrs |> Map.get("events", []) |> List.wrap()
attrs = Map.put(attrs, "events", events)
case Webhooks.create_webhook(socket.assigns.repo, attrs) do
{:ok, hook} ->
{:noreply,
socket
|> stream_insert(:hooks, hook, at: -1)
|> assign_deliveries()
|> assign_form(Webhook.changeset(%Webhook{}, %{}))
|> put_flash(:info, "Webhook added.")}
{:error, cs} ->
{:noreply, assign_form(socket, cs)}
end
end
def handle_event("delete", %{"id" => id}, socket) do
hook = Webhooks.get_webhook!(id)
if hook.repository_id == socket.assigns.repo.id do
{:ok, _} = Webhooks.delete_webhook(hook)
{:noreply,
socket
|> stream_delete(:hooks, hook)
|> assign_deliveries()
|> put_flash(:info, "Webhook removed.")}
else
{:noreply, socket}
end
end
def handle_event("toggle", %{"id" => id}, socket) do
hook = Webhooks.get_webhook!(id)
{:ok, updated} = Webhooks.update_webhook(hook, %{"enabled" => !hook.enabled})
{:noreply, stream_insert(socket, :hooks, updated)}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="space-y-6">
<GitGudWeb.RepoLive.Header.header
repo={@repo}
handle={@handle}
current_scope={@current_scope}
has_packages?={@has_packages?}
can_admin?={@can_admin?}
latest_run={@latest_run}
/>
<.repo_settings_header handle={@handle} repo={@repo} current={:webhooks} />
<.form for={@form} id="webhook-form" phx-change="validate" phx-submit="save" class="space-y-3 max-w-2xl">
<.input field={@form[:url]} label="Payload URL" required placeholder="https://..." />
<.input
field={@form[:content_type]}
type="select"
label="Content type"
options={[
{"application/json", "application/json"},
{"application/x-www-form-urlencoded", "application/x-www-form-urlencoded"}
]}
/>
<fieldset class="space-y-1">
<legend class="text-sm opacity-70">Events (none = all)</legend>
<label :for={ev <- Webhook.known_events()} class="flex items-center gap-2 text-sm">
<input type="checkbox" name="webhook[events][]" value={ev} class="checkbox checkbox-xs" />
<span class="font-mono">{ev}</span>
</label>
</fieldset>
<.input field={@form[:insecure_ssl]} type="checkbox" label="Skip TLS verification" />
<button type="submit" class="btn btn-sm btn-primary">Add webhook</button>
</.form>
<ul id="hooks" phx-update="stream" class="divide-y divide-base-300 max-w-2xl">
<li class="hidden only:block py-6 opacity-60 text-sm">No webhooks yet.</li>
<li :for={{id, hook} <- @streams.hooks} id={id} class="py-3">
<div class="flex items-center gap-2">
<span class={["badge badge-xs", hook.enabled && "badge-success"]}>
{if hook.enabled, do: "enabled", else: "disabled"}
</span>
<span class="font-mono text-sm flex-1 break-all">{hook.url}</span>
<button
type="button"
class="btn btn-xs btn-ghost"
phx-click="toggle"
phx-value-id={hook.id}
>
{if hook.enabled, do: "disable", else: "enable"}
</button>
<button
type="button"
class="btn btn-xs btn-ghost"
phx-click="delete"
phx-value-id={hook.id}
data-confirm="Delete this webhook?"
>
<.icon name="hero-trash" class="size-4" />
</button>
</div>
<p class="text-xs opacity-60 mt-1">
events: {if hook.events == [], do: "all", else: Enum.join(hook.events, ", ")}
</p>
<% deliveries = Map.get(@deliveries, hook.id, []) %>
<ul :if={deliveries != []} class="mt-2 space-y-0.5">
<li
:for={d <- deliveries}
class="text-xs font-mono opacity-70 flex items-center gap-2"
>
<span class={["badge badge-xs", delivery_class(d.status)]}>
{d.status_code || d.status}
</span>
<span>{d.event}</span>
<span :if={d.duration_ms} class="opacity-60">{d.duration_ms}ms</span>
<span class="opacity-50">{format_dt(d.inserted_at)}</span>
</li>
</ul>
</li>
</ul>
</div>
</Layouts.app>
"""
end
defp delivery_class("success"), do: "badge-success"
defp delivery_class("failure"), do: "badge-error"
defp delivery_class(_), do: "badge-ghost"
defp format_dt(%DateTime{} = dt), do: Calendar.strftime(dt, "%Y-%m-%d %H:%M")
defp format_dt(_), do: ""
end