check / check (push) Waiting to run
Each event now has its own page at /hook/ID/events/EVENTID, behind the login, showing its details, its whole body and every delivery; a resubmitted copy links to its original's page. The recent events on the webhook page link there and expand to show their bodies, only the newest expanded on load. One renderer and one template show a body the same way in the recent events, the event log and the event's page: whole up to 32 KiB, cut there in the two lists with links to the event's page and the download; JSON pretty-printed unless that would grow it past four times plus 1 KiB; over 200 lines in a scrolling box; a body holding NUL or control characters treated as binary and never dumped raw. Model: opus-5-5
135 lines
4.3 KiB
JavaScript
135 lines
4.3 KiB
JavaScript
// Webhooker client-side JavaScript
|
|
|
|
// Copy-to-clipboard, as progressive enhancement.
|
|
//
|
|
// Markup renders each copy button with the `hidden` attribute and a
|
|
// `data-copy-target` pointing at the id of the element holding the
|
|
// text. This script reveals a button only once it has both a resolvable
|
|
// target and a usable Clipboard API, so a browser without either shows
|
|
// no button at all and the text stays selectable.
|
|
(function () {
|
|
"use strict";
|
|
|
|
const revertDelayMs = 2000;
|
|
|
|
function flash(button, message) {
|
|
const original = button.getAttribute("data-copy-label");
|
|
button.textContent = message;
|
|
window.setTimeout(function () {
|
|
button.textContent = original;
|
|
}, revertDelayMs);
|
|
}
|
|
|
|
function wire(button) {
|
|
const target = document.getElementById(
|
|
button.getAttribute("data-copy-target")
|
|
);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
button.setAttribute("data-copy-label", button.textContent);
|
|
button.addEventListener("click", function () {
|
|
navigator.clipboard.writeText(target.textContent.trim()).then(
|
|
function () {
|
|
flash(button, "Copied");
|
|
},
|
|
function () {
|
|
flash(button, "Copy failed");
|
|
}
|
|
);
|
|
});
|
|
button.removeAttribute("hidden");
|
|
}
|
|
|
|
function init() {
|
|
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
|
return;
|
|
}
|
|
|
|
const buttons = document.querySelectorAll("[data-copy-target]");
|
|
buttons.forEach(wire);
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|
|
|
|
// Alpine.js components.
|
|
//
|
|
// The pages' Content-Security-Policy forbids eval, so the UI loads
|
|
// Alpine's CSP build, which cannot run expressions written in the
|
|
// markup: a directive in templates/ may only name a property or method,
|
|
// and each x-data names a component registered here. This script runs
|
|
// before Alpine, whose script tag is deferred, so this listener is in
|
|
// place when Alpine starts.
|
|
document.addEventListener("alpine:init", function () {
|
|
"use strict";
|
|
|
|
// Something a click shows and hides: the mobile menu, an add form,
|
|
// an entrypoint's edit form, an event in the event log or in the
|
|
// recent events, a delivery's attempts. It starts hidden, or shown
|
|
// when its element has the data-open attribute.
|
|
window.Alpine.data("collapsible", function () {
|
|
return {
|
|
open: false,
|
|
init() {
|
|
this.open = this.$root.hasAttribute("data-open");
|
|
},
|
|
toggle() {
|
|
this.open = !this.open;
|
|
},
|
|
get closed() {
|
|
return !this.open;
|
|
},
|
|
// Turns a downward caret up while open.
|
|
get caretClass() {
|
|
return { "rotate-180": this.open };
|
|
},
|
|
};
|
|
});
|
|
|
|
// The add target form. Only the chosen type's fields show, and the
|
|
// others are disabled so that the form does not submit them.
|
|
//
|
|
// The type is read from the type select when Alpine starts, when the
|
|
// select changes, and on pageshow. Going back to the page, the
|
|
// browser restores the type chosen before without a change event,
|
|
// in some browsers only after Alpine has started, but always before
|
|
// pageshow.
|
|
window.Alpine.data("targetForm", function () {
|
|
return {
|
|
targetType: "",
|
|
init() {
|
|
this.readType();
|
|
},
|
|
readType() {
|
|
this.targetType = this.$root.querySelector(
|
|
'select[name="type"]'
|
|
).value;
|
|
},
|
|
get isHttp() {
|
|
return this.targetType === "http";
|
|
},
|
|
get isSlack() {
|
|
return this.targetType === "slack";
|
|
},
|
|
get isDatabase() {
|
|
return this.targetType === "database";
|
|
},
|
|
get notHttp() {
|
|
return !this.isHttp;
|
|
},
|
|
get notSlack() {
|
|
return !this.isSlack;
|
|
},
|
|
get notDatabase() {
|
|
return !this.isDatabase;
|
|
},
|
|
};
|
|
});
|
|
});
|