Skip to content

feat(ui-react): add device chooser for cloud free-tier device limit#6279

Merged
otavio merged 2 commits into
masterfrom
feat/ui-react/device-chooser
May 7, 2026
Merged

feat(ui-react): add device chooser for cloud free-tier device limit#6279
otavio merged 2 commits into
masterfrom
feat/ui-react/device-chooser

Conversation

@luizhf42
Copy link
Copy Markdown
Member

@luizhf42 luizhf42 commented May 5, 2026

What

Port the Vue UI's device chooser to the React UI: when a Cloud namespace owner exceeds the free-tier three-device cap without an active subscription, present a forced modal to either subscribe or pick three devices to keep accepted; the rest are demoted to pending by the existing /api/billing/device-choice endpoint.

Why

Closes the last open Device-Management gap on the React UI parity report (shellhub-io/team#91). Without this surface, free-tier Cloud namespaces that exceed the cap leave the owner with no UI to recover — they cannot accept new devices and have no way to pick which to keep short of calling the API directly.

Changes

  • DeviceChooserTrigger: gates the modal on cloud build + device:choose permission + !namespace.billing.active + stats.registered_devices > 3, with a loading guard so the modal cannot flash for paying customers during the namespace fetch race. Dismissal does not persist — reopens on next fresh mount, matching Vue.
  • DeviceChooserDialog: hand-rolled accessible Tabs (no shared primitive exists), Suggested and All tabs sharing a single NormalizedDevice shape through the now-exported normalizeDevice. Suggested preselects the top three devices by session count from /api/billing/devices-most-used. All has hostname search (debounced), 5-per-page pagination, hard cap of three selections, and a chip strip surfacing the current selection across pages so picks cannot be silently lost when paginating or searching. Footer: Cancel / Subscribe (navigates to /settings#billing) / Accept. ESC and backdrop are blocked while the mutation is in flight; double-clicks are guarded by a ref.
  • Shared cells: extracted OnlineDot and LastSeenCell to components/common/ so the chooser and the main devices table render device status identically.
  • Shared hook: extracted useDebouncedValue from the inline pattern in pages/devices/index.tsx; both surfaces use it now.
  • Constants: FREE_TIER_DEVICE_LIMIT lives in types/billing.ts, replacing two unrelated 3 constants.
  • useDevices: gained an enabled option so the dialog skips the All-tab query while the user is on Suggested.

Testing

  • 78 new unit/component tests covering the chooser surface and the extracted useDebouncedValue hook.
  • Manual: with a Cloud free-tier namespace of >3 accepted devices, log in as owner → modal opens. Pick three → confirm → unchosen devices land in pending (not removed), dashboard refetches, modal does not reopen on reload. Log in as non-owner → no modal. Activate billing in Mongo → no modal. Run on CE → no modal.
  • Watch for: paying-customer flash on initial load (the loading-gate fix), tab persistence across a Suggested refetch (selections must not silently switch tabs), and that the All-tab search does not fire per-keystroke requests.

@luizhf42 luizhf42 requested a review from a team as a code owner May 5, 2026 17:23
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

Claude finished @luizhf42's task in 7m 18s —— View job


Code Review Complete

Reviewed 14 files across code quality, security, testing, TypeScript/React patterns, and architecture (including cross-repo verification against the cloud backend). 1 inline issue posted (medium severity).

All five review dimensions passed cleanly except for one test that passes trivially without exercising its stated behavior (Home key navigation). The architecture agent confirmed that API contracts (choices field), permission strings (device:choose), and the free-tier constant (3) all match between the React frontend and the Go/Cloud backend.

Overall this is a well-structured PR — good separation between trigger gating and dialog logic, proper loading guards to prevent modal flash, correct accessibility patterns (ARIA tabs, live regions), and thorough test coverage (78 tests).


If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

@luizhf42 luizhf42 self-assigned this May 5, 2026
@luizhf42 luizhf42 added kind/feature New feature or request area/ui product/cloud Issue/PR related to Cloud Edition labels May 5, 2026
@luizhf42 luizhf42 force-pushed the feat/ui-react/device-chooser branch from bad4b9d to cfc2c91 Compare May 5, 2026 17:41
@otavio
Copy link
Copy Markdown
Member

otavio commented May 5, 2026

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

Claude finished @otavio's task in 1m 10s —— View job


Code Review Complete

Reviewed 14 files across code quality, security, testing, TypeScript/React patterns, and architecture (including cross-repo verification against the Cloud backend) — no issues found. The code looks good as-is.

Review details

  • Security: No XSS, injection, hardcoded secrets, or authorization bypass issues. All dynamic content is rendered through React JSX (auto-escaped). The device:choose permission is enforced both client-side and server-side.
  • Architecture: API contracts (choices field format, choiceDevices/getDevicesMostUsed operation IDs), permission strings (device:choose → owner-only), and the free-tier constant (3) all match between the React frontend and the Go/Cloud backend. The enabled param added to useDevices defaults to true, so existing callers are unaffected. The normalizeDevice export is purely additive.
  • Code Quality: Well-structured separation between trigger gating (DeviceChooserTrigger) and dialog logic (DeviceChooserDialog). Loading guards correctly prevent modal flash for paying customers. The inFlightRef double-click guard works correctly — finally always resets it.
  • TypeScript/React: No any types in source files, no hook rule violations, correct early-return pattern in DeviceChooserTrigger (delegates before hooks). Zustand usage follows established patterns.
  • Testing: 78 tests cover the dialog, trigger, debounce hook, and chooser hook across rendering, tab navigation, selection mechanics, error handling, dismissal, and keyboard accessibility.

If you push additional changes and want a new review, tag @shellhub-io/admin and a team member can trigger it.

@otavio
Copy link
Copy Markdown
Member

otavio commented May 6, 2026

Could you do a rebase, so we do a new review round?

@luizhf42 luizhf42 force-pushed the feat/ui-react/device-chooser branch from cfc2c91 to aa85321 Compare May 6, 2026 20:25
@otavio
Copy link
Copy Markdown
Member

otavio commented May 6, 2026

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Claude finished @otavio's task in 10m 0s —— View job


Code Review Complete

Reviewed 13 files across code quality, security, testing, TypeScript/React patterns, and architecture (including cross-repo verification against the Cloud backend). 1 inline issue posted (medium severity).

Review details

  • Security: No XSS, injection, hardcoded secrets, or authorization bypass issues. All dynamic content is rendered through React JSX (auto-escaped). The device:choose permission is enforced both client-side and server-side.
  • Architecture: API contracts (choices field format, choiceDevices/getDevicesMostUsed operation IDs), permission strings (device:choose → owner-only), and the free-tier constant (3) all match between the React frontend and the Go/Cloud backend. The enabled param added to useDevices defaults to true, so existing callers are unaffected. The normalizeDevice export is purely additive.
  • Code Quality: Well-structured separation between trigger gating (DeviceChooserTrigger) and dialog logic (DeviceChooserDialog). Loading guards correctly prevent modal flash for paying customers. The inFlightRef double-click guard works correctly — finally always resets it.
  • TypeScript/React: No any types in source files, no hook rule violations, correct early-return pattern in DeviceChooserTrigger (delegates before hooks). Zustand usage follows established patterns.
  • Testing: 78 tests cover the dialog, trigger, debounce hook, and chooser hook across rendering, tab navigation, selection mechanics, error handling, dismissal, and keyboard accessibility.

If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

Comment thread ui-react/apps/console/src/components/billing/DeviceChooserDialog.tsx Outdated
@luizhf42 luizhf42 force-pushed the feat/ui-react/device-chooser branch from aa85321 to e7805d5 Compare May 6, 2026 20:49
@otavio
Copy link
Copy Markdown
Member

otavio commented May 6, 2026

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Claude finished @otavio's task in 26m 37s —— View job


Code Review Complete

Reviewed 13 files across code quality, security, testing, TypeScript/React patterns, and architecture (including cross-repo verification against the Cloud backend). 1 inline issue posted (medium severity).

Review details

  • Security: No XSS, injection, hardcoded secrets, or authorization bypass issues. All dynamic content is rendered through React JSX (auto-escaped). The device:choose permission is enforced both client-side and server-side. The inFlightRef double-click guard correctly prevents duplicate mutation submissions.
  • Architecture: API contracts (choices field format, choiceDevices/getDevicesMostUsed operation IDs), permission strings (device:choose → owner-only), and the free-tier constant (3) all match between the React frontend and the Go/Cloud backend. The enabled param added to useDevices defaults to true, so existing callers are unaffected. The normalizeDevice export is purely additive.
  • Code Quality: Well-structured separation between trigger gating (DeviceChooserTrigger) and dialog logic (DeviceChooserDialog). Loading guards correctly prevent modal flash for paying customers. The suggestedEmpty guard now correctly excludes the error case (fix from previous review round confirmed).
  • TypeScript/React: No any types in source files, no hook rule violations, correct early-return pattern in DeviceChooserTrigger (delegates before hooks). Zustand usage follows established patterns with proper selectors.
  • Testing: 78 tests cover the dialog, trigger, debounce hook, and chooser hook across rendering, tab navigation, selection mechanics, error handling, dismissal, and keyboard accessibility. The previously flagged Home key test now correctly switches to All before pressing Home.

If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

Comment thread ui-react/apps/console/src/components/billing/DeviceChooserTrigger.tsx Outdated
luizhf42 added 2 commits May 7, 2026 09:29
When a Cloud namespace owner exceeds three accepted devices without an
active subscription, surface a forced modal to either subscribe or pick
three devices to keep accepted; the rest are demoted to pending by the
existing /api/billing/device-choice endpoint. Extract OnlineDot,
LastSeenCell, useDebouncedValue, and normalizeDevice for reuse with the
devices page.
@luizhf42 luizhf42 force-pushed the feat/ui-react/device-chooser branch from e7805d5 to 3bb0c42 Compare May 7, 2026 12:30
@otavio
Copy link
Copy Markdown
Member

otavio commented May 7, 2026

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

Claude finished @otavio's task in 1m 15s —— View job


Code Review Complete

Reviewed 13 files across code quality, security, testing, TypeScript/React patterns, and architecture (including cross-repo verification against the Cloud backend) — no issues found. The code looks good as-is.

Review details

  • Security: No XSS, injection, hardcoded secrets, or authorization bypass issues. All dynamic content is rendered through React JSX (auto-escaped). The device:choose permission is enforced both client-side (owner-only) and server-side.
  • Architecture: API contracts (choices field format, choiceDevices/getDevicesMostUsed operation IDs), permission strings (device:choose → owner-only), and the free-tier constant (3) all match between the React frontend and the Go/Cloud backend. The enabled param added to useDevices defaults to true, so existing callers are unaffected. The normalizeDevice export is purely additive.
  • Code Quality: Well-structured separation between trigger gating (DeviceChooserTrigger) and dialog logic (DeviceChooserDialog). Loading guards correctly prevent modal flash for paying customers. The inFlightRef double-click guard works correctly — finally always resets it. The suggestedEmpty guard now correctly excludes the error case (fix from previous review round confirmed). The !stats null guard is in place (fix from latest review round confirmed).
  • TypeScript/React: No any types in source files, no hook rule violations. The outer DeviceChooserTrigger calls no hooks before its early return — all hooks live in DeviceChooserTriggerInner, which is the correct delegation pattern. Zustand usage follows established patterns with proper selectors.
  • Testing: 78 tests cover the dialog, trigger, debounce hook, and chooser hook across rendering, tab navigation, selection mechanics, error handling, dismissal, and keyboard accessibility. The previously flagged Home key test now correctly switches to All before pressing Home.

If you push additional changes and want a new review, tag @shellhub-io/admin and a team member can trigger it.

@otavio otavio merged commit 717ab97 into master May 7, 2026
14 checks passed
@otavio otavio deleted the feat/ui-react/device-chooser branch May 7, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ui kind/feature New feature or request product/cloud Issue/PR related to Cloud Edition

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants