From 2e42bbbc84efcd5283d7a92f54d1d2b649939420 Mon Sep 17 00:00:00 2001
From: RachelElysia <71795832+RachelElysia@users.noreply.github.com>
Date: Tue, 11 Nov 2025 10:37:18 -0500
Subject: [PATCH] Fleet UI: 4.77 unreleased bug fixes (#35505)
---
frontend/interfaces/software.ts | 102 ++++++++---
.../forms/PackageForm/PackageForm.tsx | 1 +
.../SoftwareOptionsSelector.tsx | 5 +-
.../cards/HostSoftwareLibrary/_styles.scss | 2 +-
.../TileActionStatus.tests.tsx | 165 +++++++++++++++++-
.../TileActionStatus/TileActionStatus.tsx | 77 +++++---
.../hosts/details/cards/Software/helpers.tsx | 2 +
7 files changed, 304 insertions(+), 50 deletions(-)
diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts
index 77521c2c30..192454d638 100644
--- a/frontend/interfaces/software.ts
+++ b/frontend/interfaces/software.ts
@@ -545,29 +545,87 @@ export interface IHostSoftware {
* - Cases where the software inventory has not yet updated to reflect a recent change
* (i.e., last_install date vs host software's updated_at date)
*/
+// Error UI statuses
+export const HOST_SOFTWARE_UI_ERROR_STATUSES = [
+ "failed_install", // Install attempt failed
+ "failed_install_update_available", // Install/update failed; newer installer version available
+ "failed_uninstall", // Uninstall attempt failed
+ "failed_uninstall_update_available", // Uninstall/update failed; newer installer version available
+ "failed_script", // Script package failed to run
+] as const;
+export type HostSoftwareUiErrorStatus = typeof HOST_SOFTWARE_UI_ERROR_STATUSES[number];
+export const isSoftwareErrorStatus = (
+ status: IHostSoftwareUiStatus
+): status is HostSoftwareUiErrorStatus =>
+ HOST_SOFTWARE_UI_ERROR_STATUSES.includes(status as HostSoftwareUiErrorStatus);
+
+// Pending UI statuses for OFFLINE hosts
+export const HOST_SOFTWARE_UI_PENDING_STATUSES = [
+ "pending_install", // Install scheduled (no newer installer version)
+ "pending_uninstall", // Uninstall scheduled
+ "pending_update", // Update scheduled (no newer installer version)
+ "pending_script", // Fleet-initiated script run scheduled
+] as const;
+export type HostSoftwareUiPendingStatus = typeof HOST_SOFTWARE_UI_PENDING_STATUSES[number];
+export const isSoftwarePendingStatus = (
+ status: IHostSoftwareUiStatus
+): status is HostSoftwareUiPendingStatus =>
+ HOST_SOFTWARE_UI_PENDING_STATUSES.includes(
+ status as HostSoftwareUiPendingStatus
+ );
+
+// In-progress UI statuses for ONLINE hosts
+export const HOST_SOFTWARE_UI_IN_PROGRESS_STATUSES = [
+ "installing", // Fleet-initiated install in progress
+ "updating", // Update (install) in progress with newer fleet installer
+ "uninstalling", // Fleet-initiated uninstall in progress
+ "running_script", // Fleet-initiated script run in progress
+] as const;
+export type HostSoftwareUiInProgressStatus = typeof HOST_SOFTWARE_UI_IN_PROGRESS_STATUSES[number];
+export const isSoftwareInProgressStatus = (
+ status: IHostSoftwareUiStatus
+): status is HostSoftwareUiInProgressStatus =>
+ HOST_SOFTWARE_UI_IN_PROGRESS_STATUSES.includes(
+ status as HostSoftwareUiInProgressStatus
+ );
+
+// Success/steady-state UI statuses
+export const HOST_SOFTWARE_UI_SUCCESS_STATUSES = [
+ "installed", // Present in inventory; no newer fleet installer version (tarballs: successful install only)
+ "uninstalled", // Not present in inventory (tarballs: successful uninstall or never installed)
+ "recently_updated", // Update applied (installer newer than inventory), but inventory not yet refreshed
+ "recently_installed", // Install applied (installer NOT newer than inventory), but inventory not yet refreshed
+ "recently_uninstalled", // Uninstall applied, but inventory not yet refreshed
+ "ran_script", // Script package ran successfully
+ "never_ran_script", // Script package never ran before
+] as const;
+export type HostSoftwareUiSuccessStatus = typeof HOST_SOFTWARE_UI_SUCCESS_STATUSES[number];
+export const isSoftwareSuccessStatus = (
+ status: IHostSoftwareUiStatus
+): status is HostSoftwareUiSuccessStatus =>
+ HOST_SOFTWARE_UI_SUCCESS_STATUSES.includes(
+ status as HostSoftwareUiSuccessStatus
+ );
+
+// Update-available UI status
+export const HOST_SOFTWARE_UI_UPDATE_AVAILABLE_STATUSES = [
+ "update_available", // In inventory, but newer fleet installer version is available
+] as const;
+export type HostSoftwareUiUpdateAvailableStatus = typeof HOST_SOFTWARE_UI_UPDATE_AVAILABLE_STATUSES[number];
+export const isSoftwareUpdateAvailableStatus = (
+ status: IHostSoftwareUiStatus
+): status is HostSoftwareUiUpdateAvailableStatus =>
+ HOST_SOFTWARE_UI_UPDATE_AVAILABLE_STATUSES.includes(
+ status as HostSoftwareUiUpdateAvailableStatus
+ );
+
+// Master UI status type, combining all:
export type IHostSoftwareUiStatus =
- | "installed" // Present in inventory; no newer fleet installer version (tarballs: successful install only)
- | "uninstalled" // Not present in inventory (tarballs: successful uninstall or never installed)
- | "installing" // ONLINE; fleet-initiated install in progress
- | "uninstalling" // ONLINE; fleet-initiated uninstall in progress
- | "recently_updated" // Update applied (installer newer than inventory), but inventory not yet refreshed
- | "recently_installed" // Install applied (installer NOT newer than inventory), but inventory not yet refreshed
- | "recently_uninstalled" // Uninstall applied, but inventory not yet refreshed
- | "updating" // ONLINE; update (install) in progress with newer fleet installer
- | "pending_install" // OFFLINE; install scheduled (no newer installer version)
- | "pending_uninstall" // OFFLINE; uninstall scheduled
- | "pending_update" // OFFLINE; update scheduled (no newer installer version)
- | "failed_install" // Install attempt failed
- | "failed_install_update_available" // Install/update failed; newer installer version available
- | "failed_uninstall" // Uninstall attempt failed
- | "failed_uninstall_update_available" // Uninstall/update failed; newer installer version available
- | "update_available" // In inventory, but newer fleet installer version is available
- // Script UI statuses
- | "ran_script" // Script package ran successfully
- | "failed_script" // Script package failed to run
- | "running_script" // ONLINE; fleet-initiated script run in progress
- | "pending_script" // OFFLINE; fleet-initiated script run scheduled
- | "never_ran_script"; // Script package never ran before
+ | HostSoftwareUiErrorStatus
+ | HostSoftwareUiPendingStatus
+ | HostSoftwareUiSuccessStatus
+ | HostSoftwareUiInProgressStatus
+ | HostSoftwareUiUpdateAvailableStatus;
/**
* Extends IHostSoftware with a computed `ui_status` field.
diff --git a/frontend/pages/SoftwarePage/components/forms/PackageForm/PackageForm.tsx b/frontend/pages/SoftwarePage/components/forms/PackageForm/PackageForm.tsx
index 8fca0947f5..a0375bcaf2 100644
--- a/frontend/pages/SoftwarePage/components/forms/PackageForm/PackageForm.tsx
+++ b/frontend/pages/SoftwarePage/components/forms/PackageForm/PackageForm.tsx
@@ -350,6 +350,7 @@ const PackageForm = ({
isExePackage={isExePackage}
isTarballPackage={isTarballPackage}
isScriptPackage={isScriptPackage}
+ isIpaPackage={isIpaPackage}
onClickPreviewEndUserExperience={() =>
onClickPreviewEndUserExperience(isIpaPackage)
}
diff --git a/frontend/pages/SoftwarePage/components/forms/SoftwareOptionsSelector/SoftwareOptionsSelector.tsx b/frontend/pages/SoftwarePage/components/forms/SoftwareOptionsSelector/SoftwareOptionsSelector.tsx
index 2cf1db359a..0c591f8ab3 100644
--- a/frontend/pages/SoftwarePage/components/forms/SoftwareOptionsSelector/SoftwareOptionsSelector.tsx
+++ b/frontend/pages/SoftwarePage/components/forms/SoftwareOptionsSelector/SoftwareOptionsSelector.tsx
@@ -42,8 +42,9 @@ const CategoriesSelector = ({
value={selectedCategories.includes(cat.value)}
onChange={onSelectCategory}
parseTarget
- />
-
{cat.label}
+ >
+ {cat.label}
+
);
})}
diff --git a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/_styles.scss b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/_styles.scss
index f9679c0c01..766fd00c64 100644
--- a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/_styles.scss
+++ b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/_styles.scss
@@ -53,7 +53,7 @@
}
&__item-action {
- min-width: 90px;
+ min-width: 94px; // Fits the "Reinstall" button without shifting
.component__tooltip-wrapper {
margin: 0; // Override 10px vertical margin
diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/components/TileActionStatus/TileActionStatus.tests.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/components/TileActionStatus/TileActionStatus.tests.tsx
index 520ce9ca0c..2a47fd74ff 100644
--- a/frontend/pages/hosts/details/cards/Software/SelfService/components/TileActionStatus/TileActionStatus.tests.tsx
+++ b/frontend/pages/hosts/details/cards/Software/SelfService/components/TileActionStatus/TileActionStatus.tests.tsx
@@ -11,7 +11,9 @@ const makeSoftware = (ui_status: string) =>
...createMockHostSoftware(),
ui_status,
} as IDeviceSoftwareWithUiStatus);
+
describe("TileActionStatus", () => {
+ // Active/pending/running statuses
it("shows spinner and installing label if installing", () => {
render(
{
expect(screen.getByTestId("spinner")).toBeInTheDocument();
});
+ it("shows spinner and installing label if pending_install", () => {
+ render(
+
+ );
+ expect(screen.getByText(/Installing.../i)).toBeInTheDocument();
+ expect(screen.getByTestId("spinner")).toBeInTheDocument();
+ });
+
it("shows spinner and updating label if updating", () => {
render(
{
expect(screen.getByTestId("spinner")).toBeInTheDocument();
});
+ it("shows spinner and updating label if pending_update", () => {
+ render(
+
+ );
+ expect(screen.getByText(/Updating.../i)).toBeInTheDocument();
+ expect(screen.getByTestId("spinner")).toBeInTheDocument();
+ });
+
+ it("shows spinner and running label if running_script", () => {
+ render(
+
+ );
+ expect(screen.getByText(/Running.../i)).toBeInTheDocument();
+ expect(screen.getByTestId("spinner")).toBeInTheDocument();
+ });
+
+ it("shows spinner and running label if pending_script", () => {
+ render(
+
+ );
+ expect(screen.getByText(/Running.../i)).toBeInTheDocument();
+ expect(screen.getByTestId("spinner")).toBeInTheDocument();
+ });
+
+ it("shows spinner and uninstalling label if uninstalling", () => {
+ render(
+
+ );
+ expect(screen.getByText(/Uninstalling.../i)).toBeInTheDocument();
+ expect(screen.getByTestId("spinner")).toBeInTheDocument();
+ });
+
+ it("shows spinner and uninstalling label if pending_uninstall", () => {
+ render(
+
+ );
+ expect(screen.getByText(/Uninstalling.../i)).toBeInTheDocument();
+ expect(screen.getByTestId("spinner")).toBeInTheDocument();
+ });
+
+ // Button/status tests
it("shows Install button for uninstalled status", () => {
const onClick = jest.fn();
render(
@@ -48,6 +117,18 @@ describe("TileActionStatus", () => {
expect(onClick).toHaveBeenCalled();
});
+ it("shows Install button for recently_uninstalled status", () => {
+ render(
+
+ );
+ expect(
+ screen.getByRole("button", { name: /Install/i })
+ ).toBeInTheDocument();
+ });
+
it("shows Update button for update_available status", () => {
render(
{
expect(screen.getByRole("button", { name: /Update/i })).toBeInTheDocument();
});
+ it("shows Update button for failed_uninstall_update_available status", () => {
+ render(
+
+ );
+ expect(screen.getByRole("button", { name: /Update/i })).toBeInTheDocument();
+ });
+
it("shows Reinstall button for installed status", () => {
render(
{
).toBeInTheDocument();
});
+ it("shows Reinstall button for recently_installed status", () => {
+ render(
+
+ );
+ expect(
+ screen.getByRole("button", { name: /Reinstall/i })
+ ).toBeInTheDocument();
+ });
+
+ it("shows Reinstall button for recently_updated status", () => {
+ render(
+
+ );
+ expect(
+ screen.getByRole("button", { name: /Reinstall/i })
+ ).toBeInTheDocument();
+ });
+
+ it("shows Reinstall button for failed_uninstall status", () => {
+ render(
+
+ );
+ expect(
+ screen.getByRole("button", { name: /Reinstall/i })
+ ).toBeInTheDocument();
+ });
+
+ it("shows Run button for never_ran_script status", () => {
+ render(
+
+ );
+ expect(screen.getByRole("button", { name: /Run/i })).toBeInTheDocument();
+ });
+
+ it("shows Rerun button for ran_script status", () => {
+ render(
+
+ );
+ expect(screen.getByRole("button", { name: /Rerun/i })).toBeInTheDocument();
+ });
+
it("shows Retry button and error display for failed_install", () => {
render(
{
expect(screen.getByTestId("error-icon")).toBeInTheDocument();
});
- it("renders nothing if tile label is null", () => {
+ it("shows Retry button for failed_script status", () => {
+ render(
+
+ );
+ expect(screen.getByRole("button", { name: /Retry/i })).toBeInTheDocument();
+ });
+
+ // Unknown/fallback status
+ it("shows Install button for unknown_status (default)", () => {
render(
);
- expect(screen.queryByRole("button")).not.toBeInTheDocument();
- expect(screen.queryByText(/Failed/i)).not.toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: /Install/i })
+ ).toBeInTheDocument();
});
});
diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/components/TileActionStatus/TileActionStatus.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/components/TileActionStatus/TileActionStatus.tsx
index 817ff05847..79e4e64946 100644
--- a/frontend/pages/hosts/details/cards/Software/SelfService/components/TileActionStatus/TileActionStatus.tsx
+++ b/frontend/pages/hosts/details/cards/Software/SelfService/components/TileActionStatus/TileActionStatus.tsx
@@ -1,5 +1,11 @@
import React from "react";
-import { IDeviceSoftwareWithUiStatus } from "interfaces/software";
+import {
+ IDeviceSoftwareWithUiStatus,
+ IHostSoftwareUiStatus,
+ isSoftwareErrorStatus,
+ isSoftwareInProgressStatus,
+ isSoftwarePendingStatus,
+} from "interfaces/software";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import Spinner from "components/Spinner";
@@ -11,42 +17,69 @@ interface TileActionStatusProps {
onActionClick: (software: IDeviceSoftwareWithUiStatus) => void;
}
-const getTileActionLabel = (software: IDeviceSoftwareWithUiStatus) => {
- if (software.ui_status === "uninstalled") {
- return "Install";
+const getTileActionLabel = (uiStatus: IHostSoftwareUiStatus): string | null => {
+ switch (uiStatus) {
+ case "uninstalled":
+ case "recently_uninstalled":
+ return "Install";
+ case "failed_install":
+ case "failed_install_update_available":
+ case "failed_script":
+ return "Retry";
+ case "update_available":
+ case "failed_uninstall_update_available":
+ return "Update";
+ case "installed":
+ case "recently_installed":
+ case "recently_updated":
+ case "failed_uninstall": // Mobile UI only shows install action despite ui_status relating to uninstall
+ return "Reinstall";
+ case "never_ran_script":
+ return "Run";
+ case "ran_script":
+ return "Rerun";
+ default:
+ return "Install";
}
- if (
- software.ui_status === "failed_install" ||
- software.ui_status === "failed_install_update_available"
- ) {
- return "Retry";
+};
+
+const getPendingOrRunningLabel = (
+ uiStatus: IHostSoftwareUiStatus
+): string | null => {
+ switch (uiStatus) {
+ case "updating":
+ case "pending_update":
+ return "Updating...";
+ case "installing":
+ case "pending_install":
+ return "Installing...";
+ case "running_script":
+ case "pending_script":
+ return "Running...";
+ case "uninstalling":
+ case "pending_uninstall":
+ return "Uninstalling...";
+ default:
+ return null;
}
- if (software.ui_status === "update_available") {
- return "Update";
- }
- if (software.ui_status === "installed") {
- return "Reinstall";
- }
- return null;
};
const TileActionStatus = ({
software,
onActionClick,
}: TileActionStatusProps) => {
- const actionLabel = getTileActionLabel(software);
- const isError =
- software.ui_status === "failed_install" ||
- software.ui_status === "failed_install_update_available";
+ const actionLabel = getTileActionLabel(software.ui_status);
+ const isError = isSoftwareErrorStatus(software.ui_status);
const isActiveAction =
- software.ui_status === "updating" || software.ui_status === "installing";
+ isSoftwareInProgressStatus(software.ui_status) ||
+ isSoftwarePendingStatus(software.ui_status);
const renderActiveActionStatus = () => {
return (
<>
- {software.ui_status === "updating" ? "Updating..." : "Installing..."}
+ {getPendingOrRunningLabel(software.ui_status)}
>
);
};
diff --git a/frontend/pages/hosts/details/cards/Software/helpers.tsx b/frontend/pages/hosts/details/cards/Software/helpers.tsx
index 673b9db920..0fbcf7f870 100644
--- a/frontend/pages/hosts/details/cards/Software/helpers.tsx
+++ b/frontend/pages/hosts/details/cards/Software/helpers.tsx
@@ -338,6 +338,8 @@ export const getInstallerActionButtonConfig = (
case "pending_uninstall":
case "uninstalling":
case "failed_uninstall":
+ case "recently_installed":
+ case "recently_updated":
return { text: "Reinstall", icon: "refresh" };
case "pending_update":
case "updating":