Break long values on the webhook page and event log at phone width (closes #391) #495
@@ -1280,7 +1280,9 @@ the event log only the newest starts expanded, and an event there expands and
|
|||||||
collapses when its row's caret or its ID is clicked, and from the keyboard, but
|
collapses when its row's caret or its ID is clicked, and from the keyboard, but
|
||||||
not when its ID is selected with the mouse, and a delivery's attempts inside it
|
not when its ID is selected with the mouse, and a delivery's attempts inside it
|
||||||
expand and collapse; and at phone width the menu button opens and closes the
|
expand and collapse; and at phone width the menu button opens and closes the
|
||||||
mobile menu. It also fails if the browser reports a console warning or error, an
|
mobile menu, and neither the webhook page nor the event log, with a delivery's
|
||||||
|
attempts open, scrolls sideways or cuts anything off at the page's or a card's
|
||||||
|
edge. It also fails if the browser reports a console warning or error, an
|
||||||
uncaught exception, or anything the policy refused. `make check` and the image
|
uncaught exception, or anything the policy refused. `make check` and the image
|
||||||
build lint it but do not run it, and `make test` leaves it out (its file is
|
build lint it but do not run it, and `make test` leaves it out (its file is
|
||||||
built only with the `browser` build tag). Run it with `make test-browser` after
|
built only with the `browser` build tag). Run it with `make test-browser` after
|
||||||
|
|||||||
@@ -17,14 +17,15 @@ import (
|
|||||||
|
|
||||||
// arrivedAt is how a page names the entrypoint an event arrived at.
|
// arrivedAt is how a page names the entrypoint an event arrived at.
|
||||||
func arrivedAt(name string) string {
|
func arrivedAt(name string) string {
|
||||||
return `Arrived at <span class="text-gray-900">` + name + `</span>`
|
return `Arrived at <span class="text-gray-900 wrap-anywhere">` + name +
|
||||||
|
`</span>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// copiedRequestArrivedAt is how a page names, for a resubmitted copy,
|
// copiedRequestArrivedAt is how a page names, for a resubmitted copy,
|
||||||
// the entrypoint the request it copies arrived at.
|
// the entrypoint the request it copies arrived at.
|
||||||
func copiedRequestArrivedAt(name string) string {
|
func copiedRequestArrivedAt(name string) string {
|
||||||
return `The request it copies arrived at <span class="text-gray-900">` +
|
return `The request it copies arrived at ` +
|
||||||
name + `</span>`
|
`<span class="text-gray-900 wrap-anywhere">` + name + `</span>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// headerBox is how a page shows an event's request header lines: as
|
// headerBox is how a page shows an event's request header lines: as
|
||||||
|
|||||||
@@ -98,20 +98,25 @@ func TestAlpineRunsUnderTheSecurityPolicy(t *testing.T) {
|
|||||||
checkRefusedNewWebhook(ctx, t, srv.URL+"/hooks/new")
|
checkRefusedNewWebhook(ctx, t, srv.URL+"/hooks/new")
|
||||||
checkEventLog(ctx, t, page+"/events", event.ID, older.ID, target.Name)
|
checkEventLog(ctx, t, page+"/events", event.ID, older.ID, target.Name)
|
||||||
checkMobileMenu(ctx, t, page)
|
checkMobileMenu(ctx, t, page)
|
||||||
|
checkPhoneWidth(ctx, t, page, page+"/events", target.Name)
|
||||||
|
|
||||||
assert.Empty(t, problems(), "the browser reported problems")
|
assert.Empty(t, problems(), "the browser reported problems")
|
||||||
}
|
}
|
||||||
|
|
||||||
// seedBrowserWebhook seeds the webhook the browser test loads, owned by
|
// seedBrowserWebhook seeds the webhook the browser test loads, owned by
|
||||||
// userID: an entrypoint, two events, and a target whose delivery of the
|
// userID: an entrypoint, two events, and a target whose delivery of the
|
||||||
// newer event failed once with a 502. It returns the webhook, the older
|
// newer event failed once with a 502. The webhook's name and the newer
|
||||||
// and the newer event, and the target.
|
// event's content type are each too long for one line on a phone. It
|
||||||
|
// returns the webhook, the older and the newer event, and the target.
|
||||||
func seedBrowserWebhook(
|
func seedBrowserWebhook(
|
||||||
t *testing.T, env *testEnv, userID string,
|
t *testing.T, env *testEnv, userID string,
|
||||||
) (*database.Webhook, *database.Event, *database.Event, *database.Target) {
|
) (*database.Webhook, *database.Event, *database.Event, *database.Target) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
webhook := env.seedWebhook(t, userID)
|
webhook := env.seedWebhook(t, userID)
|
||||||
|
require.NoError(t, env.db.DB().Model(webhook).Update(
|
||||||
|
"name", "payment_provider_production_notifications",
|
||||||
|
).Error)
|
||||||
require.NoError(t, env.db.DB().Omit(clause.Associations).Create(
|
require.NoError(t, env.db.DB().Omit(clause.Associations).Create(
|
||||||
&database.Entrypoint{
|
&database.Entrypoint{
|
||||||
WebhookID: webhook.ID,
|
WebhookID: webhook.ID,
|
||||||
@@ -126,6 +131,9 @@ func seedBrowserWebhook(
|
|||||||
|
|
||||||
webhookDB, err := env.dbMgr.GetDB(webhook.ID)
|
webhookDB, err := env.dbMgr.GetDB(webhook.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, webhookDB.Model(event).Update(
|
||||||
|
"content_type", "application/vnd.paymentprovider.event+json",
|
||||||
|
).Error)
|
||||||
require.NoError(t, webhookDB.Omit(clause.Associations).Create(
|
require.NoError(t, webhookDB.Omit(clause.Associations).Create(
|
||||||
&database.DeliveryResult{
|
&database.DeliveryResult{
|
||||||
DeliveryID: dlv.ID,
|
DeliveryID: dlv.ID,
|
||||||
@@ -1292,3 +1300,60 @@ func checkMobileMenu(ctx context.Context, t *testing.T, url string) {
|
|||||||
click(ctx, t, button)
|
click(ctx, t, button)
|
||||||
assert.True(t, hidden(ctx, menu), "the menu button does not close the menu")
|
assert.True(t, hidden(ctx, menu), "the menu button does not close the menu")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scrollsSideways reports whether the page is wider than the window. A
|
||||||
|
// page's clientWidth is the window's width less its scroll bar.
|
||||||
|
const scrollsSideways = `document.documentElement.scrollWidth >
|
||||||
|
document.documentElement.clientWidth`
|
||||||
|
|
||||||
|
// cutOffElements lists each element, without elements inside it, that
|
||||||
|
// is shown but runs past the page's edge or its card's, by more than a
|
||||||
|
// pixel of rounding. A card hides what runs past its edge.
|
||||||
|
const cutOffElements = `[...document.querySelectorAll("body *")]
|
||||||
|
.filter((el) => {
|
||||||
|
const box = el.getBoundingClientRect();
|
||||||
|
const card = el.closest(".card")?.getBoundingClientRect();
|
||||||
|
const left = card ? card.left : 0;
|
||||||
|
const right = card ? card.right : document.documentElement.clientWidth;
|
||||||
|
return el.children.length === 0 && box.width > 0 &&
|
||||||
|
(box.left < left - 1 || box.right > right + 1);
|
||||||
|
})
|
||||||
|
.map((el) => el.outerHTML.slice(0, 120))`
|
||||||
|
|
||||||
|
// checkPhoneWidth loads the webhook page, url, and its event log,
|
||||||
|
// eventLog, in a phone-sized window, the event log with the attempts of
|
||||||
|
// the newest event's delivery to targetName shown. It checks that
|
||||||
|
// neither page scrolls sideways and that nothing shown on either, no
|
||||||
|
// status, time or control, is cut off at the page's or its card's edge.
|
||||||
|
func checkPhoneWidth(
|
||||||
|
ctx context.Context, t *testing.T, url, eventLog, targetName string,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var (
|
||||||
|
sideways bool
|
||||||
|
cutOff []string
|
||||||
|
)
|
||||||
|
|
||||||
|
measure := chromedp.Tasks{
|
||||||
|
chromedp.Evaluate(scrollsSideways, &sideways),
|
||||||
|
chromedp.Evaluate(cutOffElements, &cutOff),
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, chromedp.Run(
|
||||||
|
ctx,
|
||||||
|
chromedp.EmulateViewport(phoneWidth, phoneHeight),
|
||||||
|
loadPage(url),
|
||||||
|
measure,
|
||||||
|
))
|
||||||
|
assert.False(t, sideways, "the webhook page scrolls sideways on a phone")
|
||||||
|
assert.Empty(t, cutOff, "the webhook page cuts these off on a phone")
|
||||||
|
|
||||||
|
require.NoError(t, chromedp.Run(ctx, loadPage(eventLog)))
|
||||||
|
click(ctx, t, `//span[text()="`+targetName+`"]`)
|
||||||
|
require.True(t, shown(ctx, `//span[text()="Attempt 1"]`),
|
||||||
|
"clicking the delivery does not show its attempts")
|
||||||
|
require.NoError(t, chromedp.Run(ctx, measure))
|
||||||
|
assert.False(t, sideways, "the event log scrolls sideways on a phone")
|
||||||
|
assert.Empty(t, cutOff, "the event log cuts these off on a phone")
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
|||||||
an event's own page. Spans only, since a button may hold no div. -->
|
an event's own page. Spans only, since a button may hold no div. -->
|
||||||
<span class="flex flex-1 flex-wrap items-center justify-between gap-3">
|
<span class="flex flex-1 flex-wrap items-center justify-between gap-3">
|
||||||
<span class="flex flex-wrap items-center gap-3">
|
<span class="flex flex-wrap items-center gap-3">
|
||||||
<span class="text-sm text-gray-700">{{.Target.DisplayName}}</span>
|
<span class="text-sm text-gray-700 wrap-anywhere">{{.Target.DisplayName}}</span>
|
||||||
{{if .Replay}}<span class="text-xs text-gray-500">replay</span>{{end}}
|
{{if .Replay}}<span class="text-xs text-gray-500">replay</span>{{end}}
|
||||||
<span class="text-xs {{if eq .Status "delivered"}}text-green-600{{else if eq .Status "failed"}}text-red-600{{else if eq .Status "retrying"}}text-yellow-600{{else}}text-gray-400{{end}}">{{with .Paused}}waiting: target paused after repeated failures, next try no earlier than {{.Until}} ({{.Relative}}){{else}}{{.Status}}{{end}}</span>
|
<span class="text-xs {{if eq .Status "delivered"}}text-green-600{{else if eq .Status "failed"}}text-red-600{{else if eq .Status "retrying"}}text-yellow-600{{else}}text-gray-400{{end}}">{{with .Paused}}waiting: target paused after repeated failures, next try no earlier than {{.Until}} ({{.Relative}}){{else}}{{.Status}}{{end}}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
entrypoint; the request it copies did. -->
|
entrypoint; the request it copies did. -->
|
||||||
<div class="space-y-2 text-xs">
|
<div class="space-y-2 text-xs">
|
||||||
{{if .ResubmittedFrom}}
|
{{if .ResubmittedFrom}}
|
||||||
<p class="text-gray-500">The request it copies arrived at <span class="text-gray-900">{{.Entrypoint}}</span></p>
|
<p class="text-gray-500">The request it copies arrived at <span class="text-gray-900 wrap-anywhere">{{.Entrypoint}}</span></p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="text-gray-500">Arrived at <span class="text-gray-900">{{.Entrypoint}}</span></p>
|
<p class="text-gray-500">Arrived at <span class="text-gray-900 wrap-anywhere">{{.Entrypoint}}</span></p>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .HeadersCut}}
|
{{if .HeadersCut}}
|
||||||
<p class="text-gray-500">The request headers are larger than the event log shows. <a href="{{.Body.EventURL}}" class="btn-small">Show the request headers</a></p>
|
<p class="text-gray-500">The request headers are larger than the event log shows. <a href="{{.Body.EventURL}}" class="btn-small">Show the request headers</a></p>
|
||||||
|
|||||||
@@ -6,15 +6,18 @@
|
|||||||
<!-- 108rem, half again the 72rem (max-w-6xl) of the webhook list, the
|
<!-- 108rem, half again the 72rem (max-w-6xl) of the webhook list, the
|
||||||
event log, the navbar and the footer, so an entrypoint URL fits on
|
event log, the navbar and the footer, so an entrypoint URL fits on
|
||||||
one line. An inline style, because the committed tailwind.css has
|
one line. An inline style, because the committed tailwind.css has
|
||||||
no class this wide. -->
|
no class this wide. wrap-anywhere goes only on names and
|
||||||
|
descriptions: a row too wide for a phone must still run past the
|
||||||
|
edge, where the browser test sees it, rather than break its
|
||||||
|
controls mid-word. -->
|
||||||
<div class="mx-auto px-6 py-8" style="max-width: 108rem">
|
<div class="mx-auto px-6 py-8" style="max-width: 108rem">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<a href="/hooks" class="btn-small">← Back to webhooks</a>
|
<a href="/hooks" class="btn-small">← Back to webhooks</a>
|
||||||
<div class="flex flex-wrap justify-between items-center gap-2 mt-2">
|
<div class="flex flex-wrap justify-between items-center gap-2 mt-2">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-medium text-gray-900">{{.Webhook.Name}}</h1>
|
<h1 class="text-2xl font-medium text-gray-900 wrap-anywhere">{{.Webhook.Name}}</h1>
|
||||||
{{if .Webhook.Description}}
|
{{if .Webhook.Description}}
|
||||||
<p class="text-sm text-gray-500 mt-1">{{.Webhook.Description}}</p>
|
<p class="text-sm text-gray-500 mt-1 wrap-anywhere">{{.Webhook.Description}}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -60,7 +63,7 @@
|
|||||||
{{range .Entrypoints}}
|
{{range .Entrypoints}}
|
||||||
<div class="p-4" x-data="collapsible">
|
<div class="p-4" x-data="collapsible">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2 mb-1">
|
<div class="flex flex-wrap items-center justify-between gap-2 mb-1">
|
||||||
<span x-show="closed" class="text-sm font-medium text-gray-900">{{if .Description}}{{.Description}}{{else}}Entrypoint{{end}}</span>
|
<span x-show="closed" class="text-sm font-medium text-gray-900 wrap-anywhere">{{if .Description}}{{.Description}}{{else}}Entrypoint{{end}}</span>
|
||||||
<!-- Edit shows this form in place of the
|
<!-- Edit shows this form in place of the
|
||||||
description and hides until it closes, and
|
description and hides until it closes, and
|
||||||
Cancel resets what was typed. With
|
Cancel resets what was typed. With
|
||||||
@@ -249,7 +252,7 @@
|
|||||||
{{range .Targets}}
|
{{range .Targets}}
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2 mb-1">
|
<div class="flex flex-wrap items-center justify-between gap-2 mb-1">
|
||||||
<span class="text-sm font-medium text-gray-900">{{.Name}}</span>
|
<span class="text-sm font-medium text-gray-900 wrap-anywhere">{{.Name}}</span>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="badge-info">{{if eq .Type "database"}}archive{{else}}{{.Type}}{{end}}</span>
|
<span class="badge-info">{{if eq .Type "database"}}archive{{else}}{{.Type}}{{end}}</span>
|
||||||
{{if .Active}}
|
{{if .Active}}
|
||||||
|
|||||||
@@ -3,10 +3,15 @@
|
|||||||
{{define "title"}}Full Event Log - {{.Webhook.Name}} - Webhooker{{end}}
|
{{define "title"}}Full Event Log - {{.Webhook.Name}} - Webhooker{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
|
<!-- wrap-anywhere goes only on names, IDs and content types: a row too
|
||||||
|
wide for a phone must still run past the edge, where the browser
|
||||||
|
test sees it, rather than break its statuses, times or controls
|
||||||
|
mid-word. So a target's name in an event's row, which shares its
|
||||||
|
element with the delivery's status, goes without. -->
|
||||||
<div class="max-w-6xl mx-auto px-6 py-8">
|
<div class="max-w-6xl mx-auto px-6 py-8">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<a href="/hook/{{.Webhook.ID}}" class="btn-small">← Back to {{.Webhook.Name}}</a>
|
<a href="/hook/{{.Webhook.ID}}" class="btn-small wrap-anywhere">← Back to {{.Webhook.Name}}</a>
|
||||||
<div class="flex justify-between items-center mt-2">
|
<div class="flex flex-wrap justify-between items-center gap-2 mt-2">
|
||||||
<h1 class="text-2xl font-medium text-gray-900">Full Event Log</h1>
|
<h1 class="text-2xl font-medium text-gray-900">Full Event Log</h1>
|
||||||
<!-- Under a filter, this counts the events the filter lists. -->
|
<!-- Under a filter, this counts the events the filter lists. -->
|
||||||
<span class="text-sm text-gray-500">{{if gt .TotalEvents (len .Events)}}{{len .Events}} most recent of {{.TotalEvents}} events{{else}}{{.TotalEvents}}{{if not .Show}} total{{end}} event{{if ne .TotalEvents 1}}s{{end}}{{end}}{{if eq .Show "failed"}} with a failed delivery{{else if eq .Show "pending"}} with a delivery pending or retrying{{end}}</span>
|
<span class="text-sm text-gray-500">{{if gt .TotalEvents (len .Events)}}{{len .Events}} most recent of {{.TotalEvents}} events{{else}}{{.TotalEvents}}{{if not .Show}} total{{end}} event{{if ne .TotalEvents 1}}s{{end}}{{end}}{{if eq .Show "failed"}} with a failed delivery{{else if eq .Show "pending"}} with a delivery pending or retrying{{end}}</span>
|
||||||
@@ -28,8 +33,8 @@
|
|||||||
<div role="button" tabindex="0" class="btn-small w-full flex flex-wrap justify-between gap-2" :aria-expanded="open" @mousedown="cancelPendingToggle" @click="toggleUnlessSelecting" @keydown.enter.prevent="toggle" @keydown.space.prevent="toggle">
|
<div role="button" tabindex="0" class="btn-small w-full flex flex-wrap justify-between gap-2" :aria-expanded="open" @mousedown="cancelPendingToggle" @click="toggleUnlessSelecting" @keydown.enter.prevent="toggle" @keydown.space.prevent="toggle">
|
||||||
<span class="flex flex-wrap items-center gap-3">
|
<span class="flex flex-wrap items-center gap-3">
|
||||||
<span class="badge-info">{{.Method}}</span>
|
<span class="badge-info">{{.Method}}</span>
|
||||||
<span class="text-sm font-mono text-gray-700">{{.ID}}</span>
|
<span class="text-sm font-mono text-gray-700 wrap-anywhere">{{.ID}}</span>
|
||||||
<span class="text-sm text-gray-500">{{.ContentType}}</span>
|
<span class="text-sm text-gray-500 wrap-anywhere">{{.ContentType}}</span>
|
||||||
{{if .ResubmittedFrom}}
|
{{if .ResubmittedFrom}}
|
||||||
<span class="text-xs text-gray-500" title="This event is a copy of {{.ResubmittedFromID}}">resubmitted copy</span>
|
<span class="text-xs text-gray-500" title="This event is a copy of {{.ResubmittedFromID}}">resubmitted copy</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -54,7 +59,7 @@
|
|||||||
<div x-show="open" x-cloak class="mt-3 p-3 bg-gray-50 rounded-md">
|
<div x-show="open" x-cloak class="mt-3 p-3 bg-gray-50 rounded-md">
|
||||||
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
|
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||||
<div class="text-xs text-gray-500">
|
<div class="text-xs text-gray-500">
|
||||||
{{if .ResubmittedFrom}}Resubmitted from event <a href="/hook/{{$.Webhook.ID}}/events/{{.ResubmittedFromID}}" class="btn-small font-mono">{{.ResubmittedFromID}}</a>.{{end}}
|
{{if .ResubmittedFrom}}Resubmitted from event <a href="/hook/{{$.Webhook.ID}}/events/{{.ResubmittedFromID}}" class="btn-small font-mono wrap-anywhere">{{.ResubmittedFromID}}</a>.{{end}}
|
||||||
{{if .ResubmitCount}}Resubmitted as {{.ResubmitCount}} new event{{if ne .ResubmitCount 1}}s{{end}}.{{end}}
|
{{if .ResubmitCount}}Resubmitted as {{.ResubmitCount}} new event{{if ne .ResubmitCount 1}}s{{end}}.{{end}}
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="/hook/{{$.Webhook.ID}}/events/{{.ID}}/resubmit" class="inline">
|
<form method="POST" action="/hook/{{$.Webhook.ID}}/events/{{.ID}}/resubmit" class="inline">
|
||||||
|
|||||||
Reference in New Issue
Block a user