Add webhook executions list

This commit is contained in:
Luis Eduardo Jeréz Girón
2024-09-01 23:37:04 -06:00
parent e1b65a49db
commit 0ac245bec2
5 changed files with 363 additions and 0 deletions
@@ -0,0 +1,49 @@
package webhooks
import (
"context"
"github.com/eduardolat/pgbackweb/internal/database/dbgen"
"github.com/eduardolat/pgbackweb/internal/util/paginateutil"
"github.com/google/uuid"
)
type PaginateWebhookExecutionsParams struct {
WebhookID uuid.UUID
Page int
Limit int
}
func (s *Service) PaginateWebhookExecutions(
ctx context.Context, params PaginateWebhookExecutionsParams,
) (paginateutil.PaginateResponse, []dbgen.WebhookResult, error) {
page := max(params.Page, 1)
limit := min(max(params.Limit, 1), 100)
count, err := s.dbgen.WebhooksServicePaginateWebhookExecutionsCount(
ctx, params.WebhookID,
)
if err != nil {
return paginateutil.PaginateResponse{}, nil, err
}
paginateParams := paginateutil.PaginateParams{
Page: page,
Limit: limit,
}
offset := paginateutil.CreateOffsetFromParams(paginateParams)
paginateResponse := paginateutil.CreatePaginateResponse(paginateParams, int(count))
webhookResults, err := s.dbgen.WebhooksServicePaginateWebhookExecutions(
ctx, dbgen.WebhooksServicePaginateWebhookExecutionsParams{
WebhookID: params.WebhookID,
Limit: int32(params.Limit),
Offset: int32(offset),
},
)
if err != nil {
return paginateutil.PaginateResponse{}, nil, err
}
return paginateResponse, webhookResults, nil
}
@@ -0,0 +1,9 @@
-- name: WebhooksServicePaginateWebhookExecutionsCount :one
SELECT COUNT(*) FROM webhook_results
WHERE webhook_id = @webhook_id;
-- name: WebhooksServicePaginateWebhookExecutions :many
SELECT * FROM webhook_results
WHERE webhook_id = @webhook_id
ORDER BY created_at DESC
LIMIT sqlc.arg('limit') OFFSET sqlc.arg('offset');
@@ -57,6 +57,7 @@ func listWebhooks(
html.Class("w-[40px]"),
html.Div(
html.Class("flex justify-start space-x-1"),
webhookExecutionsButton(whook.ID),
runWebhookButton(whook.ID),
editWebhookButton(whook.ID),
deleteWebhookButton(whook.ID),
@@ -26,5 +26,6 @@ func MountRouter(
parent.GET("/:webhookID/edit", h.editWebhookFormHandler)
parent.POST("/:webhookID/edit", h.editWebhookHandler)
parent.POST("/:webhookID/run", h.runWebhookHandler)
parent.GET("/:webhookID/executions", h.paginateWebhookExecutionsHandler)
parent.DELETE("/:webhookID", h.deleteWebhookHandler)
}
@@ -0,0 +1,303 @@
package webhooks
import (
"fmt"
"net/http"
"time"
lucide "github.com/eduardolat/gomponents-lucide"
"github.com/eduardolat/pgbackweb/internal/database/dbgen"
"github.com/eduardolat/pgbackweb/internal/service/webhooks"
"github.com/eduardolat/pgbackweb/internal/util/echoutil"
"github.com/eduardolat/pgbackweb/internal/util/paginateutil"
"github.com/eduardolat/pgbackweb/internal/util/strutil"
"github.com/eduardolat/pgbackweb/internal/util/timeutil"
"github.com/eduardolat/pgbackweb/internal/validate"
"github.com/eduardolat/pgbackweb/internal/view/web/alpine"
"github.com/eduardolat/pgbackweb/internal/view/web/component"
"github.com/eduardolat/pgbackweb/internal/view/web/htmx"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/maragudk/gomponents"
"github.com/maragudk/gomponents/html"
)
func (h *handlers) paginateWebhookExecutionsHandler(c echo.Context) error {
ctx := c.Request().Context()
webhookID, err := uuid.Parse(c.Param("webhookID"))
if err != nil {
return htmx.RespondToastError(c, err.Error())
}
var queryData struct {
Page int `query:"page" validate:"required,min=1"`
}
if err := c.Bind(&queryData); err != nil {
return htmx.RespondToastError(c, err.Error())
}
if err := validate.Struct(&queryData); err != nil {
return htmx.RespondToastError(c, err.Error())
}
pagination, execs, err := h.servs.WebhooksService.PaginateWebhookExecutions(
ctx, webhooks.PaginateWebhookExecutionsParams{
WebhookID: webhookID,
Page: queryData.Page,
Limit: 20,
},
)
if err != nil {
return htmx.RespondToastError(c, err.Error())
}
return echoutil.RenderGomponent(
c, http.StatusOK, webhookExecutionsList(webhookID, pagination, execs),
)
}
func webhookExecutionsList(
webhookID uuid.UUID,
pagination paginateutil.PaginateResponse,
execs []dbgen.WebhookResult,
) gomponents.Node {
if len(execs) == 0 {
return html.Tr(
html.Td(
html.ColSpan("4"),
html.Class("py-10"),
component.EmptyResults(component.EmptyResultsParams{
Title: "No executions found",
Subtitle: "Wait for the first execution to appear here",
}),
),
)
}
trs := []gomponents.Node{}
for _, exec := range execs {
durationMillis := exec.ResDuration.Int32
duration := time.Duration(durationMillis) * time.Millisecond
trs = append(trs, html.Tr(
html.Td(
html.Div(
html.Class("flex items-center space-x-2"),
webhookExecutionDetailsButton(exec, duration),
component.SpanText(fmt.Sprintf("%d", exec.ResStatus.Int16)),
),
),
html.Td(component.SpanText(exec.ReqMethod.String)),
html.Td(component.SpanText(duration.String())),
html.Td(component.SpanText(
exec.CreatedAt.Local().Format(timeutil.LayoutYYYYMMDDHHMMSSPretty),
)),
))
}
if pagination.HasNextPage {
trs = append(trs, html.Tr(
htmx.HxGet(func() string {
url := "/dashboard/webhooks/" + webhookID.String() + "/executions"
url = strutil.AddQueryParamToUrl(url, "page", fmt.Sprintf("%d", pagination.NextPage))
return url
}()),
htmx.HxTrigger("intersect once"),
htmx.HxSwap("afterend"),
htmx.HxIndicator("#webhook-executions-loading"),
))
}
return component.RenderableGroup(trs)
}
func webhookExecutionDetailsButton(
exec dbgen.WebhookResult,
duration time.Duration,
) gomponents.Node {
mo := component.Modal(component.ModalParams{
Title: "Webhook execution details",
Content: []gomponents.Node{
html.Div(
html.Class("space-y-4"),
alpine.XData(`{
processTextareas() {
const els = [
$refs.reqHeadersTextarea,
$refs.reqBodyTextarea,
$refs.resHeadersTextarea,
$refs.resBodyTextarea
]
for (const el of els) {
el.value = formatJson(el.value)
}
}
}`),
alpine.XOn("mouseenter.once", "processTextareas()"),
component.CardBoxSimple(
html.Table(
html.Class("table [&_th]:text-nowrap"),
html.Tr(
html.Td(
html.ColSpan("2"),
component.H3Text("General"),
),
),
html.Tr(
html.Th(component.SpanText("ID")),
html.Td(component.SpanText(exec.ID.String())),
),
html.Tr(
html.Th(component.SpanText("Date")),
html.Td(component.SpanText(
exec.CreatedAt.Local().Format(timeutil.LayoutYYYYMMDDHHMMSSPretty),
)),
),
),
),
component.CardBoxSimple(
html.Table(
html.Class("table [&_th]:text-nowrap"),
html.Tr(
html.Td(
html.ColSpan("2"),
component.H3Text("Request"),
),
),
html.Tr(
html.Th(component.SpanText("Method")),
html.Td(component.SpanText(exec.ReqMethod.String)),
),
html.Tr(
html.Th(component.SpanText("Headers")),
html.Td(
component.TextareaControl(component.TextareaControlParams{
Children: []gomponents.Node{
alpine.XRef("reqHeadersTextarea"),
gomponents.Text(exec.ReqHeaders.String),
},
}),
),
),
html.Tr(
html.Th(component.SpanText("Body")),
html.Td(
component.TextareaControl(component.TextareaControlParams{
Children: []gomponents.Node{
alpine.XRef("reqBodyTextarea"),
gomponents.Text(exec.ReqBody.String),
},
}),
),
),
),
),
component.CardBoxSimple(
html.Table(
html.Class("table [&_th]:text-nowrap"),
html.Tr(
html.Td(
html.ColSpan("2"),
component.H3Text("Response"),
),
),
html.Tr(
html.Th(component.SpanText("Status")),
html.Td(component.SpanText(
fmt.Sprintf("%d", exec.ResStatus.Int16),
)),
),
html.Tr(
html.Th(component.SpanText("Duration")),
html.Td(component.SpanText(duration.String())),
),
html.Tr(
html.Th(component.SpanText("Headers")),
html.Td(
component.TextareaControl(component.TextareaControlParams{
Children: []gomponents.Node{
alpine.XRef("resHeadersTextarea"),
gomponents.Text(exec.ResHeaders.String),
},
}),
),
),
html.Tr(
html.Th(component.SpanText("Body")),
html.Td(
component.TextareaControl(component.TextareaControlParams{
Children: []gomponents.Node{
alpine.XRef("resBodyTextarea"),
gomponents.Text(exec.ResBody.String),
},
}),
),
),
),
),
),
},
})
return html.Div(
html.Class("inline-block tooltip tooltip-right"),
html.Data("tip", "Show webhook execution details"),
mo.HTML,
html.Button(
html.Class("btn btn-error btn-square btn-sm btn-ghost"),
lucide.Eye(),
mo.OpenerAttr,
),
)
}
func webhookExecutionsButton(webhookID uuid.UUID) gomponents.Node {
mo := component.Modal(component.ModalParams{
Size: component.SizeMd,
Title: "Webhook executions",
Content: []gomponents.Node{
html.Table(
html.Class("table"),
html.THead(
html.Tr(
html.Th(
html.Div(
html.Class("ml-10"),
component.SpanText("Status"),
),
),
html.Th(component.SpanText("Method")),
html.Th(component.SpanText("Duration")),
html.Th(component.SpanText("Date")),
),
),
html.TBody(
htmx.HxGet(
"/dashboard/webhooks/"+webhookID.String()+"/executions?page=1",
),
htmx.HxIndicator("#webhook-executions-loading"),
htmx.HxTrigger("intersect once"),
),
),
html.Div(
html.Class("flex justify-center pt-2"),
component.HxLoadingMd("webhook-executions-loading"),
),
},
})
return html.Div(
html.Class("inline-block tooltip tooltip-right"),
html.Data("tip", "Show webhook executions"),
mo.HTML,
html.Button(
html.Class("btn btn-error btn-square btn-sm btn-ghost"),
lucide.List(),
mo.OpenerAttr,
),
)
}