From ba9283fd443a9be1664f5b3d843f03d736dab664 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 13 May 2026 15:43:48 -0400 Subject: [PATCH] Fleet UI: Update empty states in software pages (#44979) --- .../fields/InputFieldWithIcon/_styles.scss | 16 +++-- .../SoftwareAppStoreVpp.tests.tsx | 2 +- .../SoftwareAppStoreVpp.tsx | 2 +- .../SoftwareInventoryTable.tests.tsx | 26 +++++--- .../SoftwareInventoryTable.tsx | 60 +++++++++++-------- .../SoftwareInventoryTable/_styles.scss | 8 +-- .../SoftwareLibraryTable.tests.tsx | 4 +- .../SoftwareLibraryTable.tsx | 11 ++-- .../SoftwareOSTable/SoftwareOSTable.tsx | 11 ++-- .../SoftwareVulnerabilitiesTable.tests.tsx | 4 +- .../SoftwareVulnerabilitiesTable.tsx | 22 +++---- .../EmptySoftwareTable/EmptySoftwareTable.tsx | 41 +++++++++---- .../EmptyVulnerabilitiesTable.tsx | 2 +- 13 files changed, 126 insertions(+), 83 deletions(-) diff --git a/frontend/components/forms/fields/InputFieldWithIcon/_styles.scss b/frontend/components/forms/fields/InputFieldWithIcon/_styles.scss index b6ccbdeae2..1f2ea2b399 100644 --- a/frontend/components/forms/fields/InputFieldWithIcon/_styles.scss +++ b/frontend/components/forms/fields/InputFieldWithIcon/_styles.scss @@ -49,7 +49,7 @@ padding-left: 36px; } - ::placeholder { + &::placeholder { color: $ui-fleet-black-50; } @@ -58,8 +58,12 @@ } &:disabled { - color: $ui-fleet-black-50; + color: $ui-fleet-black-33; cursor: not-allowed; + + &::placeholder { + color: $ui-fleet-black-33; + } } &--error { @@ -72,10 +76,10 @@ &__input-wrapper--disabled { .input-icon-field__icon { - color: $ui-fleet-black-50; - - svg path { - fill: $ui-fleet-black-50; + svg { + path { + fill: $ui-fleet-black-33; + } } } } diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tests.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tests.tsx index 5465643969..877a5c1cbc 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tests.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tests.tsx @@ -60,7 +60,7 @@ describe("SoftwareAppStoreVpp", () => { }); expect( - screen.getByText("To add App Store apps, first enable VPP.") + screen.getByText("Enable VPP to add App Store apps (MDM required).") ).toBeInTheDocument(); expect( screen.getByRole("button", { name: "Enable VPP" }) diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tsx index 29be1b744f..5e30211882 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tsx @@ -47,7 +47,7 @@ const EnableVppMessage = ({ header="Volume Purchasing Program (VPP) isn't enabled" info={ isGlobalAdmin - ? "To add App Store apps, first enable VPP." + ? "Enable VPP to add App Store apps (MDM required)." : "To add App Store apps, ask your admin to enable VPP." } primaryButton={ diff --git a/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/SoftwareInventoryTable.tests.tsx b/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/SoftwareInventoryTable.tests.tsx index b475c10cfe..d2f37ac04e 100644 --- a/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/SoftwareInventoryTable.tests.tsx +++ b/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/SoftwareInventoryTable.tests.tsx @@ -97,15 +97,19 @@ describe("Software inventory table", () => { expect(screen.getByText("No software detected")).toBeInTheDocument(); expect( - screen.getByText("Expecting to see software? Check back later.") + screen.getByText( + "Recently installed software will appear after the next scheduled check-in." + ) ).toBeInTheDocument(); expect(screen.getByText("0 items")).toBeInTheDocument(); - expect(screen.queryByText("Search")).toBeNull(); - expect(screen.queryByText("Updated")).toBeNull(); - expect(screen.queryByText("Add filters")).toBeNull(); + expect( + screen.getByPlaceholderText("Search by name or vulnerability (CVE)") + ).toBeDisabled(); + expect(screen.getByRole("button", { name: /add filters/i })).toBeDisabled(); + expect(screen.getByText("Show versions")).toBeInTheDocument(); }); - it("Renders the page-wide empty state hiding vulnerability filtering when search query does not exist but versions toggle is applied", () => { + it("Keeps controls enabled when versions toggle is applied but no data so users can toggle back", () => { const render = createCustomRenderer({ context: { app: { @@ -144,10 +148,16 @@ describe("Software inventory table", () => { expect(screen.getByText("No software detected")).toBeInTheDocument(); expect( - screen.getByText("Expecting to see software? Check back later.") + screen.getByText( + "Recently installed software will appear after the next scheduled check-in." + ) ).toBeInTheDocument(); - expect(screen.queryByText("Search")).toBeNull(); - expect(screen.queryByText("Add filters")).toBeNull(); + // Controls stay enabled so users can toggle back to the titles view, + // which may have installers even when the versions view is empty. + expect( + screen.getByPlaceholderText("Search by name or vulnerability (CVE)") + ).toBeEnabled(); + expect(screen.getByRole("button", { name: /add filters/i })).toBeEnabled(); }); it("Renders the empty search state and vulnerability filtering when search query does not exist but vulnerability filter is applied", () => { diff --git a/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/SoftwareInventoryTable.tsx b/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/SoftwareInventoryTable.tsx index b472fa8967..1156aaba7f 100644 --- a/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/SoftwareInventoryTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/SoftwareInventoryTable.tsx @@ -171,14 +171,16 @@ const SoftwareTable = ({ return generateTableConfig(router, teamId); }, [generateTableConfig, data, router, teamId]); - // Determines if a user should be able to filter or search in the table const hasData = tableData && tableData.length > 0; const hasQuery = query !== ""; const vulnFilterDetails = getVulnFilterRenderDetails(vulnFilters); const hasVulnFilters = vulnFilterDetails.filterCount > 0; - const showFilterHeaders = - isSoftwareEnabled && (hasData || hasQuery || hasVulnFilters); + // Include showVersions — the titles view can have installers even when + // the versions view is empty, so the toggle should stay interactive. + const isTrulyEmpty = + !hasData && !hasQuery && !hasVulnFilters && !showVersions; + const controlsDisabled = !isSoftwareEnabled || isTrulyEmpty; const handleShowVersionsToggle = () => { const queryParams: Record = { @@ -227,35 +229,42 @@ const SoftwareTable = ({ } /> )} + + ); + }; + + const renderCustomControls = () => { + return ( + <> + + + ); }; - const renderCustomFiltersButton = () => { - return ( - - - - ); - }; - const renderTableHelpText = () => (
Seeing unexpected software or vulnerabilities?{" "} @@ -291,12 +300,11 @@ const SoftwareTable = ({ showMarkAllPages={false} isAllPagesSelected={false} disableNextPage={!data?.meta.has_next_results} - searchable={showFilterHeaders} + searchable + disableSearch={controlsDisabled} inputPlaceHolder="Search by name or vulnerability (CVE)" onQueryChange={onQueryChange} - customControl={ - showFilterHeaders ? renderCustomFiltersButton : undefined - } + customControl={renderCustomControls} stackControls renderCount={renderSoftwareCount} renderTableHelpText={renderTableHelpText} diff --git a/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/_styles.scss index cd327ea56d..bd622ac215 100644 --- a/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/_styles.scss @@ -11,10 +11,10 @@ } .table-container { - &__results-count { - .form-field--slider { - align-self: center; - } + .form-field--slider { + width: auto; + align-self: center; + white-space: nowrap; } &__search-input, diff --git a/frontend/pages/SoftwarePage/SoftwareLibrary/SoftwareLibraryTable/SoftwareLibraryTable.tests.tsx b/frontend/pages/SoftwarePage/SoftwareLibrary/SoftwareLibraryTable/SoftwareLibraryTable.tests.tsx index 27825dd0f0..35b7bbf191 100644 --- a/frontend/pages/SoftwarePage/SoftwareLibrary/SoftwareLibraryTable/SoftwareLibraryTable.tests.tsx +++ b/frontend/pages/SoftwarePage/SoftwareLibrary/SoftwareLibraryTable/SoftwareLibraryTable.tests.tsx @@ -80,8 +80,8 @@ describe("Software library table", () => { screen.getByRole("button", { name: "Add software" }) ).toBeInTheDocument(); expect(screen.getByText("0 items")).toBeInTheDocument(); - expect(screen.queryByText("Search")).toBeNull(); - expect(screen.queryByText("Self-service only")).toBeNull(); + expect(screen.getByPlaceholderText("Search by name")).toBeDisabled(); + expect(screen.getByText("Self-service only")).toBeInTheDocument(); }); it("Renders the empty search state and self-service toggle when self-service filter is applied", () => { diff --git a/frontend/pages/SoftwarePage/SoftwareLibrary/SoftwareLibraryTable/SoftwareLibraryTable.tsx b/frontend/pages/SoftwarePage/SoftwareLibrary/SoftwareLibraryTable/SoftwareLibraryTable.tsx index 8ebcca9501..ad5e3687d4 100644 --- a/frontend/pages/SoftwarePage/SoftwareLibrary/SoftwareLibraryTable/SoftwareLibraryTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareLibrary/SoftwareLibraryTable/SoftwareLibraryTable.tsx @@ -137,12 +137,11 @@ const SoftwareLibraryTable = ({ return generateLibraryTableConfig(router, teamId); }, [data, router, teamId]); - // Determines if a user should be able to filter or search in the table const hasData = tableData && tableData.length > 0; const hasQuery = query !== ""; - const showFilterHeaders = - isSoftwareEnabled && (hasData || hasQuery || selfServiceOnly); + const isTrulyEmpty = !hasData && !hasQuery && !selfServiceOnly; + const controlsDisabled = !isSoftwareEnabled || isTrulyEmpty; const handleSelfServiceToggle = () => { const queryParams: Record = { @@ -202,6 +201,7 @@ const SoftwareLibraryTable = ({ onChange={handleSelfServiceToggle} inactiveText="Self-service only" activeText="Self-service only" + disabled={controlsDisabled} /> ); }; @@ -272,11 +272,12 @@ const SoftwareLibraryTable = ({ showMarkAllPages={false} isAllPagesSelected={false} disableNextPage={!data?.meta.has_next_results} - searchable={showFilterHeaders} + searchable + disableSearch={controlsDisabled} inputPlaceHolder="Search by name" onQueryChange={onQueryChange} additionalQueries={String(selfServiceOnly)} - customControl={showFilterHeaders ? renderCustomControls : undefined} + customControl={renderCustomControls} stackControls renderCount={renderSoftwareCount} renderTableHelpText={renderTableHelpText} diff --git a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx index 90ff3a276e..145adacc77 100644 --- a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx @@ -94,7 +94,7 @@ const SoftwareOSTable = ({ currentPage, teamId, isLoading, - platform, + platform = "all", }: ISoftwareOSTableProps) => { const determineQueryParamChange = useCallback( (newTableQuery: ITableQueryData) => { @@ -168,11 +168,11 @@ const SoftwareOSTable = ({ router.push(path); }; - // Determines if a user should be able to filter the table const hasData = data?.os_versions && data?.os_versions.length > 0; const hasPlatformFilter = platform !== "all"; - const showFilterHeaders = isSoftwareEnabled && (hasData || hasPlatformFilter); + const isTrulyEmpty = !hasData && !hasPlatformFilter; + const controlsDisabled = !isSoftwareEnabled || isTrulyEmpty; const renderSoftwareCount = () => { if (!data) return null; @@ -180,7 +180,7 @@ const SoftwareOSTable = ({ return ( <> - {showFilterHeaders && data?.counts_updated_at && ( + {!controlsDisabled && data?.counts_updated_at && ( ); }; @@ -260,7 +261,7 @@ const SoftwareOSTable = ({ pageSize={perPage} showMarkAllPages={false} isAllPagesSelected={false} - customControl={showFilterHeaders ? renderPlatformDropdown : undefined} + customControl={renderPlatformDropdown} disableNextPage={!data?.meta.has_next_results} searchable={false} onQueryChange={onQueryChange} diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx index 41fd7bc655..40ab6c8192 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx @@ -83,7 +83,9 @@ describe("Software Vulnerabilities table", () => { expect(screen.getByText("No vulnerabilities detected")).toBeInTheDocument(); expect(screen.getByText("0 items")).toBeInTheDocument(); expect( - screen.getByText("Expecting to see vulnerabilities? Check back later.") + screen.getByText( + "Vulnerability data will appear after the next scheduled check-in." + ) ).toBeInTheDocument(); expect(screen.queryByText("Vulnerability")).toBeNull(); }); diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx index bab3be155c..66ad3efddf 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx @@ -62,7 +62,7 @@ const SoftwareVulnerabilitiesTable = ({ isSoftwareEnabled, data, emptyStateReason, - query, + query = "", perPage, orderDirection, orderKey, @@ -142,12 +142,12 @@ const SoftwareVulnerabilitiesTable = ({ [determineQueryParamChange, generateNewQueryParams, router] ); - // determines if a user be able to search in the table - const searchable = - isSoftwareEnabled && - (!!data?.vulnerabilities || - query !== "" || - showExploitedVulnerabilitiesOnly); + const hasData = (data?.vulnerabilities?.length ?? 0) > 0; + const hasQuery = query !== ""; + + const isTrulyEmpty = + !hasData && !hasQuery && !showExploitedVulnerabilitiesOnly; + const controlsDisabled = !isSoftwareEnabled || isTrulyEmpty; const vulnerabilitiesTableHeaders = useMemo(() => { if (!data) return []; @@ -246,6 +246,7 @@ const SoftwareVulnerabilitiesTable = ({ newValue && handleExploitedVulnFilterDropdownChange(newValue.value) } variant="table-filter" + isDisabled={controlsDisabled} /> ); }; @@ -275,14 +276,13 @@ const SoftwareVulnerabilitiesTable = ({ showMarkAllPages={false} isAllPagesSelected={false} disableNextPage={!data?.meta.has_next_results} - searchable={searchable} + searchable + disableSearch={controlsDisabled} searchQueryColumn="vulnerability" inputPlaceHolder="Search by CVE" searchToolTipText={VULNERABILITIES_SEARCH_BOX_TOOLTIP} onQueryChange={onQueryChange} - customControl={ - searchable ? renderExploitedVulnerabilitiesDropdown : undefined - } + customControl={renderExploitedVulnerabilitiesDropdown} stackControls renderCount={renderVulnerabilityCount} renderTableHelpText={renderTableHelpText} diff --git a/frontend/pages/SoftwarePage/components/tables/EmptySoftwareTable/EmptySoftwareTable.tsx b/frontend/pages/SoftwarePage/components/tables/EmptySoftwareTable/EmptySoftwareTable.tsx index 3c809eb372..bfa7cbc882 100644 --- a/frontend/pages/SoftwarePage/components/tables/EmptySoftwareTable/EmptySoftwareTable.tsx +++ b/frontend/pages/SoftwarePage/components/tables/EmptySoftwareTable/EmptySoftwareTable.tsx @@ -17,10 +17,21 @@ export interface IEmptySoftwareTableProps { platform?: HostPlatform; } -const generateTypeText = ( +/** Maps tableName to the truly-empty (no filters) info text. */ +const EMPTY_INFO_BY_TABLE: Record = { + software: + "Recently installed software will appear after the next scheduled check-in.", + "operating systems": + "Operating system data will appear after the next scheduled check-in.", + vulnerabilities: + "Vulnerability data will appear after the next scheduled check-in.", +}; + +/** Returns the display name used in filtered info text (e.g. "vulnerable software"). */ +const getFilteredTypeText = ( tableName: string, vulnFilters?: ISoftwareVulnFiltersParams -) => { +): string => { if (vulnFilters?.vulnerable) { return "vulnerable software"; } @@ -35,15 +46,13 @@ const EmptySoftwareTable = ({ installableSoftwareExists, platform, }: IEmptySoftwareTableProps): JSX.Element => { - const softwareTypeText = generateTypeText(tableName, vulnFilters); - const { filterCount: vulnFiltersCount } = getVulnFilterRenderDetails( vulnFilters ); const isFiltered = vulnFiltersCount > 0 || !noSearchQuery; - const getEmptySoftwareInfo = (): IEmptyStateProps => { + const getEmptyStateProps = (): IEmptyStateProps => { if (isSoftwareDisabled) { return { header: "Software inventory disabled", @@ -61,11 +70,6 @@ const EmptySoftwareTable = ({ }; } - let info = `Expecting to see ${softwareTypeText}? Check back later.`; - if (isAndroid(platform || "")) { - info = `${info} It may take up to 24 hours for Android to report the software.`; - } - if (!isFiltered) { if (installableSoftwareExists) { return { @@ -73,21 +77,34 @@ const EmptySoftwareTable = ({ info: "Install software on your hosts to see versions.", }; } + + let info = EMPTY_INFO_BY_TABLE[tableName] ?? EMPTY_INFO_BY_TABLE.software; + if (isAndroid(platform || "")) { + info = `${info} It may take up to 24 hours for Android to report the software.`; + } + return { header: `No ${tableName} detected`, info, }; } + // Filtered/search state: use type-aware text (e.g. "vulnerable software") + const typeText = getFilteredTypeText(tableName, vulnFilters); + let info = `Expecting to see ${typeText}? Check back later.`; + if (isAndroid(platform || "")) { + info = `${info} It may take up to 24 hours for Android to report the software.`; + } + return { header: "No items match the current search criteria", info, }; }; - const emptySoftware = getEmptySoftwareInfo(); + const emptyState = getEmptyStateProps(); - return ; + return ; }; export default EmptySoftwareTable; diff --git a/frontend/pages/SoftwarePage/components/tables/SoftwareVulnerabilitiesTable/EmptyVulnerabilitiesTable/EmptyVulnerabilitiesTable.tsx b/frontend/pages/SoftwarePage/components/tables/SoftwareVulnerabilitiesTable/EmptyVulnerabilitiesTable/EmptyVulnerabilitiesTable.tsx index 768471a5d5..fa95f65378 100644 --- a/frontend/pages/SoftwarePage/components/tables/SoftwareVulnerabilitiesTable/EmptyVulnerabilitiesTable/EmptyVulnerabilitiesTable.tsx +++ b/frontend/pages/SoftwarePage/components/tables/SoftwareVulnerabilitiesTable/EmptyVulnerabilitiesTable/EmptyVulnerabilitiesTable.tsx @@ -26,7 +26,7 @@ const emptyStateDetails: Record< > = { "no-vulns-detected": { header: "No vulnerabilities detected", - info: "Expecting to see vulnerabilities? Check back later.", + info: "Vulnerability data will appear after the next scheduled check-in.", }, "no-matching-items": { header: "No items match the current search criteria",