Skip to content

Fleet UI: New empty states - #43896

Merged
lukeheath merged 14 commits into
mainfrom
35483-new-empty-states
Apr 27, 2026
Merged

Fleet UI: New empty states#43896
lukeheath merged 14 commits into
mainfrom
35483-new-empty-states

Conversation

@RachelElysia

@RachelElysia RachelElysia commented Apr 21, 2026

Copy link
Copy Markdown
Member

Issue

Closes #35483

Description

  • Update all non-inline empty states to use component

Screenrecording

Watch in 2x

https://fleetdm.zoom.us/clips/share/fqdB2Hq9QRGCat3mt-j_wA

Checklist for submitter

If some of the following don't apply, delete the relevant line.

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

Testing

  • Added/updated automated tests
    • No time, will do a followup PR for unit test of EmptyState.tsx
  • QA'd all new/changed functionality manually

Summary by CodeRabbit

  • New Features

    • Added a reusable Empty State component and Storybook examples for interactive variants.
  • Visual Improvements

    • Refreshed, more consistent empty-state design (typography, spacing, layout, and variants) across tables, modals, dashboards, and setup pages.
  • Chores

    • Replaced numerous bespoke empty UIs with the shared Empty State to unify behavior and simplify maintenance.

@codecov

codecov Bot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.79487% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.77%. Comparing base (d16e724) to head (d22700b).
⚠️ Report is 24 commits behind head on main.

Files with missing lines Patch % Lines
...maryCard/TitleVersionsTable/TitleVersionsTable.tsx 33.33% 2 Missing and 2 partials ⚠️
frontend/pages/DashboardPage/cards/Munki/Munki.tsx 0.00% 2 Missing ⚠️
...ails/cards/LocalUserAccounts/LocalUserAccounts.tsx 0.00% 2 Missing ⚠️
...es/hosts/details/cards/MunkiIssues/MunkiIssues.tsx 0.00% 2 Missing ⚠️
frontend/components/TargetsInput/TargetsInput.tsx 0.00% 1 Missing ⚠️
frontend/pages/DashboardPage/cards/MDM/MDM.tsx 50.00% 1 Missing ⚠️
...s/DashboardPage/cards/OperatingSystems/OSTable.tsx 0.00% 1 Missing ⚠️
...figProfileStatusTable/ConfigProfileStatusTable.tsx 0.00% 1 Missing ⚠️
...rience/cards/BootstrapPackage/BootstrapPackage.tsx 0.00% 1 Missing ⚠️
...ts/BootstrapPackageTable/BootstrapPackageTable.tsx 0.00% 1 Missing ⚠️
... and 6 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #43896      +/-   ##
==========================================
- Coverage   66.78%   66.77%   -0.01%     
==========================================
  Files        2627     2626       -1     
  Lines      211157   211169      +12     
  Branches     9422     9547     +125     
==========================================
- Hits       141017   141007      -10     
- Misses      57321    57343      +22     
  Partials    12819    12819              
Flag Coverage Δ
frontend 54.74% <71.79%> (-0.07%) ⬇️

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.

@lukeheath

Copy link
Copy Markdown
Member

@RachelElysia I tried to watch your video but I'm getting a black screen with audio only. Is the video working for you?

@RachelElysia

Copy link
Copy Markdown
Member Author

@lukeheath !? Me neither 🫠 I'll re-record Monday, fix any merge conflicts and ping ya again

@RachelElysia
RachelElysia force-pushed the 35483-new-empty-states branch from b8f987e to bfc9385 Compare April 27, 2026 15:11
@RachelElysia
RachelElysia marked this pull request as ready for review April 27, 2026 17:11
Copilot AI review requested due to automatic review settings April 27, 2026 17:11
@RachelElysia
RachelElysia requested a review from a team as a code owner April 27, 2026 17:11

@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 normalizes Fleet’s non-inline UI empty states by introducing a new EmptyState component (with a “ghost table” backdrop) and swapping many existing EmptyTable/custom empty-state renderers to use it.

Changes:

  • Added frontend/components/EmptyState (component, styles, and Storybook stories).
  • Replaced EmptyTable and various bespoke empty-state markup across pages/tables/modals with EmptyState.
  • Removed GenericMsgWithNavButton and updated call sites to render EmptyState + a button directly.

Reviewed changes

Copilot reviewed 101 out of 102 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx Replace “no results” message with EmptyState.
frontend/pages/queries/details/components/QueryReport/QueryReport.tsx Replace table empty state with EmptyState.
frontend/pages/queries/details/components/NoResults/NoResults.tsx Replace EmptyTable with EmptyState for query-results empty states.
frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx Use EmptyState for reports table empty state.
frontend/pages/policies/edit/components/PolicyResultsTable/PolicyResultsTable.tsx Swap custom empty markup for EmptyState.
frontend/pages/policies/edit/components/PolicyResults/PolicyResults.tsx Replace “no results” message with EmptyState.
frontend/pages/policies/edit/components/PolicyErrorsTable/PolicyErrorsTable.tsx Swap custom empty markup for EmptyState.
frontend/pages/policies/ManagePoliciesPage/components/PolicyRunScriptModal/PolicyRunScriptModal.tsx Use EmptyState when no scripts are available.
frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx Use EmptyState for policies table empty state.
frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/_styles.scss Remove styles for old “no software” empty markup.
frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx Use EmptyState when no installable software titles exist.
frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTable.tsx Use EmptyState for packs table empty state.
frontend/pages/labels/ManageLabelsPage/LabelsTable/LabelsTable.tsx Use EmptyState for labels table empty state.
frontend/pages/hosts/details/cards/Software/SelfService/components/SelfServiceTiles/SelfServiceTiles.tsx Use EmptyState for self-service search-empty state.
frontend/pages/hosts/details/cards/Software/SelfService/components/SelfServiceTable/SelfServiceTable.tsx Use EmptyState for self-service table empty state.
frontend/pages/hosts/details/cards/Software/SelfService/SelfServiceCard/SelfServiceCard.tsx Replace EmptyTable with EmptyState for error/empty UI.
frontend/pages/hosts/details/cards/Software/SelfService/SelfServiceCard/SelfServiceCard.tests.tsx Update assertions for adjusted empty-state copy.
frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx Update empty-state component import to EmptyState.
frontend/pages/hosts/details/cards/Queries/_styles.scss Remove styles used only by old custom empty markup.
frontend/pages/hosts/details/cards/Queries/HostQueries.tsx Replace custom empty markup with EmptyState.
frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx Replace EmptyTable with EmptyState for unsupported/empty states.
frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssues.tsx Replace EmptyTable with EmptyState.
frontend/pages/hosts/details/cards/LocalUserAccounts/LocalUserAccounts.tsx Replace EmptyTable with EmptyState in multiple states.
frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibraryTable/HostSoftwareLibraryTable.tsx Replace EmptyTable with EmptyState for unsupported hosts.
frontend/pages/hosts/details/cards/Activity/EmptyFeed/_styles.scss Remove bespoke empty-feed styles (now EmptyState).
frontend/pages/hosts/details/cards/Activity/EmptyFeed/EmptyFeed.tsx Replace custom empty-feed markup with EmptyState.
frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx Replace EmptyTable empty states with EmptyState.
frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss Remove bespoke “no queries” styles (now EmptyState).
frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/SelectQueryModal.tsx Replace “no queries” blocks with EmptyState.
frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/RunScriptModal.tsx Replace EmptyTable with EmptyState.
frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx Replace EmptyTable unsupported-software state with EmptyState.
frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTable.tsx Replace EmptyTable with EmptyState.
frontend/pages/hosts/ManageHostsPage/components/RunScriptBatchModal/RunScriptBatchModal.tsx Replace EmptyTable with EmptyState and set variant.
frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx Replace EmptyTable usage with EmptyState.
frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx Replace EmptyTable with EmptyState.
frontend/pages/admin/TeamManagementPage/components/EmptyTeamsTable.tsx Replace EmptyTable with EmptyState.
frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/components/EmptyUsersTable.tsx Replace EmptyTable with EmptyState (both search/non-search).
frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/EndUserMigrationSection/EndUserMigrationSection.tsx Replace EmptyTable with EmptyState.
frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsAutomaticEnrollmentPage/_styles.scss Remove now-unused empty-tenant modifier selector.
frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsAutomaticEnrollmentPage/WindowsAutomaticEnrollmentPage.tsx Replace card-wrapped empty message with EmptyState.
frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/VppPage.tsx Replace EmptyTable/GenericMsgWithNavButton with EmptyState.
frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/_styles.scss Remove bespoke “add ABM” empty styles.
frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/AppleBusinessManagerPage.tsx Replace GenericMsgWithNavButton and custom empty markup with EmptyState.
frontend/pages/admin/IntegrationsPage/cards/Integrations/components/EmptyIntegrationsTable.tsx Replace EmptyTable with EmptyState.
frontend/pages/admin/IntegrationsPage/cards/Integrations/Integrations.tsx Remove redundant Card wrapper around empty state.
frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityCard/_styles.scss Remove bespoke add-CA empty styles.
frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityCard/AddCertAuthorityCard.tsx Replace Card-based empty UI with EmptyState.
frontend/pages/SoftwarePage/components/tables/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx Replace EmptyTable with EmptyState for vuln empty states.
frontend/pages/SoftwarePage/components/tables/SoftwareVulnerabilitiesTable/EmptyVulnerabilitiesTable/EmptyVulnerabilitiesTable.tsx Replace EmptyTable with EmptyState and adjust empty-state logic.
frontend/pages/SoftwarePage/components/tables/OSKernelsTable/OSKernelsTable.tsx Replace EmptyTable with EmptyState.
frontend/pages/SoftwarePage/components/tables/EmptySoftwareTable/EmptySoftwareTable.tsx Replace EmptyTable with EmptyState.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTable.tsx Replace card-wrapped EmptyTable with EmptyState.
frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTable.tsx Replace EmptyTable with EmptyState.
frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore/SoftwareAppStoreVpp/_styles.scss Remove bespoke VPP empty-state styles.
frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tsx Replace VPP empty-state markup with EmptyState.
frontend/pages/ManageControlsPage/Variables/Variables.tsx Replace EmptyTable with EmptyState for variables empty state.
frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx Loosen assertion to match updated empty-state copy.
frontend/pages/ManageControlsPage/SetupExperience/cards/Users/Users.tsx Minor import/format cleanup.
frontend/pages/ManageControlsPage/SetupExperience/cards/SetupAssistant/SetupAssistant.tsx Replace GenericMsgWithNavButton with EmptyState for prerequisites.
frontend/pages/ManageControlsPage/SetupExperience/cards/RunScript/RunScript.tsx Replace GenericMsgWithNavButton with EmptyState for prerequisites.
frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareTable/InstallSoftwareTable.tsx Replace EmptyTable with EmptyState.
frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareForm/InstallSoftwareForm.tsx Replace EmptyTable with EmptyState.
frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/InstallSoftware.tsx Replace GenericMsgWithNavButton with EmptyState for prerequisites.
frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageTable/BootstrapPackageTable.tsx Replace EmptyTable with EmptyState.
frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/BootstrapPackage.tsx Replace GenericMsgWithNavButton with EmptyState for prerequisites.
frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/_styles.scss Remove bespoke empty-scripts styling.
frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx Use EmptyState for empty script library state.
frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/_styles.scss Remove bespoke empty styles.
frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tsx Use EmptyState for batch progress tab empty states.
frontend/pages/ManageControlsPage/Scripts/ScriptBatchDetailsPage/_styles.scss Remove bespoke empty styles.
frontend/pages/ManageControlsPage/Scripts/ScriptBatchDetailsPage/ScriptBatchDetailsPage.tsx Use EmptyState for batch details tab empty states.
frontend/pages/ManageControlsPage/OSUpdates/components/OSVersionsEmptyState/_styles.scss Remove now-unused empty-table margin styles.
frontend/pages/ManageControlsPage/OSUpdates/components/OSVersionsEmptyState/OSVersionsEmptyState.tsx Replace card-wrapped EmptyTable with EmptyState.
frontend/pages/ManageControlsPage/OSUpdates/OSUpdates.tsx Replace GenericMsgWithNavButton with EmptyState for prerequisites.
frontend/pages/ManageControlsPage/OSSettings/cards/Passwords/Passwords.tsx Replace GenericMsgWithNavButton with EmptyState for prerequisites.
frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTable.tsx Replace EmptyTable with EmptyState.
frontend/pages/ManageControlsPage/OSSettings/cards/ConfigurationProfiles/components/ConfigProfileStatusTable/ConfigProfileStatusTable.tsx Replace EmptyTable with EmptyState.
frontend/pages/ManageControlsPage/OSSettings/cards/ConfigurationProfiles/ConfigurationProfiles.tsx Replace GenericMsgWithNavButton with EmptyState for prerequisites.
frontend/pages/ManageControlsPage/OSSettings/cards/Certificates/Certificates.tsx Replace GenericMsgWithNavButton with EmptyState for prerequisites.
frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tsx Replace EmptyTable with EmptyState.
frontend/pages/DashboardPage/cards/Munki/Munki.tsx Replace EmptyTable with EmptyState.
frontend/pages/DashboardPage/cards/MDM/MDM.tsx Replace EmptyTable with EmptyState.
frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx Replace EmptyTable with EmptyState and set variant.
frontend/components/queries/PackQueriesTable/PackQueriesTable.tsx Replace bespoke “no queries” block and search-empty state with EmptyState.
frontend/components/queries/LiveResults/AwaitingResults/AwaitingResults.tsx Replace EmptyTable with EmptyState.
frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/_styles.scss Remove bespoke “no hosts” styles.
frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/SelectTargetsMenu.jsx Replace bespoke “no hosts” copy block with EmptyState.
frontend/components/TargetsInput/_styles.scss Remove bespoke empty-search styles.
frontend/components/TargetsInput/TargetsInput.tsx Replace bespoke empty-search markup with EmptyState.
frontend/components/TargetLabelSelector/TargetLabelSelector.tsx Add note explaining why EmptyState isn’t used.
frontend/components/TabNav/_styles.scss Remove styles for deprecated .no-results-message markup.
frontend/components/LiveQuery/SelectTargets.tsx Add note explaining why EmptyState isn’t used.
frontend/components/GenericMsgWithNavButton/index.ts Remove deprecated component export.
frontend/components/GenericMsgWithNavButton/_styles.scss Remove deprecated component styles.
frontend/components/GenericMsgWithNavButton/GenericMsgWithNavButton.tsx Remove deprecated component implementation.
frontend/components/GenericMsgWithNavButton/GenericMsgWithNavButton.tests.tsx Remove deprecated component tests.
frontend/components/EnrollSecrets/EnrollSecretModal/EnrollSecretModal.tsx Replace EmptyTable with EmptyState for enroll secrets empty state.
frontend/components/EmptyState/index.ts New barrel export for EmptyState.
frontend/components/EmptyState/_styles.scss New EmptyState ghost-table styling and variants.
frontend/components/EmptyState/EmptyState.tsx New EmptyState component implementation.
frontend/components/EmptyState/EmptyState.stories.tsx New Storybook stories covering variants/widths/buttons.
changes/35483-empty-states Changelog entry for updated empty states.
Comments suppressed due to low confidence (1)

frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx:1767

  • EmptyState doesn't define a graphicName prop, but the object passed here includes graphicName. This will fail type-checking and break the build. Either remove graphicName from the props passed to EmptyState (and stop basing this on IEmptyTableProps), or add graphicName to IEmptyStateProps if you need it.

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

Comment on lines +96 to 105
<EmptyState
variant="list"
header="Additional configuration required"
info="Supported on macOS. To customize, first turn on automatic enrollment."
path={PATHS.ADMIN_INTEGRATIONS_MDM}
buttonText="Turn on"
router={router}
primaryButton={
<Button onClick={() => router.push(PATHS.ADMIN_INTEGRATIONS_MDM)}>
Turn on
</Button>
}
/>

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

This renders a "Turn on" button unconditionally. Since the Setup Experience area isn't role-restricted (only premium-gated), non-global admins may see a CTA that navigates to an admin-only page. If you want to preserve the previous behavior from GenericMsgWithNavButton, gate primaryButton on isGlobalAdmin (via AppContext) or otherwise ensure only authorized roles see the button.

Copilot uses AI. Check for mistakes.

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.

ClaudeCode:
Whoever is generating these review comments doesn't seem aware of the route-level and tab-level guards. You can push back on both with the same reasoning:

  • Route guard: AuthAnyMaintainerAdminTechnicianRoutes blocks observers/observer+
  • Tab filter: technicians are excluded from Setup Experience tabs
  • Only admins and maintainers reach these components, and both can access the MDM integrations page

Comment on lines +137 to 146
<EmptyState
variant="form"
header="Additional configuration required"
info="To customize, first turn on automatic enrollment."
buttonText="Turn on"
path={PATHS.ADMIN_INTEGRATIONS_MDM}
router={router}
primaryButton={
<Button onClick={() => router.push(PATHS.ADMIN_INTEGRATIONS_MDM)}>
Turn on
</Button>
}
/>

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

This "Turn on" CTA is now always rendered. Previously, the removed GenericMsgWithNavButton only showed the navigation button to global admins; SetupExperience itself doesn't restrict access by role (it only checks isPremiumTier). Consider restoring the role-gating (e.g., primaryButton={isGlobalAdmin ? ... : undefined}) to avoid showing a non-functional admin-only action to non-global admins.

Copilot uses AI. Check for mistakes.

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.

There are two layers of protection:

  1. Route level (AuthAnyMaintainerAdminTechnicianRoutes) — blocks observers and observer+ from reaching /controls at all
  2. Tab level (lines 120-122 in ManageControlsPage) — further blocks technicians from the Setup Experience tab

Only admins and maintainers (global or team) can reach the "Turn on" button. Both roles have permission to navigate to the MDM integrations page it links to. So the claim that there's a missing role-gate
is not accurate — the button is already properly restricted.

Comment on lines +220 to +228
<EmptyState
variant="form"
header="Additional configuration required"
info="Supported on macOS. To customize, first turn on automatic enrollment."
buttonText="Turn on"
path={PATHS.ADMIN_INTEGRATIONS_MDM}
router={router}
primaryButton={
<Button onClick={() => router.push(PATHS.ADMIN_INTEGRATIONS_MDM)}>
Turn on
</Button>
}

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

The empty-state "Turn on" button is now always shown. The old GenericMsgWithNavButton component gated this CTA to global admins, and SetupExperience doesn't appear to restrict access by role. To avoid presenting an action that non-global admins can't complete, consider making primaryButton conditional on isGlobalAdmin (AppContext) or handling authorization at the route level.

Copilot uses AI. Check for mistakes.

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.

Same thing — same wrong premise, same answer. All three of these (SetupAssistant, RunScript, BootstrapPackage) are cards within Setup Experience, so they all inherit the same route-level and tab-level
protection. The review comment is copy-paste of the same incorrect analysis.

Comment thread frontend/pages/queries/details/components/NoResults/NoResults.tsx
@coderabbitai

coderabbitai Bot commented Apr 27, 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: 01b924ae-eb84-4853-b153-e803e68c71ea

📥 Commits

Reviewing files that changed from the base of the PR and between 02e9e94 and d22700b.

📒 Files selected for processing (1)
  • frontend/components/EnrollSecrets/EnrollSecretModal/EnrollSecretModal.tsx
✅ Files skipped from review due to trivial changes (1)
  • frontend/components/EnrollSecrets/EnrollSecretModal/EnrollSecretModal.tsx

Walkthrough

Introduces a new reusable EmptyState React component (TSX + SCSS) with Storybook stories and a new IEmptyStateProps interface (removing graphicName). Replaces many bespoke empty-state UIs across the frontend by swapping usages of EmptyTable and GenericMsgWithNavButton for EmptyState, updates imports/usages, and deletes or trims local SCSS rules tied to removed markup. Adds changelog entry changes/35483-empty-states. No public API removals beyond renaming the empty-state interface.

Possibly related PRs

  • fleetdm/fleet PR 41929: Modifies the same OS Settings/Passwords file’s empty-state handling, overlapping with this PR’s EmptyState substitution.
  • fleetdm/fleet PR 31750: Adds/changes ScriptBatchProgress and related scripts UI; overlaps where this PR replaced local script empty states with EmptyState.
  • fleetdm/fleet PR 37314: Implements Certificates-related UI changes in OS Settings; intersects with this PR’s refactor of Certificates empty-state rendering.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete per the template: missing changes file confirmation, missing automated test completion status details, and missing several security/database/configuration checklist items. Add a changes file entry, complete all applicable checklist items, and clarify automated testing plans in the description or follow-up.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fleet UI: New empty states' directly describes the primary change of updating empty state UI components throughout the Fleet UI.
Linked Issues check ✅ Passed The PR successfully replaces all non-inline empty state UIs with the unified EmptyState component across 80+ files, implementing the normalization goal and design alignment from issue #35483.
Out of Scope Changes check ✅ Passed All changes align with the stated objective of updating non-inline empty states to use the EmptyState component; no out-of-scope modifications identified beyond this primary goal.
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 35483-new-empty-states

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: 7

Caution

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

⚠️ Outside diff range comments (11)
frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx (1)

33-64: ⚠️ Potential issue | 🟡 Minor

Remove unused imports.

EmptyState, CustomLink, and SUPPORT_LINK are imported on Lines 33–35 but never used in this file. EmptySoftwareTable (which EmptyComponent correctly delegates to) already handles those dependencies internally. Remove the three unused imports to reduce bundle noise and avoid linter warnings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx`
around lines 33 - 64, The file imports unused symbols EmptyState, CustomLink,
and SUPPORT_LINK; remove those three unused imports from the top of
HostSoftwareTable.tsx to eliminate lint warnings and reduce bundle noise—locate
the import block that currently declares EmptyState, CustomLink, and
SUPPORT_LINK and delete those identifiers so only the actually used imports
(e.g., EmptySoftwareTable, PLATFORM_DISPLAY_NAMES, isVulnUnsupportedPlatform if
present) remain; verify EmptyComponent and EmptySoftwareTable still compile and
run after the removal.
frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tsx (1)

19-26: ⚠️ Potential issue | 🟡 Minor

Header fallback never triggers — produces "No undefined operating systems detected".

In the changed JSX:

header={`No${
  ` ${PLATFORM_DISPLAY_NAMES[platform]}` || ""
} operating systems detected`}

The inner template literal ` ${PLATFORM_DISPLAY_NAMES[platform]}` is always a non-empty string (it has a leading space), so the || "" short-circuit is dead code. When selectedPlatform is not in PLATFORM_DISPLAY_NAMES (e.g., "all"), the rendered header becomes "No undefined operating systems detected" instead of "No operating systems detected".

This pre-dates the PR but sits inside the changed <EmptyState> expression — worth fixing while you’re here.

🛠 Proposed fix
-const EmptyOS = (platform: PlatformValueOptions): JSX.Element => (
-  <EmptyState
-    className={`${baseClass}__os-empty-table`}
-    header={`No${
-      ` ${PLATFORM_DISPLAY_NAMES[platform]}` || ""
-    } operating systems detected`}
-    info="This report is updated every hour to protect the performance of your
-  devices."
-  />
-);
+const EmptyOS = (platform: PlatformValueOptions): JSX.Element => {
+  const platformName = PLATFORM_DISPLAY_NAMES[platform];
+  return (
+    <EmptyState
+      className={`${baseClass}__os-empty-table`}
+      header={`No${platformName ? ` ${platformName}` : ""} operating systems detected`}
+      info="This report is updated every hour to protect the performance of your devices."
+    />
+  );
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tsx` around lines
19 - 26, The header builds a string that can render "undefined" because the
inner template literal (` ${PLATFORM_DISPLAY_NAMES[platform]}`) always returns a
non-empty string; update the logic so the platform display name is only appended
when it exists — e.g., compute a displayName variable from
PLATFORM_DISPLAY_NAMES[platform] and use displayName ? ` No ${displayName}
operating systems detected` (or construct `No${displayName ? ` ${displayName}` :
""} operating systems detected`) in the EmptyState header; target the header
expression near the EmptyState JSX and the PLATFORM_DISPLAY_NAMES/platform usage
to implement this conditional.
frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTable.tsx (1)

92-98: ⚠️ Potential issue | 🟡 Minor

Typo in user-facing empty-state copy.

"Expecting to status data?" is missing a verb. Likely intended "Expecting to see status data?", matching the wording used by other empty-state messages in this PR (e.g., the new EmptyUsersTable's "Expecting to see users?").

✏️ Proposed fix
         emptyComponent={() => (
           <EmptyState
             header="No disk encryption status"
-            info="Expecting to status data? Try again in a few seconds as the system
-              catches up."
+            info="Expecting to see status data? Try again in a few seconds as the system catches up."
           />
         )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTable.tsx`
around lines 92 - 98, In DiskEncryptionTable update the user-facing EmptyState
copy: replace the malformed string "Expecting to status data? Try again in a few
seconds as the system catches up." with "Expecting to see status data? Try again
in a few seconds as the system catches up." so the EmptyState (used in
DiskEncryptionTable) matches the phrasing used elsewhere (e.g.,
EmptyUsersTable).
frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTable.tsx (1)

4-4: ⚠️ Potential issue | 🔴 Critical

Remove IEmptyTableProps import and refactor emptyState() to match EmptyState's actual prop signature.

EmptyState uses IEmptyStateProps, which does not include graphicName. The code currently types the emptyState() result as IEmptyTableProps (the old interface for the EmptyTable component) and passes graphicName={emptyState().graphicName} on line 135, which EmptyState silently ignores. Additionally, calling emptyState() four times per render (lines 135–138) unnecessarily reconstructs the primaryButton <Button> JSX each time—call it once instead.

🔧 Proposed fix
-import { IEmptyTableProps } from "interfaces/empty_table";
 import Button from "components/buttons/Button";
@@
-  // TODO: useCallback search string
-  const emptyState = () => {
-    const emptyPacks: IEmptyTableProps = {
-      graphicName: "empty-packs",
-      header: "You don't have any packs",
-      info:
-        "Query packs allow you to schedule recurring queries for your hosts.",
-      primaryButton: (
-        <Button
-          className={`${baseClass}__create-button`}
-          onClick={onCreatePackClick}
-        >
-          Create new pack
-        </Button>
-      ),
-    };
-    if (searchString) {
-      delete emptyPacks.graphicName;
-      emptyPacks.header = "No packs match the current search criteria";
-      emptyPacks.info =
-        "Expecting to see packs? Try again in a few seconds as the system catches up.";
-      delete emptyPacks.primaryButton;
-    }
-    return emptyPacks;
-  };
+  const emptyState = () => {
+    if (searchString) {
+      return {
+        header: "No packs match the current search criteria",
+        info:
+          "Expecting to see packs? Try again in a few seconds as the system catches up.",
+        primaryButton: undefined,
+      };
+    }
+    return {
+      header: "You don't have any packs",
+      info: "Query packs allow you to schedule recurring queries for your hosts.",
+      primaryButton: (
+        <Button
+          className={`${baseClass}__create-button`}
+          onClick={onCreatePackClick}
+        >
+          Create new pack
+        </Button>
+      ),
+    };
+  };
@@
-        emptyComponent={() => (
-          <EmptyState
-            graphicName={emptyState().graphicName}
-            header={emptyState().header}
-            info={emptyState().info}
-            primaryButton={emptyState().primaryButton}
-          />
-        )}
+        emptyComponent={() => {
+          const empty = emptyState();
+          return (
+            <EmptyState
+              header={empty.header}
+              info={empty.info}
+              primaryButton={empty.primaryButton}
+            />
+          );
+        }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTable.tsx` at
line 4, Remove the unused import IEmptyTableProps and update emptyState() to
return the actual EmptyState prop shape (IEmptyStateProps) instead of
IEmptyTableProps; stop passing the non-existent graphicName prop to <EmptyState>
and instead pass only the props defined on IEmptyStateProps. Also build the
primaryButton JSX once (create const { primaryButton, ...emptyProps } =
emptyState(); or cache the result in a variable) and reuse that primaryButton
when rendering rather than calling emptyState() multiple times; update all
references (emptyState(), primaryButton, EmptyState) accordingly.
frontend/components/queries/PackQueriesTable/PackQueriesTable.tsx (1)

86-128: ⚠️ Potential issue | 🟡 Minor

Inconsistent terminology: "query" vs. "report" within the same component.

This change introduces mixed labels:

  • Empty-search header (line 88): "No queries match your search criteria"
  • Toolbar action button (lines 94–95): name: "add query", buttonText: "Add query"
  • No-data empty state (line 115): "Your pack has no reports"
  • No-data CTA (line 123): "Add report"

Per frontend/pages/hosts/details/cards/Queries/HostQueries.tsx:71-76,141-150, the established pattern is "report" for scheduled queries in the UI. Either align all four labels to "report" (and consider the modal title "Add query" in frontend/pages/packs/EditPackPage/components/PackQueryEditorModal/PackQueryEditorModal.tsx:186), or revert the empty-state CTA back to "Add query" so a single page doesn't switch terminology mid-flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/queries/PackQueriesTable/PackQueriesTable.tsx` around
lines 86 - 128, The component uses mixed terminology ("query" vs "report")
across PackQueriesTable: normalize labels to "report" to match the rest of the
app (e.g., HostQueries) — update the EmptyState header, actionButton.name,
actionButton.buttonText, and primaryButton text in PackQueriesTable to use
"report" instead of "query"; also review PackQueryEditorModal (Add query modal
title) and change its title to "Add report" if you want full consistency, or
alternatively revert the PackQueriesTable CTA back to "Add query" so the page
uses a single term consistently.
frontend/pages/admin/TeamManagementPage/components/EmptyTeamsTable.tsx (1)

42-57: ⚠️ Potential issue | 🟠 Major

EmptyState does not support graphics—add illustration back or confirm removal is intentional.

The previous EmptyTable component rendered a graphic via graphicName="empty-teams", but EmptyState has no graphic, illustration, or image prop. The component currently renders only ghost skeletons, header text, and buttons. Confirm this visual change aligns with design, or add a graphic prop to EmptyState to restore the illustration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/pages/admin/TeamManagementPage/components/EmptyTeamsTable.tsx`
around lines 42 - 57, EmptyState in EmptyTeamsTable.tsx replaced the previous
EmptyTable that used graphicName="empty-teams", so the illustration disappeared;
either restore the illustration by adding a graphic/illustration prop to
EmptyState and pass graphicName="empty-teams" (or a new prop name) from
EmptyTeamsTable, or intentionally remove the graphic and update the component
naming/comment to reflect that change. Locate EmptyTeamsTable.tsx (where
EmptyState is rendered) and the EmptyState component implementation, add a
graphic prop (e.g., graphicName or illustration) to EmptyState, wire it through
its renderer to display the same "empty-teams" asset, and update primaryButton
usage if needed; alternatively, if removal is intentional, add a short comment
in EmptyTeamsTable.tsx clarifying the design decision and remove any leftover
references to graphicName/EmptyTable.
frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx (1)

1733-1770: ⚠️ Potential issue | 🟠 Major

Use JSX syntax for EmptyState and remove the unsupported graphicName prop.

Two issues in this block:

  1. Calling EmptyState({...}) as a plain function is not idiomatic React. Use JSX syntax:

    Suggested change
    -          {EmptyState({
    -            graphicName: emptyState().graphicName,
    -            header: emptyState().header,
    -            info: emptyState().info,
    -            additionalInfo: emptyState().additionalInfo,
    -            primaryButton: emptyState().primaryButton,
    -          })}
    +          <EmptyState
    +            header={emptyState().header}
    +            info={emptyState().info}
    +            additionalInfo={emptyState().additionalInfo}
    +            primaryButton={emptyState().primaryButton}
    +          />
  2. graphicName is not supported by EmptyState. The component's prop interface does not include graphicName, so passing it has no effect. Remove it from the call. (Note: EmptyState renders a ghost table skeleton instead of a graphic illustration, which is intentional per the redesign.)

  3. additionalInfo is never set by emptyState(), so the prop always passes undefined and has no visible effect. Remove it unless it should be populated with content.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx` around lines 1733 -
1770, The current block calls EmptyState as a plain function and passes an
unsupported graphicName prop (and an unused additionalInfo), causing
non-idiomatic React and dead props; fix by calling EmptyState with JSX instead
of EmptyState(...), compute const state = emptyState() once (referencing
emptyState, maybeEmptyHosts, includesFilterQueryParam, canEnrollHosts,
toggleAddHostsModal) and return <EmptyState header={state.header}
info={state.info} primaryButton={state.primaryButton} />; remove graphicName and
additionalInfo from the props unless you intentionally add content for
additionalInfo in emptyState().
frontend/pages/ManageControlsPage/Scripts/ScriptBatchDetailsPage/ScriptBatchDetailsPage.tsx (1)

52-59: ⚠️ Potential issue | 🟡 Minor

Minor copy issues in EMPTY_STATE_DETAILS.

  • Line 54: trailing space at the end of "Hosts with error results appear here. " will surface in the rendered info text.
  • Line 58: copy uses British spelling "cancelled" while the status key (and the tab label below) is the American "canceled". Worth aligning for consistency.
✏️ Proposed fix
   ran: "Hosts with successful script results appear here.",
-  errored: "Hosts with error results appear here. ",
+  errored: "Hosts with error results appear here.",
   pending: "Compatible hosts that haven't run the script appear here.",
   incompatible:
     "Targeted hosts with incompatible operating systems appear here.",
-  canceled: "Hosts where this script run was cancelled appear here.",
+  canceled: "Hosts where this script run was canceled appear here.",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/ManageControlsPage/Scripts/ScriptBatchDetailsPage/ScriptBatchDetailsPage.tsx`
around lines 52 - 59, EMPTY_STATE_DETAILS has minor copy issues: remove the
trailing space from the errored value and align spelling for the canceled
key/value; update the errored entry string (no trailing space) and change the
"cancelled" text to American spelling "canceled" so the value for the canceled
key in the EMPTY_STATE_DETAILS record (type ScriptBatchHostStatus) matches the
status key and tab label.
frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageTable/BootstrapPackageTable.tsx (1)

63-65: ⚠️ Potential issue | 🟡 Minor

Typo in info copy.

"Expecting to status data?" is missing a verb — should read "Expecting to see status data?" to match the phrasing used elsewhere (e.g., TargetsInput.tsx: "Expecting to see hosts? ...").

✏️ Proposed fix
       <EmptyState
         header="No bootstrap package status"
-        info="Expecting to status data? Try again in a few seconds as the system
-            catches up."
+        info="Expecting to see status data? Try again in a few seconds as the system catches up."
       />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageTable/BootstrapPackageTable.tsx`
around lines 63 - 65, Fix the typo in the JSX info prop inside the
BootstrapPackageTable component: replace the incorrect string "Expecting to
status data? Try again in a few seconds as the system catches up." with the
corrected copy "Expecting to see status data? Try again in a few seconds as the
system catches up." so the info text matches phrasing used elsewhere (look for
the info prop in BootstrapPackageTable).
frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx (2)

83-85: ⚠️ Potential issue | 🟡 Minor

Verify empty-info rendering parity.

When !canAddOrDeletePolicies, info is set to "" (empty string). EmptyTable and EmptyState may treat truthiness differently — e.g., EmptyState could still render an empty <p>/spacing for "" whereas EmptyTable skipped it. Worth a quick visual check that the read-only empty state still looks right (likely better to set info: undefined / delete emptyState.info).

🔧 Suggested adjustment
   if (!canAddOrDeletePolicies) {
-    emptyState.info = "";
+    delete emptyState.info;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx`
around lines 83 - 85, The empty-state info is being set to an empty string when
!canAddOrDeletePolicies which may render differently between EmptyTable and
EmptyState; in PoliciesTable.tsx, update the logic around emptyState (the
emptyState object modified when evaluating canAddOrDeletePolicies) to remove the
info property or set it to undefined instead of "" so the read-only empty state
renders consistently (verify by checking EmptyTable and EmptyState rendering
afterwards).

65-70: ⚠️ Potential issue | 🟡 Minor

Re-type emptyState to match the component it's passed to.

emptyState is typed as IEmptyTableProps but consumed by EmptyState, which expects IEmptyStateProps. Although graphicName is being passed to EmptyState (line 150), that component does not accept it in its props interface—it will be silently ignored. This type mismatch is misleading and obscures the fact that graphicName has no effect. Change the type annotation to IEmptyStateProps to ensure consistency between the type label and what the component actually accepts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx`
around lines 65 - 70, The emptyState constant is currently typed as
IEmptyTableProps but is passed into the EmptyState component which expects
IEmptyStateProps and does not accept graphicName; change the type annotation of
emptyState from IEmptyTableProps to IEmptyStateProps, remove or move any
unsupported props (e.g., graphicName) so they align with EmptyState's prop
shape, and ensure the constant's keys match the IEmptyStateProps interface
(update the emptyState declaration and any usages of graphicName accordingly).
🧹 Nitpick comments (17)
frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx (1)

58-58: Consider asserting exact empty-state text instead of a loose regex.

/No custom variables/i is broader than necessary and would also match text like "These are no custom variables removed." Since the new EmptyState should render a deterministic header, prefer an exact string (or at minimum an anchored regex) so casing/wording regressions are caught.

Suggested change
-        expect(screen.getByText(/No custom variables/i)).toBeInTheDocument();
+        expect(screen.getByText("No custom variables")).toBeInTheDocument();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx` at line 58,
The test currently uses a loose regex expect(screen.getByText(/No custom
variables/i)).toBeInTheDocument(); which may match unintended text; change it to
assert the exact empty-state header—either use a string matcher
expect(screen.getByText('No custom variables')).toBeInTheDocument(); or an
anchored regex expect(screen.getByText(/^No custom
variables$/i)).toBeInTheDocument(); so the assertion in Variables.tests.tsx
targets the deterministic EmptyState header precisely.
changes/35483-empty-states (1)

1-1: Add trailing newline at EOF.

The file lacks a trailing newline, per the AI summary. POSIX convention recommends a final newline; many tools/linters flag its absence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@changes/35483-empty-states` at line 1, The file changes/35483-empty-states is
missing a trailing newline at EOF; open that file and add a single newline
character at the end so the file ends with a blank line (POSIX-compliant), save
the file, and recommit the change to satisfy linters and conventions.
frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityCard/_styles.scss (1)

1-2: Delete the now-empty stylesheet.

The file contains only a comment and is not imported anywhere. Remove frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityCard/_styles.scss to keep the codebase clean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityCard/_styles.scss`
around lines 1 - 2, The stylesheet _styles.scss for AddCertAuthorityCard is
empty (only a comment) and unused; delete the file and remove any lingering
imports referencing AddCertAuthorityCard/_styles.scss (or imports in the
AddCertAuthorityCard component) so there are no broken references.
frontend/pages/policies/edit/components/PolicyErrorsTable/PolicyErrorsTable.tsx (1)

46-46: Consider adding info copy for parity with the design system.

Most other migrated empty states in this PR pair header with info text. With only a header here, the ghost-table backdrop will look quite sparse and gives users no guidance on what "No hosts are online" implies. If the Figma design includes secondary copy for this case, please thread it through the info prop; otherwise feel free to disregard.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/policies/edit/components/PolicyErrorsTable/PolicyErrorsTable.tsx`
at line 46, The empty state in PolicyErrorsTable's emptyComponent currently only
passes a header to EmptyState (<EmptyState header="No hosts are online" />); add
an info prop to match the design system and other migrated empty states (e.g.,
<EmptyState header="No hosts are online" info="...secondary guidance..." />) —
update the emptyComponent in PolicyErrorsTable.tsx to include appropriate
secondary copy (use the Figma text if available or a short explanatory sentence)
so the ghost-table backdrop isn't sparse.
frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareForm/InstallSoftwareForm.tsx (1)

235-250: Rename className to match current component and remove unused class reference.

The className={${baseClass}__empty-table} is indeed a leftover from when this rendered EmptyTable. The corresponding SCSS rule (install-software-form__empty-table) does not exist in _styles.scss, making this class a no-op. Rename to __empty-state to align with the current component and improve code clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareForm/InstallSoftwareForm.tsx`
around lines 235 - 250, The EmptyState rendered by renderEmptyState uses a stale
class name `${baseClass}__empty-table`; update it to `${baseClass}__empty-state`
to match the current component and existing SCSS, i.e. in InstallSoftwareForm
change the className prop on the EmptyState element from
`${baseClass}__empty-table` to `${baseClass}__empty-state`; ensure no other
references to `__empty-table` remain in this component.
frontend/pages/SoftwarePage/components/tables/EmptySoftwareTable/EmptySoftwareTable.tsx (1)

4-4: Stale type: switch from IEmptyTableProps to IEmptyStateProps.

getEmptySoftwareInfo now feeds EmptyState, which doesn't support graphicName/additionalInfo table-only fields. Using IEmptyTableProps as the return type is misleading and could mask future bugs (e.g., someone adding graphicName here expecting it to render).

♻️ Proposed refactor
-import { IEmptyTableProps } from "interfaces/empty_table";
+import { IEmptyStateProps } from "components/EmptyState/EmptyState";
@@
-  const getEmptySoftwareInfo = (): IEmptyTableProps => {
+  const getEmptySoftwareInfo = (): Pick<IEmptyStateProps, "header" | "info"> => {

Also applies to: 58-58

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/SoftwarePage/components/tables/EmptySoftwareTable/EmptySoftwareTable.tsx`
at line 4, The return type used in EmptySoftwareTable/getEmptySoftwareInfo is
stale: replace IEmptyTableProps with IEmptyStateProps (update the import and any
type annotations for getEmptySoftwareInfo and its usages) so the function
returns only fields supported by EmptyState; also remove or stop returning
table-only fields like graphicName and additionalInfo from getEmptySoftwareInfo
to avoid misleading callers and runtime mismatches with the EmptyState
component.
frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx (1)

180-200: Dead graphicName logic — EmptyState ignores it.

emptyParams is typed as IEmptyTableProps and seeds graphicName: "empty-queries", but EmptyState (per frontend/components/EmptyState/EmptyState.tsx) does not accept graphicName. The prop is silently dropped when spread on Line 297, and the delete emptyParams.graphicName at Line 198 has no effect. This is misleading and will confuse future maintainers expecting a graphic to render.

♻️ Proposed cleanup
-  const emptyParams: IEmptyTableProps = {
-    graphicName: "empty-queries",
-    header: "You don't have any reports",
-  };
+  const emptyParams: IEmptyStateProps = {
+    header: "You don't have any reports",
+  };
@@
   if (searchQuery || curTargetedPlatformFilter !== "all") {
-    delete emptyParams.graphicName;
     emptyParams.header = "No matching reports";
     emptyParams.info = "No reports match the current filters.";
   }

Remove the now-unused IEmptyTableProps import and import IEmptyStateProps from the EmptyState module instead.

Also applies to: 297-297

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx`
around lines 180 - 200, The emptyParams object in QueriesTable is typed as
IEmptyTableProps and seeds graphicName, but EmptyState ignores graphicName;
update the import and typing to use IEmptyStateProps (replace IEmptyTableProps
with IEmptyStateProps), remove the graphicName property initialization and the
subsequent delete emptyParams.graphicName call, and adjust the emptyParams
builds (the header/info mutations and conditional branches around currentTeamId,
searchQuery, and curTargetedPlatformFilter) to only set properties that
EmptyState actually consumes before the object is spread into <EmptyState /> in
the QueriesTable component.
frontend/pages/SoftwarePage/components/tables/SoftwareVulnerabilitiesTable/EmptyVulnerabilitiesTable/EmptyVulnerabilitiesTable.tsx (1)

23-55: Dead graphicName entries — EmptyState ignores them.

Every entry in emptyStateDetails plus defaultEmptyState still carries graphicName, and the records are typed as Partial<IEmptyTableProps> / IEmptyTableProps. After migrating to EmptyState (which has no graphicName prop), these values are silently dropped on Line 107 (<EmptyState {...emptyState} />). This is dead configuration that will mislead readers/maintainers.

♻️ Proposed cleanup
-import { IEmptyTableProps } from "interfaces/empty_table";
+import { IEmptyStateProps } from "components/EmptyState/EmptyState";
@@
-const emptyStateDetails: Record<
-  IVulnerabilitiesEmptyStateReason,
-  Partial<IEmptyTableProps>
-> = {
+const emptyStateDetails: Record<
+  IVulnerabilitiesEmptyStateReason,
+  Partial<IEmptyStateProps>
+> = {
   "no-vulns-detected": {
-    graphicName: "empty-search-question",
     header: "No vulnerabilities detected",
     info: "Expecting to see vulnerabilities? Check back later.",
   },
   "no-matching-items": {
-    graphicName: "empty-search-question",
     header: "No items match the current search criteria",
     info: "Expecting to see vulnerabilities? Check back later.",
   },
   "invalid-cve": {
-    graphicName: "empty-search-exclamation",
     header: "That vulnerability (CVE) is not valid",
     info:
       'Try updating your search to use CVE format: "CVE-YYYY-<4 or more digits>"',
   },
   "unknown-cve": {
-    graphicName: "empty-search-question",
     header: "This is not a known CVE",
     info: "None of Fleet's vulnerability sources are aware of this CVE.",
     additionalInfo: <LearnMoreLink />,
   },
   "known-vuln": {
-    graphicName: "empty-search-check",
     header:
       "This is a known vulnerability (CVE), but it wasn't detected on any hosts",
     additionalInfo: <LearnMoreLink />,
   },
 };
@@
-  const defaultEmptyState: IEmptyTableProps = {
-    graphicName: "empty-search-question",
+  const defaultEmptyState: IEmptyStateProps = {
     header: "No items match the current search criteria",
     info: "Expecting to see vulnerabilities? Check back later.",
   };

Also applies to: 83-91

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/SoftwarePage/components/tables/SoftwareVulnerabilitiesTable/EmptyVulnerabilitiesTable/EmptyVulnerabilitiesTable.tsx`
around lines 23 - 55, The emptyStateDetails and defaultEmptyState objects
include a dead graphicName property that EmptyState ignores; remove all
graphicName entries and update the types so callers can't accidentally include
it (e.g., change the type of emptyStateDetails/defaultEmptyState from
Partial<IEmptyTableProps> / IEmptyTableProps to Partial<Omit<IEmptyTableProps,
"graphicName">> or define a new type without graphicName), and ensure the
<EmptyState {...emptyState} /> usage and any tests/fixtures are updated to match
the new type.
frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssues.tsx (1)

40-56: Deduplicate the empty-state copy.

The same header/info props are rendered for both the "TableContainer empty" and "no munkiIssues" branches. Hoist into a shared element (or render the EmptyState once when munkiIssues?.length is falsy and let TableContainer handle the with-data empty case) to avoid drift if the copy changes later.

♻️ Suggested refactor
-      {munkiIssues?.length ? (
+      {(() => {
+        const emptyState = (
+          <EmptyState
+            header="No Munki issues detected"
+            info="The last time Munki ran on this host, no issues were reported."
+          />
+        );
+        return munkiIssues?.length ? (
           <div className={deviceType || ""}>
             <TableContainer
               columnConfigs={tableHeaders}
               data={tableMunkiIssues || []}
               isLoading={isLoading}
               defaultSortHeader="name"
               defaultSortDirection="asc"
               resultsTitle="issue"
-              emptyComponent={() => (
-                <EmptyState
-                  header="No Munki issues detected"
-                  info="The last time Munki ran on this host, no issues were reported."
-                />
-              )}
+              emptyComponent={() => emptyState}
               showMarkAllPages={false}
               isAllPagesSelected={false}
               isClientSidePagination
             />
           </div>
-      ) : (
-        <EmptyState
-          header="No Munki issues detected"
-          info="The last time Munki ran on this host, no issues were reported."
-        />
-      )}
+        ) : (
+          emptyState
+        );
+      })()}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssues.tsx` around lines
40 - 56, The EmptyState JSX is duplicated in MunkiIssues.tsx for both the
TableContainer empty case and the fallback when munkiIssues is falsy; refactor
by hoisting the EmptyState into a single shared element: compute a single const
(e.g. emptyMunki = <EmptyState header="No Munki issues detected" info="The last
time Munki ran on this host, no issues were reported." />) or render EmptyState
once when munkiIssues?.length is falsy and pass only the data-driven props into
TableContainer, replacing the two duplicated JSX blocks so future copy changes
are made in one place; update references to munkiIssues and TableContainer
accordingly.
frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx (1)

100-125: Minor: drop unnecessary fragments around plain-text headers.

header={<>Policies are not supported for this host</>} can simply be header="Policies are not supported for this host" on lines 101 and 116. The fragment is only needed where the value is a composite expression (e.g., the line 130-135 block).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx` around lines
100 - 125, The EmptyState headers in HostPolicies.tsx currently wrap plain text
in fragments (header={<>Policies are not supported for this host</>}); replace
those fragment-wrapped values with simple string props (header="Policies are not
supported for this host") for both occurrences (the iOS branch and the Android
branch) so the header prop receives a plain string instead of a JSX fragment;
keep the existing JSX fragments only for composite header/info values like the
other block that uses CustomLink.
frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/RunScriptModal.tsx (1)

121-124: Consider width="small" for the modal context.

Per IEmptyStateProps's own JSDoc — "default" renders 3 ghost columns (page-level), "small" renders 2 (modal-level) — this empty state is rendered inside Modal, so passing width="small" would better match the documented intent and the modal's narrower layout.

✏️ Proposed change
             <EmptyState
+              width="small"
               header="No scripts available for this host"
               info="Expecting to see scripts? Close this modal and try again."
             />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/RunScriptModal.tsx`
around lines 121 - 124, The EmptyState used in RunScriptModal is rendered inside
a modal but currently relies on the default width; update the EmptyState
component invocation (the EmptyState element in RunScriptModal) to pass
width="small" per IEmptyStateProps so it uses the 2-column modal layout instead
of the 3-column page layout.
frontend/pages/ManageControlsPage/OSUpdates/components/OSVersionsEmptyState/OSVersionsEmptyState.tsx (1)

5-5: Unused baseClass constant.

baseClass is no longer referenced after dropping the prior wrapper/className usage. Consider removing it to avoid dead code.

♻️ Proposed cleanup
 import EmptyState from "components/EmptyState";

-const baseClass = "os-versions-empty-state";
-
 const OSVersionsEmptyState = () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/ManageControlsPage/OSUpdates/components/OSVersionsEmptyState/OSVersionsEmptyState.tsx`
at line 5, Remove the dead constant by deleting the unused `baseClass`
declaration (const baseClass = "os-versions-empty-state") from
OSVersionsEmptyState.tsx; if the intent was to keep a BEM/class reference,
instead reintroduce its use in the component's wrapper/className, otherwise
simply remove the `baseClass` symbol to eliminate the unused variable.
frontend/pages/hosts/details/cards/Activity/EmptyFeed/_styles.scss (1)

1-2: Delete the _styles.scss file.

It contains only a comment and is not imported by any component. Removing it will clean up unused files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/pages/hosts/details/cards/Activity/EmptyFeed/_styles.scss` around
lines 1 - 2, Remove the unused stylesheet file named _styles.scss (it only
contains a comment and is not imported anywhere); delete the file from the
repository and ensure no references/imports remain (verify related components
like the EmptyState/Activity feed rely on its styles instead of this file).
frontend/components/EnrollSecrets/EnrollSecretModal/EnrollSecretModal.tsx (1)

70-131: Optional: extract the duplicated "Add secret" button.

The exact same GitOpsModeTooltipWrapper + Button (with identical props, classes, icon, and onClick={addNewSecretClick}) is rendered both in the non-empty branch (Lines 71–86) and as the primaryButton of the empty state (Lines 115–130). Extracting to a single local helper/variable would remove ~15 lines of duplication and prevent future drift between the two call sites.

♻️ Sketch of the extraction
+  const addSecretButton = (
+    <GitOpsModeTooltipWrapper
+      entityType="secrets"
+      position="right"
+      tipOffset={8}
+      renderChildren={(disableChildren) => (
+        <Button
+          disabled={disableChildren}
+          onClick={addNewSecretClick}
+          className={`${baseClass}__add-secret-btn`}
+          variant="brand-inverse-icon"
+          iconStroke
+        >
+          Add secret <Icon name="plus" color="core-fleet-green" />
+        </Button>
+      )}
+    />
+  );

…and pass addSecretButton both inside the header <div> and as primaryButton={addSecretButton}.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/EnrollSecrets/EnrollSecretModal/EnrollSecretModal.tsx`
around lines 70 - 131, Extract the duplicated "Add secret" JSX into a single
local variable (e.g., const addSecretButton) inside EnrollSecretModal: move the
GitOpsModeTooltipWrapper + Button (the instance using addNewSecretClick,
baseClass + "__add-secret-btn", variant="brand-inverse-icon", iconStroke, and
Icon name="plus") into that variable and then replace both occurrences (the
header div and EmptyState primaryButton) with addSecretButton so both render the
same shared element.
frontend/pages/hosts/details/cards/Activity/EmptyFeed/EmptyFeed.tsx (1)

11-20: Optional: this thin wrapper may now be redundant.

EmptyFeed is now a near-passthrough to EmptyState (just renaming title/messageheader/info). Consider inlining <EmptyState variant="list" header={...} info={...} /> at the call sites and removing this file/component to reduce indirection. Not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/pages/hosts/details/cards/Activity/EmptyFeed/EmptyFeed.tsx` around
lines 11 - 20, The EmptyFeed component is a thin passthrough that only maps
props (title→header, message→info) to EmptyState; remove the indirection by
replacing usages of <EmptyFeed title=... message=... className=... /> with
<EmptyState variant="list" header={title} info={message} className={className}
/> at call sites and then delete the EmptyFeed component and its IEmptyFeedProps
type if unused elsewhere (search for EmptyFeed and IEmptyFeedProps to update
imports and remove the file).
frontend/components/EmptyState/_styles.scss (1)

187-192: Avoid !important on __ghost-input background.

!important is rarely needed for decorative skeleton primitives that aren't visible to assistive tech and aren't expected to be themed externally. If this is here to override a global input/button reset, prefer increasing specificity (e.g., .empty-state__ghost-input { ... } already has class specificity, so the override is likely unnecessary) or document the override target inline.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/EmptyState/_styles.scss` around lines 187 - 192, Remove
the unnecessary !important on the .__ghost-input background; update the
.__ghost-input rule (the selector for the ghost input block) to rely on its
current class specificity or, if you truly need to override a global reset,
increase specificity (e.g., use the full component selector like
.empty-state__ghost-input) or add an inline comment documenting the exact reset
you're overriding so the intent is clear—then remove the !important declaration
from the background-color property in the __ghost-input rule.
frontend/components/EmptyState/EmptyState.stories.tsx (1)

15-19: Use labels configuration to customize the "undefined" option label.

Storybook's select control renders undefined as the literal string "undefined", which is a confusing label for the default variant. Use the labels option to provide a friendlier display name while preserving the actual undefined value passed to the component.

♻️ Proposed refactor
     variant: {
       control: "select",
       options: [undefined, "list", "header-list", "form"],
+      labels: {
+        undefined: "Default",
+      },
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/EmptyState/EmptyState.stories.tsx` around lines 15 - 19,
The story's argType for the variant control currently shows the raw "undefined"
string; update the variant argType (the control for variant in
EmptyState.stories.tsx) to include a labels mapping that maps the undefined
option to a friendly label (e.g., "Default" or "Auto") while keeping its value
as undefined in the options array, so the select displays the friendly label but
still passes undefined to the component. Locate the variant argType and add a
labels object keyed by the option values (including undefined) to provide the
custom display text.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 980bc68f-5590-4b45-8bde-ee553f4f576d

📥 Commits

Reviewing files that changed from the base of the PR and between 11c872a and bfc9385.

📒 Files selected for processing (102)
  • changes/35483-empty-states
  • frontend/components/EmptyState/EmptyState.stories.tsx
  • frontend/components/EmptyState/EmptyState.tsx
  • frontend/components/EmptyState/_styles.scss
  • frontend/components/EmptyState/index.ts
  • frontend/components/EnrollSecrets/EnrollSecretModal/EnrollSecretModal.tsx
  • frontend/components/GenericMsgWithNavButton/GenericMsgWithNavButton.tests.tsx
  • frontend/components/GenericMsgWithNavButton/GenericMsgWithNavButton.tsx
  • frontend/components/GenericMsgWithNavButton/_styles.scss
  • frontend/components/GenericMsgWithNavButton/index.ts
  • frontend/components/LiveQuery/SelectTargets.tsx
  • frontend/components/TabNav/_styles.scss
  • frontend/components/TargetLabelSelector/TargetLabelSelector.tsx
  • frontend/components/TargetsInput/TargetsInput.tsx
  • frontend/components/TargetsInput/_styles.scss
  • frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/SelectTargetsMenu.jsx
  • frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/_styles.scss
  • frontend/components/queries/LiveResults/AwaitingResults/AwaitingResults.tsx
  • frontend/components/queries/PackQueriesTable/PackQueriesTable.tsx
  • frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx
  • frontend/pages/DashboardPage/cards/MDM/MDM.tsx
  • frontend/pages/DashboardPage/cards/Munki/Munki.tsx
  • frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tsx
  • frontend/pages/ManageControlsPage/OSSettings/cards/Certificates/Certificates.tsx
  • frontend/pages/ManageControlsPage/OSSettings/cards/ConfigurationProfiles/ConfigurationProfiles.tsx
  • frontend/pages/ManageControlsPage/OSSettings/cards/ConfigurationProfiles/components/ConfigProfileStatusTable/ConfigProfileStatusTable.tsx
  • frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTable.tsx
  • frontend/pages/ManageControlsPage/OSSettings/cards/Passwords/Passwords.tsx
  • frontend/pages/ManageControlsPage/OSUpdates/OSUpdates.tsx
  • frontend/pages/ManageControlsPage/OSUpdates/components/OSVersionsEmptyState/OSVersionsEmptyState.tsx
  • frontend/pages/ManageControlsPage/OSUpdates/components/OSVersionsEmptyState/_styles.scss
  • frontend/pages/ManageControlsPage/Scripts/ScriptBatchDetailsPage/ScriptBatchDetailsPage.tsx
  • frontend/pages/ManageControlsPage/Scripts/ScriptBatchDetailsPage/_styles.scss
  • frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tsx
  • frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/_styles.scss
  • frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx
  • frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/_styles.scss
  • frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/BootstrapPackage.tsx
  • frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageTable/BootstrapPackageTable.tsx
  • frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/InstallSoftware.tsx
  • frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareForm/InstallSoftwareForm.tsx
  • frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareTable/InstallSoftwareTable.tsx
  • frontend/pages/ManageControlsPage/SetupExperience/cards/RunScript/RunScript.tsx
  • frontend/pages/ManageControlsPage/SetupExperience/cards/SetupAssistant/SetupAssistant.tsx
  • frontend/pages/ManageControlsPage/SetupExperience/cards/Users/Users.tsx
  • frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx
  • frontend/pages/ManageControlsPage/Variables/Variables.tsx
  • frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tsx
  • frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore/SoftwareAppStoreVpp/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTable.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTable.tsx
  • frontend/pages/SoftwarePage/components/tables/EmptySoftwareTable/EmptySoftwareTable.tsx
  • frontend/pages/SoftwarePage/components/tables/OSKernelsTable/OSKernelsTable.tsx
  • frontend/pages/SoftwarePage/components/tables/SoftwareVulnerabilitiesTable/EmptyVulnerabilitiesTable/EmptyVulnerabilitiesTable.tsx
  • frontend/pages/SoftwarePage/components/tables/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx
  • frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityCard/AddCertAuthorityCard.tsx
  • frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityCard/_styles.scss
  • frontend/pages/admin/IntegrationsPage/cards/Integrations/Integrations.tsx
  • frontend/pages/admin/IntegrationsPage/cards/Integrations/components/EmptyIntegrationsTable.tsx
  • frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/AppleBusinessManagerPage.tsx
  • frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/_styles.scss
  • frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/VppPage.tsx
  • frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsAutomaticEnrollmentPage/WindowsAutomaticEnrollmentPage.tsx
  • frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsAutomaticEnrollmentPage/_styles.scss
  • frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/EndUserMigrationSection/EndUserMigrationSection.tsx
  • frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/components/EmptyUsersTable.tsx
  • frontend/pages/admin/TeamManagementPage/components/EmptyTeamsTable.tsx
  • frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx
  • frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
  • frontend/pages/hosts/ManageHostsPage/components/RunScriptBatchModal/RunScriptBatchModal.tsx
  • frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTable.tsx
  • frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
  • frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/RunScriptModal.tsx
  • frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/SelectQueryModal.tsx
  • frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss
  • frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx
  • frontend/pages/hosts/details/cards/Activity/EmptyFeed/EmptyFeed.tsx
  • frontend/pages/hosts/details/cards/Activity/EmptyFeed/_styles.scss
  • frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibraryTable/HostSoftwareLibraryTable.tsx
  • frontend/pages/hosts/details/cards/LocalUserAccounts/LocalUserAccounts.tsx
  • frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssues.tsx
  • frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx
  • frontend/pages/hosts/details/cards/Queries/HostQueries.tsx
  • frontend/pages/hosts/details/cards/Queries/_styles.scss
  • frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx
  • frontend/pages/hosts/details/cards/Software/SelfService/SelfServiceCard/SelfServiceCard.tests.tsx
  • frontend/pages/hosts/details/cards/Software/SelfService/SelfServiceCard/SelfServiceCard.tsx
  • frontend/pages/hosts/details/cards/Software/SelfService/components/SelfServiceTable/SelfServiceTable.tsx
  • frontend/pages/hosts/details/cards/Software/SelfService/components/SelfServiceTiles/SelfServiceTiles.tsx
  • frontend/pages/labels/ManageLabelsPage/LabelsTable/LabelsTable.tsx
  • frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTable.tsx
  • frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx
  • frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/_styles.scss
  • frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx
  • frontend/pages/policies/ManagePoliciesPage/components/PolicyRunScriptModal/PolicyRunScriptModal.tsx
  • frontend/pages/policies/edit/components/PolicyErrorsTable/PolicyErrorsTable.tsx
  • frontend/pages/policies/edit/components/PolicyResults/PolicyResults.tsx
  • frontend/pages/policies/edit/components/PolicyResultsTable/PolicyResultsTable.tsx
  • frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx
  • frontend/pages/queries/details/components/NoResults/NoResults.tsx
  • frontend/pages/queries/details/components/QueryReport/QueryReport.tsx
  • frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx
💤 Files with no reviewable changes (18)
  • frontend/pages/ManageControlsPage/SetupExperience/cards/Users/Users.tsx
  • frontend/components/GenericMsgWithNavButton/_styles.scss
  • frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/_styles.scss
  • frontend/pages/ManageControlsPage/OSUpdates/components/OSVersionsEmptyState/_styles.scss
  • frontend/pages/hosts/details/cards/Queries/_styles.scss
  • frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/_styles.scss
  • frontend/components/TabNav/_styles.scss
  • frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/_styles.scss
  • frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsAutomaticEnrollmentPage/_styles.scss
  • frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/_styles.scss
  • frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss
  • frontend/components/GenericMsgWithNavButton/GenericMsgWithNavButton.tsx
  • frontend/pages/ManageControlsPage/Scripts/ScriptBatchDetailsPage/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore/SoftwareAppStoreVpp/_styles.scss
  • frontend/components/GenericMsgWithNavButton/index.ts
  • frontend/components/GenericMsgWithNavButton/GenericMsgWithNavButton.tests.tsx
  • frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/_styles.scss
  • frontend/components/TargetsInput/_styles.scss

Comment thread frontend/components/EmptyState/_styles.scss Outdated
Comment thread frontend/components/EmptyState/_styles.scss
Comment on lines 135 to 141
if (isError)
return (
<EmptyTable
header="Error loading software."
{...(isMobileView && { variant: "mobile" })}
<EmptyState
header="Error loading software"
{...(isMobileView && { variant: "list" })}
/>
);

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.

⚠️ Potential issue | 🟡 Minor

Verify error UX: skeleton ghost rows for an error state may look like loading.

EmptyState renders skeleton/ghost columns (or a ghost list when variant="list") as visual filler, which is well-suited to "no data yet" states but can be misleading for an error state — users may assume the page is still loading. Consider:

  • Using a dedicated error component (e.g., DataError, as used in the sibling Certificates.tsx/VppPage.tsx files in this PR), or
  • Adding info copy clarifying the failure and a retry CTA via primaryButton.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/hosts/details/cards/Software/SelfService/SelfServiceCard/SelfServiceCard.tsx`
around lines 135 - 141, The current SelfServiceCard uses EmptyState when isError
which shows skeleton/ghost UI and can be mistaken for loading; update
SelfServiceCard to render the dedicated error pattern instead — either replace
the EmptyState return for the isError branch with the shared DataError component
(as used in Certificates.tsx / VppPage.tsx) or enhance the EmptyState by adding
explicit error copy via the info prop and a retry primaryButton that invokes the
card's reload handler; target the isError branch in SelfServiceCard and ensure
the chosen component or props clearly convey failure and offer retry.

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.

@mike-j-thomas - just a consideration for error state

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 me wonder why we weren't using our normal error state here. Maybe because it happens often with expired tokens, no idea, so not going to touch this out of scope.

@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 (3)
frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx (1)

86-90: ⚠️ Potential issue | 🟡 Minor

Dead code: delete emptyState.primaryButton has no effect.

primaryButton is never assigned to emptyState in this file (only header and info are set at lines 65-69, and only header/info are mutated in the conditionals above). The delete on line 87 is therefore a no-op — likely a leftover from the previous shape of emptyState. Either drop the line or, if a CTA is expected on the empty state, set primaryButton somewhere upstream.

♻️ Suggested cleanup
   if (searchQuery || isFiltered) {
-    delete emptyState.primaryButton;
     emptyState.header = "No matching policies";
     emptyState.info = "No policies match the current filters.";
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx`
around lines 86 - 90, The delete on emptyState.primaryButton is dead code
because primaryButton is never set in this component; remove the line to clean
up. Locate the conditional that checks (searchQuery || isFiltered) in
PoliciesTable.tsx and delete the statement "delete emptyState.primaryButton"; if
instead a CTA is intended, initialize or assign emptyState.primaryButton where
emptyState is constructed (refer to the emptyState object) rather than deleting
it here.
frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx (1)

196-199: ⚠️ Potential issue | 🔴 Critical

Leftover delete emptyParams.graphicName from the old EmptyTable API.

graphicName is no longer part of IEmptyStateProps and is never assigned to emptyParams in this file, so this line is dead code and will fail TypeScript compilation (Property 'graphicName' does not exist on type 'IEmptyStateProps'). This is a stale remnant from the EmptyTable migration that needs to be removed.

🐛 Proposed fix
   if (searchQuery || curTargetedPlatformFilter !== "all") {
-    delete emptyParams.graphicName;
     emptyParams.header = "No matching reports";
     emptyParams.info = "No reports match the current filters.";
   } else if (!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx`
around lines 196 - 199, The code contains a stale reference to
emptyParams.graphicName which no longer exists on the IEmptyStateProps type and
causes a TypeScript error; remove the delete emptyParams.graphicName line from
the QueriesTable component (the block that sets emptyParams.header and
emptyParams.info when searchQuery or curTargetedPlatformFilter !== "all") so
only valid properties on emptyParams are used and the code compiles against the
new EmptyTable/IEmptyStateProps API.
frontend/interfaces/empty_state.ts (1)

1-8: 🛠️ Refactor suggestion | 🟠 Major

Interface diverges from the canonical IEmptyStateProps in EmptyState.tsx.

The component at frontend/components/EmptyState/EmptyState.tsx defines its own IEmptyStateProps that additionally includes width?: "default" | "small" and variant?: "list" | "header-list" | "form". The version exported here is missing both. Two same-named interfaces diverging will silently allow consumers typed against this file to omit/typo those props, and any future addition (e.g., a new variant) will need to be made in two places.

Prefer re-exporting the canonical interface from the component so there is a single source of truth:

♻️ Proposed refactor
-export interface IEmptyStateProps {
-  header?: JSX.Element | string;
-  info?: JSX.Element | string;
-  additionalInfo?: JSX.Element | string;
-  className?: string;
-  primaryButton?: JSX.Element;
-  secondaryButton?: JSX.Element;
-}
+export type { IEmptyStateProps } from "components/EmptyState/EmptyState";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/interfaces/empty_state.ts` around lines 1 - 8, The exported
IEmptyStateProps here diverges from the canonical one defined in the EmptyState
component (EmptyState.tsx) which includes width?: "default" | "small" and
variant?: "list" | "header-list" | "form"; remove the duplicate interface and
re-export the canonical type instead by importing and re-exporting
IEmptyStateProps from the EmptyState component so there's a single source of
truth (ensure references to IEmptyStateProps in this file now point to the
imported/re-exported symbol).
🧹 Nitpick comments (1)
frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx (1)

146-153: Optional: spread emptyState instead of enumerating fields.

Since emptyState is already typed as IEmptyStateProps, you can spread it directly. This also avoids passing additionalInfo (which is never set in this file and is therefore always undefined) and keeps the call site resilient if more props are added later.

♻️ Proposed refactor
-        emptyComponent={() => (
-          <EmptyState
-            header={emptyState.header}
-            info={emptyState.info}
-            additionalInfo={emptyState.additionalInfo}
-            primaryButton={emptyState.primaryButton}
-          />
-        )}
+        emptyComponent={() => <EmptyState {...emptyState} />}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx`
around lines 146 - 153, Replace the explicit prop enumeration for the EmptyState
render with a single spread of the existing emptyState object: locate the
emptyComponent render in PoliciesTable where EmptyState is called
(emptyComponent={() => (<EmptyState ... />)}), and change it to pass
{...emptyState} so EmptyState receives the typed IEmptyStateProps directly; this
removes the redundant additionalInfo={emptyState.additionalInfo} and makes the
call resilient to future prop additions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@frontend/interfaces/empty_state.ts`:
- Around line 1-8: The exported IEmptyStateProps here diverges from the
canonical one defined in the EmptyState component (EmptyState.tsx) which
includes width?: "default" | "small" and variant?: "list" | "header-list" |
"form"; remove the duplicate interface and re-export the canonical type instead
by importing and re-exporting IEmptyStateProps from the EmptyState component so
there's a single source of truth (ensure references to IEmptyStateProps in this
file now point to the imported/re-exported symbol).

In
`@frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx`:
- Around line 86-90: The delete on emptyState.primaryButton is dead code because
primaryButton is never set in this component; remove the line to clean up.
Locate the conditional that checks (searchQuery || isFiltered) in
PoliciesTable.tsx and delete the statement "delete emptyState.primaryButton"; if
instead a CTA is intended, initialize or assign emptyState.primaryButton where
emptyState is constructed (refer to the emptyState object) rather than deleting
it here.

In
`@frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx`:
- Around line 196-199: The code contains a stale reference to
emptyParams.graphicName which no longer exists on the IEmptyStateProps type and
causes a TypeScript error; remove the delete emptyParams.graphicName line from
the QueriesTable component (the block that sets emptyParams.header and
emptyParams.info when searchQuery or curTargetedPlatformFilter !== "all") so
only valid properties on emptyParams are used and the code compiles against the
new EmptyTable/IEmptyStateProps API.

---

Nitpick comments:
In
`@frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx`:
- Around line 146-153: Replace the explicit prop enumeration for the EmptyState
render with a single spread of the existing emptyState object: locate the
emptyComponent render in PoliciesTable where EmptyState is called
(emptyComponent={() => (<EmptyState ... />)}), and change it to pass
{...emptyState} so EmptyState receives the typed IEmptyStateProps directly; this
removes the redundant additionalInfo={emptyState.additionalInfo} and makes the
call resilient to future prop additions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d3e645bc-0c60-466c-9cb4-942c0f201d28

📥 Commits

Reviewing files that changed from the base of the PR and between e4b506d and 02e9e94.

📒 Files selected for processing (12)
  • frontend/components/EmptyTable/EmptyTable.stories.tsx
  • frontend/components/EmptyTable/EmptyTable.tsx
  • frontend/components/EmptyTable/_styles.scss
  • frontend/components/EmptyTable/index.ts
  • frontend/interfaces/empty_state.ts
  • frontend/pages/SoftwarePage/components/tables/EmptySoftwareTable/EmptySoftwareTable.tsx
  • frontend/pages/SoftwarePage/components/tables/SoftwareVulnerabilitiesTable/EmptyVulnerabilitiesTable/EmptyVulnerabilitiesTable.tsx
  • frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
  • frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTable.tsx
  • frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx
  • frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx
  • frontend/pages/queries/details/components/NoResults/NoResults.tsx
💤 Files with no reviewable changes (4)
  • frontend/components/EmptyTable/index.ts
  • frontend/components/EmptyTable/_styles.scss
  • frontend/components/EmptyTable/EmptyTable.stories.tsx
  • frontend/components/EmptyTable/EmptyTable.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/pages/SoftwarePage/components/tables/EmptySoftwareTable/EmptySoftwareTable.tsx
  • frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTable.tsx
  • frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: Analyze (javascript)

Failed stage: Initialize CodeQL [❌]

Failed test name: ""

Failure summary:

The action failed during github/codeql-action/init@f6e388ebf0efc915c6c5b165b019ee61a6746a38 while
setting up CodeQL tools:
- Setup CodeQL tools errored with: Unable to download and extract CodeQL
CLI: Failed to find a release of the CodeQL tools that contains CodeQL CLI 2.25.2.
- Because the
CodeQL CLI version 2.25.2 could not be found in any available CodeQL tools release, the init step
could not complete, and the job stopped before producing debugging artifacts.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

707:  ##[endgroup]
708:  ##[group]Run github/codeql-action/init@f6e388ebf0efc915c6c5b165b019ee61a6746a38
709:  with:
710:  languages: javascript
711:  config-file: .github/workflows/config/codeql.yml
712:  token: ***
713:  matrix: {
714:    "language": "javascript"
715:  }
716:  setup-python-dependencies: true
717:  debug: false
718:  env:
719:  GOTOOLCHAIN: local
720:  ##[endgroup]
721:  ##[group]Setup CodeQL tools
722:  ##[error]Unable to download and extract CodeQL CLI: Failed to find a release of the CodeQL tools that contains CodeQL CLI 2.25.2.
723:  Post job cleanup.
724:  ##[warning]Debugging artifacts are unavailable since the 'init' Action failed before it could produce any.
725:  Post job cleanup.

@lukeheath lukeheath left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

✨ 💖 ✨ Looks great!

Much better UX, and the implementation is cleaner and more efficient AND a net reduction of 20 lines of code.

I tried to find something that AI bots hadn't, but no luck. Nice work!

@lukeheath
lukeheath merged commit 5ba261d into main Apr 27, 2026
19 of 21 checks passed
@lukeheath
lukeheath deleted the 35483-new-empty-states branch April 27, 2026 22:54
lukeheath added a commit that referenced this pull request Apr 27, 2026
sgress454 added a commit that referenced this pull request Apr 28, 2026
Resolves #44301

This PR updates the `fork-ts-checker-webpack-plugin` to resolve the
errors we were seeing when running it. Since it now runs cleanly, it
identified a few typescript errors that need to be resolved at the same
time:

* `<EmptyState>` no longer takes a `graphicName` param since it's been
redesigned in #43896
* `<PoliciesCard>` was reworked in #43411 and no longer takes `router`.

I verified that this compiles cleanly in `make generate` and `make
generate-dev`.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Simplified component properties and prop handling across host details,
packs, and queries pages.
* Refined empty-state configuration in packs management and queries
tables.

* **Chores**
* Updated development build tool dependency from version 6.5.0 to 9.1.0
for TypeScript type-checking integration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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.

Normalize empty states in Fleet UI

4 participants