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