From 229481fc791ca3ee9397515f6de65b6f2b843289 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:28:55 -0500 Subject: [PATCH] Fleet UI: Update Android status tooltips + global activities (#37185) --- frontend/interfaces/activity.ts | 6 +- .../ActivityFeedFilters/_styles.scss | 8 +- .../InstallerStatusTable.tests.tsx | 174 +++++++++++++++++- .../InstallerStatusTable.tsx | 3 + .../InstallerStatusTableConfig.tsx | 79 ++++---- .../SoftwareInstallerCard.tsx | 1 + 6 files changed, 225 insertions(+), 46 deletions(-) diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index a16343c5fa..954086726b 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -270,7 +270,7 @@ export interface IActivityDetails { } export const ACTIVITY_DISPLAY_NAME_MAP: Record = { - added_app_store_app: "Added App Store (VPP) app", + added_app_store_app: "Added App Store app", // Includes VPP and Android Playstore apps added_bootstrap_package: "Added bootstrap package", added_conditional_access_microsoft: "Added conditional access: Microsoft", added_custom_scep_proxy: "Added certificate authority (CA): custom SCEP", @@ -299,7 +299,7 @@ export const ACTIVITY_DISPLAY_NAME_MAP: Record = { created_team: "Added team", created_user: "Added user", created_windows_profile: "Added configuration profile: Windows", - deleted_app_store_app: "Deleted App Store (VPP) app", + deleted_app_store_app: "Deleted App Store app", // Includes VPP and Android Playstore apps deleted_bootstrap_package: "Deleted bootstrap package", deleted_conditional_access_microsoft: "Deleted conditional access: Microsoft", deleted_custom_scep_proxy: "Deleted certificate authority (CA): custom SCEP", @@ -334,7 +334,7 @@ export const ACTIVITY_DISPLAY_NAME_MAP: Record = { disabled_windows_mdm_migration: "Turned off Windows MDM migration", edited_activity_automations: "Edited activity automations", edited_agent_options: "Edited agent options", - edited_app_store_app: "Edited App Store (VPP) app", + edited_app_store_app: "Edited App Store app", // Includes VPP and Android Playstore apps edited_conditional_access_microsoft: "Edited conditional access: Microsoft", edited_custom_scep_proxy: "Edited certificate authority (CA): custom SCEP", edited_declaration_profile: "GitOps: edited declaration (DDM) profiles", diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/components/ActivityFeedFilters/_styles.scss b/frontend/pages/DashboardPage/cards/ActivityFeed/components/ActivityFeedFilters/_styles.scss index 5b5b14cfdd..adbd45f2db 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/components/ActivityFeedFilters/_styles.scss +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/components/ActivityFeedFilters/_styles.scss @@ -7,11 +7,11 @@ flex: 1; min-width: 0; // allows the dropdowns to shrink properly in flex container } - + &__dropdown-filters { display: grid; grid-template-areas: "type date sort"; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: 4fr 3fr 3fr; gap: $pad-medium; } @@ -30,8 +30,8 @@ // this container is defined in the dashboard page .scss file @container activity-feed-card (width < 436px) { .activity-feed-filters__dropdown-filters { - grid-template-columns: repeat(2, 1fr); - grid-template-areas: + grid-template-columns: repeat(2, 1fr); + grid-template-areas: "type type" "date sort"; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tests.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tests.tsx index 8cb6951af3..2a5945329e 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tests.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tests.tsx @@ -1,6 +1,9 @@ import React from "react"; -import { screen } from "@testing-library/react"; +import { screen, within, waitFor } from "@testing-library/react"; import { createCustomRenderer } from "test/test-utils"; +import { getPathWithQueryParams } from "utilities/url"; +import PATHS from "router/paths"; + import InstallerStatusTable from "./InstallerStatusTable"; describe("InstallerStatusTable", () => { @@ -17,13 +20,168 @@ describe("InstallerStatusTable", () => { // Check cell values (always "hosts", even for 1) const cells = screen.getAllByRole("cell"); - expect(cells[0]).toHaveTextContent("0 hosts"); - expect(cells[1]).toHaveTextContent("1 host"); - expect(cells[2]).toHaveTextContent("3 hosts"); - // Check the anchor and its text in each cell - expect(cells[0].querySelector("a.link-cell")).toHaveTextContent("0 hosts"); - expect(cells[1].querySelector("a.link-cell")).toHaveTextContent("1 host"); - expect(cells[2].querySelector("a.link-cell")).toHaveTextContent("3 hosts"); + const installedLink = cells[0].querySelector("a.link-cell"); + const pendingLink = cells[1].querySelector("a.link-cell"); + const failedLink = cells[2].querySelector("a.link-cell"); + + expect(installedLink).toHaveTextContent("0 hosts"); + expect(pendingLink).toHaveTextContent("1 host"); + expect(failedLink).toHaveTextContent("3 hosts"); + }); + + it("renders correct header titles for install vs script package", () => { + const { rerender } = render( + + ); + + let headers = screen.getAllByRole("columnheader"); + expect(headers[0]).toHaveTextContent("Installed"); + expect(headers[1]).toHaveTextContent("Pending"); + expect(headers[2]).toHaveTextContent("Failed"); + + rerender( + + ); + + headers = screen.getAllByRole("columnheader"); + expect(headers[0]).toHaveTextContent("Ran"); + expect(headers[1]).toHaveTextContent("Pending"); + expect(headers[2]).toHaveTextContent("Failed"); + }); + + it("renders different tooltips for Android Play Store vs non-Android for pending", async () => { + // non-Android: pending install/uninstall message + const { user, rerender } = render( + + ); + + let pendingHeader = screen.getByText(/pending/i); + + await user.hover(pendingHeader); + + await waitFor(() => { + expect( + screen.getByText(/Fleet is installing\/uninstalling or will/i) + ).toBeInTheDocument(); + }); + + // Android: Play Store–style message + rerender( + + ); + + pendingHeader = screen.getByText(/pending/i); + + await user.hover(pendingHeader); + + await waitFor(() => { + expect( + screen.getByText(/Software will be installed or configuration will/i) + ).toBeInTheDocument(); + }); + }); + + it("hides installed tooltip for Android Play Store app", async () => { + const { user, rerender } = render( + + ); + + let installedHeader = screen.getByText(/installed/i); + + await user.hover(installedHeader); + + await waitFor(() => { + expect( + screen.getByText(/Software is installed on these hosts/i) + ).toBeInTheDocument(); + }); + + rerender( + + ); + + installedHeader = screen.getByText(/installed/i); + + await user.hover(installedHeader); + + // Installed tooltip returns null for Android Play Store + await waitFor(() => { + expect( + screen.queryByText(/Software is installed on these hosts/i) + ).not.toBeInTheDocument(); + }); + }); + + it("renders failed tooltip text correctly for Android vs non-Android", async () => { + const { user, rerender } = render( + + ); + + let failedHeader = screen.getByText(/failed/i); + + await user.hover(failedHeader); + + await waitFor(() => { + expect( + screen.getByText(/These hosts failed to install\/uninstall software/i) + ).toBeInTheDocument(); + }); + + rerender( + + ); + + failedHeader = screen.getByText(/failed/i); + + await user.hover(failedHeader); + + await waitFor(() => { + expect( + screen.getByText( + /Software failed to install or configuration failed to apply/i + ) + ).toBeInTheDocument(); + }); }); }); diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx index 2807bf15f4..09bf8f4e75 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx @@ -17,6 +17,7 @@ interface IInstallerStatusTableProps { status: ISoftwarePackageStatus | ISoftwareAppStoreAppStatus; isLoading?: boolean; isScriptPackage?: boolean; + isAndroidPlayStoreApp?: boolean; } const InstallerStatusTable = ({ @@ -26,6 +27,7 @@ const InstallerStatusTable = ({ status, isLoading = false, isScriptPackage = false, + isAndroidPlayStoreApp = false, }: IInstallerStatusTableProps) => { const classNames = classnames(baseClass, className); @@ -34,6 +36,7 @@ const InstallerStatusTable = ({ softwareId, teamId, isScriptPackage, + isAndroidPlayStoreApp, }); return ( diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx index 91caafc69b..6c8dee0923 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { ReactNode } from "react"; import { ISoftwareTitleVersion } from "interfaces/software"; import PATHS from "router/paths"; @@ -9,12 +9,14 @@ import LinkCell from "components/TableContainer/DataTable/LinkCell"; import TooltipWrapper from "components/TooltipWrapper"; import Icon from "components/Icon"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; +import { isAndroid } from "interfaces/platform"; interface ISoftwareTitleDetailsTableConfigProps { softwareId?: number; teamId?: number; baseClass?: string; isScriptPackage?: boolean; + isAndroidPlayStoreApp?: boolean; } interface ICellProps { cell: { @@ -28,7 +30,7 @@ interface ICellProps { interface IStatusDisplayOption { displayName: string; iconName: "success" | "pending-outline" | "error"; - tooltip: React.ReactNode; + tooltip: (isAndroidPlayStoreApp?: boolean) => React.ReactNode; } // "pending" and "failed" each encompass both "_install" and "_uninstall" sub-statuses @@ -45,42 +47,56 @@ const STATUS_DISPLAY_OPTIONS: Record< installed: { displayName: "Installed", iconName: "success", - tooltip: ( - <> - Software is installed on these hosts (install script finished -
- with exit code 0). Currently, if the software is uninstalled, the -
- "Installed" status won't be updated. - - ), + tooltip: (isAndroidPlayStoreApp) => { + return isAndroidPlayStoreApp ? null : ( + <> + Software is installed on these hosts (install script finished +
+ with exit code 0). Currently, if the software is uninstalled, the +
+ "Installed" status won't be updated. + + ); + }, }, pending: { displayName: "Pending", iconName: "pending-outline", - tooltip: ( - <> - Fleet is installing/uninstalling or will -
- do so when the host comes online. - - ), + tooltip: (isAndroidPlayStoreApp) => { + return isAndroidPlayStoreApp ? ( + <> + Software will be installed or configuration will +
+ be applied the next time the host checks in. + + ) : ( + <> + Fleet is installing/uninstalling or will +
+ do so when the host comes online. + + ); + }, }, failed: { displayName: "Failed", iconName: "error", - tooltip: ( - <> - These hosts failed to install/uninstall software. -
- Click on a host to view error(s). - - ), + tooltip: (isAndroidPlayStoreApp) => { + return isAndroidPlayStoreApp ? ( + <>Software failed to install or configuration failed to apply. + ) : ( + <> + These hosts failed to install/uninstall software. +
+ Click on a host to view error(s). + + ); + }, }, ran_script: { displayName: "Ran", iconName: "success", - tooltip: ( + tooltip: () => ( <> The script successfully
@@ -91,7 +107,7 @@ const STATUS_DISPLAY_OPTIONS: Record< pending_script: { displayName: "Pending", iconName: "pending-outline", - tooltip: ( + tooltip: () => ( <> Fleet is running the script or will do so
@@ -102,7 +118,7 @@ const STATUS_DISPLAY_OPTIONS: Record< failed_script: { displayName: "Failed", iconName: "error", - tooltip: ( + tooltip: () => ( <> These hosts failed to run the script.
@@ -117,6 +133,7 @@ const generateSoftwareTitleDetailsTableConfig = ({ teamId, baseClass, isScriptPackage, + isAndroidPlayStoreApp, }: ISoftwareTitleDetailsTableConfigProps) => { const tableHeaders = [ { @@ -130,7 +147,7 @@ const generateSoftwareTitleDetailsTableConfig = ({ const titleWithTooltip = (