Skip to content

Custom host vitals: Controls > Variables tab + secret variable created_at (#48555)#48634

Merged
nulmete merged 5 commits into
44954-custom-host-vitalsfrom
48555-controls-variables-custom-host-vitals
Jul 3, 2026
Merged

Custom host vitals: Controls > Variables tab + secret variable created_at (#48555)#48634
nulmete merged 5 commits into
44954-custom-host-vitalsfrom
48555-controls-variables-custom-host-vitals

Conversation

@nulmete

@nulmete nulmete commented Jul 2, 2026

Copy link
Copy Markdown
Member

Related issue: Resolves #48555

Custom host vitals (#44954) let admins define custom fields that surface on the host details page and can be referenced in scripts and configuration profiles. This PR builds the Controls > Variables frontend: a Global variables / Custom host vitals sub-nav, and the Custom host vitals CRUD tab (add/edit/delete, mock-backed until the API lands). It also exposes created_at on the custom variables list endpoint so the table's Created column renders.

Checklist for submitter

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

Will add changes file in the feature branch directly.

Testing

  • Added/updated automated tests

  • QA'd all new/changed functionality manually

Screen.Recording.2026-07-02.at.1.52.45.PM.mov

Summary by CodeRabbit

  • New Features

    • Added a dedicated “Global variables” and “Custom host vitals” experience under Controls, with navigation between sections.
    • Users can now add, edit, delete, search, and copy values for custom host vitals.
    • Variable values now display clearer tokens and updated-time information.
  • Bug Fixes

    • Improved routing so section-specific Controls URLs load correctly.
    • Added creation timestamps to variable listings for more complete record details.

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 54.72313% with 139 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.97%. Comparing base (a4086dc) to head (59a7c18).

Files with missing lines Patch % Lines
...ntend/services/entities/custom_host_vitals_mock.ts 35.00% 26 Missing ⚠️
...ddCustomHostVitalModal/AddCustomHostVitalModal.tsx 7.40% 25 Missing ⚠️
...tCustomHostVitalModal/EditCustomHostVitalModal.tsx 7.40% 25 Missing ⚠️
.../cards/CustomHostVitalsTab/CustomHostVitalsTab.tsx 69.69% 20 Missing ⚠️
...tend/pages/ManageControlsPage/Variables/helpers.ts 17.39% 19 Missing ⚠️
...ustomHostVitalModal/DeleteCustomHostVitalModal.tsx 11.76% 15 Missing ⚠️
...ariables/cards/GlobalVariables/GlobalVariables.tsx 88.88% 7 Missing ⚠️
...ustomHostVitalsTab/CustomHostVitalsTableConfig.tsx 89.47% 2 Missing ⚠️
Additional details and impacted files
@@                     Coverage Diff                      @@
##           44954-custom-host-vitals   #48634      +/-   ##
============================================================
+ Coverage                     67.90%   67.97%   +0.06%     
============================================================
  Files                          3677     3685       +8     
  Lines                        233665   233897     +232     
  Branches                      12410    12459      +49     
============================================================
+ Hits                         158679   158990     +311     
+ Misses                        60722    60603     -119     
- Partials                      14264    14304      +40     
Flag Coverage Δ
backend 69.65% <100.00%> (+0.10%) ⬆️
frontend 58.85% <54.57%> (-0.11%) ⬇️

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

☔ View full report in Codecov by Harness.
📢 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.

};
}

const Variables = ({ router, location }: IVariablesProps) => {

@nulmete nulmete Jul 2, 2026

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.

this was moved to GlobalVariables.tsx, and is now a Table instead of a PaginatedList (Figma: https://www.figma.com/design/kccHNGXUTi7e6A0L7WIfJh/-44954-Add-edit-delete-custom-host-vitals?node-id=5428-14629&t=3gpZX9MXKbrXMQjB-0)

Variables is now the container for GlobalVariablesTab + CustomHostVitalsTab.

type SecretVariableIdentifier struct {
ID uint `json:"id" db:"id"`
Name string `json:"name" name:"name"`
CreatedAt string `json:"created_at" db:"created_at"`

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.

Following Figma now showing Created as one of the columns for Global Variables.

Comment thread docs/REST API/rest-api.md
Comment on lines +15640 to +15642
"name": "SOME_API_TOKEN",
"created_at": "2020-11-13T22:57:12Z",
"updated_at": "2020-11-13T22:57:12Z"

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.

@rachaelshaw let me know if it's fine to keep this change in this PR or include it as part of #48369

@nulmete nulmete marked this pull request as ready for review July 2, 2026 16:54
@nulmete nulmete requested review from a team and rachaelshaw as code owners July 2, 2026 16:54

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

@nulmete

nulmete commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

This PR restructures the Controls > Variables page into a sub-navigation shell with "Global variables" and "Custom host vitals" sections. Existing custom-variables logic is moved into a new GlobalVariables card. A new CustomHostVitalsTab card is added with list/search/count, empty states, and add/edit/delete modals backed by a new custom_host_vitals frontend service and mock API, new TypeScript interfaces, a new endpoint constant, and new routes/paths. On the backend, ListSecretVariables now selects created_at, and SecretVariableIdentifier gains a CreatedAt field, with corresponding test updates.

Sequence Diagram(s)

See diagrams embedded in the hidden review stack artifact for section routing, service data flow, tab data flow, and modal mutation flow.

Possibly related PRs

  • fleetdm/fleet#46196: Both PRs modify server/fleet/secret_variables.go and secret variable API plumbing, adding created_at support.
  • fleetdm/fleet#48124: Both PRs touch the copy-to-clipboard/token-copy implementation in frontend/pages/ManageControlsPage/Variables/Variables.tsx and its successors.

Suggested labels: frontend, backend, enhancement

Suggested reviewers: (none specified in provided context)

Poem
A rabbit hops through nav-tree new,
Global vars and Vitals, side by side in view.
Tokens copied, forms validated with care,
Modals for add, edit, delete — all there.
Created_at now trails behind each name,
Hop, hop — the Controls page won't be the same! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The main UI work is present, but the global activity-feed items were explicitly deferred, so #48555 is not fully satisfied. Implement the add/edit/delete activity-feed entries for custom host vitals, or document the intentional deferral in the linked issue.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately highlights the Controls > Variables custom host vitals work plus the created_at change.
Description check ✅ Passed The description includes the related issue, summary, testing, and checklist context; the omitted checklist items are noncritical.
Out of Scope Changes check ✅ Passed The code changes align with the Controls > Variables and created_at objectives, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 48555-controls-variables-custom-host-vitals

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.

@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 (5)
frontend/services/entities/custom_host_vitals_mock.ts (2)

68-86: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

No empty-name guard in mock addCustomHostVital.

The mock only rejects duplicates; an empty/whitespace-only trimmed name is accepted and stored. If a caller's client-side validation doesn't strictly mirror this (e.g. validates the untrimmed value), a blank-named vital could slip through to the mock list. Worth double-checking against AddCustomHostVitalModal's validation logic.

🤖 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/services/entities/custom_host_vitals_mock.ts` around lines 68 - 86,
The mock add flow in addCustomHostVital only checks for duplicate names and
still accepts empty or whitespace-only values after trimming. Add the same
empty-name validation used by AddCustomHostVitalModal before creating the
ICustomHostVital, and reject blank inputs consistently so the mock cannot store
a vital with an empty name.

48-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Mock ignores pagination params.

getCustomHostVitals never applies params.page/params.per_page, and meta.has_next_results/has_previous_results are hardcoded to false. Pagination controls in CustomHostVitalsTab won't be exercised meaningfully against this mock until it's swapped for the real API.

🤖 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/services/entities/custom_host_vitals_mock.ts` around lines 48 - 66,
`getCustomHostVitals` in the custom host vitals mock currently ignores
pagination and always returns all filtered items with `meta.has_next_results`
and `meta.has_previous_results` set to false. Update the mock to honor
`IListCustomHostVitalsApiParams.page` and `per_page` by slicing the filtered
results accordingly, and compute the meta flags from the remaining items so
`CustomHostVitalsTab` can exercise pagination behavior realistically.
frontend/pages/ManageControlsPage/Variables/_styles.scss (1)

6-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated __token/__actions/spinner styling between .global-variables and .custom-host-vitals-tab.

Consider extracting a shared SCSS mixin/placeholder for the common token/actions/spinner rules to avoid drift between the two card styles.

🤖 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/_styles.scss` around lines 6 -
50, The `.global-variables` and `.custom-host-vitals-tab` blocks repeat the same
`__token`, `__actions`, and centered spinner styling, so refactor the shared
rules into a reusable SCSS mixin or placeholder. Update the styles in
`_styles.scss` to apply that shared abstraction from both card containers,
keeping only the truly unique bits in each block so the token/actions layout
stays consistent without duplication.
frontend/pages/ManageControlsPage/Variables/components/EditCustomHostVitalModal/EditCustomHostVitalModal.tsx (1)

70-77: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider short-circuiting when the name is unchanged.

onClickSave always calls updateCustomHostVital() even if name.trim() === vital.name, triggering an unnecessary write/round-trip.

♻️ Optional short-circuit
  const onClickSave = () => {
    const validation = validateFormData({ name }, true);
    if (!validation.isValid) {
      setFormValidation(validation);
      return;
    }
+   if (name.trim() === vital.name) {
+     onCancel();
+     return;
+   }
    updateCustomHostVital();
  };
🤖 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/components/EditCustomHostVitalModal/EditCustomHostVitalModal.tsx`
around lines 70 - 77, onClickSave in EditCustomHostVitalModal always triggers
updateCustomHostVital even when the edited name is unchanged. Add a
short-circuit after validateFormData so that, if name.trim() matches vital.name,
the handler returns early and skips the update call; keep the existing
validation and setFormValidation behavior intact.
frontend/pages/ManageControlsPage/Variables/cards/CustomHostVitalsTab/CustomHostVitalsTab.tsx (1)

1-207: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

No test coverage for the new CRUD tab.

Unlike GlobalVariables.tests.tsx mentioned elsewhere in this stack, there's no test file for CustomHostVitalsTab. Given this drives add/edit/delete/search flows with permission and GitOps gating, tests would help lock in behavior once wired to the real API.

🤖 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/cards/CustomHostVitalsTab/CustomHostVitalsTab.tsx`
around lines 1 - 207, Add test coverage for CustomHostVitalsTab, since it now
drives add/edit/delete/search behavior and permission/GitOps gating. Create a
focused test file for the CustomHostVitalsTab component that exercises the key
flows in renderContent, renderAddButton, and the modal handlers (onClickAdd,
onSaveAdd, onSaveEdit, onDeleted). Mock AppContext, useGitOpsMode, and the
customHostVitalsAPI query so you can verify admin vs non-admin behavior,
GitOps-disabled button state, empty-state vs table rendering, search filtering,
and that add/edit/delete actions trigger refetch and close the modals.
🤖 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/ManageControlsPage/Variables/Variables.tsx`:
- Around line 33-44: The redirect guard in Variables.tsx only handles unknown
truthy sections, so the bare variables route never rewrites to the default URL.
Update the logic around currentSection and the router.replace call so the
component also redirects when section is undefined, using DEFAULT_SECTION.path
(and preserving location.search) in the Variables component. Keep the existing
unknown-section fallback behavior, but ensure both missing and invalid sections
land on DEFAULT_SECTION.urlSection.

In `@frontend/router/index.tsx`:
- Around line 344-345: The bare variables route is still rendering Variables
instead of redirecting to global-variables, so /controls/variables never reaches
the intended section. Update the routing in router/index.tsx so the variables
path explicitly redirects to /controls/variables/global-variables, while keeping
variables/:section mapped to Variables for sectioned URLs. Use the existing
Route definitions around Variables to locate and adjust the navigation behavior.

---

Nitpick comments:
In `@frontend/pages/ManageControlsPage/Variables/_styles.scss`:
- Around line 6-50: The `.global-variables` and `.custom-host-vitals-tab` blocks
repeat the same `__token`, `__actions`, and centered spinner styling, so
refactor the shared rules into a reusable SCSS mixin or placeholder. Update the
styles in `_styles.scss` to apply that shared abstraction from both card
containers, keeping only the truly unique bits in each block so the
token/actions layout stays consistent without duplication.

In
`@frontend/pages/ManageControlsPage/Variables/cards/CustomHostVitalsTab/CustomHostVitalsTab.tsx`:
- Around line 1-207: Add test coverage for CustomHostVitalsTab, since it now
drives add/edit/delete/search behavior and permission/GitOps gating. Create a
focused test file for the CustomHostVitalsTab component that exercises the key
flows in renderContent, renderAddButton, and the modal handlers (onClickAdd,
onSaveAdd, onSaveEdit, onDeleted). Mock AppContext, useGitOpsMode, and the
customHostVitalsAPI query so you can verify admin vs non-admin behavior,
GitOps-disabled button state, empty-state vs table rendering, search filtering,
and that add/edit/delete actions trigger refetch and close the modals.

In
`@frontend/pages/ManageControlsPage/Variables/components/EditCustomHostVitalModal/EditCustomHostVitalModal.tsx`:
- Around line 70-77: onClickSave in EditCustomHostVitalModal always triggers
updateCustomHostVital even when the edited name is unchanged. Add a
short-circuit after validateFormData so that, if name.trim() matches vital.name,
the handler returns early and skips the update call; keep the existing
validation and setFormValidation behavior intact.

In `@frontend/services/entities/custom_host_vitals_mock.ts`:
- Around line 68-86: The mock add flow in addCustomHostVital only checks for
duplicate names and still accepts empty or whitespace-only values after
trimming. Add the same empty-name validation used by AddCustomHostVitalModal
before creating the ICustomHostVital, and reject blank inputs consistently so
the mock cannot store a vital with an empty name.
- Around line 48-66: `getCustomHostVitals` in the custom host vitals mock
currently ignores pagination and always returns all filtered items with
`meta.has_next_results` and `meta.has_previous_results` set to false. Update the
mock to honor `IListCustomHostVitalsApiParams.page` and `per_page` by slicing
the filtered results accordingly, and compute the meta flags from the remaining
items so `CustomHostVitalsTab` can exercise pagination behavior realistically.
🪄 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: b9189019-b8d3-4fc8-ae6e-5f5065599659

📥 Commits

Reviewing files that changed from the base of the PR and between a4086dc and 91b951f.

⛔ Files ignored due to path filters (1)
  • docs/REST API/rest-api.md is excluded by !**/*.md
📒 Files selected for processing (26)
  • frontend/interfaces/custom_host_vitals.ts
  • frontend/pages/ManageControlsPage/Variables/Variables.tsx
  • frontend/pages/ManageControlsPage/Variables/VariablesNavItems.tsx
  • frontend/pages/ManageControlsPage/Variables/_styles.scss
  • frontend/pages/ManageControlsPage/Variables/cards/CustomHostVitalsTab/CustomHostVitalsTab.tsx
  • frontend/pages/ManageControlsPage/Variables/cards/CustomHostVitalsTab/CustomHostVitalsTableConfig.tsx
  • frontend/pages/ManageControlsPage/Variables/cards/CustomHostVitalsTab/index.ts
  • frontend/pages/ManageControlsPage/Variables/cards/GlobalVariables/GlobalVariables.tests.tsx
  • frontend/pages/ManageControlsPage/Variables/cards/GlobalVariables/GlobalVariables.tsx
  • frontend/pages/ManageControlsPage/Variables/cards/GlobalVariables/GlobalVariablesTableConfig.tsx
  • frontend/pages/ManageControlsPage/Variables/cards/GlobalVariables/index.ts
  • frontend/pages/ManageControlsPage/Variables/components/AddCustomHostVitalModal/AddCustomHostVitalModal.tsx
  • frontend/pages/ManageControlsPage/Variables/components/AddCustomHostVitalModal/helpers.ts
  • frontend/pages/ManageControlsPage/Variables/components/AddCustomHostVitalModal/index.ts
  • frontend/pages/ManageControlsPage/Variables/components/DeleteCustomHostVitalModal/DeleteCustomHostVitalModal.tsx
  • frontend/pages/ManageControlsPage/Variables/components/DeleteCustomHostVitalModal/index.ts
  • frontend/pages/ManageControlsPage/Variables/components/EditCustomHostVitalModal/EditCustomHostVitalModal.tsx
  • frontend/pages/ManageControlsPage/Variables/components/EditCustomHostVitalModal/index.ts
  • frontend/router/index.tsx
  • frontend/router/paths.ts
  • frontend/services/entities/custom_host_vitals.ts
  • frontend/services/entities/custom_host_vitals_mock.ts
  • frontend/utilities/endpoints.ts
  • server/datastore/mysql/secret_variables.go
  • server/datastore/mysql/secret_variables_test.go
  • server/fleet/secret_variables.go

Comment on lines +33 to +44
const currentSection =
navItems.find((item) => item.urlSection === section) ?? DEFAULT_SECTION;

const onDeleteVariable = () => {
setShowDeleteModal(false);
refetch();
};
// Redirect unknown sections to the default section.
if (
section &&
currentSection === DEFAULT_SECTION &&
section !== DEFAULT_SECTION.urlSection
) {
router.replace(DEFAULT_SECTION.path.concat(location.search));
return null;
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Bare /controls/variables doesn't redirect to /controls/variables/global-variables.

The redirect guard only fires when section is truthy and unrecognized. When section is undefined (i.e., the user hits the bare variables route registered in router/index.tsx), currentSection silently falls back to DEFAULT_SECTION and the component renders that card in place — the URL is never rewritten to /controls/variables/global-variables, contradicting the stated acceptance criterion.

🔧 Proposed fix
   // Redirect unknown sections to the default section.
-  if (
-    section &&
-    currentSection === DEFAULT_SECTION &&
-    section !== DEFAULT_SECTION.urlSection
-  ) {
+  if (!section || (currentSection === DEFAULT_SECTION && section !== DEFAULT_SECTION.urlSection)) {
     router.replace(DEFAULT_SECTION.path.concat(location.search));
     return null;
   }
📝 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 currentSection =
navItems.find((item) => item.urlSection === section) ?? DEFAULT_SECTION;
const onDeleteVariable = () => {
setShowDeleteModal(false);
refetch();
};
// Redirect unknown sections to the default section.
if (
section &&
currentSection === DEFAULT_SECTION &&
section !== DEFAULT_SECTION.urlSection
) {
router.replace(DEFAULT_SECTION.path.concat(location.search));
return null;
}
const currentSection =
navItems.find((item) => item.urlSection === section) ?? DEFAULT_SECTION;
// Redirect unknown sections to the default section.
if (
!section ||
(currentSection === DEFAULT_SECTION &&
section !== DEFAULT_SECTION.urlSection)
) {
router.replace(DEFAULT_SECTION.path.concat(location.search));
return null;
}
🤖 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.tsx` around lines 33 -
44, The redirect guard in Variables.tsx only handles unknown truthy sections, so
the bare variables route never rewrites to the default URL. Update the logic
around currentSection and the router.replace call so the component also
redirects when section is undefined, using DEFAULT_SECTION.path (and preserving
location.search) in the Variables component. Keep the existing unknown-section
fallback behavior, but ensure both missing and invalid sections land on
DEFAULT_SECTION.urlSection.

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.

addressed in f2352ac

Comment thread frontend/router/index.tsx
Comment on lines 344 to +345
<Route path="variables" component={Variables} />
<Route path="variables/:section" component={Variables} />

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Bare variables route never redirects to global-variables.

Per PR objectives, /controls/variables should redirect to /controls/variables/global-variables. With both variables and variables/:section routed to the same Variables component, the redirect entirely depends on client-side logic in Variables.tsx. As flagged there, that logic doesn't fire for the no-section case — see the Variables.tsx comment for details.

🤖 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/router/index.tsx` around lines 344 - 345, The bare variables route
is still rendering Variables instead of redirecting to global-variables, so
/controls/variables never reaches the intended section. Update the routing in
router/index.tsx so the variables path explicitly redirects to
/controls/variables/global-variables, while keeping variables/:section mapped to
Variables for sectioned URLs. Use the existing Route definitions around
Variables to locate and adjust the navigation behavior.

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.

addressed in f2352ac

Comment thread frontend/utilities/endpoints.ts Outdated
@@ -361,4 +361,6 @@ export default {
},
// custom variables endpoints
VARIABLES: `/${API_VERSION}/fleet/custom_variables`,

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.

nit: Maybe we should rename this to GLOBAL_VARIABLES on a different PR to keep the language consistent?

params: IListCustomHostVitalsApiParams
): Promise<IListCustomHostVitalsResponse> {
const { CUSTOM_HOST_VITALS } = endpoints;
const path = `${CUSTOM_HOST_VITALS}?${buildQueryStringFromParams({

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.

This can be simplified by using the getPathWithQueryParams helper

(currentSection === DEFAULT_SECTION &&
section !== DEFAULT_SECTION.urlSection)
) {
router.replace(DEFAULT_SECTION.path.concat(location.search));

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.

per our best practices, this needs to be inside an useEffect hook

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.

makes sense

setShowDeleteModal(false);
refetch();
};
// Redirect the bare route (no section) and unknown sections to the default

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.

Would it make sense to add a redirect directive at the router layer for variables -> global-variables? That would simplify things a little bit here, LMKYT

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.

I think either approach works, but I believe the codebase uses this useEffect pattern catch "bad" routes + preserve URL search parameters.

I did some quick testing by removing the useEffect and instead relying on a catch-all Route + IndexRoute, but:

  • we'd need two onEnter callbacks (repeated) to keep the URL search parameters
  • we'd need to remove the :section
    • keeping :section allows us extending the tabs easily in the future if we wanted to add another tab.
    • if we removed it, we'd have to add the tab and also declare it as a route

What I tried:

<Route path="variables">
  <IndexRoute
    onEnter={(nextState, replace) =>
      replace(
        `${PATHS.CONTROLS_VARIABLES_GLOBAL_VARIABLES}${nextState.location.search}`
      )
    }
  />
  <Route path="global-variables" component={Variables} />
  <Route path="custom-host-vitals" component={Variables} />
  <Route
    path="*"
    onEnter={(nextState, replace) =>
      replace(
        `${PATHS.CONTROLS_VARIABLES_GLOBAL_VARIABLES}${nextState.location.search}`
      )
    }
  />
</Route>

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.

let me know if you have a strong preference, @juan-fdz-hawa -- but I'm tempted to keep the useEffect as I see it a little bit simpler and already centralized

className={`${baseClass}__side-nav`}
navItems={navItems.map((navItem) => ({
...navItem,
path: navItem.path.concat(location.search),

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.

nit: "${navItem.path}${location.search}"

setShowAddModal(false);
refetch();
};
const currentSection =

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.

Decomposing this will allow you to simplify the "redirect to default" guard bellow:

const defaultSection = navItems[0];                                                                                                                                                                                                                       
const matchedSection = navItems.find((item) => item.urlSection === section);                                                                                                                                                                              
const currentSection = matchedSection ?? defaultSection;                                                                                                                                                                                                  
...
useEffect(() => {                                                                                                                                                                                                                                         
    if (section && !matchedSection) {                                                                                                                                                                                                                       
      router.replace(`${defaultSection.path}${location.search}`);                                                                                                                                                                                           
    }                                                                                                                                                                                                                                                       
  }, [section, matchedSection, defaultSection.path, location.search, router]); 

label="Name"
name="name"
error={formValidation.name?.message}
helpText="This will be the vital's label on the host detail page."

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.

Should we add a max length constraint here? inputOptions={{ maxLength: NAME_MAX_LENGTH }}

label="Name"
name="name"
error={formValidation.name?.message}
helpText="This will be the vital's label on the host detail page."

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.

Should we add a max length constraint here? inputOptions={{ maxLength: NAME_MAX_LENGTH }}

import {
validateFormData,
ICustomHostVitalFormValidation,
} from "../AddCustomHostVitalModal/helpers";

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.

I think you should move the helper to some other place for cleaner semantics, if the helper is going to be used by both the AddCustomHostVitalModal and EditCustomHostVitalModal components, then it should be in a place common to both.

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.

yeah honestly I kind of one-shotted this and didn't pay much attention to the final architecture, but I agree and will move this to maybe a helpers file one level up from where it's used

@juan-fdz-hawa

Copy link
Copy Markdown
Contributor

There are some gaps on the enforcement of GitOps mode. For Global variables, the in-page "Add variable" button is correctly disabled in GitOps mode, but there are ways to bypass this via either Command-palette entry or deep-link.

Global variables are also deletable on GitOps mode (not sure if that should be changed here).

@juan-fdz-hawa

Copy link
Copy Markdown
Contributor

You might want to update the command palette to include commands for the custom host vitals functionality added here - that could be done in a separate PR, up to you

@nulmete

nulmete commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

There are some gaps on the enforcement of GitOps mode. For Global variables, the in-page "Add variable" button is correctly disabled in GitOps mode, but there are ways to bypass this via either Command-palette entry or deep-link.

Global variables are also deletable on GitOps mode (not sure if that should be changed here).

I tried not to change logic of Global variables -- just moving it to the new GlobalVariables.tsx file, but you make a good point and I will revisit with Rachael to see what is the expectation for Global variables.

You might want to update the command palette to include commands for the custom host vitals functionality added here - that could be done in a separate PR, up to you

Yeah I have a separate story for that 👍 (was thinking of including it here but don't want this to be a monster of a PR): #48686

@nulmete nulmete requested a review from juan-fdz-hawa July 3, 2026 14:05
@nulmete nulmete merged commit 6525921 into 44954-custom-host-vitals Jul 3, 2026
39 checks passed
@nulmete nulmete deleted the 48555-controls-variables-custom-host-vitals branch July 3, 2026 14:20
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.

3 participants