Improve device detail view
build / build-linux-amd64 (push) Successful in 1m53s

This commit is contained in:
2026-05-22 23:03:06 +02:00
parent aeee852835
commit 7b6526a2e8
3 changed files with 50 additions and 4 deletions
+40
View File
@@ -101,8 +101,48 @@
a.classList.toggle('active', a === active); a.classList.toggle('active', a === active);
}); });
} }
// Dynamic deep-links: `#devices/<id>` and `#devices/<id>/exec`.
// The detail / exec fragments are designed to swap into the
// devices index page's `#devices-region` section, so when we
// land here from a page refresh we have to chain two ajax calls:
// first render the parent page (which provides `#devices-region`),
// then swap the fragment into it. htmx.ajax has returned a Promise
// since 1.9.4, so the `.then` chain is safe at our pinned 1.9.10.
const DEEP_LINK_PATTERNS = [
{
re: /^#devices\/([^/]+)\/exec$/,
parent: '/admin/pages/devices',
fragment: id => `/admin/pages/devices/${encodeURIComponent(id)}/exec`,
},
{
re: /^#devices\/([^/]+)$/,
parent: '/admin/pages/devices',
fragment: id => `/admin/pages/devices/${encodeURIComponent(id)}/detail`,
},
];
function loadDeepLink(hash) {
for (const p of DEEP_LINK_PATTERNS) {
const m = hash.match(p.re);
if (!m) continue;
const id = decodeURIComponent(m[1]);
Promise.resolve(
htmx.ajax('GET', p.parent, { target: '#main', swap: 'innerHTML' })
).then(() =>
htmx.ajax('GET', p.fragment(id), {
target: '#devices-region',
swap: 'innerHTML',
})
);
return true;
}
return false;
}
function loadFromHash() { function loadFromHash() {
const hash = location.hash || '#users'; const hash = location.hash || '#users';
if (loadDeepLink(hash)) {
refreshActive();
return;
}
const subUrl = SUB_ROUTES[hash]; const subUrl = SUB_ROUTES[hash];
if (subUrl) { if (subUrl) {
htmx.ajax('GET', subUrl, { target: '#main', swap: 'innerHTML' }); htmx.ajax('GET', subUrl, { target: '#main', swap: 'innerHTML' });
+8 -3
View File
@@ -1074,12 +1074,14 @@ fn render_device_row(
</a> </a>
<button class="w-full text-left px-2 py-1 text-xs hover:bg-slate-800 rounded" <button class="w-full text-left px-2 py-1 text-xs hover:bg-slate-800 rounded"
hx-get="/admin/pages/devices/{id}/detail" hx-get="/admin/pages/devices/{id}/detail"
hx-target="#devices-region" hx-swap="innerHTML"> hx-target="#devices-region" hx-swap="innerHTML"
hx-push-url="#devices/{id}">
{details} {details}
</button> </button>
<button class="w-full text-left px-2 py-1 text-xs hover:bg-slate-800 rounded" <button class="w-full text-left px-2 py-1 text-xs hover:bg-slate-800 rounded"
hx-get="/admin/pages/devices/{id}/exec" hx-get="/admin/pages/devices/{id}/exec"
hx-target="#devices-region" hx-swap="innerHTML"> hx-target="#devices-region" hx-swap="innerHTML"
hx-push-url="#devices/{id}/exec">
{run_command} {run_command}
</button> </button>
<hr class="border-slate-700 my-1" /> <hr class="border-slate-700 my-1" />
@@ -1146,12 +1148,15 @@ pub async fn list_fragment(
/// "Back to devices" — refetches the devices table fragment via HTMX /// "Back to devices" — refetches the devices table fragment via HTMX
/// and swaps it back into `#devices-region`. Used by the detail page. /// and swaps it back into `#devices-region`. Used by the detail page.
/// `hx-push-url="#devices"` resets the address bar to the list-level
/// hash so a subsequent refresh lands on the list, not the detail.
fn back_button(lang: Lang) -> String { fn back_button(lang: Lang) -> String {
format!( format!(
r##"<button class="text-xs text-sky-300 hover:text-sky-200" r##"<button class="text-xs text-sky-300 hover:text-sky-200"
hx-get="/admin/pages/devices/list-fragment" hx-get="/admin/pages/devices/list-fragment"
hx-target="#devices-region" hx-target="#devices-region"
hx-swap="innerHTML">{label}</button>"##, hx-swap="innerHTML"
hx-push-url="#devices">{label}</button>"##,
label = t(lang, "devices.back"), label = t(lang, "devices.back"),
) )
} }
+2 -1
View File
@@ -227,7 +227,8 @@ async fn render_page(
<h2 class="text-lg font-semibold">{heading} <code class="font-mono text-sky-300">{id}</code></h2> <h2 class="text-lg font-semibold">{heading} <code class="font-mono text-sky-300">{id}</code></h2>
<button class="text-xs text-sky-300 hover:text-sky-200" <button class="text-xs text-sky-300 hover:text-sky-200"
hx-get="/admin/pages/devices/list-fragment" hx-get="/admin/pages/devices/list-fragment"
hx-target="#devices-region" hx-swap="innerHTML">{back}</button> hx-target="#devices-region" hx-swap="innerHTML"
hx-push-url="#devices">{back}</button>
</header>"##, </header>"##,
heading = t(lang, "exec.heading"), heading = t(lang, "exec.heading"),
id = html_escape(peer_id), id = html_escape(peer_id),