Skip to main content

max / makenotwork

Describe the SSH-keys tables as tables again Both were Node::List, because a table row could not carry an action and each of these has a destructive button on every row. That cost the column headers, and most on the tokens table: name, scope, expiry and last use are four independent facts read down the column, and as a list they were one primary plus a run-on meta string. quasi@b4e3e21 closed the gap and makeover-webview 0.23.0 gives a described table a layout, so both are tables with their template's own columns. The trailing empty column is the one the templates already had an empty <th> for. Not committed with this: server/Cargo.lock. It carries churn that predates this work (docengine 0.5.0, the 0.11.14 bump) and this change cannot be separated from it inside one file, so the pin moves in Cargo.toml alone and the lock re-resolves.
Author: Max Johnson <me@maxj.phd> · 2026-08-10 23:20 UTC
Signed with PGP, not checked
Commit: 9cb5f4481776e488ec93509d70a7f7f9ab59df9f
Parent: 3c50de4
2 files changed, +79 insertions, -40 deletions
@@ -236,7 +236,7 @@
236 236 # Emits static/geometry.css (makeover-geometry) and static/layout.css
237 237 # (makeover-webview). The same generator GO and BB run; only the output paths
238 238 # differ, since the server serves its stylesheets rather than bundling them.
239 - makeover-build = "0.14.1"
239 + makeover-build = "0.16.0"
240 240 # Read directly for SizeClass, which the breakpoint guard in build.rs compares
241 241 # the stylesheets against. makeover-build does not re-export it, so the pin
242 242 # lives here and has to track the one makeover-build resolves.
@@ -27,20 +27,18 @@
27 27 //! will typically look like, and therefore what the blocking-pool question is
28 28 //! actually about.
29 29 //!
30 - //! # Two vocabulary gaps this screen hit
30 + //! # The vocabulary gaps this screen found
31 31 //!
32 32 //! Both filed on quasicoherent rather than worked around silently.
33 33 //!
34 - //! 1. **A table row cannot carry an action.** `Node::Table`'s rows are
35 - //! [`Cells`], which offer `activate` and nothing else, so a table whose rows
36 - //! each have a Remove button cannot be said. `Node::List` can say it, so both
37 - //! lists here are lists, and the column headers the templates had are gone.
38 - //! That is a real loss on the tokens list, which has four columns.
34 + //! 1. **A table row could not carry an action**, so both tables here had to be
35 + //! lists and lost their column headers. **Closed** by quasi@`b4e3e21`: a
36 + //! table cell holds controls as well as a string, and both are tables again.
39 37 //! 2. **No date field.** The token form's expiry was `<input type="date">` and
40 - //! is now `Text`. See [`add_token_form`].
38 + //! is now `Text`. Still open. See [`add_token_form`].
41 39
42 40 use makeover_layout as layout;
43 - use quasi_router::screen::{Act, Choice, Field, Row};
41 + use quasi_router::screen::{Act, Cell, Cells, Choice, Column, Field};
44 42 use quasi_router::{Action, Node, RegionKind, Request, Response, RouteError, Slot};
45 43 use quasi_webview::{Shell, Webview};
46 44
@@ -163,21 +161,40 @@
163 161 if keys.is_empty() {
164 162 return Node::empty("No SSH keys registered.");
165 163 }
166 - Node::list(keys.iter().map(|key| {
167 - Row::new(key.fingerprint.clone())
168 - .secondary(key.label.clone())
169 - .meta(format!("Added {}", key.added))
170 - .act(
171 - Act::new(
172 - "Remove",
173 - Action::post(format!("/api/users/me/ssh-keys/{}/delete", key.id)),
174 - )
175 - // The template asked with hx-confirm. Said here, a terminal host
176 - // asks in its own way and no host can forget to ask.
177 - .confirm("Remove this SSH key?")
178 - .tone(layout::Tone::Danger),
179 - )
180 - }))
164 + Node::Table {
165 + // The template's four columns, in its order, the last one the empty
166 + // header its actions sit under.
167 + columns: vec![
168 + Column::new("Fingerprint")
169 + .width(layout::Width::Fill)
170 + .priority(layout::Priority::Essential),
171 + Column::new("Label").width(layout::Width::Content),
172 + Column::new("Added")
173 + .width(layout::Width::Content)
174 + .priority(layout::Priority::Optional),
175 + Column::new("")
176 + .width(layout::Width::Content)
177 + .priority(layout::Priority::Essential),
178 + ],
179 + rows: keys
180 + .iter()
181 + .map(|key| {
182 + Cells::new([
183 + Cell::new(key.fingerprint.clone()),
184 + Cell::new(key.label.clone()),
185 + Cell::new(format!("Added {}", key.added)),
186 + Cell::acts([Act::new(
187 + "Remove",
188 + Action::post(format!("/api/users/me/ssh-keys/{}/delete", key.id)),
189 + )
190 + // The template asked with hx-confirm. Said here, a terminal
191 + // host asks in its own way and no host can forget to ask.
192 + .confirm("Remove this SSH key?")
193 + .tone(layout::Tone::Danger)]),
194 + ])
195 + })
196 + .collect(),
197 + }
181 198 }
182 199
183 200 /// The add-a-key form.
@@ -221,22 +238,44 @@
221 238 if tokens.is_empty() {
222 239 return Node::empty("No access tokens.");
223 240 }
224 - Node::list(tokens.iter().map(|token| {
225 - Row::new(token.name.clone())
226 - .secondary(token.scope)
227 - .meta(format!(
228 - "Expires {} - last used {}",
229 - token.expires, token.last_used
230 - ))
231 - .act(
232 - Act::new(
233 - "Revoke",
234 - Action::post(format!("/api/users/me/git-tokens/{}/delete", token.id)),
235 - )
236 - .confirm("Revoke this token?")
237 - .tone(layout::Tone::Danger),
238 - )
239 - }))
241 + Node::Table {
242 + // The four independent facts the list version had to run together into
243 + // one `meta` string, back in their own columns. This is the table that
244 + // lost the most by being a list: name, scope, expiry and last use are
245 + // read down the column, which is what a table is for.
246 + columns: vec![
247 + Column::new("Name")
248 + .width(layout::Width::Fill)
249 + .priority(layout::Priority::Essential),
250 + Column::new("Scope").width(layout::Width::Content),
251 + Column::new("Expires")
252 + .width(layout::Width::Content)
253 + .priority(layout::Priority::Optional),
254 + Column::new("Last used")
255 + .width(layout::Width::Content)
256 + .priority(layout::Priority::Optional),
257 + Column::new("")
258 + .width(layout::Width::Content)
259 + .priority(layout::Priority::Essential),
260 + ],
261 + rows: tokens
262 + .iter()
263 + .map(|token| {
264 + Cells::new([
265 + Cell::new(token.name.clone()),
266 + Cell::new(token.scope),
267 + Cell::new(token.expires.clone()),
268 + Cell::new(token.last_used.clone()),
269 + Cell::acts([Act::new(
270 + "Revoke",
271 + Action::post(format!("/api/users/me/git-tokens/{}/delete", token.id)),
272 + )
273 + .confirm("Revoke this token?")
274 + .tone(layout::Tone::Danger)]),
275 + ])
276 + })
277 + .collect(),
278 + }
240 279 }
241 280
242 281 /// The mint-a-token form.