TokenList
API tokens with name, prefix, created, last used, scopes and expiry, and a Revoke that asks first.
Demo
| Token | Created | Last used | Scopes | Actions |
|---|---|---|---|---|
| CI deployqt_live_8f3a… | Sep 14, 2026 | Oct 27, 2026 | readdeploy | |
| Laptopqt_live_c01d… | Oct 1, 2026 · Expires Dec 31, 2027 | Never | readwrite | |
| Old scriptqt_live_77aa… | Feb 3, 2025 · Expired | Aug 19, 2025 |
Revoke one: it asks first.
No tokens yet.
Get it
It needs theme.qwe, as every component does, and confirm_button.qwe; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/confirm_button.qwe$ curl -O https://quillolang.org/components/ui/token_list.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/token_list.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/token_list.qwe
let token_list_demo_tokens = [ { id: 7, name: "CI deploy", prefix: "qt_live_8f3a", created: "2026-09-14", last_used: "2026-10-27T09:12:00Z", scopes: ["read", "deploy"] }, { id: 8, name: "Laptop", prefix: "qt_live_c01d", created: "2026-10-01", last_used: null, expires: "2027-12-31", scopes: ["read", "write"] }, { id: 9, name: "Old script", prefix: "qt_live_77aa", created: "2025-02-03", last_used: "2025-08-19", expires: "2026-02-03" },]component TokenListDemoLive() { state revoked = [] <div.demo-stack> <TokenList tokens={token_list_demo_tokens} on_revoke={(t) => { revoked = revoked | push(t.name) }}/> <p.demo-note>{len(revoked) == 0 ? "Revoke one: it asks first." : `Revoked: ${join(revoked, ", ")}.`}</p> </div>}view TokenListDemo() = <div.demo-stack> <TokenListDemoLive/> <TokenList revoke_action="#token_list" tokens={take(token_list_demo_tokens, 1)}/> <TokenList tokens={[]}/> </div>Source
ui/token_list.qwe
// TokenList: the API tokens someone has made: each with its name, the start of// it, when it was made and last used, what it may do, and a Revoke button that// asks first. It uses ConfirmButton (ui/confirm_button.qwe) and the Button styles.//// <TokenList revoke_action="/settings/tokens/revoke" tokens={[// { id: 7, name: "CI deploy", prefix: "qt_live_8f3a", created: "2026-09-14", last_used: "2026-10-27T09:12:00Z", scopes: ["read", "deploy"] },// { id: 8, name: "Laptop", prefix: "qt_live_c01d", created: "2026-10-01", expires: "2026-12-31" },// ]}/>// <TokenList tokens={tokens} on_revoke={(t) => { revoke(t.id) }}/>//// tokens: a list of { id, name, prefix?, created?, last_used?, scopes?, expires? }.// created, last_used and expires are ISO dates or date-times, shown as "Oct 14,// 2026" (last_used null is "Never"; an expiry gone by says "Expired"). scopes:// words shown as small tags.// Revoking: revoke_action makes the list a <form method="post"> to that address,// and each Revoke sends `revoke` = the token's id, after a confirmation. Inside// one of your components, on_revoke(token) is called instead, and the row goes.// empty: what it says with no tokens ("No tokens yet."). label: the table's// name ("API tokens").import ui/confirm_button.qwe use ConfirmButtonimport ui/theme.qwe use ui_clslet token_list_css = css`.ui-tokens { display: grid; gap: 10px; margin: 0; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-tokens-scroll { overflow-x: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); }.ui-tokens table { width: 100%; border-collapse: collapse; }.ui-tokens th, .ui-tokens td { padding: 10px 12px; border-bottom: 1px solid var(--ui-border); text-align: left; vertical-align: middle; white-space: nowrap; }.ui-tokens tbody tr:last-child td { border-bottom: 0; }.ui-tokens th { background: var(--ui-subtle); color: var(--ui-muted); font-size: 12px; font-weight: 600; letter-spacing: .03em; text-transform: uppercase; }.ui-tokens-name { font-weight: 600; }.ui-tokens-prefix { display: block; margin-top: 2px; color: var(--ui-muted); font: 12px/1.3 var(--ui-mono); }.ui-tokens-date { color: var(--ui-muted); font-variant-numeric: tabular-nums; }.ui-tokens-never { color: var(--ui-muted); font-style: italic; }.ui-tokens-expired { color: var(--ui-danger); font-weight: 600; }.ui-tokens-scopes { display: flex; flex-wrap: wrap; gap: 4px; }.ui-tokens-scope { padding: 1px 7px; border-radius: 999px; background: var(--ui-accent-soft); color: var(--ui-accent); font-size: 12px; font-weight: 500; }.ui-tokens td:last-child { text-align: right; }.ui-tokens-empty { padding: 28px 16px; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius-sm); color: var(--ui-muted); text-align: center; }@media (max-width: 640px) { .ui-tokens .ui-tokens-col-used, .ui-tokens .ui-tokens-col-scopes { display: none; } }`let token_list_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]// An ISO date or date-time as "Oct 14, 2026"; other text as it is.fn token_list_date(v) -> str { let p = split(slice(str(v), 0, 10), "-") let month = len(p) == 3 && len(p[0]) == 4 ? token_list_months[(int(p[1]) ?? 1) - 1] : null month != null ? `${month} ${int(p[2]) ?? p[2]}, ${p[0]}` : str(v)}component TokenList(tokens: list = [], revoke_action: str? = null, on_revoke = null, empty: str = "No tokens yet.", label: str = "API tokens", class: str? = null) { style global token_list_css state gone = [] let today = date.today() let rows = tokens | filter((t) => !contains(gone, t.id)) let body = len(rows) == 0 ? <p.ui-tokens-empty>{empty}</p> : <div.ui-tokens-scroll><table aria-label={label}> <thead><tr> <th scope="col">Token</th> <th scope="col">Created</th> <th.ui-tokens-col-used scope="col">Last used</th> <th.ui-tokens-col-scopes scope="col">Scopes</th> <th scope="col"><span.ui-sr>Actions</span></th> </tr></thead> <tbody> {for t in rows => <tr key={t.id}> <td> <span.ui-tokens-name>{t.name}</span> {t.prefix != null && <span.ui-tokens-prefix>{t.prefix}…</span>} </td> <td.ui-tokens-date> {t.created != null ? token_list_date(t.created) : "—"} {t.expires != null && (str(t.expires) < today ? <span.ui-tokens-expired> · Expired</span> : <span> · Expires {token_list_date(t.expires)}</span>)} </td> <td.ui-tokens-col-used.ui-tokens-date>{t.last_used != null ? token_list_date(t.last_used) : <span.ui-tokens-never>Never</span>}</td> <td.ui-tokens-col-scopes>{t.scopes != null && len(t.scopes) > 0 && <span.ui-tokens-scopes> {for s in t.scopes => <span.ui-tokens-scope>{s}</span>} </span>}</td> <td> <ConfirmButton size="sm" variant="outline" name="revoke" value={str(t.id)} title="Revoke this token?" ok="Revoke" message={`"${t.name}" stops working at once. Anything using it will need a new token.`} on_confirm={on_revoke != null ? () => { gone = gone | push(t.id) on_revoke(t) } : null}>Revoke</ConfirmButton> </td> </tr>} </tbody> </table></div> revoke_action != null && on_revoke == null ? <form class={ui_cls(["ui-tokens", class])} method="post" action={revoke_action}>{body}</form> : <div class={ui_cls(["ui-tokens", class])}>{body}</div>}Related
PermissionsMatrixRoles across, permissions down, a checkbox at each: a form sends the pairs ticked, a column box ticks a whole role.6 views · 6 downloadsApiKeyFieldA secret shown masked in a read-only field, with Show and Copy buttons and a hidden input for forms.5 views · 6 downloadsDangerZoneA red-bordered block of destructive actions (DangerAction), each a form, link or callback that asks before it acts.6 views · 8 downloadsSessionListSigned-in devices with browser, system, place and time, this one marked, and Sign out for each or all the others.6 views · 5 downloadsAuditLogWho did what to what and when, newest first, filtered by person and action, searched, and paged.5 views · 7 downloadsServerStatusServices with up, degraded, down or maintenance, their uptime and the last days as bars, summed up in one line.6 views · 6 downloads
Thread
Thread 0 posts
On TokenList: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.