Inline service information into the table to make it more accessible

This commit is contained in:
Johannes Schriewer 2026-08-30 20:50:31 +02:00
parent bd6a503241
commit a3b5bcf2c5
6 changed files with 1779 additions and 12 deletions

File diff suppressed because it is too large Load diff

View file

@ -176,11 +176,20 @@ button:hover { border-color: var(--accent); }
.dot-busy { background: var(--busy); }
.dot-sleep { background: var(--accent); }
.svc-name { cursor: pointer; color: var(--accent); }
.svc-row { cursor: pointer; }
.svc-name { color: var(--accent); }
.svc-name:hover { text-decoration: underline; }
.detail-pane { margin-top: 4px; }
.detail-pane:empty { display: none; }
.svc-caret {
display: inline-block;
margin-right: 6px;
color: var(--muted);
font-size: 10px;
transition: transform .15s;
}
.svc-row.open .svc-caret { transform: rotate(90deg); color: var(--accent); }
.svc-detail-row[hidden] { display: none; }
.svc-detail-row td { background: #12161d; padding: 10px 12px; white-space: normal; }
.table tbody tr.svc-detail-row:hover { background: #12161d; }
.detail-inner {
background: var(--bg-panel);
border: 1px solid var(--border);

View file

@ -180,12 +180,53 @@
htmx.trigger(form, "submit");
});
// keyboard: Enter/Space on service names
// ---------- services accordion ----------
const openSvc = new Set();
function loadSvcDetail(unit, target) {
htmx.ajax("GET", "/api/services/" + encodeURIComponent(unit) + "/detail", { target });
}
function toggleSvcRow(row) {
const detailRow = row.nextElementSibling;
if (!detailRow || !detailRow.classList.contains("svc-detail-row")) return;
const unit = row.dataset.unit;
if (openSvc.has(unit)) {
openSvc.delete(unit);
row.classList.remove("open");
detailRow.hidden = true;
} else {
openSvc.add(unit);
row.classList.add("open");
detailRow.hidden = false;
loadSvcDetail(unit, detailRow.querySelector(".svc-detail"));
}
}
document.body.addEventListener("click", (e) => {
const row = e.target.closest("#services tr.svc-row");
if (!row || e.target.closest("button")) return;
toggleSvcRow(row);
});
document.body.addEventListener("keydown", (e) => {
const el = e.target.closest(".svc-name");
if (el && (e.key === "Enter" || e.key === " ")) {
e.preventDefault();
htmx.trigger(el, "click");
toggleSvcRow(el.closest("tr.svc-row"));
}
});
document.body.addEventListener("htmx:afterSwap", (e) => {
if (e.target.id !== "services" || !openSvc.size) return;
for (const row of e.target.querySelectorAll("tr.svc-row")) {
const unit = row.dataset.unit;
if (!openSvc.has(unit)) continue;
const detailRow = row.nextElementSibling;
if (!detailRow || !detailRow.classList.contains("svc-detail-row")) continue;
row.classList.add("open");
detailRow.hidden = false;
loadSvcDetail(unit, detailRow.querySelector(".svc-detail"));
}
});
})();

View file

@ -88,8 +88,7 @@
</select>
<button type="submit">filter</button>
</form>
<div id="services" class="panel" hx-get="/api/services" hx-trigger="load, every 5s" hx-swap="outerHTML" hx-include="#svc-filters"></div>
<div id="service-detail" class="detail-pane"></div>
<div id="services" class="panel" hx-get="/api/services" hx-trigger="load, every 15s" hx-swap="outerHTML" hx-include="#svc-filters"></div>
</section>
<section id="tab-plugins" class="tab hidden">

View file

@ -1,5 +1,4 @@
<div class="detail-inner">
<h3 class="mono">{{ unit }}</h3>
{% if error %}<div class="alert">{{ error }}</div>{% endif %}
<div class="kv">
<div><span class="muted">state:</span> {{ props.get("ActiveState", "?") }} ({{ props.get("SubState", "?") }})</div>

View file

@ -1,4 +1,4 @@
<div id="services" class="panel" hx-get="/api/services" hx-trigger="every 5s" hx-swap="outerHTML" hx-include="#svc-filters">
<div id="services" class="panel" hx-get="/api/services" hx-trigger="every 15s" hx-swap="outerHTML" hx-include="#svc-filters">
{% if error %}<div class="alert">{{ error }}</div>{% endif %}
<div class="muted small">system: {{ state }} &middot; {{ units | length }} units</div>
<div class="table-wrap">
@ -27,9 +27,9 @@
{% if u.sub in ("running", "exited") and u.active == "active" %}{% set dot = "dot-run" %}{% endif %}
{% if u.active == "failed" %}{% set dot = "dot-failed" %}{% endif %}
{% if u.sub in ("activating", "deactivating", "reloading") %}{% set dot = "dot-busy" %}{% endif %}
<tr>
<tr class="svc-row" data-unit="{{ u.name }}">
<td><span class="dot {{ dot }}"></span></td>
<td class="mono svc-name" hx-get="/api/services/{{ u.name }}/detail" hx-target="#service-detail" hx-swap="innerHTML" role="button" tabindex="0" title="click for details">{{ u.name }}</td>
<td class="mono svc-name" role="button" tabindex="0" title="click for details"><span class="svc-caret"></span>{{ u.name }}</td>
<td class="muted cell-clip" title="{{ u.desc }}">{{ u.desc }}</td>
<td class="muted">{{ u.active }}{% if u.sub and u.sub not in ("running", "dead", "exited") %} <span class="small">({{ u.sub }})</span>{% endif %}</td>
<td class="muted">{{ u.enabled or "&mdash;" }}</td>
@ -43,6 +43,9 @@
<button class="btn" hx-post="/api/services/{{ u.name }}/action" hx-vals='{"action":"{% if u.enabled == "enabled" or u.enabled == "indirect" or u.enabled == "static" %}disable{% else %}enable{% endif %}"}' hx-include="#svc-filters" hx-target="#services" hx-swap="outerHTML">{% if u.enabled == "enabled" or u.enabled == "indirect" or u.enabled == "static" %}disable{% else %}enable{% endif %}</button>
</td>
</tr>
<tr class="svc-detail-row" hidden data-unit="{{ u.name }}">
<td colspan="6"><div class="svc-detail"></div></td>
</tr>
{% else %}
<tr><td colspan="6" class="muted">no units match</td></tr>
{% endfor %}