Map additional pending statuses to "pending" UI in host details > OS Settings table (#37375)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**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>
This commit is contained in:
jacobshandling
2025-12-17 11:02:46 -08:00
committed by GitHub
co-authored by Victor Lyuboslavsky
parent b8b50ed7e2
commit 9a6ef5ba52
8 changed files with 153 additions and 54 deletions
+4 -4
View File
@@ -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 {
+7 -2
View File
@@ -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,
@@ -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,
});
@@ -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(
<OSSettingStatusCell
profileName="Test cert"
status="pending"
operationType="install"
hostPlatform="android"
profileUUID={FLEET_ANDROID_CERTIFICATE_TEMPLATE_PROFILE_ID}
/>
);
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(
<OSSettingStatusCell
profileName="Test cert"
status="delivering"
operationType="install"
hostPlatform="android"
profileUUID={FLEET_ANDROID_CERTIFICATE_TEMPLATE_PROFILE_ID}
/>
);
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(
<OSSettingStatusCell
profileName="Test cert"
status="delivered"
operationType="install"
hostPlatform="android"
profileUUID={FLEET_ANDROID_CERTIFICATE_TEMPLATE_PROFILE_ID}
/>
);
const statusText = screen.getByText("Enforcing (pending)");
expect(statusText).toBeInTheDocument();
await user.hover(statusText);
expect(
screen.getByText(/The host is running the command/)
).toBeInTheDocument();
});
});
@@ -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
@@ -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<ProfileStatus, ProfileDisplayOption>;
type ProfileDisplayConfig = Record<ProfileOperationType, OperationTypeOption>;
@@ -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 = {
@@ -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,
@@ -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