Fleet UI: 4.77 unreleased bug fixes (#35505)

This commit is contained in:
RachelElysia
2025-11-11 10:37:18 -05:00
committed by GitHub
parent 3135e9a0a7
commit 2e42bbbc84
7 changed files with 304 additions and 50 deletions
+80 -22
View File
@@ -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.
@@ -350,6 +350,7 @@ const PackageForm = ({
isExePackage={isExePackage}
isTarballPackage={isTarballPackage}
isScriptPackage={isScriptPackage}
isIpaPackage={isIpaPackage}
onClickPreviewEndUserExperience={() =>
onClickPreviewEndUserExperience(isIpaPackage)
}
@@ -42,8 +42,9 @@ const CategoriesSelector = ({
value={selectedCategories.includes(cat.value)}
onChange={onSelectCategory}
parseTarget
/>
<div className={`${baseClass}__label-name`}>{cat.label}</div>
>
<div className={`${baseClass}__label-name`}>{cat.label}</div>
</Checkbox>
</div>
);
})}
@@ -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
@@ -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(
<TileActionStatus
@@ -23,6 +25,17 @@ describe("TileActionStatus", () => {
expect(screen.getByTestId("spinner")).toBeInTheDocument();
});
it("shows spinner and installing label if pending_install", () => {
render(
<TileActionStatus
software={makeSoftware("pending_install")}
onActionClick={jest.fn()}
/>
);
expect(screen.getByText(/Installing.../i)).toBeInTheDocument();
expect(screen.getByTestId("spinner")).toBeInTheDocument();
});
it("shows spinner and updating label if updating", () => {
render(
<TileActionStatus
@@ -34,6 +47,62 @@ describe("TileActionStatus", () => {
expect(screen.getByTestId("spinner")).toBeInTheDocument();
});
it("shows spinner and updating label if pending_update", () => {
render(
<TileActionStatus
software={makeSoftware("pending_update")}
onActionClick={jest.fn()}
/>
);
expect(screen.getByText(/Updating.../i)).toBeInTheDocument();
expect(screen.getByTestId("spinner")).toBeInTheDocument();
});
it("shows spinner and running label if running_script", () => {
render(
<TileActionStatus
software={makeSoftware("running_script")}
onActionClick={jest.fn()}
/>
);
expect(screen.getByText(/Running.../i)).toBeInTheDocument();
expect(screen.getByTestId("spinner")).toBeInTheDocument();
});
it("shows spinner and running label if pending_script", () => {
render(
<TileActionStatus
software={makeSoftware("pending_script")}
onActionClick={jest.fn()}
/>
);
expect(screen.getByText(/Running.../i)).toBeInTheDocument();
expect(screen.getByTestId("spinner")).toBeInTheDocument();
});
it("shows spinner and uninstalling label if uninstalling", () => {
render(
<TileActionStatus
software={makeSoftware("uninstalling")}
onActionClick={jest.fn()}
/>
);
expect(screen.getByText(/Uninstalling.../i)).toBeInTheDocument();
expect(screen.getByTestId("spinner")).toBeInTheDocument();
});
it("shows spinner and uninstalling label if pending_uninstall", () => {
render(
<TileActionStatus
software={makeSoftware("pending_uninstall")}
onActionClick={jest.fn()}
/>
);
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(
<TileActionStatus
software={makeSoftware("recently_uninstalled")}
onActionClick={jest.fn()}
/>
);
expect(
screen.getByRole("button", { name: /Install/i })
).toBeInTheDocument();
});
it("shows Update button for update_available status", () => {
render(
<TileActionStatus
@@ -58,6 +139,16 @@ describe("TileActionStatus", () => {
expect(screen.getByRole("button", { name: /Update/i })).toBeInTheDocument();
});
it("shows Update button for failed_uninstall_update_available status", () => {
render(
<TileActionStatus
software={makeSoftware("failed_uninstall_update_available")}
onActionClick={jest.fn()}
/>
);
expect(screen.getByRole("button", { name: /Update/i })).toBeInTheDocument();
});
it("shows Reinstall button for installed status", () => {
render(
<TileActionStatus
@@ -70,6 +161,62 @@ describe("TileActionStatus", () => {
).toBeInTheDocument();
});
it("shows Reinstall button for recently_installed status", () => {
render(
<TileActionStatus
software={makeSoftware("recently_installed")}
onActionClick={jest.fn()}
/>
);
expect(
screen.getByRole("button", { name: /Reinstall/i })
).toBeInTheDocument();
});
it("shows Reinstall button for recently_updated status", () => {
render(
<TileActionStatus
software={makeSoftware("recently_updated")}
onActionClick={jest.fn()}
/>
);
expect(
screen.getByRole("button", { name: /Reinstall/i })
).toBeInTheDocument();
});
it("shows Reinstall button for failed_uninstall status", () => {
render(
<TileActionStatus
software={makeSoftware("failed_uninstall")}
onActionClick={jest.fn()}
/>
);
expect(
screen.getByRole("button", { name: /Reinstall/i })
).toBeInTheDocument();
});
it("shows Run button for never_ran_script status", () => {
render(
<TileActionStatus
software={makeSoftware("never_ran_script")}
onActionClick={jest.fn()}
/>
);
expect(screen.getByRole("button", { name: /Run/i })).toBeInTheDocument();
});
it("shows Rerun button for ran_script status", () => {
render(
<TileActionStatus
software={makeSoftware("ran_script")}
onActionClick={jest.fn()}
/>
);
expect(screen.getByRole("button", { name: /Rerun/i })).toBeInTheDocument();
});
it("shows Retry button and error display for failed_install", () => {
render(
<TileActionStatus
@@ -94,14 +241,26 @@ describe("TileActionStatus", () => {
expect(screen.getByTestId("error-icon")).toBeInTheDocument();
});
it("renders nothing if tile label is null", () => {
it("shows Retry button for failed_script status", () => {
render(
<TileActionStatus
software={makeSoftware("failed_script")}
onActionClick={jest.fn()}
/>
);
expect(screen.getByRole("button", { name: /Retry/i })).toBeInTheDocument();
});
// Unknown/fallback status
it("shows Install button for unknown_status (default)", () => {
render(
<TileActionStatus
software={makeSoftware("unknown_status")}
onActionClick={jest.fn()}
/>
);
expect(screen.queryByRole("button")).not.toBeInTheDocument();
expect(screen.queryByText(/Failed/i)).not.toBeInTheDocument();
expect(
screen.getByRole("button", { name: /Install/i })
).toBeInTheDocument();
});
});
@@ -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 (
<>
<Spinner size="x-small" includeContainer={false} centered={false} />
{software.ui_status === "updating" ? "Updating..." : "Installing..."}
{getPendingOrRunningLabel(software.ui_status)}
</>
);
};
@@ -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":