6.4 KiB · text 5af55d9
defmodule GitGudWeb.WebhookLive.Org do
@moduledoc """
Org-level webhooks. Org admins manage hooks that fire for events on
every repository the org owns. Shows a compact recent-deliveries log
under each hook.
"""
use GitGudWeb, :live_view
alias GitGud.Organizations
alias GitGud.Webhooks
alias GitGud.Webhooks.Webhook
@impl true
def mount(%{"handle" => handle}, _session, socket) do
org = Organizations.get_organization_by_handle!(handle)
user = socket.assigns.current_scope && socket.assigns.current_scope.user
cond do
is_nil(user) ->
{:ok,
socket
|> put_flash(:error, "Sign in to manage webhooks.")
|> redirect(to: ~p"/users/log-in")}
not Organizations.org_admin?(org, user) ->
{:ok,
socket
|> put_flash(:error, "Org admin only.")
|> redirect(to: ~p"/orgs/#{handle}")}
true ->
{:ok,
socket
|> assign(:org, org)
|> assign(:page_title, "Webhooks — @#{org.handle}")
|> assign_form(Webhook.changeset(%Webhook{}, %{}))
|> load_hooks()}
end
end
defp load_hooks(socket) do
hooks = Webhooks.list_webhooks(socket.assigns.org)
socket
|> assign(:hooks, hooks)
|> assign(:deliveries, Webhooks.recent_deliveries_by_hook(hooks))
end
defp assign_form(socket, cs), do: assign(socket, :form, to_form(cs))
@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.org, attrs) do
{:ok, _hook} ->
{:noreply,
socket
|> load_hooks()
|> 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.organization_id == socket.assigns.org.id do
{:ok, _} = Webhooks.delete_webhook(hook)
{:noreply, socket |> load_hooks() |> put_flash(:info, "Webhook removed.")}
else
{:noreply, socket}
end
end
def handle_event("toggle", %{"id" => id}, socket) do
hook = Webhooks.get_webhook!(id)
if hook.organization_id == socket.assigns.org.id do
{:ok, _} = Webhooks.update_webhook(hook, %{"enabled" => !hook.enabled})
{:noreply, load_hooks(socket)}
else
{:noreply, socket}
end
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="space-y-6">
<.org_settings_header org={@org} current={:webhooks} />
<p class="text-sm opacity-70 max-w-2xl">
Org webhooks fire for events on <em>every</em> repository
<span class="font-mono">@{@org.handle}</span>
owns — in addition to any per-repo hooks.
</p>
<.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 class="divide-y divide-base-300 max-w-2xl">
<li :if={@hooks == []} class="py-6 opacity-60 text-sm">No webhooks yet.</li>
<li :for={hook <- @hooks} 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