Skip to content

Compute form: progressive validation, cluster cards, presets and a live estimate - #6

Open
timurbazhirov wants to merge 4 commits into
mainfrom
feature/SOF-8023-progressive-compute-validation
Open

Compute form: progressive validation, cluster cards, presets and a live estimate#6
timurbazhirov wants to merge 4 commits into
mainfrom
feature/SOF-8023-progressive-compute-validation

Conversation

@timurbazhirov

@timurbazhirov timurbazhirov commented Aug 16, 2026

Copy link
Copy Markdown
Member

Summary

Two phases of the Job Designer UX update (SOF-8023, plan), both in the compute form: 1.1 stops the form complaining before the reader has done anything, and 2.3 gives it a cluster choice worth making and says what the answer costs.

2.3 needs a @mat3ra/cove release carrying SelectableCard, MetricTile, SegmentedMeter and NumericStepperInput (mat3ra/cove#97). Merge order is cove → this.

1.1 Progressive validation

A new job opens the Compute tab on four red "The field is required" errors — complaints about things the reader has not been given a chance to do yet. The form validates live, so every required field reports itself on first paint. Errors are now shown only for fields the reader has actually edited.

Touched fields are tracked from the field id RJSF already passes as onChange's second argument — no diffing, no dependency on widget internals. customValidate then skips addError for fields not in that set. The decision logic is a pure module (src/utils/touchedFields.ts) with unit tests.

A new showAllErrors prop (default false) reveals everything at once for the cases where the whole form must answer for itself — submit, and the job designer's preflight.

One subtlety worth flagging: the RJSForm is keyed on showAllErrors. RJSF only re-validates when its schema or form data change, so flipping the prop alone left the previous validation result on screen — the toggle silently did nothing. The remount forces a fresh pass. It happens on submit, not while typing, so the cost isn't felt.

Case Before After
Empty compute, first paint Time Limit, Queue, Nodes, Cores all required no errors
After clearing the Nodes field that field's error that field's error (unchanged)
Empty compute, showAllErrors identical to the "before" column

The last row is the point of the table: showAllErrors reproduces today's behavior exactly, so nothing is lost — only deferred. I confirmed the "before" column by stashing the change and re-running the same script against the original code.

2.3 Cluster cards, presets and a live estimate

The step asked for a cluster, a queue, nodes, cores and a walltime as five schema fields — a dropdown listing cluster names and nothing to choose between them on, four required fields empty on arrival, and no statement anywhere of what the answers add up to. A reader could configure 4 × 32 × 12 h without being told that is 1536 core-hours, three times their quota, until the run failed.

Behind useComputeCards (default off), four fields move out of the schema form into their own surface above it:

  • ClusterCards — each cluster as a card carrying price, max walltime and typical queue wait, so the choice is a comparison rather than a guess. Selecting one also picks that cluster's first real queue, since a queue chosen on the previous cluster is meaningless on this one.
  • ComputeResources — presets (Debug / Standard / Production) clamped to the cluster's published limits, so the button that exists to avoid an invalid configuration cannot produce one; bounded steppers for nodes and cores; walltime as the HH:MM:SS the scheduler takes. Typed values past a limit are allowed and flagged inline, on the same touched-field rule as 1.1.
  • ComputeEstimatePanel — core-hours, cost and queue wait as metric tiles over a quota meter. Each degrades on its own: core-hours need only the job, cost needs a published price, the meter needs an injected allowance. A tile with nothing behind it shows an em dash, never a zero.

How it stays out of trouble

  • The four fields are hidden, not removed, so the ESSE schema still validates them and nothing downstream learns a second shape.
  • Writes go through the same handleFormUpdate path as a keystroke in the schema form — same touched-field bookkeeping, same validate-then-onUpdate gate.
  • Espresso's advanced options are untouched. The group left holding only two documentation links is retitled Documentation, because calling it "Cluster" would send readers looking for a picker that moved.

utils/computeEstimate is public API on purpose

Three surfaces need the same answer to "what does this job cost" — the panel here, job-designer's context-strip chip, and its submit preflight's budget check — and three implementations would be worse than none. It lives here, beside the form that holds the live values, and is exported. job-designer carries an identical copy with a TODO to delete it once this ships.

Verification

npm test green (44 tests), tsc clean, lint clean. Measured in the running demo rather than assumed:

  • presets fill the fields and the estimate follows synchronously;
  • Production on a 2-node cluster applies as 2 × 16 × 6 h, not the nominal 4 × 32 × 24 h;
  • the Nodes stepper disables at the cluster's limit, while a typed 24 h walltime reports "Over the 6 h queue limit" and the meter reads "over by 268 core·h";
  • toggling the surface off brings all four schema fields back.

Demo changes

  • "Start empty" reproduces the state a new job actually starts in — the case 1.1 exists for.
  • "Show all errors" and "Cards + estimate" switches flip between the modes.
  • Mock cluster metadata and a quota, standing in for what the webapp would inject.
  • LOAD JSON now remounts the form. ComputeForm derives its form data in its constructor, so loading JSON previously updated the state but left the form untouched — a pre-existing demo bug.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DK8KomMescJvMQNSEfeRR8

A new job opened the Compute tab on four red 'The field is required'
errors - complaints about things the reader had not been given a chance
to do yet. The form validates live, so every required field reported
itself on first paint.

Errors are now shown only for fields the reader has actually edited,
tracked from the field id RJSF passes to onChange. A new showAllErrors
prop (default off) reveals everything at once for the cases where the
whole form must answer for itself - submit, and the preflight check
coming in phase 2.

Note the RJSForm remount keyed on showAllErrors: RJSF only re-validates
when its schema or form data change, so without it the toggle left the
previous validation result on screen.

Demo: 'Start empty' reproduces the state a new job starts in, and the
'Show all errors' switch flips between the two modes. LOAD JSON now
remounts the form too - ComputeForm derives its data in the constructor,
so loading JSON previously did nothing to the form.

SOF-8023 phase 1.1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DK8KomMescJvMQNSEfeRR8
claude added 3 commits August 16, 2026 20:28
dist/ is tracked in this repo and the published package ships it ("files":
["/dist", "/src"]). There is no prepublishOnly build here, so a stale dist
is what consumers would get - and dist/utils/touchedFields.js was missing
entirely, which ComputeForm.js imports.

The husky pre-commit hook does this automatically (npm run transpile &&
git add dist/), but only once hooks are installed; this repo has no
'prepare: husky install' script, so a fresh clone commits without it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DK8KomMescJvMQNSEfeRR8
The hook that regenerates dist has never run: husky was not a dependency
and no prepare script installed it, so a fresh clone had .husky/pre-commit
sitting inert. That is why four packages landed src changes with a stale
or entirely missing dist - including new modules whose emitted code
imported files that were never built.

Adds husky + 'prepare: husky install' (matching cove, the one repo where
this works), and drops 'npx lint-staged' from the hook: lint-staged is
neither a dependency nor configured here, so arming the hook with that
line would have failed and blocked every commit. Linting is enforced in
CI regardless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DK8KomMescJvMQNSEfeRR8
The compute step asked for a cluster, a queue, nodes, cores and a
walltime as five schema fields — a dropdown listing cluster names and
nothing to choose between them on, four required fields empty on
arrival, and no statement anywhere of what the answers add up to. A
reader could configure 4 × 32 × 12 h without being told that is 1536
core-hours, three times the quota, until the run failed.

Opt-in via `useComputeCards`, which moves four fields out of the schema
form and into their own surface above it:

- ClusterCards — each cluster as a card carrying price, max walltime and
  typical queue wait, so the choice is a comparison rather than a guess.
- ComputeResources — presets (Debug / Standard / Production) clamped to
  the cluster's published limits, so the button meant to avoid an
  invalid configuration cannot produce one; bounded steppers for nodes
  and cores; walltime as the HH:MM:SS the scheduler takes. Typed values
  past a limit are allowed and flagged inline, on the same touched-field
  rule as the rest of the form.
- ComputeEstimatePanel — core-hours, cost and queue wait as metric tiles
  over a quota meter. Each degrades on its own: core-hours need only the
  job, cost needs a published price, the meter needs an injected
  allowance, and a tile with nothing behind it shows an em dash, never a
  zero.

utils/computeEstimate.ts is the canonical home for the arithmetic —
job-designer currently carries a copy for its context strip and submit
preflight, and should import this once ive publishes.

Writes go through the same handleFormUpdate path as a keystroke in the
schema form, so touched-field bookkeeping and the validate-then-onUpdate
gate are unchanged. The hidden fields stay in the schema and keep
validating. Espresso's advanced options are untouched; the cluster group
left holding only two links is retitled Documentation rather than
pointing at a picker that has moved.

Verified in the demo: presets fill the fields and the estimate follows
synchronously; Production on a 2-node cluster applies as 2 × 16 × 6 h;
the nodes stepper disables at the cluster's limit while a typed 24 h
walltime reports "Over the 6 h queue limit" and the meter shows "over by
268 core·h"; toggling the surface off brings all four schema fields
back. 44 unit tests pass.

Note: requires a @mat3ra/cove release carrying SelectableCard,
MetricTile, SegmentedMeter and NumericStepperInput.

Refs SOF-8023.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DK8KomMescJvMQNSEfeRR8
@timurbazhirov timurbazhirov changed the title Progressive validation in the compute form Compute form: progressive validation, cluster cards, presets and a live estimate Aug 17, 2026

Copy link
Copy Markdown
Member Author

run-js-tests is red, and will stay red until mat3ra/cove#97 merges and publishes. Flagging it so nobody spends time looking for a defect that isn't there.

The whole failure is tsc resolving three imports against the published cove (2026.7.18-4), which predates the primitives phase 2.3 uses:

src/components/ComputeResources.tsx(1,33): error TS2307: Cannot find module
  '@mat3ra/cove/dist/mui/components/numeric-stepper/NumericStepperInput'
src/components/ComputeResources.tsx(141,32): error TS7006: Parameter 'nodes' implicitly has an 'any' type
src/components/ComputeResources.tsx(160,32): error TS7006: Parameter 'ppn' implicitly has an 'any' type

The two TS7006s are downstream of the TS2307 — without the component's types the callback parameters have nothing to infer from. There is no fourth error.

I'm deliberately not fixing this here. The available workarounds are all worse than a red check that tells the truth: an ambient module declaration would shadow cove's real types once it does publish, and vendoring the primitives into ive would duplicate what cove#97 exists to own. Against a local cove build the typecheck is clean, all 44 tests pass, and the behaviour in the PR description was measured in the running demo.

Sequence: merge and publish cove#97, bump the pin here, re-run. If you would rather have a mergeable PR today, say so and I'll split the branch — 1.1 (progressive validation) has no cove dependency and is green on its own.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants