feat(web): click Executions' Started time to toggle relative/absolute
Both renderings are pre-rendered server-side into a <button>'s data-relative/data-absolute attributes; app.js swaps textContent (and the hover title, to the opposite value) on click, delegated on document.body so it survives the card's own 5s htmx poll without needing to re-wire after every swap. Click again to swap back.
This commit is contained in:
@@ -359,6 +359,14 @@ table is wide as a result — 13 columns — and scrolls horizontally inside the
|
||||
card's own `.table-wrap` rather than the page, same as the other wide tables
|
||||
(Templates, Services).
|
||||
|
||||
**Started is click-to-toggle.** It renders relative ("2h ago") by default;
|
||||
clicking swaps to the absolute local timestamp, and clicking again swaps
|
||||
back. Both strings are pre-rendered server-side into the button's
|
||||
`data-relative`/`data-absolute` attributes, so the swap (`app.js`, delegated
|
||||
on `document.body`) is instant and needs no request — and keeps working
|
||||
after the card's own 5s poll replaces the row, since the listener isn't on
|
||||
the button itself.
|
||||
|
||||
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
|
||||
non-admin fallback. Results are cached for 10s, so the card's 5s refresh costs
|
||||
|
||||
@@ -501,6 +501,22 @@ input[type="text"], input[type="number"], select {
|
||||
/* Muted inline text (e.g. a relative-time suffix next to a file name). */
|
||||
.text-dim { color: var(--text-dim); }
|
||||
|
||||
/* Executions "Started" cell: reset to read as plain inline text (no button
|
||||
* chrome), with a dotted underline as the click-to-toggle affordance --
|
||||
* not colour alone, same rule the rest of the app follows. app.js swaps
|
||||
* between its data-relative/data-absolute values on click. */
|
||||
.time-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
text-decoration: underline dotted;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.time-toggle:hover { color: var(--text); }
|
||||
|
||||
/* ── notices / confirm / result panels ───────────────────────────────── */
|
||||
|
||||
.notice {
|
||||
|
||||
@@ -204,6 +204,22 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Executions "Started" cell: click swaps between the relative and
|
||||
// absolute time already rendered into the button's own data attributes --
|
||||
// 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.
|
||||
document.body.addEventListener("click", function (evt) {
|
||||
var btn = evt.target.closest(".time-toggle");
|
||||
if (!btn) {
|
||||
return;
|
||||
}
|
||||
var next = btn.dataset.showing === "relative" ? "absolute" : "relative";
|
||||
var shown = next === "relative" ? "absolute" : "relative";
|
||||
btn.textContent = btn.dataset[next];
|
||||
btn.title = btn.dataset[shown];
|
||||
btn.dataset.showing = next;
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
observeStack();
|
||||
processSlot(document.getElementById("action-slot"));
|
||||
|
||||
@@ -89,7 +89,9 @@
|
||||
<td>{{ run.duration_sec | duration }}</td>
|
||||
<td>
|
||||
{% if run.started %}
|
||||
<span class="text-dim" title="{{ run.started | localtime }}">{{ run.started | relative_time }}</span>
|
||||
<button type="button" class="time-toggle text-dim" data-showing="relative"
|
||||
data-relative="{{ run.started | relative_time }}" data-absolute="{{ run.started | localtime }}"
|
||||
title="{{ run.started | localtime }}">{{ run.started | relative_time }}</button>
|
||||
{% else %}
|
||||
<span class="text-dim">—</span>
|
||||
{% endif %}
|
||||
@@ -137,7 +139,9 @@
|
||||
<td><span class="text-dim">—</span></td>
|
||||
<td>
|
||||
{% if parsed.started %}
|
||||
<span class="text-dim" title="{{ parsed.started | localtime }}">{{ parsed.started | relative_time }}</span>
|
||||
<button type="button" class="time-toggle text-dim" data-showing="relative"
|
||||
data-relative="{{ parsed.started | relative_time }}" data-absolute="{{ parsed.started | localtime }}"
|
||||
title="{{ parsed.started | localtime }}">{{ parsed.started | relative_time }}</button>
|
||||
{% else %}
|
||||
<span class="text-dim">—</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -340,6 +340,34 @@ def test_status_badge_has_no_icon_for_non_terminal_states(
|
||||
assert "#i-x" not in resp.text
|
||||
|
||||
|
||||
def test_started_cell_is_a_click_to_toggle_button(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Both the relative and absolute renderings are pre-rendered into the
|
||||
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
|
||||
depends on, not the click itself."""
|
||||
_stub_view(
|
||||
monkeypatch,
|
||||
ExecutionsView(
|
||||
runs=[_execution(started="2026-07-25T11:50:00Z")],
|
||||
unmatched_vms=[],
|
||||
source="admin",
|
||||
error=None,
|
||||
),
|
||||
)
|
||||
app = create_app(_make_config(tmp_path))
|
||||
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
|
||||
client = TestClient(app)
|
||||
|
||||
resp = client.get("/executions")
|
||||
|
||||
assert '<button type="button" class="time-toggle text-dim"' in resp.text
|
||||
assert 'data-showing="relative"' in resp.text
|
||||
assert 'data-relative="' in resp.text
|
||||
assert 'data-absolute="' in resp.text
|
||||
|
||||
|
||||
def test_no_cell_stacks_two_facts_on_separate_lines(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user