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.
# Terminal 1 — backend
cratestack studio run        # binds 127.0.0.1:7878

# Terminal 2 — UI
cd crates/cratestack-studio/ui
trunk serve                  # serves the SPA on 127.0.0.1:8080
Open 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

# Trunk and the wasm target
cargo install trunk
rustup target add wasm32-unknown-unknown
Trunk itself fetches 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:
PaneWhat it does
HeaderWorkspace name + target switcher. Each option shows the mode (ro/rw) and capability (db/api).
Left sidebarModels in the selected target’s schema, clickable.
Main paneRecords table for the selected model, with cursor-based pagination (Previous / Next).
Right drawerSelected 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 like Post.author).
  • A page of related rows (for List-arity fields like Customer.posts).
If the field doesn’t exist or isn’t a relation, the panel surfaces the API’s error message. The relation picker is a typed dropdown built from the model’s relation fields. Labels show <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/Decimal IDs render as "value".to_owned(),
  • Int IDs as 42_i64.
The snippet appears in a code block below the button so you can also hand-copy.

CORS in dev

The UI runs on 127.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:
[workspace]
name = "studio"
cors_dev = false             # default is `true`
The Trunk dev server’s [[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 with mode = "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 via window.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:
ScalarControl
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 / Uuidplain text
The model-list endpoint (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 / delete and displays it with bound parameters.
    • Export JSON / Export CSV links pointing at the export endpoint so the browser downloads the file.
  • Drift dots on each model in the sidebar. ⚠ drift in amber when columns don’t match; ✕ table in red when the table is missing entirely. Driven by GET /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).
See the tools reference for the underlying endpoints.

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 EXPLAIN against 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.
The UI itself is plain Leptos CSR — fork it via cratestack studio eject if you need to customize the visual style or wire in proprietary actions ahead of the upstream’s schedule.