fix(web): Started toggle survives the Executions card's 5s poll
The card's own htmx poll re-renders the whole table from the server every
5s, always starting fresh at "relative" -- so a toggled-to-absolute cell
snapped back on the next poll. Each button now has a stable id
(started-{run_id}-{attempt}, or started-vm-{vmx} on an unmatched-VM row);
app.js remembers which format was showing per id and re-applies it to the
fresh button after every swap, rather than freezing the node itself (which
would also freeze its relative-time text, badge, duration, everything else
in that row).
This commit is contained in:
+8
-4
@@ -362,10 +362,14 @@ card's own `.table-wrap` rather than the page, same as the other wide tables
|
|||||||
**Started is click-to-toggle.** It renders relative ("2h ago") by default;
|
**Started is click-to-toggle.** It renders relative ("2h ago") by default;
|
||||||
clicking swaps to the absolute local timestamp, and clicking again swaps
|
clicking swaps to the absolute local timestamp, and clicking again swaps
|
||||||
back. Both strings are pre-rendered server-side into the button's
|
back. Both strings are pre-rendered server-side into the button's
|
||||||
`data-relative`/`data-absolute` attributes, so the swap (`app.js`, delegated
|
`data-relative`/`data-absolute` attributes, so the swap (`app.js`) is
|
||||||
on `document.body`) is instant and needs no request — and keeps working
|
instant and needs no request. The card's own 5s poll re-renders the whole
|
||||||
after the card's own 5s poll replaces the row, since the listener isn't on
|
table from the server, always starting back at relative — so which format
|
||||||
the button itself.
|
was showing is remembered client-side, keyed by the button's stable `id`
|
||||||
|
(`started-{run_id}-{attempt}`, or `started-vm-{vmx}` for an unmatched VM
|
||||||
|
row), and re-applied to the fresh button after every poll rather than
|
||||||
|
frozen onto the old node (which would also freeze its relative-time text,
|
||||||
|
badge, duration — everything else in that row).
|
||||||
|
|
||||||
Runs come from Gitea's Actions API: `/api/v1/admin/actions/runs` (every repo
|
Runs come from Gitea's Actions API: `/api/v1/admin/actions/runs` (every repo
|
||||||
on the instance, admin token) with `/api/v1/user/actions/runs` as the
|
on the instance, admin token) with `/api/v1/user/actions/runs` as the
|
||||||
|
|||||||
@@ -197,27 +197,56 @@
|
|||||||
}).observe(stack, { childList: true });
|
}).observe(stack, { childList: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.addEventListener("htmx:afterSwap", function (evt) {
|
|
||||||
var target = evt.detail && evt.detail.target;
|
|
||||||
if (target && target.id === "action-slot") {
|
|
||||||
processSlot(target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Executions "Started" cell: click swaps between the relative and
|
// Executions "Started" cell: click swaps between the relative and
|
||||||
// absolute time already rendered into the button's own data attributes --
|
// absolute time already rendered into the button's own data attributes --
|
||||||
// no request. Delegated on body (not the button itself) so it keeps
|
// no request. Delegated on body (not the button itself) so it keeps
|
||||||
// working after the Executions card's own 5s htmx poll replaces the row.
|
// working after the Executions card's own 5s htmx poll replaces the row.
|
||||||
|
//
|
||||||
|
// The poll re-renders the whole card from the server every 5s, always
|
||||||
|
// starting back at "relative" -- so the choice is remembered here, keyed
|
||||||
|
// by the button's own id (stable across polls: run id/attempt or vmx),
|
||||||
|
// and re-applied to the fresh button restoreTimeToggles() finds after each
|
||||||
|
// swap, rather than freezing the node itself (which would also freeze its
|
||||||
|
// relative-time text, badge, duration -- everything else in the row).
|
||||||
|
var timeToggleState = {};
|
||||||
|
|
||||||
|
function applyTimeToggle(btn, showing) {
|
||||||
|
var shown = showing === "relative" ? "absolute" : "relative";
|
||||||
|
btn.textContent = btn.dataset[showing];
|
||||||
|
btn.title = btn.dataset[shown];
|
||||||
|
btn.dataset.showing = showing;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreTimeToggles(root) {
|
||||||
|
var buttons = root.querySelectorAll ? root.querySelectorAll(".time-toggle[id]") : [];
|
||||||
|
Array.prototype.forEach.call(buttons, function (btn) {
|
||||||
|
var saved = timeToggleState[btn.id];
|
||||||
|
if (saved && saved !== btn.dataset.showing) {
|
||||||
|
applyTimeToggle(btn, saved);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.body.addEventListener("click", function (evt) {
|
document.body.addEventListener("click", function (evt) {
|
||||||
var btn = evt.target.closest(".time-toggle");
|
var btn = evt.target.closest(".time-toggle");
|
||||||
if (!btn) {
|
if (!btn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var next = btn.dataset.showing === "relative" ? "absolute" : "relative";
|
var next = btn.dataset.showing === "relative" ? "absolute" : "relative";
|
||||||
var shown = next === "relative" ? "absolute" : "relative";
|
applyTimeToggle(btn, next);
|
||||||
btn.textContent = btn.dataset[next];
|
if (btn.id) {
|
||||||
btn.title = btn.dataset[shown];
|
timeToggleState[btn.id] = next;
|
||||||
btn.dataset.showing = next;
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener("htmx:afterSwap", function (evt) {
|
||||||
|
var target = evt.detail && evt.detail.target;
|
||||||
|
if (target && target.id === "action-slot") {
|
||||||
|
processSlot(target);
|
||||||
|
}
|
||||||
|
if (target) {
|
||||||
|
restoreTimeToggles(target);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
<td>{{ run.duration_sec | duration }}</td>
|
<td>{{ run.duration_sec | duration }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if run.started %}
|
{% if run.started %}
|
||||||
<button type="button" class="time-toggle text-dim" data-showing="relative"
|
<button type="button" id="started-{{ run.run_id }}-{{ run.attempt }}" class="time-toggle text-dim" data-showing="relative"
|
||||||
data-relative="{{ run.started | relative_time }}" data-absolute="{{ run.started | localtime }}"
|
data-relative="{{ run.started | relative_time }}" data-absolute="{{ run.started | localtime }}"
|
||||||
title="{{ run.started | localtime }}">{{ run.started | relative_time }}</button>
|
title="{{ run.started | localtime }}">{{ run.started | relative_time }}</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
<td><span class="text-dim">—</span></td>
|
<td><span class="text-dim">—</span></td>
|
||||||
<td>
|
<td>
|
||||||
{% if parsed.started %}
|
{% if parsed.started %}
|
||||||
<button type="button" class="time-toggle text-dim" data-showing="relative"
|
<button type="button" id="started-vm-{{ vm.vmx | vmx_id }}" class="time-toggle text-dim" data-showing="relative"
|
||||||
data-relative="{{ parsed.started | relative_time }}" data-absolute="{{ parsed.started | localtime }}"
|
data-relative="{{ parsed.started | relative_time }}" data-absolute="{{ parsed.started | localtime }}"
|
||||||
title="{{ parsed.started | localtime }}">{{ parsed.started | relative_time }}</button>
|
title="{{ parsed.started | localtime }}">{{ parsed.started | relative_time }}</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -346,11 +346,17 @@ def test_started_cell_is_a_click_to_toggle_button(
|
|||||||
"""Both the relative and absolute renderings are pre-rendered into the
|
"""Both the relative and absolute renderings are pre-rendered into the
|
||||||
button's data attributes -- app.js swaps textContent/title on click, no
|
button's data attributes -- app.js swaps textContent/title on click, no
|
||||||
round trip. Server-side, this test can only pin the markup the toggle
|
round trip. Server-side, this test can only pin the markup the toggle
|
||||||
depends on, not the click itself."""
|
depends on, not the click itself.
|
||||||
|
|
||||||
|
The id must be stable across the card's own 5s poll (run_id/attempt, not
|
||||||
|
anything generated fresh per render) -- app.js keys its client-side
|
||||||
|
"which format is showing" memory off it to survive the next poll's
|
||||||
|
server-rendered reset back to relative.
|
||||||
|
"""
|
||||||
_stub_view(
|
_stub_view(
|
||||||
monkeypatch,
|
monkeypatch,
|
||||||
ExecutionsView(
|
ExecutionsView(
|
||||||
runs=[_execution(started="2026-07-25T11:50:00Z")],
|
runs=[_execution(run_id=412, attempt=1, started="2026-07-25T11:50:00Z")],
|
||||||
unmatched_vms=[],
|
unmatched_vms=[],
|
||||||
source="admin",
|
source="admin",
|
||||||
error=None,
|
error=None,
|
||||||
@@ -362,7 +368,8 @@ def test_started_cell_is_a_click_to_toggle_button(
|
|||||||
|
|
||||||
resp = client.get("/executions")
|
resp = client.get("/executions")
|
||||||
|
|
||||||
assert '<button type="button" class="time-toggle text-dim"' in resp.text
|
assert 'id="started-412-1"' in resp.text
|
||||||
|
assert 'class="time-toggle text-dim"' in resp.text
|
||||||
assert 'data-showing="relative"' in resp.text
|
assert 'data-showing="relative"' in resp.text
|
||||||
assert 'data-relative="' in resp.text
|
assert 'data-relative="' in resp.text
|
||||||
assert 'data-absolute="' in resp.text
|
assert 'data-absolute="' in resp.text
|
||||||
|
|||||||
Reference in New Issue
Block a user