Skip to content

fix(console): ui audit fixes — forms, VNC, events, status badges#4

Open
IvanHunters wants to merge 13 commits into
mainfrom
feat/ui-audit-fixes
Open

fix(console): ui audit fixes — forms, VNC, events, status badges#4
IvanHunters wants to merge 13 commits into
mainfrom
feat/ui-audit-fixes

Conversation

@IvanHunters
Copy link
Copy Markdown
Contributor

@IvanHunters IvanHunters commented May 11, 2026

Summary

  • Display authenticated user name in the header
  • Add missing sidebar icons for all application kinds; replace invalid
    TCPBalancer icon with Lucide Network
  • Redirect disabled modules to tenant edit form instead of marketplace
  • Deterministic Back/Cancel navigation on the edit page (goes to resource
    detail, not browser history)
  • VMDisk: add dropdowns for source.disk.name and source.image.name
  • RJSF forms: fix stale auto-select overriding user's disk selection and
    first-click-does-nothing on array AddButton; emit schema defaults on
    mount so spec is never empty on first submit
  • VNC tab: add cursor-pointer to toolbar buttons, enable resizeSession for
    proper guest resolution
  • Show Terminating badge when a resource has deletionTimestamp set
  • Events tab: include Helm-managed pods and PVCs via instance label selector
  • External IPs: filter LoadBalancer services server-side via fieldSelector

Test plan

  • Header shows logged-in user name
  • Sidebar icons render for all kinds including TCPBalancer
  • Disabled module card navigates to tenant edit form
  • Edit page Back/Cancel goes to resource detail
  • VMDisk create form shows disk/image dropdowns in source selection
  • VMInstance disks array: first click on +add adds a row; selecting a
    disk from the dropdown persists the selection
  • New tenant/app form: clicking Deploy without touching fields submits
    non-empty spec with schema defaults
  • VNC toolbar buttons show pointer cursor; guest resolution matches
    container size
  • Deleting a resource shows Terminating badge
  • Events tab shows DataVolume/PVC events for VMDisk instances
  • External IPs page filters correctly

Summary by CodeRabbit

  • New Features

    • Added user identification support in the console interface
    • Enhanced disk and image field selection with Kubernetes resource dropdowns
    • Improved conditional navigation for modules based on tenant context
    • Added Helm-managed resource tracking in event monitoring
  • Bug Fixes

    • Fixed status badge display for terminating resources
    • Corrected form state synchronization with external property changes
    • Improved VNC session resizing functionality
    • Optimized LoadBalancer service filtering for performance

Review Change Stack

ohotnikov.ivan added 12 commits May 11, 2026 20:19
In edit mode, Back and Cancel navigate to the resource detail page
instead of relying on browser history, which is unstable on direct links.
In detail view, Back navigates to the resource list page.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Clicking a disabled module previously navigated to the marketplace,
which created a standalone ApplicationInstance bypassing the tenant
reconciler. Now it navigates to the tenant edit form where the user
can enable the module via the appropriate spec flag.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Fetches /oauth2/userinfo on startup alongside the app config and
passes the email/user field to AppShell. Falls back gracefully when
the endpoint is unavailable (dev mode without auth).

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Covers BootBox, ExternalDNS, FoundationDB, Info, OpenSearch, SeaweedFS,
and TCPBalancer. TCPBalancer and OpenSearch use Simple Icons (haproxy,
opensearch); the rest use Lucide fallbacks.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
loadConfig and loadUsername now abort after 5 seconds so the app
always mounts even when the K8s API or oauth2-proxy is unreachable.
Previously a hanging fetch would block Promise.all indefinitely,
leaving the root div empty.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
…etwork for TCPBalancer

haproxy does not exist in the Simple Icons package (cdn.jsdelivr.net returns 404),
causing a broken mask-image in the sidebar. Move TCPBalancer to the Lucide fallback
using the Network icon.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
SourceField now fetches existing VMDisks from the tenant namespace and
available images from cozy-public (PVCs with vm-default-images- prefix),
rendering them as native selects instead of plain text inputs.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
- Add cursor-pointer to toolbar buttons (Tailwind resets cursor to auto)
- Enable resizeSession so noVNC sends SetDesktopSize to the VM guest,
  matching the container resolution instead of upscaling a low-res framebuffer

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Resources with deletionTimestamp set now show a "Terminating" muted badge
instead of the Ready/NotReady condition badge in both the list and detail views.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Events tab now also loads pods and PVCs by the Helm release instance label
(app.kubernetes.io/instance) so resources like DataVolume that share the
HelmRelease name appear in the event stream alongside app-label resources.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
- VMDiskWidget: remove `disks` from useEffect deps so k8s watch events
  do not re-trigger auto-select and overwrite a user's disk selection;
  also fixes first-click-does-nothing on the disks array AddButton
- SchemaForm: emit getDefaultFormState on schema load so the parent spec
  is never empty when the user submits without touching any field (#5)
- rjsf-templates: add cursor-pointer to AddButton, strip non-button RJSF
  props (uiSchema, registry, iconType) from IconButton to avoid React warnings

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
…Selector

Previously all services were fetched and filtered on the client. Now uses
fieldSelector=spec.type=LoadBalancer matching the upstream dashboard behavior.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

📝 Walkthrough

Walkthrough

The PR introduces user identity integration into the console app, fixes form state synchronization issues across multiple components, adds dynamic dropdown selection for disk and image fields sourced from Kubernetes resources, corrects navigation patterns across several pages, enhances status display to show termination state, expands resource event scoping to include Helm-managed resources, and applies UI polish to VNC connectivity and button styling.

Changes

Console App Enhancements

Layer / File(s) Summary
Configuration & User Identity Loading
apps/console/src/lib/config.ts, apps/console/src/main.tsx
New fetchWithTimeout helper and loadUsername() function fetch user identity from OAuth endpoint. Bootstrap loads config and username concurrently, sets document title, and passes both to App component.
App Component Username Integration
apps/console/src/App.tsx
ShellProps extended with optional username field. App signature updated to accept and forward username to Shell UI.
Form State Synchronization Fixes
apps/console/src/components/ResourceQuotasField.tsx, apps/console/src/components/SchemaForm.tsx, apps/console/src/components/VMDiskWidget.tsx
ResourceQuotasField uses expectedValueRef to prevent external prop overwrites. SchemaForm emits defaults when schema changes via useEffect. VMDiskWidget guards first-disk auto-selection with autoSelectedRef to run once per mount.
Dynamic Disk and Image Selection
apps/console/src/components/SourceField.tsx
New helper hooks query VMDisks and filtered PVCs from Kubernetes. Renders <select> dropdowns for disk/image subfields instead of text inputs, with loading and empty states.
Navigation Route Corrections
apps/console/src/routes/ApplicationOrderPage.tsx, apps/console/src/routes/detail/ApplicationDetailPage.tsx, apps/console/src/routes/ModulesPage.tsx
ApplicationOrderPage and ApplicationDetailPage replace navigate(-1) with explicit list/edit routes. ModulesPage adds tenant-aware routing: disabled modules link to tenant edit page when tenant selected, or disabled otherwise.
Status Badge and Termination Display
apps/console/src/routes/ApplicationListPage.tsx, apps/console/src/routes/detail/ApplicationDetailPage.tsx
Status column checks metadata.deletionTimestamp and renders "Terminating" badge before falling back to ready/notready status logic.
Resource Filtering and Helm Integration
apps/console/src/routes/ExternalIpsPage.tsx, apps/console/src/routes/detail/EventsTab.tsx
ExternalIpsPage filters services server-side using fieldSelector. EventsTab expands event scope by querying both standard and Helm-labeled child resources, computing Helm release name from spec.release.prefix.
Presentation and Styling Updates
apps/console/src/components/rjsf-templates.tsx, apps/console/src/routes/detail/VncTab.tsx, apps/console/src/lib/sidebar-icons.tsx
VncTab enables RFB session resizing and clears desktopSize on reconnect. rjsf-templates adds border styling and destructures unused props. Sidebar icon coverage expanded for OpenSearch, BootBox, ExternalDNS, VMDisk, and others.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • cozystack/cozystack-ui#2: Overlaps VNC session management logic (resizeSession behavior) and rjsf-templates button styling changes.

Suggested reviewers

  • kvaps

Poem

🐰 Whiskers twitch with delight,
Forms now sync without a fight!
Disks and images dropdown down,
Navigation routes through the town.
User names flow, Helm resources glow—
What a grand refactor show! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% 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(console): ui audit fixes — forms, VNC, events, status badges' directly and accurately summarizes the main changes across multiple components in the console, covering forms, VNC functionality, events, and status badge updates.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ui-audit-fixes

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

Copy link
Copy Markdown

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

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 several enhancements to the console, including user identity fetching, improved navigation logic for edit modes, and more robust resource status reporting, such as the addition of a 'Terminating' state. Key technical improvements include dynamic disk and image selection in the SourceField component, optimized Kubernetes service listing using field selectors, and expanded event tracking for Helm-managed resources. Feedback provided identifies a potential data loss issue in SchemaForm where a stale ref could overwrite asynchronously loaded form data, and suggests refining the useEffect dependencies in VMDiskWidget to ensure reliable auto-selection when the disk list is populated after the initial render.


const onChangeRef = useRef(onChange)
onChangeRef.current = onChange
const initialFormDataRef = useRef(formData)
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

The initialFormDataRef is initialized only once during the component's mount and is never updated. If formData is provided asynchronously by the parent (for example, after an API fetch), this ref will remain empty. When the schema is loaded and the useEffect runs, getDefaultFormState will use the stale empty object as a base, and the subsequent onChange(defaults) call will overwrite the parent's newly loaded data with just the schema defaults.

To fix this, ensure the ref is updated on every render so that the effect always uses the most recent formData when calculating the default state.

Suggested change
const initialFormDataRef = useRef(formData)
const initialFormDataRef = useRef(formData)
initialFormDataRef.current = formData

}
}, [required, value, disks, isLoading, onChange])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [required, value, isLoading])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The disks array should be included in the dependency array of this useEffect. If the disk list is initially empty and then populated via a background update (while isLoading remains false), the auto-selection logic will not trigger because the effect won't re-run. The concern mentioned in the comments about overwriting user selection is already mitigated by the !value check; if a user has already made a selection, the effect will simply do nothing. Including disks ensures that the first disk is automatically selected as soon as the list becomes non-empty.

Suggested change
}, [required, value, isLoading])
}, [required, value, isLoading, disks])

- VncTab: reset desktopSize on reconnect to avoid stale aspect-ratio
  locking the layout behind the loading overlay
- VMDiskWidget: replace omitted-dep workaround with an autoSelectedRef flag
  so auto-select fires on initial disk load while respecting user changes;
  restores all proper deps so the linter is satisfied
- SchemaForm: guard defaults emission with emittedSchemaRef so an unexpected
  schema identity change cannot overwrite in-progress user input
- ResourceQuotasField: sync KnownRowEditor local state (checked, localNum)
  with external value changes via expectedValueRef to distinguish own onChange
  events from parent-driven resets (e.g. yaml/form mode switch)
- ModulesPage: remove hardcoded "root" tenant fallback; disable card with
  aria-disabled when no tenant is selected to prevent silent 404 navigation

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
@IvanHunters IvanHunters marked this pull request as ready for review May 11, 2026 21:46
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
apps/console/src/routes/detail/EventsTab.tsx (1)

71-82: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prevent transient false empty state while Pods/PVCs are still loading.

Line 175 can show “No events found” before related Pod/PVC names arrive, because only eventsList loading is checked. This can briefly hide valid events.

💡 Suggested fix
- const { data: podsList } = useK8sList<K8sPod>(
+ const { data: podsList, isLoading: isPodsLoading } = useK8sList<K8sPod>(
...
- const { data: helmPodsList } = useK8sList<K8sPod>(
+ const { data: helmPodsList, isLoading: isHelmPodsLoading } = useK8sList<K8sPod>(
...
- const { data: pvcList } = useK8sList<K8sPVC>(
+ const { data: pvcList, isLoading: isPvcLoading } = useK8sList<K8sPVC>(
...
- const { data: helmPvcList } = useK8sList<K8sPVC>(
+ const { data: helmPvcList, isLoading: isHelmPvcLoading } = useK8sList<K8sPVC>(
...
+ const isRelatedResourcesLoading =
+   isPodsLoading || isHelmPodsLoading || isPvcLoading || isHelmPvcLoading
+
- if (isLoading) {
+ if (isLoading || isRelatedResourcesLoading) {

Also applies to: 85-96, 99-110, 113-124, 163-175

🤖 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/detail/EventsTab.tsx` around lines 71 - 82, The
empty-state check currently only looks at eventsList loading and can render “No
events found” before related pods/pvcs arrive; update the render/conditional
logic in EventsTab to consider the loading state of the related useK8sList hooks
(podsList, pvcsList) together with eventsList (e.g., use podsList?.isLoading or
pvcsList?.isLoading) so you only show the “No events found” message when
eventsList is loaded AND neither podsList nor pvcsList are still loading (or
when all three are loaded and empty). Adjust the same checks where similar
empty-state messages occur (the other blocks referencing useK8sList for
pods/pvcs/events) so they combine the loading flags rather than relying on
eventsList alone; keep tenantNamespace and labelSelector usage unchanged.
🧹 Nitpick comments (1)
apps/console/src/components/rjsf-templates.tsx (1)

22-23: 💤 Low value

Consider using underscore prefix for intentionally unused variables.

The current pattern works correctly by excluding uiSchema and registry from the button props spread. However, using underscore prefixes is more idiomatic for signaling intentionally unused variables:

- // eslint-disable-next-line `@typescript-eslint/no-unused-vars`
- const { icon, className, uiSchema, registry, iconType, ...btnProps } = props
+ const { icon, className, uiSchema: _uiSchema, registry: _registry, iconType, ...btnProps } = props

This convention makes the intent explicit without requiring eslint suppressions.

🤖 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/rjsf-templates.tsx` around lines 22 - 23, Rename
the intentionally unused destructured variables to use an underscore prefix
(e.g., change uiSchema and registry to _uiSchema and _registry) in the props
destructuring where const { icon, className, uiSchema, registry, iconType,
...btnProps } = props is defined, remove the now-unnecessary
eslint-disable-next-line comment, and keep the rest of the spread into btnProps
unchanged so the unused intent is explicit and ESLint warnings are avoided.
🤖 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 171-183: The effect currently uses a one-time captured
initialFormDataRef (set from formData at mount) when calling
getDefaultFormState, which can produce stale defaults; update the effect to use
the latest formData as the base before computing defaults (either assign
initialFormDataRef.current = formData at the start of the useEffect or pass the
current formData directly into getDefaultFormState) so emitted defaults reflect
the latest values; keep the rest of the emittedSchemaRef and onChangeRef usage
the same to avoid extra re-renders.

In `@apps/console/src/routes/ModulesPage.tsx`:
- Around line 117-119: The current card uses a Link with to={canNavigate ?
target : "#"} so when canNavigate is false keyboard users can still
focus/activate it and change the URL hash; update the render so that when
canNavigate is false you do NOT render a navigable Link (render a
non-interactive container like a div or a role="group" element) or render a true
non-focusable element (remove href/to and set tabIndex={-1},
aria-disabled="true", and prevent activation handlers) instead; locate the card
JSX that uses to, target and canNavigate in ModulesPage.tsx and change the
conditional to render a non-link container when !canNavigate, preserving the
same classes and accessible aria-disabled state.

---

Outside diff comments:
In `@apps/console/src/routes/detail/EventsTab.tsx`:
- Around line 71-82: The empty-state check currently only looks at eventsList
loading and can render “No events found” before related pods/pvcs arrive; update
the render/conditional logic in EventsTab to consider the loading state of the
related useK8sList hooks (podsList, pvcsList) together with eventsList (e.g.,
use podsList?.isLoading or pvcsList?.isLoading) so you only show the “No events
found” message when eventsList is loaded AND neither podsList nor pvcsList are
still loading (or when all three are loaded and empty). Adjust the same checks
where similar empty-state messages occur (the other blocks referencing
useK8sList for pods/pvcs/events) so they combine the loading flags rather than
relying on eventsList alone; keep tenantNamespace and labelSelector usage
unchanged.

---

Nitpick comments:
In `@apps/console/src/components/rjsf-templates.tsx`:
- Around line 22-23: Rename the intentionally unused destructured variables to
use an underscore prefix (e.g., change uiSchema and registry to _uiSchema and
_registry) in the props destructuring where const { icon, className, uiSchema,
registry, iconType, ...btnProps } = props is defined, remove the now-unnecessary
eslint-disable-next-line comment, and keep the rest of the spread into btnProps
unchanged so the unused intent is explicit and ESLint warnings are avoided.
🪄 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: df226560-7e7a-410c-b906-0678d7276cf1

📥 Commits

Reviewing files that changed from the base of the PR and between 2ff7cae and de2f98b.

📒 Files selected for processing (16)
  • apps/console/src/App.tsx
  • apps/console/src/components/ResourceQuotasField.tsx
  • apps/console/src/components/SchemaForm.tsx
  • apps/console/src/components/SourceField.tsx
  • apps/console/src/components/VMDiskWidget.tsx
  • apps/console/src/components/rjsf-templates.tsx
  • apps/console/src/lib/config.ts
  • apps/console/src/lib/sidebar-icons.tsx
  • apps/console/src/main.tsx
  • apps/console/src/routes/ApplicationListPage.tsx
  • apps/console/src/routes/ApplicationOrderPage.tsx
  • apps/console/src/routes/ExternalIpsPage.tsx
  • apps/console/src/routes/ModulesPage.tsx
  • apps/console/src/routes/detail/ApplicationDetailPage.tsx
  • apps/console/src/routes/detail/EventsTab.tsx
  • apps/console/src/routes/detail/VncTab.tsx

Comment on lines +171 to +183
const initialFormDataRef = useRef(formData)
const emittedSchemaRef = useRef<RJSFSchema | null>(null)

// Emit defaults to parent once per schema so spec is never empty on first submit.
// Uses initialFormDataRef so edit-mode existing values are preserved as base.
// emittedSchemaRef prevents re-running on unrelated re-renders and avoids
// overwriting user data if the schema object changes identity unexpectedly.
useEffect(() => {
if (!schema || Object.keys(schema).length === 0) return
if (emittedSchemaRef.current === schema) return
emittedSchemaRef.current = schema
const defaults = getDefaultFormState(validator, schema, initialFormDataRef.current ?? {}, schema)
onChangeRef.current(defaults)
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 | 🟠 Major | ⚡ Quick win

Use the latest formData as the defaults base.

Line 171 captures formData only once, but Line 182 computes defaults later. If formData changes before emission (or schema changes in-place), defaults can be derived from stale data and overwrite expected values.

💡 Suggested fix
-  const initialFormDataRef = useRef(formData)
+  const latestFormDataRef = useRef(formData)
+  latestFormDataRef.current = formData
   const emittedSchemaRef = useRef<RJSFSchema | null>(null)
@@
-    // Uses initialFormDataRef so edit-mode existing values are preserved as base.
+    // Uses latest formData so async-loaded edit values are preserved as base.
@@
-    const defaults = getDefaultFormState(validator, schema, initialFormDataRef.current ?? {}, schema)
+    const defaults = getDefaultFormState(validator, schema, latestFormDataRef.current ?? {}, schema)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const initialFormDataRef = useRef(formData)
const emittedSchemaRef = useRef<RJSFSchema | null>(null)
// Emit defaults to parent once per schema so spec is never empty on first submit.
// Uses initialFormDataRef so edit-mode existing values are preserved as base.
// emittedSchemaRef prevents re-running on unrelated re-renders and avoids
// overwriting user data if the schema object changes identity unexpectedly.
useEffect(() => {
if (!schema || Object.keys(schema).length === 0) return
if (emittedSchemaRef.current === schema) return
emittedSchemaRef.current = schema
const defaults = getDefaultFormState(validator, schema, initialFormDataRef.current ?? {}, schema)
onChangeRef.current(defaults)
const latestFormDataRef = useRef(formData)
latestFormDataRef.current = formData
const emittedSchemaRef = useRef<RJSFSchema | null>(null)
// Emit defaults to parent once per schema so spec is never empty on first submit.
// Uses latest formData so async-loaded edit values are preserved as base.
// emittedSchemaRef prevents re-running on unrelated re-renders and avoids
// overwriting user data if the schema object changes identity unexpectedly.
useEffect(() => {
if (!schema || Object.keys(schema).length === 0) return
if (emittedSchemaRef.current === schema) return
emittedSchemaRef.current = schema
const defaults = getDefaultFormState(validator, schema, latestFormDataRef.current ?? {}, schema)
onChangeRef.current(defaults)
🤖 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 171 - 183, The
effect currently uses a one-time captured initialFormDataRef (set from formData
at mount) when calling getDefaultFormState, which can produce stale defaults;
update the effect to use the latest formData as the base before computing
defaults (either assign initialFormDataRef.current = formData at the start of
the useEffect or pass the current formData directly into getDefaultFormState) so
emitted defaults reflect the latest values; keep the rest of the
emittedSchemaRef and onChangeRef usage the same to avoid extra re-renders.

Comment on lines +117 to +119
to={canNavigate ? target : "#"}
aria-disabled={!canNavigate}
className={`flex items-center gap-3 rounded-lg border border-slate-200 bg-white px-4 py-3 transition-shadow ${canNavigate ? "hover:shadow-sm" : "opacity-50 cursor-not-allowed pointer-events-none"}`}
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

Disabled cards are still keyboard-activatable via Link.

Line 117 uses to="#" for disabled cards, so keyboard users can still activate it and change URL hash. Render a non-link container when !canNavigate, or fully disable focus/activation.

Suggested fix
-  return (
-    <Link
-      to={canNavigate ? target : "#"}
-      aria-disabled={!canNavigate}
-      className={`flex items-center gap-3 rounded-lg border border-slate-200 bg-white px-4 py-3 transition-shadow ${canNavigate ? "hover:shadow-sm" : "opacity-50 cursor-not-allowed pointer-events-none"}`}
-    >
+  const cardClassName = `flex items-center gap-3 rounded-lg border border-slate-200 bg-white px-4 py-3 transition-shadow ${canNavigate ? "hover:shadow-sm" : "opacity-50 cursor-not-allowed"}`
+
+  if (!canNavigate) {
+    return (
+      <div
+        aria-disabled="true"
+        className={cardClassName}
+      >
+        {/* content */}
+      </div>
+    )
+  }
+
+  return (
+    <Link
+      to={target}
+      className={cardClassName}
+    >
       ...
     </Link>
   )
🤖 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/ModulesPage.tsx` around lines 117 - 119, The current
card uses a Link with to={canNavigate ? target : "#"} so when canNavigate is
false keyboard users can still focus/activate it and change the URL hash; update
the render so that when canNavigate is false you do NOT render a navigable Link
(render a non-interactive container like a div or a role="group" element) or
render a true non-focusable element (remove href/to and set tabIndex={-1},
aria-disabled="true", and prevent activation handlers) instead; locate the card
JSX that uses to, target and canNavigate in ModulesPage.tsx and change the
conditional to render a non-link container when !canNavigate, preserving the
same classes and accessible aria-disabled state.

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.

1 participant