Studio UI
CrateStack Studio ships a browser-based UI built with Leptos and served by Trunk. It’s a thin CSR app that consumes the read API — every action you can do in the browser maps one-to-one to a JSON endpoint.Phase 1b ships the UI as a sibling crate at
crates/cratestack-studio/ui/ in the framework repo. The two
development paths below run it through a Trunk dev server alongside
cratestack studio run.Phase 2 adds two ways to take the UI with you when you’re done
iterating: the embed-ui cargo feature
bundles the SPA into the Studio binary so you can hand cratestack to
a colleague, and cratestack studio eject
writes the sources to a writable directory you can fork.Running the UI locally
You’ll need two terminals — one for the Studio backend, one for the Trunk dev server.http://127.0.0.1:8080. Trunk’s [[proxy]] block in
Trunk.toml forwards /api/* to the backend, so the browser sees a
single origin.
Prerequisites
wasm-bindgen and any other tooling on first
build. Tailwind is pulled from a CDN in index.html, so there’s no
Node toolchain in the picture.
What the UI shows you
The UI is one page, four panes:| Pane | What it does |
|---|---|
| Header | Workspace name + target switcher. Each option shows the mode (ro/rw) and capability (db/api). |
| Left sidebar | Models in the selected target’s schema, clickable. |
| Main pane | Records table for the selected model, with cursor-based pagination (Previous / Next). |
| Right drawer | Selected row’s fields, a relation-follow input, and a “Copy Rust query” button. |
Pagination
The Previous / Next buttons stack cursors locally so navigation is stateless on the server side.Next is disabled when the API’s
next_cursor is null (you’ve hit the end). Previous is disabled
on the first page.
Following relations
Type a relation field name (author, posts, etc.) into the input
in the drawer and click Follow. The result panel below shows:
- A single related row (for
Required-arity fields likePost.author). - A page of related rows (for
List-arity fields likeCustomer.posts).
<field> → <target> (<arity>) so you
can pick the right traversal without leaving the drawer.
Copy Rust query
The drawer’s Copy Rust query button calls/api/targets/:key/models/:m/snippet?pk=… and writes the returned
find_unique snippet to the system clipboard using the browser’s
Clipboard API. Same shape as the rest of the snippet endpoint:
String/Cuid/Uuid/DecimalIDs render as"value".to_owned(),IntIDs as42_i64.
CORS in dev
The UI runs on127.0.0.1:8080; the backend on 127.0.0.1:7878. To
let the browser cross those origins, Studio enables a permissive
CORS layer by default. Disable it in studio.toml when binding to a
wider interface:
[[proxy]] block forwards /api/* to the
backend, which also avoids the cross-origin hop when you keep the
proxy in place.
Writing data (RW targets)
On targets withmode = "rw" the UI exposes three additional flows.
+ New button
Above the records table on RW targets: a + New button opens an
inline form with one input per writable field. Submitting calls
POST /api/targets/:key/models/:m/records. Per-field validation
errors surface inline (see the
validators reference for the full list).
Edit in the drawer
Selecting a row and clicking Edit turns the drawer’s field list into editable inputs. Save PATCHes the row; the API’s response replaces the drawer’s view. Validation errors from the server appear inline next to each field that failed.Delete in the drawer
A Delete button next to Edit confirms viawindow.confirm(), fires DELETE …/records/:pk, and clears the
drawer on success.
Mode badge
Each model header shows a small badge reflecting the target’s mode:RO in slate, RW in green. Studio also hides the write buttons on
RO targets — the badge is there to communicate intent before users
click anything.
Typed editors (Phase 1d)
The create form and the drawer’s edit mode dispatch on each field’s declared scalar instead of painting a single text box everywhere:| Scalar | Control |
|---|---|
| enum | <select> populated from the schema’s variants |
Json | <textarea> (parsed on submit) |
DateTime | <input type="datetime-local"> (auto-normalized to UTC Z) |
Float / Decimal | <input type="number" step="any"> |
Int | <input type="number" step="1"> |
Boolean | <select> (true/false; blank → null on optional) |
String / Cuid / Uuid | plain text |
GET /api/targets/:key/models) carries
is_enum and enum_variants per field so the UI can render the
dropdown without a second fetch.
Power tools (Phase 4)
Three additions to the records pane and two to the header — together they round out the read-API surface for poking around the system:- Tools row above the records table:
- An op selector + Show SQL button that fetches the rendered
SQL Studio would run for
list/get/create/update/deleteand displays it with bound parameters. - Export JSON / Export CSV links pointing at the export endpoint so the browser downloads the file.
- An op selector + Show SQL button that fetches the rendered
SQL Studio would run for
- Drift dots on each model in the sidebar.
⚠ driftin amber when columns don’t match;✕ tablein red when the table is missing entirely. Driven byGET /api/targets/:key/drift. - Schema search in the header. Type a term and matching models / fields / enums show up in a dropdown right below the input.
- Audit button in the header opens a 28rem overlay listing the most recent CREATE / UPDATE / DELETE operations from the in-memory ring buffer (cap 500, FIFO).
What’s not in the UI yet
- Persistent audit log. Today the buffer lives in process memory only — fine for local admin, not for an org-wide log.
- EXPLAIN / query plan beside the SQL preview. The preview ships
the rendered SQL; running
EXPLAINagainst the live database is out of scope. - Inline diff between schema and live DB. Drift reports missing/extra columns; a column-by-column diff would be a Phase 5 ergonomic upgrade.
cratestack studio eject if you need to customize the visual
style or wire in proprietary actions ahead of the upstream’s schedule.