From 9a6ef5ba522045e5a8bc1ea0dfc0da910c58d101 Mon Sep 17 00:00:00 2001
From: jacobshandling <61553566+jacobshandling@users.noreply.github.com>
Date: Wed, 17 Dec 2025 11:02:46 -0800
Subject: [PATCH] Map additional pending statuses to "pending" UI in host
details > OS Settings table (#37375)
**Related issue:** Resolves #36683
# Checklist for submitter
- [x] Added/updated automated tests
- [ ] QA'd all new/changed functionality manually
---------
Co-authored-by: Victor Lyuboslavsky <2685025+getvictor@users.noreply.github.com>
---
frontend/interfaces/host.ts | 8 +--
frontend/interfaces/mdm.ts | 9 ++-
.../AddCertificateModal.tests.tsx | 30 +++-----
.../OSSettingStatusCell.tests.tsx | 70 ++++++++++++++++++-
.../OSSettingStatusCell.tsx | 53 ++++++++++----
.../OSSettingStatusCell/helpers.ts | 20 ++++--
.../OSSettingsTable/OSSettingsTableConfig.tsx | 5 +-
.../OSSettingsIndicator.tsx | 12 ++--
8 files changed, 153 insertions(+), 54 deletions(-)
diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts
index 453bf5596e..b8dfe0de2e 100644
--- a/frontend/interfaces/host.ts
+++ b/frontend/interfaces/host.ts
@@ -97,7 +97,7 @@ export interface IMunkiData {
export type MacDiskEncryptionActionRequired = "log_out" | "rotate_key";
-export type HostCertStatus =
+export type HostAndroidCertStatus =
| "verified"
| "failed"
// all below display "pending" in UI
@@ -105,9 +105,9 @@ export type HostCertStatus =
| "delivering"
| "delivered";
-export interface IHostCert {
+export interface IHostAndroidCert {
name: string;
- status: HostCertStatus;
+ status: HostAndroidCertStatus;
operation_type: "install" | "remove";
detail: string;
}
@@ -117,7 +117,7 @@ export interface IOSSettings {
status: DiskEncryptionStatus | null;
detail: string;
};
- certificates: IHostCert[];
+ certificates: IHostAndroidCert[];
}
interface IMdmMacOsSettings {
diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts
index c2a1f31555..fa3ad84774 100644
--- a/frontend/interfaces/mdm.ts
+++ b/frontend/interfaces/mdm.ts
@@ -1,4 +1,5 @@
import { IConfigServerSettings } from "./config";
+import { HostAndroidCertStatus } from "./host";
export interface IMdmApple {
common_name: string;
@@ -185,7 +186,11 @@ export interface IHostMdmProfile {
name: string;
operation_type: ProfileOperationType | null;
platform: ProfilePlatform;
- status: MdmProfileStatus | MdmDDMProfileStatus | LinuxDiskEncryptionStatus;
+ status:
+ | MdmProfileStatus
+ | MdmDDMProfileStatus
+ | LinuxDiskEncryptionStatus
+ | HostAndroidCertStatus;
detail: string;
scope: ProfileScope | null;
managed_local_account: string | null;
@@ -200,7 +205,7 @@ export type DiskEncryptionStatus =
| "failed"
| "removing_enforcement";
-/** Currently windows disk enxryption status will only be one of these four
+/** Currently windows disk encryption status will only be one of these four
values. In the future we may add more. */
export type WindowsDiskEncryptionStatus = Extract<
DiskEncryptionStatus,
diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/Certificates/components/AddCertificateModal/AddCertificateModal.tests.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/Certificates/components/AddCertificateModal/AddCertificateModal.tests.tsx
index 926f6e697b..7f6e265d19 100644
--- a/frontend/pages/ManageControlsPage/OSSettings/cards/Certificates/components/AddCertificateModal/AddCertificateModal.tests.tsx
+++ b/frontend/pages/ManageControlsPage/OSSettings/cards/Certificates/components/AddCertificateModal/AddCertificateModal.tests.tsx
@@ -4,11 +4,7 @@ import { http, HttpResponse } from "msw";
import { screen, waitFor } from "@testing-library/react";
import { ICertificate } from "services/entities/certificates";
import mockServer from "test/mock-server";
-import {
- renderWithSetup,
- baseUrl,
- createCustomRenderer,
-} from "test/test-utils";
+import { baseUrl, createCustomRenderer } from "test/test-utils";
import AddCertModal from "./AddCertificateModal";
import { INVALID_NAME_MSG, NAME_TOO_LONG_MSG, USED_NAME_MSG } from "./helpers";
@@ -49,9 +45,15 @@ const mockExistingCerts: ICertificate[] = [
];
describe("AddCertModal", () => {
- it("renders the modal with all form fields", async () => {
+ beforeEach(() => {
mockServer.use(getCAsHandler);
mockServer.use(createCertHandler);
+ });
+ afterEach(() => {
+ mockServer.resetHandlers();
+ });
+
+ it("renders the modal with all form fields", async () => {
const render = createCustomRenderer({
withBackendMock: true,
});
@@ -80,8 +82,6 @@ describe("AddCertModal", () => {
});
it("disables Create button when Name field is empty", async () => {
- mockServer.use(getCAsHandler);
- mockServer.use(createCertHandler);
const render = createCustomRenderer({
withBackendMock: true,
});
@@ -109,8 +109,6 @@ describe("AddCertModal", () => {
});
it("shows error for Name with invalid characters and disables Create button", async () => {
- mockServer.use(getCAsHandler);
- mockServer.use(createCertHandler);
const render = createCustomRenderer({
withBackendMock: true,
});
@@ -138,8 +136,6 @@ describe("AddCertModal", () => {
});
it("shows error for Name that already exists and disables Create button", async () => {
- mockServer.use(getCAsHandler);
- mockServer.use(createCertHandler);
const render = createCustomRenderer({
withBackendMock: true,
});
@@ -167,8 +163,6 @@ describe("AddCertModal", () => {
});
it("shows error for Name with more than 255 characters and disables Create button", async () => {
- mockServer.use(getCAsHandler);
- mockServer.use(createCertHandler);
const render = createCustomRenderer({
withBackendMock: true,
});
@@ -197,8 +191,6 @@ describe("AddCertModal", () => {
});
it("disables Create button when Certificate authority is not selected", async () => {
- mockServer.use(getCAsHandler);
- mockServer.use(createCertHandler);
const render = createCustomRenderer({
withBackendMock: true,
});
@@ -227,8 +219,6 @@ describe("AddCertModal", () => {
});
it("disables Create button when Subject name is empty", async () => {
- mockServer.use(getCAsHandler);
- mockServer.use(createCertHandler);
const render = createCustomRenderer({
withBackendMock: true,
});
@@ -263,8 +253,6 @@ describe("AddCertModal", () => {
});
it("full flow is okay when all fields are valid", async () => {
- mockServer.use(getCAsHandler);
- mockServer.use(createCertHandler);
const render = createCustomRenderer({
withBackendMock: true,
});
@@ -307,8 +295,6 @@ describe("AddCertModal", () => {
});
it("calls onExit when Cancel button is clicked", async () => {
- mockServer.use(getCAsHandler);
- mockServer.use(createCertHandler);
const render = createCustomRenderer({
withBackendMock: true,
});
diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tests.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tests.tsx
index 58f103bafa..f55c34cf59 100644
--- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tests.tsx
+++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tests.tsx
@@ -1,7 +1,10 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { createCustomRenderer } from "test/test-utils";
-import { ProfileOperationType } from "interfaces/mdm";
+import {
+ FLEET_ANDROID_CERTIFICATE_TEMPLATE_PROFILE_ID,
+ ProfileOperationType,
+} from "interfaces/mdm";
import OSSettingStatusCell from "./OSSettingStatusCell";
describe("OS setting status cell", () => {
@@ -40,4 +43,69 @@ describe("OS setting status cell", () => {
expect(screen.getByText(/verifying/)).toBeInTheDocument();
});
+
+ // Android cert statuses
+ it("Displays Pending UI for 'pending' status with optype 'install'", async () => {
+ const customRender = createCustomRenderer();
+
+ const { user } = customRender(
+
+ );
+
+ const statusText = screen.getByText("Enforcing (pending)");
+ expect(statusText).toBeInTheDocument();
+
+ await user.hover(statusText);
+ expect(
+ screen.getByText(/The host is running the command/)
+ ).toBeInTheDocument();
+ });
+ it("Displays Pending UI for 'delivering' status with optype 'install'", async () => {
+ const customRender = createCustomRenderer();
+
+ const { user } = customRender(
+
+ );
+
+ const statusText = screen.getByText("Enforcing (pending)");
+ expect(statusText).toBeInTheDocument();
+
+ await user.hover(statusText);
+ expect(
+ screen.getByText(/The host is running the command/)
+ ).toBeInTheDocument();
+ });
+ it("Displays Pending UI for 'delivered' status with optype 'install'", async () => {
+ const customRender = createCustomRenderer();
+
+ const { user } = customRender(
+
+ );
+
+ const statusText = screen.getByText("Enforcing (pending)");
+ expect(statusText).toBeInTheDocument();
+
+ await user.hover(statusText);
+ expect(
+ screen.getByText(/The host is running the command/)
+ ).toBeInTheDocument();
+ });
});
diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tsx
index d56c197b69..12ea168c50 100644
--- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tsx
+++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tsx
@@ -19,7 +19,9 @@ import {
LINUX_DISK_ENCRYPTION_DISPLAY_CONFIG,
PROFILE_DISPLAY_CONFIG,
ProfileDisplayOption,
+ ProfileStatus,
WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG,
+ WindowsDiskEncryptionDisplayStatus,
} from "./helpers";
const baseClass = "os-settings-status-cell";
@@ -52,17 +54,38 @@ const OSSettingStatusCell = ({
) {
switch (status) {
case "pending":
- displayOption = {
- statusText:
- operationType === "install"
- ? "Enforcing (pending)"
- : "Removing enforcement (pending)",
- iconName: "pending-outline",
- tooltip: () =>
- operationType === "install"
- ? "The host is running the command to apply settings or will run it when the host comes online."
- : "The host is running the command to remove settings or will run it when the host comes online.",
- };
+ if (operationType === "install") {
+ displayOption = {
+ statusText: "Enforcing (pending)",
+ iconName: "pending-outline",
+ tooltip:
+ "The host is running the command to apply settings or will run it when the host comes online.",
+ };
+ } else {
+ displayOption = {
+ statusText: "Removing enforcement (pending)",
+ iconName: "pending-outline",
+ tooltip:
+ "The host is running the command to remove settings or will run it when the host comes online.",
+ };
+ }
+ break;
+ case "delivering":
+ case "delivered":
+ if (operationType === "install") {
+ // note that thise case is identical to the "pending" case above for install operation
+ // separation allows catching the below error case
+ displayOption = {
+ statusText: "Enforcing (pending)",
+ iconName: "pending-outline",
+ tooltip:
+ "The host is running the command to apply settings or will run it when the host comes online.",
+ };
+ } else {
+ throw new Error(
+ "Received unexpected 'delivering' or 'delivered' status for remove operation"
+ );
+ }
break;
case "verified":
displayOption = {
@@ -90,9 +113,13 @@ const OSSettingStatusCell = ({
status !== "success" &&
status !== "acknowledged"
) {
- displayOption = WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG[status];
+ displayOption =
+ WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG[
+ status as WindowsDiskEncryptionDisplayStatus
+ ];
} else if (operationType) {
- displayOption = PROFILE_DISPLAY_CONFIG[operationType]?.[status];
+ displayOption =
+ PROFILE_DISPLAY_CONFIG[operationType]?.[status as ProfileStatus];
}
const isDeviceUser = window.location.pathname
diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts
index bcc993c149..6d5beb9f56 100644
--- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts
+++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts
@@ -22,10 +22,14 @@ export type ProfileDisplayOption = {
tooltip: TooltipInnerContentOption | null;
} | null;
-type OperationTypeOption = Record<
+type MacProfileSpecificStatus = "success" | "acknowledged";
+type AndroidCertSpecificStatus = "delivered" | "delivering";
+
+export type ProfileStatus = Exclude<
OsSettingsTableStatusValue,
- ProfileDisplayOption
+ AndroidCertSpecificStatus
>;
+type OperationTypeOption = Record;
type ProfileDisplayConfig = Record;
@@ -103,10 +107,14 @@ export const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
},
};
-type WindowsDiskEncryptionDisplayConfig = Omit<
+export type WindowsDiskEncryptionDisplayStatus = Exclude<
+ ProfileStatus,
+ MacProfileSpecificStatus | AndroidCertSpecificStatus
+>;
+
+type WindowsDiskEncryptionDisplayConfig = Pick<
OperationTypeOption,
- // windows disk encryption does not have these states
- "success" | "acknowledged"
+ WindowsDiskEncryptionDisplayStatus
>;
export const WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG: WindowsDiskEncryptionDisplayConfig = {
@@ -144,7 +152,7 @@ export const WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG: WindowsDiskEncryptionDispla
type LinuxDiskEncryptionDisplayConfig = Omit<
OperationTypeOption,
- "success" | "pending" | "acknowledged" | "verifying"
+ MacProfileSpecificStatus | AndroidCertSpecificStatus | "pending" | "verifying"
>;
export const LINUX_DISK_ENCRYPTION_DISPLAY_CONFIG: LinuxDiskEncryptionDisplayConfig = {
diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx
index cad19a7666..1ef6d76ad7 100644
--- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx
+++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx
@@ -2,7 +2,7 @@ import React from "react";
import { Column } from "react-table";
import { IStringCellProps } from "interfaces/datatable_config";
-import { IHostMdmData } from "interfaces/host";
+import { HostAndroidCertStatus, IHostMdmData } from "interfaces/host";
import {
FLEET_FILEVAULT_PROFILE_DISPLAY_NAME,
IHostMdmProfile,
@@ -37,7 +37,8 @@ export type INonDDMProfileStatus = MdmProfileStatus | "action_required";
export type OsSettingsTableStatusValue =
| MdmDDMProfileStatus
- | INonDDMProfileStatus;
+ | INonDDMProfileStatus
+ | HostAndroidCertStatus;
const generateTableConfig = (
canResendProfiles: boolean,
diff --git a/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx b/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx
index 3a145e4f0e..18adaec8b1 100644
--- a/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx
+++ b/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx
@@ -47,7 +47,11 @@ const countHostProfilesByStatus = (
(acc, { status }) => {
if (status === "failed") {
acc.failed += 1;
- } else if (status === "pending" || status === "action_required") {
+ } else if (
+ ["pending", "action_required", "delivering", "delivered"].includes(
+ status
+ )
+ ) {
acc.pending += 1;
} else if (status === "verifying") {
acc.verifying += 1;
@@ -77,9 +81,9 @@ const countHostProfilesByStatus = (
* https://fleetdm.com/handbook/company/why-this-way#why-make-it-obvious-when-stuff-breaks
*/
const getHostProfilesStatusForDisplay = (
- hostMacSettings: IHostMdmProfile[]
+ hostProfiles: IHostMdmProfile[]
): MdmProfileStatusForDisplay => {
- const counts = countHostProfilesByStatus(hostMacSettings);
+ const counts = countHostProfilesByStatus(hostProfiles);
switch (true) {
case !!counts.failed:
return "Failed";
@@ -87,7 +91,7 @@ const getHostProfilesStatusForDisplay = (
return "Pending";
case !!counts.verifying:
return "Verifying";
- case counts.verified === hostMacSettings.length:
+ case counts.verified === hostProfiles.length:
return "Verified";
default:
// something is broken