Skip to content

Fleet UI: Command palette followup - #46346

Merged
RachelElysia merged 6 commits into
mainfrom
43757-command-palette-followup
May 29, 2026
Merged

Fleet UI: Command palette followup#46346
RachelElysia merged 6 commits into
mainfrom
43757-command-palette-followup

Conversation

@RachelElysia

@RachelElysia RachelElysia commented May 28, 2026

Copy link
Copy Markdown
Member

Issue

Closes #43757

Description

Testing

  • Added/updated automated tests
  • QA'd all new/changed functionality manually

Summary by CodeRabbit

  • New Features

    • Improved fleet-switch behavior in the command palette for more reliable navigation.
  • Bug Fixes

    • Stronger permission gating for “Manage report automations” and other automation deep-links.
    • Keyboard shortcuts (Cmd/Ctrl+K, switch fleet) no longer intercept input for users without palette access.
  • Improvements

    • Deep-link query params (manage_automations, add_script, add_variable, create_fleet, etc.) are consistently stripped after processing.
    • More accurate deep-link gating across dashboards, scripts, variables, fleets, hosts, policies, and queries.
  • Tests

    • Updated/added tests for command palette interactions, permission gating, fleet-switch URLs, and deep-links.

Review Change Stack

Follow-ups to the merged Fleet Spotlight (#43756):

- Fleet switcher: extract buildFleetSwitchUrl helper; mirror
  useTeamIdParam.handleTeamChange's strip rules so page/team_id
  and Hosts-only fleet-scoped params (script_batch_execution_*,
  software_status on All) don't leak across fleets.
- Add hosts / manage enroll secrets: new withTeamRequired helper
  so the "Unassigned" chip on All-fleets matches the URL the
  modal opens against. Free-tier suppresses the chip and the
  fleet_id=0 redirect-strip dance.
- Policy automations: use withTeamId for the four sub-items
  instead of manual fleet_id interpolation.
- Manage report automations: gate the palette item on
  canManageReportAutomations (admin) instead of canWrite.
- No-access users: gate global key handlers on !isNoAccess so
  Cmd+K and the Escape capture listener don't intercept
  shortcuts for a palette they can't open.
- Cmd+K: normalize key case so Caps Lock doesn't break the
  toggle.
- valueToParentId: normalize the cmdk value before lookup so
  arrow-key auto-expand actually fires.
- Back button: tabIndex=-1 so Radix's open-autofocus lands on
  the input (Backspace/Escape still navigate back).

Deep-link permission gates (param alone must not bypass
in-page authorization):
- ManageQueriesPage: gate ?manage_automations=1 on
  canManageAutomations && isManageAutomationsEnabled.
- ManageFleetsPage: gate ?create_fleet=1 on shared
  isCreateFleetDisabled (Primo + GitOps).
- DashboardPage: gate ?manage_automations=1 on
  canEditActivityFeedAutomations.

Routing convention:
- ScriptLibrary and ManageFleetsPage: replace
  window.history.replaceState with router.replace so
  react-router's location stays in sync.
- .claude/rules/fleet-frontend.md: add Routing & URL state
  section codifying the pattern.
Round 2 of the deep-link audit — pages that opened modals from
URL params without re-checking the same predicate the in-page
action uses. Same shape as the prior commit: gate the modal
open on the page's existing permission predicate; strip the
param either way so refreshes don't keep trying.

- ManageHostsPage: gate ?add_hosts=1 and ?manage_enroll_secrets=1
  on canEnrollHosts (hoisted above the deep-link effects).
- ManagePoliciesPage: wrap the manage_automations type switch
  in canManageAutomations; fix the stale single-key dep array.
- SoftwarePage: add isGlobalAdmin to the manage_automations gate
  (mirrors canManageSoftwareAutomations); keep the wait-for-config
  semantics so the param survives until ready.
- Variables: rewrite the ?add_variable=1 handler from
  window.location.search + window.history.replaceState to
  location.query + router.replace, and gate on canEdit. Added
  router/location props via the usual InjectedRouter pattern.
  Updated 11 existing tests to pass the new props via baseProps.

After this commit, every palette-emitted deep-link (add_hosts,
manage_enroll_secrets, add_script, add_variable, create_fleet,
and the four manage_automations destinations) is gated on the
same predicate as its in-page entry point, and no page handler
uses window.* APIs to read or mutate the URL.
@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 46.06742% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.91%. Comparing base (7c33552) to head (8112c4c).
⚠️ Report is 66 commits behind head on main.

Files with missing lines Patch % Lines
...policies/ManagePoliciesPage/ManagePoliciesPage.tsx 0.00% 18 Missing ⚠️
...es/queries/ManageQueriesPage/ManageQueriesPage.tsx 0.00% 12 Missing ⚠️
...nd/pages/hosts/ManageHostsPage/ManageHostsPage.tsx 27.27% 8 Missing ⚠️
frontend/pages/SoftwarePage/SoftwarePage.tsx 0.00% 5 Missing ⚠️
...d/pages/ManageControlsPage/Variables/Variables.tsx 40.00% 3 Missing ⚠️
...ntend/components/CommandPalette/CommandPalette.tsx 75.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #46346      +/-   ##
==========================================
+ Coverage   66.89%   66.91%   +0.02%     
==========================================
  Files        2782     2793      +11     
  Lines      221734   221876     +142     
  Branches    11356    11444      +88     
==========================================
+ Hits       148334   148474     +140     
+ Misses      59999    59998       -1     
- Partials    13401    13404       +3     
Flag Coverage Δ
frontend 56.48% <46.06%> (+0.19%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Revert the withTeamRequired/teamRequiredDestination experiment.
The earlier change made add-hosts and manage-enroll-secrets route
to Unassigned (fleet_id=0) when invoked from All fleets so the
"Unassigned" chip matched the URL. The better behavior is to not
switch teams at all: on All fleets these commands operate against
the global enroll secrets, which is what users expect.

- Switch both items back to withTeamId — drops fleet_id on All
  fleets, preserving the user's current team context.
- Remove the chip entirely for these commands (no destination
  switch, no chip needed).
- Delete the now-unused withTeamRequired callback,
  teamRequiredDestination derivation, and their interface/test
  scaffolding.

Also tighten canManageReportAutomations to (isGlobalAdmin ||
isTeamAdmin) so it matches ManageQueriesPage exactly — the
palette URL targets the current team via withTeamId, so an
isAnyTeamAdmin user who isn't admin of the current team would
land on Reports and find no Manage automations button.
@RachelElysia
RachelElysia marked this pull request as ready for review May 28, 2026 18:06
Copilot AI review requested due to automatic review settings May 28, 2026 18:06
@RachelElysia
RachelElysia requested a review from a team as a code owner May 28, 2026 18:06

@claude claude 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.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR follows up on the Fleet command palette by tightening deep-link handling, aligning command visibility with destination-page permissions, and moving fleet-switch URL logic into a tested helper.

Changes:

  • Adds permission-aware query-param handling for command-palette deep links across pages.
  • Refactors and tests command palette fleet-switch URL generation and item gating.
  • Documents frontend routing guidance to use react-router instead of direct window history/location mutation.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
frontend/pages/SoftwarePage/SoftwarePage.tsx Gates software automation deep-link opening on config/team/admin state.
frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx Gates report automation deep-link opening on admin access and available queries.
frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx Gates policy automation modal deep-links on automation permissions.
frontend/pages/ManageControlsPage/Variables/Variables.tsx Moves add-variable deep-link handling to router/location props with edit gating.
frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx Updates Variables tests to pass router/location props.
frontend/pages/ManageControlsPage/Scripts/Scripts.tsx Adds add_script to script page query typing.
frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx Replaces direct window history mutation with router-based add-script deep-link handling.
frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx Adds add-script deep-link tests for admins and technicians.
frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx Gates add-hosts and enroll-secret deep-links on enrollment permissions.
frontend/pages/DashboardPage/DashboardPage.tsx Gates activity automation deep-link handling on global-admin/all-fleets access.
frontend/pages/admin/ManageFleetsPage/ManageFleetsPage.tsx Adds router-based create-fleet deep-link handling and shared create-disabled logic.
frontend/components/CommandPalette/helpers.ts Adds report automation permission context and buildFleetSwitchUrl.
frontend/components/CommandPalette/helpers.tests.ts Adds tests for report automation gating and fleet-switch URL behavior.
frontend/components/CommandPalette/groups/derivations.ts Removes unused team-required chip derivation.
frontend/components/CommandPalette/groups/commands.ts Removes add-host/enroll-secret destination chip usage.
frontend/components/CommandPalette/groups/automations.ts Narrows report automation gating and normalizes policy automation paths.
frontend/components/CommandPalette/CommandPalette.tsx Uses fleet-switch helper, improves shortcut handling, and adds no-access protections.
frontend/components/CommandPalette/CommandPalette.tests.tsx Adds a no-access shortcut interception test.
.claude/rules/fleet-frontend.md Documents react-router URL state conventions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +79 to 81
if (location.query.create_fleet !== "1") return;
if (!isCreateFleetDisabled) {
setShowCreateFleetModal(true);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

will fix, thanks

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed

Comment on lines +186 to +191
if (location.query.manage_automations !== "1") return;
if (canEditActivityFeedAutomations) {
setShowActivityFeedAutomationsModal(true);
// Clean up the query param from the URL, preserving other params
const { manage_automations, ...rest } = location.query;
router.replace({ pathname, query: rest });
}
}, [location.query.manage_automations, pathname, router]);
const { manage_automations, ...rest } = location.query;
router.replace({ pathname, query: rest });

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ok thanks

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed

Comment on lines +247 to +252
if (queryParams?.add_hosts !== "1") return;
if (canEnrollHosts) {
setShowAddHostsModal(true);
const { add_hosts, ...rest } = queryParams;
router.replace({ pathname: location.pathname, query: rest });
}
}, [queryParams?.add_hosts, location.pathname, router]);
const { add_hosts, ...rest } = queryParams;
router.replace({ pathname: location.pathname, query: rest });

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ok thanks

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed

Comment on lines +258 to +263
if (queryParams?.manage_enroll_secrets !== "1") return;
if (canEnrollHosts) {
setShowEnrollSecretModal(true);
const { manage_enroll_secrets, ...rest } = queryParams;
router.replace({ pathname: location.pathname, query: rest });
}
}, [queryParams?.manage_enroll_secrets, location.pathname, router]);
const { manage_enroll_secrets, ...rest } = queryParams;
router.replace({ pathname: location.pathname, query: rest });

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 98bbf1f6-42a2-48c1-a1b2-9136ba1ca150

📥 Commits

Reviewing files that changed from the base of the PR and between 7eb56ec and 8112c4c.

📒 Files selected for processing (3)
  • frontend/components/CommandPalette/CommandPalette.tests.tsx
  • frontend/components/CommandPalette/groups/automations.ts
  • frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/components/CommandPalette/CommandPalette.tests.tsx
  • frontend/components/CommandPalette/groups/automations.ts

Walkthrough

This PR adds buildFleetSwitchUrl and tests, updates CommandPalette to use it, introduces canManageReportAutomations, removes teamRequiredDestination, prevents keyboard registration and Escape-to-root handling for no-access users, normalizes highlight lookup, and sets Back button tabIndex. It also standardizes deep-link handling across Variables, ManageHosts, ScriptLibrary, Dashboard, Software, ManagePolicies, ManageQueries, and ManageFleets to gate modal opening on permissions/data readiness and to strip triggering query params via router.replace.

Possibly related PRs

  • fleetdm/fleet#46254: Changes to ManagePoliciesPage deep-linking and ManageAutomationsModal entry points that overlap this PR’s manage_automations behavior.
  • fleetdm/fleet#46023: Overlaps ManagePoliciesPage “manage_automations” modal and permission-gating integration.
  • fleetdm/fleet#43756: Earlier CommandPalette/spotlight work introducing isNoAccess gating and keyboard UX that this PR further refines.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fleet UI: Command palette followup' accurately reflects the main objective of this PR, which is a followup from AI review of the previously merged Command Palette work (#43757).
Description check ✅ Passed The PR description is minimal but adequate: it references the related issue (#43757), indicates this is a followup from AI review, and confirms testing was completed with checkboxes marked. However, it lacks detailed explanation of specific changes made.
Linked Issues check ✅ Passed The PR implements key objectives from #43757: refactors permission gating for report automations, improves keyboard shortcut handling for no-access users, fixes fleet-switch URL construction via buildFleetSwitchUrl, adds support for deep-link query parameters across multiple pages with permission checks, and improves test coverage for these features.
Out of Scope Changes check ✅ Passed All changes are scoped to Command Palette functionality and related deep-link handling improvements across pages. Updates to Variables, ManageHostsPage, Scripts, and other pages are consistent with the objective to properly gate and handle deep-link query parameters, which aligns with PR #43757 objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 43757-command-palette-followup

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx (1)

16-19: ⚡ Quick win

Add explicit deep-link tests for add_variable gating and param cleanup.

This suite wires the new props, but it doesn’t verify the new behavior (add_variable=1 opens only for editors and always gets stripped). Adding those two cases would lock in the security/UX contract.

🤖 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 `@frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx` around lines
16 - 19, Add two unit tests that verify deep-link behavior for the add_variable
query param: (1) when rendering the Variables page (the test subject using
baseProps and createMockRouter) as an editor, include location.query = {
add_variable: "1" } and assert the add-variable modal/dialog is opened and that
the router.replace (or router.push used for cleanup) is called to strip the
add_variable param; (2) when rendering as a non-editor with the same
location.query, assert the modal does NOT open and that router.replace is still
called to remove the add_variable param. Mock or stub the permission/role check
you use in the component (so tests can simulate editor vs non-editor), and
verify the query cleanup call and modal-open state in each test.
🤖 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 `@frontend/pages/admin/ManageFleetsPage/ManageFleetsPage.tsx`:
- Around line 68-85: The deep-link useEffect opens/removes the ?create_fleet
param before config is loaded because isCreateFleetDisabled is computed when
config is undefined; change the effect (useEffect) to first guard for config
availability (e.g., return if config is undefined/null), then evaluate
isCreateFleetDisabled and only after that decide to call
setShowCreateFleetModal(true) and call router.replace to strip create_fleet from
location.query; reference isCreateFleetDisabled, useEffect, location.query,
setShowCreateFleetModal, and router.replace so the check and param removal
happen only after config is present.

In `@frontend/pages/SoftwarePage/SoftwarePage.tsx`:
- Around line 246-252: The code returns early when !isAllTeamsSelected &&
!isPrimoMode, which prevents removing the manage_automations query param; always
strip manage_automations from queryParams regardless of the gate. Update the
effect so that extraction and router.replace({ pathname: location.pathname,
query: rest }) (using queryParams and manage_automations) always runs, and only
conditionally call setShowManageAutomationsModal when isGlobalAdmin (keeping
checks on isAllTeamsSelected and isPrimoMode for showing the modal), ensuring
manage_automations is removed from the URL even if the modal is not shown.

---

Nitpick comments:
In `@frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx`:
- Around line 16-19: Add two unit tests that verify deep-link behavior for the
add_variable query param: (1) when rendering the Variables page (the test
subject using baseProps and createMockRouter) as an editor, include
location.query = { add_variable: "1" } and assert the add-variable modal/dialog
is opened and that the router.replace (or router.push used for cleanup) is
called to strip the add_variable param; (2) when rendering as a non-editor with
the same location.query, assert the modal does NOT open and that router.replace
is still called to remove the add_variable param. Mock or stub the
permission/role check you use in the component (so tests can simulate editor vs
non-editor), and verify the query cleanup call and modal-open state in each
test.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ff556984-8e84-4d39-8288-98cb0700e292

📥 Commits

Reviewing files that changed from the base of the PR and between d313300 and 1807830.

⛔ Files ignored due to path filters (1)
  • .claude/rules/fleet-frontend.md is excluded by !**/*.md
📒 Files selected for processing (18)
  • frontend/components/CommandPalette/CommandPalette.tests.tsx
  • frontend/components/CommandPalette/CommandPalette.tsx
  • frontend/components/CommandPalette/groups/automations.ts
  • frontend/components/CommandPalette/groups/commands.ts
  • frontend/components/CommandPalette/groups/derivations.ts
  • frontend/components/CommandPalette/helpers.tests.ts
  • frontend/components/CommandPalette/helpers.ts
  • frontend/pages/DashboardPage/DashboardPage.tsx
  • frontend/pages/ManageControlsPage/Scripts/Scripts.tsx
  • frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx
  • frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx
  • frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx
  • frontend/pages/ManageControlsPage/Variables/Variables.tsx
  • frontend/pages/SoftwarePage/SoftwarePage.tsx
  • frontend/pages/admin/ManageFleetsPage/ManageFleetsPage.tsx
  • frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
  • frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
  • frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx
💤 Files with no reviewable changes (2)
  • frontend/components/CommandPalette/groups/commands.ts
  • frontend/components/CommandPalette/groups/derivations.ts

Comment thread frontend/pages/admin/ManageFleetsPage/ManageFleetsPage.tsx Outdated
Comment thread frontend/pages/SoftwarePage/SoftwarePage.tsx Outdated
On a fresh page load, role flags and config can still be unset when
deep-link effects first run. The previous code treated those falsy
gates as authoritative and stripped the query param, so an authorized
user landing directly on ?add_hosts=1, ?create_fleet=1,
?manage_automations=1, etc. could be silently denied the modal.

Each effect now waits for the relevant slice of AppContext (config,
isGlobalAdmin, isRouteOk) before evaluating the gate or stripping the
param. Software page additionally always strips after gate evaluation
so a team-scoped load doesn't leave manage_automations stuck in the
URL.
@RachelElysia
RachelElysia marked this pull request as ready for review May 29, 2026 13:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
frontend/pages/SoftwarePage/SoftwarePage.tsx (1)

243-259: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add hydration gate for isGlobalAdmin to prevent race condition.

The effect waits for isSoftwareConfigLoaded but not for isGlobalAdmin !== undefined. If config loads before the role flag is set, the modal won't open for an authorized admin and the query param will be stripped prematurely. When isGlobalAdmin later becomes true, the param is already gone and the effect early-returns at line 244.

ManageHostsPage (lines 250, 269 in this PR) follows the standardized pattern by gating on both config readiness and isGlobalAdmin !== undefined. SoftwarePage should match that approach.

🔐 Proposed fix to add the hydration gate
   useEffect(() => {
     if (queryParams?.manage_automations !== "1") return;
     if (!isSoftwareConfigLoaded) return;
+    if (isGlobalAdmin === undefined) return;
     if (isGlobalAdmin && (isAllTeamsSelected || isPrimoMode)) {
       setShowManageAutomationsModal(true);
     }
🤖 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 `@frontend/pages/SoftwarePage/SoftwarePage.tsx` around lines 243 - 259, The
effect in SoftwarePage's useEffect that handles queryParams/manage_automations
needs a hydration gate for isGlobalAdmin: currently it only waits for
isSoftwareConfigLoaded and may strip the query before isGlobalAdmin is known.
Update the early-return logic in the useEffect (the block using queryParams,
isSoftwareConfigLoaded, setShowManageAutomationsModal, and router.replace) to
also return early when isGlobalAdmin === undefined (i.e., wait for isGlobalAdmin
to be hydrated), mirroring the pattern used in ManageHostsPage, and keep
isGlobalAdmin in the dependency list so the modal opens correctly once the flag
becomes defined.
🤖 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.

Outside diff comments:
In `@frontend/pages/SoftwarePage/SoftwarePage.tsx`:
- Around line 243-259: The effect in SoftwarePage's useEffect that handles
queryParams/manage_automations needs a hydration gate for isGlobalAdmin:
currently it only waits for isSoftwareConfigLoaded and may strip the query
before isGlobalAdmin is known. Update the early-return logic in the useEffect
(the block using queryParams, isSoftwareConfigLoaded,
setShowManageAutomationsModal, and router.replace) to also return early when
isGlobalAdmin === undefined (i.e., wait for isGlobalAdmin to be hydrated),
mirroring the pattern used in ManageHostsPage, and keep isGlobalAdmin in the
dependency list so the modal opens correctly once the flag becomes defined.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fd877f6b-25e7-41f6-9da3-e15e04e4f853

📥 Commits

Reviewing files that changed from the base of the PR and between 1807830 and 7eb56ec.

📒 Files selected for processing (4)
  • frontend/pages/DashboardPage/DashboardPage.tsx
  • frontend/pages/SoftwarePage/SoftwarePage.tsx
  • frontend/pages/admin/ManageFleetsPage/ManageFleetsPage.tsx
  • frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/pages/admin/ManageFleetsPage/ManageFleetsPage.tsx
  • frontend/pages/DashboardPage/DashboardPage.tsx

Swap fireEvent.click → user.click for the "View host" navigation test
in the command palette suite. Keep fireEvent for the chevron sub-item
toggle (and add a note explaining why) — cmdk's userEvent-aware
selection handlers fire the parent item's onSelect on user.click,
navigating instead of toggling. A bare fireEvent.click respects the
chevron's stopPropagation.

Also add a ScriptLibrary regression test covering the second-deep-link
flow: arrive via ?add_script=1 → modal opens, simulate the param
strip, user closes the modal, palette pushes ?add_script=1 again →
modal reopens. At the component level the effect handles this
correctly; the test guards against regressions there.
@RachelElysia
RachelElysia marked this pull request as draft May 29, 2026 16:11
@RachelElysia
RachelElysia marked this pull request as ready for review May 29, 2026 17:26
@RachelElysia

Copy link
Copy Markdown
Member Author

@claude review

Comment on lines 574 to +601
useEffect(() => {
const param = queryParams?.manage_automations;
if (!param) return;

switch (param) {
case "webhooks":
setShowOtherWorkflowsModal(true);
break;
case "install_software":
setShowInstallSoftwareModal(true);
break;
case "run_script":
setShowPolicyRunScriptModal(true);
break;
case "calendar":
setShowCalendarEventsModal(true);
break;
case "conditional_access":
setShowConditionalAccessModal(true);
break;
default:
if (canManageAutomations) {
switch (param) {
case "webhooks":
setShowOtherWorkflowsModal(true);
break;
case "install_software":
setShowInstallSoftwareModal(true);
break;
case "run_script":
setShowPolicyRunScriptModal(true);
break;
case "calendar":
setShowCalendarEventsModal(true);
break;
case "conditional_access":
setShowConditionalAccessModal(true);
break;
default:
}
}

const { manage_automations, ...rest } = queryParams;
router.replace({ pathname: location.pathname, query: rest });
}, [queryParams?.manage_automations]);
}, [queryParams, location.pathname, router, canManageAutomations]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 ManagePoliciesPage's new ?manage_automations=… effect (lines 574-601) has the same hydration race that Copilot flagged and you fixed in DashboardPage / ManageHostsPage: canManageAutomations is falsy while AppContext role flags are still undefined, so on a direct/refreshed/shared load of the URL the palette emits, the switch block is skipped, router.replace strips the param, and the authorized admin never sees the modal. Adding the same if (isGlobalAdmin === undefined || !isRouteOk) return; gate you already applied elsewhere fixes it — isRouteOk is already in scope from useTeamIdParam. Variables.tsx and ScriptLibrary.tsx have the same shape but are protected by the AuthAnyMaintainerAdminTechnicianRoutes wrapper on /controls, so the race can't manifest there in practice; still worth a defensive guard for consistency.

Extended reasoning...

The race

canManageAutomations (line 246) resolves to isGlobalAdmin || isGlobalMaintainer || isTeamMaintainer || isTeamAdmin. The first two come from AppContext and start as undefined (frontend/context/app.tsx:235). setPermissions only populates them once both currentUser and config have hydrated (frontend/context/app.tsx:295-297 returns {} when either is null). The team-scoped flags from useTeamIdParam also short-circuit to false until currentTeam?.id is set (frontend/hooks/useTeamIdParam.ts:512-516). So canManageAutomations is falsy on the first render.

The new effect at ManagePoliciesPage.tsx:574-601 has only one guard — if (!param) return;. There is no hydration gate. The effect runs to completion in the falsy-role state, skips the switch block, and unconditionally calls router.replace to strip the param.

Why the previously-merged fixes in the same PR don't cover this one

This PR's commit 7eb56ec5 ("Wait for AppContext hydration before evaluating deep-links") added if (isGlobalAdmin === undefined || !isRouteOk) return; to DashboardPage, ManageHostsPage (×2), ManageFleetsPage, and SoftwarePage after Copilot flagged the identical race. ManagePoliciesPage exposes isRouteOk from the same useTeamIdParam destructure (line 234) but never got the gate — likely an oversight, because the permission check (if (canManageAutomations)) was added in this PR's commit 953899dc and the matching hydration gate was not.

Step-by-step proof for /policies/manage?manage_automations=webhooks

Assume a global admin uses the new palette item this PR ships (manage-policy-automations-webhooks in automations.ts:90-101) on a cold tab.

  1. Initial render: currentUser is undefined, so AuthenticatedRoutes blocks the children — no harm yet.
  2. SET_CURRENT_USER reducer fires after /me resolves. setPermissions runs with state.config == null and returns {} (app.tsx:295-297). So currentUser is now truthy but isGlobalAdmin, isGlobalMaintainer, etc. are still undefined. ManagePoliciesPage mounts.
  3. Effect runs with queryParams.manage_automations === "webhooks", canManageAutomations === undefined. The if (canManageAutomations) branch is skipped. router.replace({ pathname, query: rest }) strips manage_automations.
  4. SET_CONFIG fires when /config resolves. setPermissions now produces real values. canManageAutomations becomes true. The effect re-runs (it depends on canManageAutomations), but queryParams.manage_automations is gone, so the if (!param) return; early-return fires. The modal never opens.

The URL is now clean and the user has no signal that anything went wrong — they simply land on /policies/manage and have to discover Manage automations through the page UI instead.

Impact and reachability

The palette this PR introduces emits exactly these URLs (groups/automations.ts:90-178 builds manage-policy-automations-webhooks, …-install_software, …-run_script, …-calendar, …-conditional_access all pointing at ${paths.MANAGE_POLICIES}?manage_automations=…). Anyone using those palette items, or anyone who bookmarks / shares one, hits the race on cold-tab loads. React 18 batching mitigates most cases where setCurrentUser and setConfig commit together, but Fleet's fetchCurrentUser dispatches setCurrentUser then awaits a separate fetchConfig() network roundtrip — those commits are not batched together, so step 2 above is observable rather than hypothetical.

Fix

Same pattern you applied four times in this PR:

useEffect(() => {
  const param = queryParams?.manage_automations;
  if (!param) return;
  if (isGlobalAdmin === undefined || !isRouteOk) return; // <-- add this
  if (canManageAutomations) { ... switch ... }
  const { manage_automations, ...rest } = queryParams;
  router.replace({ pathname: location.pathname, query: rest });
}, [queryParams, location.pathname, router, canManageAutomations, isGlobalAdmin, isRouteOk]);

Addressing the route-guard refutation for Variables.tsx and ScriptLibrary.tsx

The synthesis bundled in a claim that Variables.tsx (lines 67-79) and ScriptLibrary.tsx (lines 66-75) have the same race. The structure is the same, but routing/index.tsx wraps /controls in AuthAnyMaintainerAdminTechnicianRoutes (line 309), which returns null and schedules a 403 when all six role flags are simultaneously falsy/undefined — i.e., during the exact window the race would otherwise manifest. So the inner effect never runs in the bad state, and the modal-leak / param-strip bug cannot actually be observed on those two pages.

That's why I'm flagging this strictly as a ManagePoliciesPage bug (which is on /policies/manage and has no such wrapper at router/index.tsx:422). The matching guard on Variables.tsx and ScriptLibrary.tsx would be a nice defensive consistency tweak — the rest of the PR adopted the same pattern — but it isn't correctness-blocking on its own, so I wouldn't hold the PR for it.

🔬 also observed by copilot-pull-request-reviewer

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

will followup Monday

@RachelElysia
RachelElysia merged commit def7f00 into main May 29, 2026
27 checks passed
@RachelElysia
RachelElysia deleted the 43757-command-palette-followup branch May 29, 2026 18:33
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.

Fleet spotlight (⌘ + K)

3 participants