Dashboard

TokenList

API tokens with name, prefix, created, last used, scopes and expiry, and a Revoke that asks first.

#dashboard#security#settings#table#admin#interactive

Demo

Preview
TokenCreatedLast usedScopesActions
CI deployqt_live_8f3a…Sep 14, 2026Oct 27, 2026readdeploy
Laptopqt_live_c01d…Oct 1, 2026 · Expires Dec 31, 2027Neverreadwrite
Old scriptqt_live_77aa…Feb 3, 2025 · ExpiredAug 19, 2025

Revoke one: it asks first.

TokenCreatedLast usedScopesActions
CI deployqt_live_8f3a…Sep 14, 2026Oct 27, 2026readdeploy

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.qwe
main.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>}
Thread
68Useful? Like it, or add to its thread below.

Thread 0 posts

On TokenList: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.