feat: add webhook event history UI page #164
56
internal/handlers/webhook_events.go
Normal file
56
internal/handlers/webhook_events.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
|
"sneak.berlin/go/upaas/internal/models"
|
||||||
|
"sneak.berlin/go/upaas/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// webhookEventsLimit is the number of webhook events to show in history.
|
||||||
|
const webhookEventsLimit = 100
|
||||||
|
|
||||||
|
// HandleAppWebhookEvents returns the webhook event history handler.
|
||||||
|
func (h *Handlers) HandleAppWebhookEvents() http.HandlerFunc {
|
||||||
|
tmpl := templates.GetParsed()
|
||||||
|
|
||||||
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
appID := chi.URLParam(request, "id")
|
||||||
|
|
||||||
|
application, findErr := models.FindApp(request.Context(), h.db, appID)
|
||||||
|
if findErr != nil {
|
||||||
|
h.log.Error("failed to find app", "error", findErr)
|
||||||
|
http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if application == nil {
|
||||||
|
http.NotFound(writer, request)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
events, eventsErr := application.GetWebhookEvents(
|
||||||
|
request.Context(),
|
||||||
|
webhookEventsLimit,
|
||||||
|
)
|
||||||
|
if eventsErr != nil {
|
||||||
|
h.log.Error("failed to get webhook events",
|
||||||
|
"error", eventsErr,
|
||||||
|
"app", appID,
|
||||||
|
)
|
||||||
|
|
||||||
|
events = []*models.WebhookEvent{}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := h.addGlobals(map[string]any{
|
||||||
|
"App": application,
|
||||||
|
"Events": events,
|
||||||
|
}, request)
|
||||||
|
|
||||||
|
h.renderTemplate(writer, tmpl, "webhook_events.html", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,20 @@ func (w *WebhookEvent) Reload(ctx context.Context) error {
|
|||||||
return w.scan(row)
|
return w.scan(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShortCommit returns a truncated commit SHA for display.
|
||||||
|
func (w *WebhookEvent) ShortCommit() string {
|
||||||
|
if !w.CommitSHA.Valid {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sha := w.CommitSHA.String
|
||||||
|
if len(sha) > shortCommitLength {
|
||||||
|
return sha[:shortCommitLength]
|
||||||
|
}
|
||||||
|
|
||||||
|
return sha
|
||||||
|
}
|
||||||
|
|
||||||
func (w *WebhookEvent) insert(ctx context.Context) error {
|
func (w *WebhookEvent) insert(ctx context.Context) error {
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO webhook_events (
|
INSERT INTO webhook_events (
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ func (s *Server) SetupRoutes() {
|
|||||||
r.Post("/apps/{id}/deploy", s.handlers.HandleAppDeploy())
|
r.Post("/apps/{id}/deploy", s.handlers.HandleAppDeploy())
|
||||||
r.Post("/apps/{id}/deployments/cancel", s.handlers.HandleCancelDeploy())
|
r.Post("/apps/{id}/deployments/cancel", s.handlers.HandleCancelDeploy())
|
||||||
r.Get("/apps/{id}/deployments", s.handlers.HandleAppDeployments())
|
r.Get("/apps/{id}/deployments", s.handlers.HandleAppDeployments())
|
||||||
|
r.Get("/apps/{id}/webhooks", s.handlers.HandleAppWebhookEvents())
|
||||||
r.Get("/apps/{id}/deployments/{deploymentID}/logs", s.handlers.HandleDeploymentLogsAPI())
|
r.Get("/apps/{id}/deployments/{deploymentID}/logs", s.handlers.HandleDeploymentLogsAPI())
|
||||||
r.Get("/apps/{id}/deployments/{deploymentID}/download", s.handlers.HandleDeploymentLogDownload())
|
r.Get("/apps/{id}/deployments/{deploymentID}/download", s.handlers.HandleDeploymentLogDownload())
|
||||||
r.Get("/apps/{id}/logs", s.handlers.HandleAppLogs())
|
r.Get("/apps/{id}/logs", s.handlers.HandleAppLogs())
|
||||||
|
|||||||
@@ -77,7 +77,10 @@
|
|||||||
|
|
||||||
<!-- Webhook URL -->
|
<!-- Webhook URL -->
|
||||||
<div class="card p-6 mb-6">
|
<div class="card p-6 mb-6">
|
||||||
<h2 class="section-title mb-4">Webhook URL</h2>
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="section-title">Webhook URL</h2>
|
||||||
|
<a href="/apps/{{.App.ID}}/webhooks" class="text-primary-600 hover:text-primary-800 text-sm">Event History</a>
|
||||||
|
</div>
|
||||||
<p class="text-sm text-gray-500 mb-3">Add this URL as a push webhook in your Gitea repository:</p>
|
<p class="text-sm text-gray-500 mb-3">Add this URL as a push webhook in your Gitea repository:</p>
|
||||||
<div class="copy-field" x-data="copyButton('webhook-url')">
|
<div class="copy-field" x-data="copyButton('webhook-url')">
|
||||||
<code id="webhook-url" class="copy-field-value text-xs">{{.WebhookURL}}</code>
|
<code id="webhook-url" class="copy-field-value text-xs">{{.WebhookURL}}</code>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ func initTemplates() {
|
|||||||
"app_detail.html",
|
"app_detail.html",
|
||||||
"app_edit.html",
|
"app_edit.html",
|
||||||
"deployments.html",
|
"deployments.html",
|
||||||
|
"webhook_events.html",
|
||||||
}
|
}
|
||||||
|
|
||||||
pageTemplates = make(map[string]*template.Template)
|
pageTemplates = make(map[string]*template.Template)
|
||||||
|
|||||||
79
templates/webhook_events.html
Normal file
79
templates/webhook_events.html
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}Webhook Events - {{.App.Name}} - µPaaS{{end}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
{{template "nav" .}}
|
||||||
|
|
||||||
|
<main class="max-w-4xl mx-auto px-4 py-8">
|
||||||
|
<div class="mb-6">
|
||||||
|
<a href="/apps/{{.App.ID}}" class="text-primary-600 hover:text-primary-800 inline-flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||||
|
</svg>
|
||||||
|
Back to {{.App.Name}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-header">
|
||||||
|
<h1 class="text-2xl font-medium text-gray-900">Webhook Events</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .Events}}
|
||||||
|
<div class="card overflow-hidden">
|
||||||
|
<table class="table">
|
||||||
|
<thead class="table-header">
|
||||||
|
<tr>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Event</th>
|
||||||
|
<th>Branch</th>
|
||||||
|
<th>Commit</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-body">
|
||||||
|
{{range .Events}}
|
||||||
|
<tr>
|
||||||
|
<td class="text-gray-500 text-sm whitespace-nowrap">
|
||||||
|
<span x-data="relativeTime('{{.CreatedAt.Format `2006-01-02T15:04:05Z07:00`}}')" x-text="display" class="cursor-default" title="{{.CreatedAt.Format `2006-01-02 15:04:05`}}"></span>
|
||||||
|
</td>
|
||||||
|
<td class="text-gray-700 text-sm">{{.EventType}}</td>
|
||||||
|
<td class="font-mono text-gray-500 text-sm">{{.Branch}}</td>
|
||||||
|
<td class="font-mono text-gray-500 text-xs">
|
||||||
|
{{if and .CommitSHA.Valid .CommitURL.Valid}}
|
||||||
|
<a href="{{.CommitURL.String}}" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:text-primary-800">{{.ShortCommit}}</a>
|
||||||
|
{{else if .CommitSHA.Valid}}
|
||||||
|
{{.ShortCommit}}
|
||||||
|
{{else}}
|
||||||
|
<span class="text-gray-400">-</span>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if .Matched}}
|
||||||
|
{{if .Processed}}
|
||||||
|
<span class="badge-success">Matched</span>
|
||||||
|
{{else}}
|
||||||
|
<span class="badge-warning">Matched (pending)</span>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<span class="badge-neutral">No match</span>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="card">
|
||||||
|
<div class="empty-state">
|
||||||
|
<svg class="empty-state-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="empty-state-title">No webhook events yet</h3>
|
||||||
|
<p class="empty-state-description">Webhook events will appear here once your repository sends push notifications.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</main>
|
||||||
|
{{end}}
|
||||||
Reference in New Issue
Block a user