Skip to content

fix(forms): stop VM disk selection being dropped on deploy#25

Merged
Aleksei Sviridkin (lexfrei) merged 4 commits into
mainfrom
fix/vminstance-disk-select-race
Jun 1, 2026
Merged

fix(forms): stop VM disk selection being dropped on deploy#25
Aleksei Sviridkin (lexfrei) merged 4 commits into
mainfrom
fix/vminstance-disk-select-race

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

What

On the "Deploy new instance" form, a disk shown as selected in the disks dropdown was intermittently dropped on create — the resulting VMInstance ended up with spec.disks: []. The loss was entirely client-side, before submit.

Two interacting defects in VMDiskWidget caused it:

  1. A required disk <select> rendered no empty <option>, so a value-less control visually displayed the first disk while the form value stayed empty — the UI showed a disk "selected" that the form data did not hold.
  2. The value was committed from a post-load useEffect; on a cold cache a fast Deploy click raced that effect and submitted a disk row with no name.

Changes

  • VMDiskWidget: drop the effect-based auto-select so the value is only ever set by an explicit pick (no timing-dependent drop); always render an explicit placeholder, disabled when the field is required; keep the committed value visible as a stable option across async re-renders of the disk list.
  • Apply the same placeholder + stable-option treatment to StorageClassWidget and BackupClassWidget, the sibling list-fed selects with the same latent issue. StorageClassWidget keeps its cluster-default auto-select — a real default, unlike an arbitrary first item.
  • The Deploy/Create/Save buttons live outside RJSF and bypassed its validation entirely. SchemaForm now exposes a validate() handle delegating to RJSF's validateForm, and all six form pages gate their submit on it so an invalid spec can no longer reach the API. YAML mode is left for the API to reject.

Testing

  • New tests cover all three widgets, the SchemaForm.validate() handle, and the submit gate on each of the six consumer pages (an invalid form must not POST/PUT; a valid one must).
  • pnpm typecheck and pnpm test (235 tests) pass.

Summary by CodeRabbit

  • New Features

    • Form submission now validates inputs before creating or updating resources to prevent invalid submissions.
    • Improved placeholder handling in dropdown widgets during loading states.
  • Bug Fixes

    • Dropdown widgets now preserve user selections even when data loads asynchronously.
    • Better handling of required vs. optional field placeholders and default selections.
  • Tests

    • Added comprehensive test coverage for form validation gates and widget behavior.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@lexfrei, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 53 minutes and 40 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 488b9033-3f53-40fb-8564-1a5aceab1ec6

📥 Commits

Reviewing files that changed from the base of the PR and between 7284b55 and 3803066.

📒 Files selected for processing (20)
  • apps/console/src/components/BackupClassWidget.test.tsx
  • apps/console/src/components/BackupClassWidget.tsx
  • apps/console/src/components/SchemaForm.test.tsx
  • apps/console/src/components/SchemaForm.tsx
  • apps/console/src/components/StorageClassWidget.test.tsx
  • apps/console/src/components/StorageClassWidget.tsx
  • apps/console/src/components/VMDiskWidget.test.tsx
  • apps/console/src/components/VMDiskWidget.tsx
  • apps/console/src/routes/ApplicationOrderPage.test.tsx
  • apps/console/src/routes/ApplicationOrderPage.tsx
  • apps/console/src/routes/BackupCreatePage.test.tsx
  • apps/console/src/routes/BackupCreatePage.tsx
  • apps/console/src/routes/BackupJobCreatePage.test.tsx
  • apps/console/src/routes/BackupJobCreatePage.tsx
  • apps/console/src/routes/BackupPlanCreatePage.test.tsx
  • apps/console/src/routes/BackupPlanCreatePage.tsx
  • apps/console/src/routes/BackupResourceEditPage.test.tsx
  • apps/console/src/routes/BackupResourceEditPage.tsx
  • apps/console/src/routes/BackupRestoreJobCreatePage.test.tsx
  • apps/console/src/routes/BackupRestoreJobCreatePage.tsx
📝 Walkthrough

Walkthrough

This PR introduces an imperative validation API to SchemaForm via forwarded refs and normalizes placeholder/selection logic across three selector widgets. Six page components integrate the new validation gates to prevent invalid form submissions, while three widgets refactor their placeholder rendering and auto-selection behaviors.

Changes

Core SchemaForm Validation Infrastructure

Layer / File(s) Summary
SchemaForm validation ref API
apps/console/src/components/SchemaForm.tsx
SchemaForm imports forwardRef and useImperativeHandle, exports SchemaFormHandle interface with validate() method, wraps the component with forwardRef, creates formRef targeting the RJSF Form instance, and exposes validate() that calls formRef.current.validateForm().
SchemaForm validation ref tests
apps/console/src/components/SchemaForm.test.tsx
Test suite adds createRef and act imports, defines a validate(ref) helper, and adds two test cases verifying validation failure when required fields are missing and validation success when fields are present.

Widget Placeholder & Selection Normalization

Layer / File(s) Summary
BackupClassWidget placeholder logic
apps/console/src/components/BackupClassWidget.tsx, apps/console/src/components/BackupClassWidget.test.tsx
BackupClassWidget computes a single placeholder string based on isLoading, available items, and required state. The select always renders an explicit disabled placeholder option. Tests verify placeholder behavior for required widgets, preservation of committed values during pending loads, and clearing optional selections.
StorageClassWidget default class detection
apps/console/src/components/StorageClassWidget.tsx, apps/console/src/components/StorageClassWidget.test.tsx
StorageClassWidget introduces DEFAULT_CLASS_ANNOTATION constant to mark cluster defaults, derives currentValue, computes dynamic placeholder, stops disabling select during load, and preserves uncommitted values via extra option. Tests verify required placeholder behavior, value persistence during pending loads, and auto-selection of annotated defaults.
VMDiskWidget auto-selection removal
apps/console/src/components/VMDiskWidget.tsx, apps/console/src/components/VMDiskWidget.test.tsx
VMDiskWidget removes effect-driven auto-selection of the first disk, derives currentValue, computes dynamic placeholder, always renders explicit placeholder option, and preserves committed values during async loads. Tests verify no automatic onChange, placeholder behavior for required selections, value persistence during pending loads, and user-driven onChange calls.

Page Submission Validation Gates

Layer / File(s) Summary
ApplicationOrderPage validation gate
apps/console/src/routes/ApplicationOrderPage.tsx, apps/console/src/routes/ApplicationOrderPage.test.tsx
ApplicationOrderPage imports SchemaFormHandle, creates schemaFormRef, and validates form before submission in form mode. Tests verify validation blocks create mutation on failure and allows it on success, while YAML mode bypasses the gate.
BackupCreatePage validation gate
apps/console/src/routes/BackupCreatePage.tsx, apps/console/src/routes/BackupCreatePage.test.tsx
BackupCreatePage uses schemaFormRef to validate before handleSubmit creates resource. Tests verify create mutation is not called on validation failure and is called once on validation success.
BackupJobCreatePage validation gate
apps/console/src/routes/BackupJobCreatePage.tsx, apps/console/src/routes/BackupJobCreatePage.test.tsx
BackupJobCreatePage creates schemaFormRef and adds pre-submit validation check to abort creation when form spec is invalid. Tests drive UI flow and assert create mutation invocation based on validation outcome.
BackupPlanCreatePage validation gate
apps/console/src/routes/BackupPlanCreatePage.tsx, apps/console/src/routes/BackupPlanCreatePage.test.tsx
BackupPlanCreatePage imports useRef and SchemaFormHandle, maintains schemaFormRef, and adds explicit validation gate in handleSubmit. Tests assert create mutation is skipped on validation failure and called on validation success.
BackupResourceEditPage validation gate
apps/console/src/routes/BackupResourceEditPage.tsx, apps/console/src/routes/BackupResourceEditPage.test.tsx
BackupResourceEditPage imports SchemaFormHandle, maintains schemaFormRef, and explicitly calls validate() in handleSubmit. Tests verify update mutation is skipped on validation failure and called once on validation success.
BackupRestoreJobCreatePage validation gate
apps/console/src/routes/BackupRestoreJobCreatePage.tsx, apps/console/src/routes/BackupRestoreJobCreatePage.test.tsx
BackupRestoreJobCreatePage imports SchemaFormHandle, creates schemaFormRef, and adds pre-submit validation that aborts create mutation when validation fails. Tests verify mutation invocation is gated by validation outcome.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

area/console

Suggested reviewers

  • kitsunoff
  • myasnikovdaniil
  • androndo

Poem

🐰 With refs and handles held so tight,
Form validation sees the light—
Three widgets dance with placeholders new,
Six pages guard their paths true,
Schema checked before you submit,
Invalid specs? They don't commit! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "fix(forms): stop VM disk selection being dropped on deploy" directly addresses the primary bug fix in the PR: preventing disk selection loss during deployment. It is specific, concise, and clearly summarizes the main change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/vminstance-disk-select-race

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added size/XXL This PR changes 1000+ lines, ignoring generated files area/forms Issues or PRs related to RJSF schema forms and widgets (backup, external-ips, storage-class, etc.) kind/bug Categorizes issue or PR as related to a bug labels Jun 1, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces explicit validation gating across several form pages (such as ApplicationOrderPage, BackupCreatePage, BackupJobCreatePage, BackupPlanCreatePage, BackupResourceEditPage, and BackupRestoreJobCreatePage) by exposing a validate method on SchemaForm via forwardRef and useImperativeHandle. It also refactors several widgets (BackupClassWidget, StorageClassWidget, and VMDiskWidget) to always render explicit placeholders, prevent accidental auto-selection of the first item, and keep committed values visible during loading. Comprehensive unit tests have been added for these widgets and form pages. Regarding the feedback, a critical bug was identified in StorageClassWidget.tsx where resetting hasAutoDefaulted.current to false when clearing the selection triggers the auto-select effect again, making it impossible to clear the field. It is recommended to set hasAutoDefaulted.current = true on any user interaction to permanently disable automatic defaulting.

Comment on lines 56 to 59
onChange={(e) => {
if (!e.target.value) hasAutoDefaulted.current = false
onChange(e.target.value || undefined)
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a bug in the onChange handler where hasAutoDefaulted.current is reset to false when the user clears the selection (i.e., selects -- None --).

Because hasAutoDefaulted.current is reset to false and the value becomes undefined, the useEffect hook's auto-select condition is met again on the next render:
!hasAutoDefaulted.current && !value && defaultSC && !isLoading

This causes the widget to immediately re-select the default storage class, making it impossible for the user to ever clear the field or select -- None --.

Additionally, if the form is in edit mode with an existing storage class, hasAutoDefaulted.current starts as false. If the user clears the field, the same auto-select effect will trigger and force-select the default storage class.

To fix both issues, we should set hasAutoDefaulted.current = true on any user interaction (including clearing the field) to permanently disable any automatic defaulting.

Suggested change
onChange={(e) => {
if (!e.target.value) hasAutoDefaulted.current = false
onChange(e.target.value || undefined)
}}
onChange={(e) => {
hasAutoDefaulted.current = true
onChange(e.target.value || undefined)
}}

@lexfrei Aleksei Sviridkin (lexfrei) marked this pull request as ready for review June 1, 2026 13:42
A required disk select rendered no empty <option>, so a value-less
control visually showed the first disk while the form value stayed
empty; the value was then committed from a post-load effect, and a fast
submit on a cold cache raced that effect and produced spec.disks: [].

Render an explicit placeholder option even when required, keep the
committed value visible as a stable option across async re-renders, and
drop the effect-based auto-select so the value is only ever set by an
explicit pick. The drop is no longer timing-dependent.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
…option

The required storage-class select rendered no empty <option>, so a
value-less control visually showed the first class while the form value
stayed empty, and a committed value could be dropped on an async
re-render of the class list.

Render an explicit placeholder even when required and keep the committed
value visible as a stable option. The cluster-default auto-select is
kept — a default class is a real default, unlike an arbitrary first item.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
…required

The empty <option> was only rendered when the field was optional, so a
required backup-class select with no value would visually show the first
class while the form value stayed empty.

Render the placeholder regardless of required, matching the disk and
storage-class selects. The stable committed-value option is unchanged.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The application deploy page and the five backup create/edit pages render
SchemaForm with the submit button outside RJSF, so the Deploy/Create/Save
handlers sent the spec to the API without ever running RJSF validation. A
required field left empty — e.g. a disk row whose name was never picked —
was submitted and silently produced an invalid or empty result.

Expose a validate() handle on SchemaForm (delegating to RJSF's
validateForm) and call it from every one of those submit handlers; an
invalid form renders its errors inline and the submit is aborted.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@lexfrei Aleksei Sviridkin (lexfrei) force-pushed the fix/vminstance-disk-select-race branch from 7284b55 to 3803066 Compare June 1, 2026 13:48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/console/src/components/StorageClassWidget.tsx (1)

38-43: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't re-arm the auto-default after a manual clear.

Line 57 sets hasAutoDefaulted.current back to false, so if a default storage class exists, clearing the field causes the effect on Lines 38-43 to immediately write that default back on the next render. That makes optional storageClass impossible to keep empty and can silently submit a value the user explicitly removed.

Suggested fix
       onChange={(e) => {
-        if (!e.target.value) hasAutoDefaulted.current = false
         onChange(e.target.value || undefined)
       }}

Also applies to: 56-58

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/components/StorageClassWidget.tsx` around lines 38 - 43, The
bug is that hasAutoDefaulted.current is being reset (lines 56-58) so clearing
the field re-triggers the effect on next render and re-applies the default
storage class; stop re-arming the auto-default. Remove or guard the code that
sets hasAutoDefaulted.current = false (the clear handler/reset logic) so it is
not flipped when the user manually clears the input; instead only
initialize/clear that ref when the defaultSC changes or on mount. Update
references in the component where hasAutoDefaulted is modified (the useEffect
that auto-applies defaultSC, any clear/handler that sets it to false) so manual
clears do not reset the flag while preserving the original auto-apply behavior
when defaultSC first appears.
apps/console/src/routes/BackupCreatePage.tsx (1)

77-100: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run SchemaForm.validate() before the page-level spec alerts.

The new gate is currently downstream of the manual applicationRef / strategyRef / takenAt checks, so those invalid form states still exit via alert(...) without ever rendering RJSF inline errors. That means form-mode submission is not consistently gated by the new validation path this PR is introducing.

Suggested change
   const handleSubmit = async () => {
     if (!name.trim()) {
       alert("Name is required")
       return
     }

+    if (!schemaFormRef.current?.validate()) return
+
     if (!formData.applicationRef?.kind || !formData.applicationRef?.name) {
       alert("Application reference is required")
       return
     }

     if (!formData.strategyRef?.kind || !formData.strategyRef?.name) {
       alert("Strategy reference is required")
       return
     }

     if (!formData.takenAt) {
       alert("Taken at timestamp is required")
       return
     }
-
-    // The submit button lives outside RJSF and bypasses its validation, so
-    // trigger it explicitly; an invalid spec renders errors inline and aborts.
-    if (schemaFormRef.current && !schemaFormRef.current.validate()) return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/routes/BackupCreatePage.tsx` around lines 77 - 100, In
handleSubmit, run the RJSF validation via schemaFormRef.current.validate()
before performing the page-level alerts so RJSF inline errors are rendered for
invalid spec state; specifically, in the handleSubmit function call
schemaFormRef.current?.validate() (and bail if it returns false) at the top of
the function (or at least before the applicationRef/strategyRef/takenAt alert
checks) so the schema form's validation gate runs first and prevents the manual
alert early-exits.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/console/src/components/SchemaForm.tsx`:
- Around line 289-292: The useImperativeHandle implementation for validate
currently returns true when formRef.current is null; update the handler in
useImperativeHandle so that validate calls formRef.current?.validateForm() and,
if the inner ref is missing, returns false (fail closed) instead of true; locate
the useImperativeHandle invocation that references formRef and the validate
method and change the fallback from true to false so callers treat a missing
RJSF ref as invalid.

---

Outside diff comments:
In `@apps/console/src/components/StorageClassWidget.tsx`:
- Around line 38-43: The bug is that hasAutoDefaulted.current is being reset
(lines 56-58) so clearing the field re-triggers the effect on next render and
re-applies the default storage class; stop re-arming the auto-default. Remove or
guard the code that sets hasAutoDefaulted.current = false (the clear
handler/reset logic) so it is not flipped when the user manually clears the
input; instead only initialize/clear that ref when the defaultSC changes or on
mount. Update references in the component where hasAutoDefaulted is modified
(the useEffect that auto-applies defaultSC, any clear/handler that sets it to
false) so manual clears do not reset the flag while preserving the original
auto-apply behavior when defaultSC first appears.

In `@apps/console/src/routes/BackupCreatePage.tsx`:
- Around line 77-100: In handleSubmit, run the RJSF validation via
schemaFormRef.current.validate() before performing the page-level alerts so RJSF
inline errors are rendered for invalid spec state; specifically, in the
handleSubmit function call schemaFormRef.current?.validate() (and bail if it
returns false) at the top of the function (or at least before the
applicationRef/strategyRef/takenAt alert checks) so the schema form's validation
gate runs first and prevents the manual alert early-exits.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d56ac85-6981-4daf-a584-8e3916421481

📥 Commits

Reviewing files that changed from the base of the PR and between e3ceabf and 7284b55.

📒 Files selected for processing (20)
  • apps/console/src/components/BackupClassWidget.test.tsx
  • apps/console/src/components/BackupClassWidget.tsx
  • apps/console/src/components/SchemaForm.test.tsx
  • apps/console/src/components/SchemaForm.tsx
  • apps/console/src/components/StorageClassWidget.test.tsx
  • apps/console/src/components/StorageClassWidget.tsx
  • apps/console/src/components/VMDiskWidget.test.tsx
  • apps/console/src/components/VMDiskWidget.tsx
  • apps/console/src/routes/ApplicationOrderPage.test.tsx
  • apps/console/src/routes/ApplicationOrderPage.tsx
  • apps/console/src/routes/BackupCreatePage.test.tsx
  • apps/console/src/routes/BackupCreatePage.tsx
  • apps/console/src/routes/BackupJobCreatePage.test.tsx
  • apps/console/src/routes/BackupJobCreatePage.tsx
  • apps/console/src/routes/BackupPlanCreatePage.test.tsx
  • apps/console/src/routes/BackupPlanCreatePage.tsx
  • apps/console/src/routes/BackupResourceEditPage.test.tsx
  • apps/console/src/routes/BackupResourceEditPage.tsx
  • apps/console/src/routes/BackupRestoreJobCreatePage.test.tsx
  • apps/console/src/routes/BackupRestoreJobCreatePage.tsx

Comment on lines +289 to +292
useImperativeHandle(
ref,
() => ({ validate: () => formRef.current?.validateForm() ?? true }),
[],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make validate() fail closed when the RJSF ref is missing.

This currently returns true on formRef.current === null, which means every caller will treat the form as valid and still submit if the inner ref ever fails to bind. Since this handle is now the submit gate, the safer fallback is false.

Suggested fix
   useImperativeHandle(
     ref,
-    () => ({ validate: () => formRef.current?.validateForm() ?? true }),
+    () => ({ validate: () => formRef.current?.validateForm() ?? false }),
     [],
   )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/components/SchemaForm.tsx` around lines 289 - 292, The
useImperativeHandle implementation for validate currently returns true when
formRef.current is null; update the handler in useImperativeHandle so that
validate calls formRef.current?.validateForm() and, if the inner ref is missing,
returns false (fail closed) instead of true; locate the useImperativeHandle
invocation that references formRef and the validate method and change the
fallback from true to false so callers treat a missing RJSF ref as invalid.

@IvanHunters

Copy link
Copy Markdown
Contributor

Verdict

LGTM with non-blocking notes.

The two interacting defects are addressed at the right layer (widget-level placeholder + stable-option, page-level submit gate via SchemaForm.validate()), the three list-fed selects are fixed consistently, all six SchemaForm consumer pages gate their submit, and both pnpm typecheck and pnpm test (248 tests) pass cleanly on the PR head.

Findings

[MINOR] apps/console/src/components/SchemaForm.tsx:291validate() defaults to true when the inner RJSF ref is not yet attached.

useImperativeHandle exposes () => formRef.current?.validateForm() ?? true. If a caller ever invokes the handle before <Form> has mounted (or after an unmount race), the gate reports the form as valid and lets submit proceed. In current consumer pages this is unreachable — the form is rendered synchronously above the Deploy button — but the safer default for a gate is to fail closed: ?? false. Same one-line change, no observable behaviour difference in the happy path. Worth tightening so the next consumer page that wires the gate cannot accidentally bypass it during a mount race.

[MINOR] apps/console/src/components/StorageClassWidget.tsx:37-43 — auto-default effect retains the original async-commit shape that the PR removes from VMDiskWidget.

The PR justifies keeping it ("a default storage class is a real, meaningful default worth pre-filling") and the new submit gate plus disabled placeholder neutralise it for required fields. The residual exposure is a non-required storageClass on a cold cache where the user hits Deploy before the cluster-default annotation has been read; the resource is then created without the storage class the user visually expected. This is the same race class the PR fixes for disks, just for a much smaller blast radius. Not a regression introduced by this PR; flagging because the asymmetric handling between the three widgets is worth a comment trail or a follow-up.

Claim mismatches

[PARTIAL] "pnpm typecheck and pnpm test (235 tests) pass." — Actual on PR head: 248 tests pass across 33 files. The fix direction is correct; the number in the PR body is stale. Cosmetic, no action needed beyond optionally refreshing the description.

Red-team concerns

  • YAML mode in ApplicationOrderPage (line 134) deliberately bypasses the client-side gate (mode === "form" && …). The PR body acknowledges "YAML mode is left for the API to reject". That is an acceptable UX gap given Monaco edits raw YAML, but worth a short on-screen hint when the API rejects so users in YAML mode are not confused by why form-mode caught the error and YAML did not. Not blocking.
  • Empty disk list on a required disks[].name field: the placeholder is disabled when required and the gate blocks submit. Verified by VMDiskWidget.test.tsx:102-118 plus the per-page gate tests.
  • validateForm() throwing: RJSF returns booleans, the ?? true only handles nullish. If a future RJSF version threw, the submit handler would propagate and the page would surface it via the existing alert(Failed: …) catch. Acceptable.

Recommended follow-ups

  • Tighten SchemaForm.validate() fallback to ?? false (fail-closed gate).
  • Decide whether StorageClassWidget's auto-default deserves the same "removed effect" treatment when the field is optional, or whether the gate + acknowledged trade-off is the long-term answer; either way add a short comment pointing forward so the next session does not re-litigate the asymmetry.
  • Refresh the "(235 tests)" line in the PR body if/when convenient.

@lexfrei Aleksei Sviridkin (lexfrei) merged commit 9c95976 into main Jun 1, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/forms Issues or PRs related to RJSF schema forms and widgets (backup, external-ips, storage-class, etc.) kind/bug Categorizes issue or PR as related to a bug size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants