Skip to main content

Petstore: Porting to the Console

The petstore console is a worked example. This page is the short version of "how do I bring these patterns into an admin UI I already have": which of the four patterns to take, what each one needs from your app, and how to choose which to adopt. It does not repeat the framework pages. Each section points at the page that owns the detail and at the petstore page that shows it running.

Start with the tour overview and the Console Tour if you have not seen the console yet. If you are building a page from scratch rather than porting, read Building a Console Page instead.

Before you port anything​

Move the chrome first. The four patterns below all assume the console shell is in place: a <@console> base template, a <@navigation> tree and the shared assets from ConsoleChromeMixin. Without it there is no header to hang a badge on and no card runtime to mount a job log. The chrome is described in Admin-Console Chrome, and the petstore's version of it is on Console Tour.

Then take the patterns one at a time. They are independent, so you can adopt any subset. A good order, from least to most design work:

  1. the async-job plumbing (mostly a swap of what you already have);
  2. the audit page (a read-only view);
  3. the header badge (one endpoint and one directive);
  4. staged edits (the one that changes how your app behaves).

Header badge​

A badge is a small counter in the page header that polls a JSON endpoint and links to the page that explains the number. The petstore uses it for the count of pending changes.

What you need:

  • a count endpoint that answers GET with a JSON object carrying a numeric total;
  • one <@badge> directive in the base template, placed after </@navigation>, not inside the navigation block.

Leave the badge unscoped unless you want it hidden on other sections. A nav scope would hide it on every page outside that scope. The petstore's badge sets refreshesAllowMissing, so applying or discarding an item refreshes a card on the pending page without failing on pages where that card is not mounted.

The attributes and the polling, backoff and abort behaviour are on <@badge> and BadgeDef. The petstore example, including the exact directive, is on Pending Changes.

Staged edits​

Staged edits turn "save writes immediately" into "save queues a change that someone applies or discards". The petstore queues edits to pets, with the photo field excluded. See the box on Pet Detail.

What you need:

  • a store that holds the pending change with its own lifecycle: queued, applied, failed, discarded;
  • three actions on a page: apply one, retry one, discard a set (bulk discard should be idempotent);
  • a server-mode table over the queue, so the page scales past what the browser holds.

Porting this is the largest change because it alters write semantics. Decide first what "apply" means in your app, for example posting to an external system versus committing locally, and what a failed apply leaves behind. The action contract is on Row Actions and Bulk Mutation; the lifecycle and the discard outcome are on Pending Changes.

Async jobs with the job-log card​

Long-running work should return a job reference at once and let the browser follow it. The petstore's restock action does that and shows the stock job-log card.

What you need:

  • your resource implements the async-jobs mixin and exposes a job registry bean;
  • a start action that takes an idempotency key, and a limit that refuses new jobs with a 429 when too many are running;
  • a job-log card on the page, with cancel.

The one habit to change is key minting. If your app generates a random UUID per request as the idempotency key, a double-submitted form starts two jobs. Mint a self-targeted key when the form renders and carry it back on submit, so a replay resolves to the job that already exists. That is IdempotencyKey.mintSelfTargeted, and it needs no server-side key store. If your code carries a workaround for resolving a scheme the framework now handles, re-check it against the Juneau version you build with before copying it.

The mixin, the registry and the limit are on Async Jobs; the card is on Job Log Card. The petstore example is Jobs. If the job streams output rather than a log, see also Terminal Region and Terminal View Region.

Porting a bespoke row-job widget​

Many consoles grow a hand-written widget that starts a job for one table row: a form in the row's detail panel that posts a request, streams a log, and writes the answer back into the panel. These usually become declarations, with no custom JavaScript.

  • A form plus a log in the detail panel becomes a job-log region on the table's detail, with populate: 'job-log' and the form in config.submit.
  • A hidden id or value copied from the row becomes a field with type: 'hidden' and source: 'row.<field>'. A visible field prefilled from the row takes source too.
  • Code that writes the result into a textarea or a cell becomes output, for example { target: { field: 'answer' }, from: 'row.answer' }, or target.selector for an element in the panel's detail grid.
  • A button with its own fetch becomes helpers.jobButton inside a small populator, or a call to JuneauViews.jobs.run.
  • Code that stops the table's polling while the panel is open can go: the poll already waits while an expanded panel runs a job, and a collapsed row's job keeps running and re-attaches on expand.

The server side changes too. A widget that minted keys with a placeholder target, so any key worked on any row, moves to a key endpoint, POST .../{id}/key, returning JobKeyLedger.response(ledger.issue(action, rowId, actor)), and a submit endpoint that checks the path id against RowJobSubmit.parse(body).targetId() and calls verify. Apply the action's own authorization on the key endpoint, take the row only from the verified targetId, and keep the row endpoint separate from any self-targeted dialog endpoint. See Row Jobs and Job Log Card; the petstore example is the species table on Jobs.

Audit page with a server-mode /query​

An audit page is the cheapest way to see the console end to end. It is a leaf page with one table, filled by a POST /query endpoint that receives the paging, sort and filter state and returns one page of rows.

What you need:

  • a row type for what you record (who, what, when);
  • a query method on the thing that records it, taking the table state;
  • a page with a datatables card in server mode, and a /query endpoint that answers it.

If you already write an event log or a history table, you probably have the data and need only the endpoint and the card. The petstore keeps this deliberately small, to the point that the builder twin is about ten lines. See Audit for the page and Data Tables for server mode.

Choosing what to adopt​

Nobody needs all of these patterns. Take the ones that match something your app already does by hand, and leave the rest.

If your app already hasAdopt
A count or status shown in a nav label, such as "Review (3)"The header badge, so the count leaves the label and updates on its own.
In-memory drafts or pending repliesThe staged-edits shape: a pending-changes page with apply, retry and discard.
Per-class random keys for POSTs, or a long-running actionmintSelfTargeted for replay-safe keys, and the job-log card for the run.
A write journal or an event historyAn audit page over a paged /query.
A start-run POST that returns an error when the same request is sent twiceA self-targeted key, so a replay returns the existing run.
A hand-written per-row job form or buttonA job-log row-detail region or helpers.jobButton, with keys from JobKeyLedger.

Skipping a pattern is normal: if the run engine already has its own staged preview, or a badge would add little, skip it.

Checklist​

  • The console chrome renders and every page sits in the navigation tree.
  • Any badge comes after </@navigation> and its endpoint returns a numeric total.
  • Async starts take a self-targeted key, and a limit refuses extra jobs with a 429.
  • Chrome values reach the template through facts only; see Console Tour.
  • Misconfiguration fails loudly. Pin your own cases the way the petstore does on FailLoud Catalogue.
  • A template page has a builder twin, or a test that fails if the two drift; see Template and Builder Twins.

See also​