From dae35c887bf4f171babd7690d09eaf7b681b5e8f Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Tue, 19 Aug 2025 09:04:07 -0400 Subject: [PATCH] FE: Add tests to install/uninstall modals (#31992) --- frontend/__mocks__/softwareMock.ts | 27 +++ .../SoftwareInstallDetailsModal.tests.tsx | 163 ++++++++++++++++++ .../SoftwareInstallDetailsModal.tsx | 102 +++++++---- .../SoftwareUninstallDetailsModal.tests.tsx | 155 +++++++++++++++++ .../SoftwareUninstallDetailsModal.tsx | 96 +++++++---- .../VppInstallDetailsModal.tests.tsx | 106 +++++++++++- .../VppInstallDetailsModal.tsx | 80 ++++++--- .../SoftwareUpdateModal.tests.tsx | 5 + 8 files changed, 629 insertions(+), 105 deletions(-) create mode 100644 frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tests.tsx create mode 100644 frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tests.tsx diff --git a/frontend/__mocks__/softwareMock.ts b/frontend/__mocks__/softwareMock.ts index 3aa8de3b5b..1a10ba779d 100644 --- a/frontend/__mocks__/softwareMock.ts +++ b/frontend/__mocks__/softwareMock.ts @@ -9,6 +9,7 @@ import { IAppStoreApp, IFleetMaintainedApp, IFleetMaintainedAppDetails, + ISoftwareInstallResult, } from "interfaces/software"; import { ISoftwareTitlesResponse, @@ -305,3 +306,29 @@ export const createMockFleetMaintainedAppDetails = ( ) => { return { ...DEFAULT_FLEET_MAINTAINED_APP_DETAILS_MOCK, ...overrides }; }; + +const DEFAULT_SOFTWARE_INSTALL_RESULT: ISoftwareInstallResult = { + host_display_name: "Test Host", + install_uuid: "uuid-123", + software_title: "CoolApp", + software_title_id: 1, + software_package: "com.cool.app", + host_id: 42, + status: "installed", + detail: "", + output: "", + pre_install_query_output: "", + post_install_script_output: "", + created_at: "2025-08-10T12:00:00Z", + updated_at: "2025-08-10T12:05:00Z", + self_service: false, +}; + +export const createMockSoftwareInstallResult = ( + overrides?: Partial +) => { + return { + ...DEFAULT_SOFTWARE_INSTALL_RESULT, + ...overrides, + }; +}; diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tests.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tests.tsx new file mode 100644 index 0000000000..bbb8afb99f --- /dev/null +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tests.tsx @@ -0,0 +1,163 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { renderWithSetup } from "test/test-utils"; +import { createMockSoftwareInstallResult } from "__mocks__/softwareMock"; +import { StatusMessage, ModalButtons } from "./SoftwareInstallDetailsModal"; + +describe("SoftwareInstallDetailsModal - StatusMessage component", () => { + it("renders basic 'is installed' message when not installed by fleet (no installResult provided)", () => { + render(); + expect(screen.getByText(/CoolApp/)).toBeInTheDocument(); + expect(screen.getByText(/is installed/)).toBeInTheDocument(); + }); + + it("on software library page/pending activity, renders pending install message with host and package name", () => { + render( + + ); + + expect(screen.queryByTestId("pending-outline-icon")).toBeInTheDocument(); + expect( + screen.getByText(/is installing or will install/) + ).toBeInTheDocument(); + expect(screen.getByText(/\(com\.cool\.app\)/)).toBeInTheDocument(); + expect(screen.getByText(/Test Host/)).toBeInTheDocument(); + expect(screen.getByText(/when it comes online/)).toBeInTheDocument(); + }); + + it("on device user page, renders failed install with retry option with contact link", () => { + render( + + ); + + expect(screen.queryByTestId("error-icon")).toBeInTheDocument(); + expect(screen.getByText(/failed to install/)).toBeInTheDocument(); + expect(screen.getByText(/CoolApp/)).toBeInTheDocument(); + // Host name should not be rendered for device user page + expect(screen.queryByText(/Test Host/)).not.toBeInTheDocument(); + expect(screen.getByText(/You can retry/)).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: /contact your IT admin/ }) + ).toHaveAttribute("href", "http://support"); + }); + + it("on device user page, renders failed install with retry option without contact link", () => { + render( + + ); + + expect(screen.queryByTestId("error-icon")).toBeInTheDocument(); + expect(screen.getByText(/failed to install/)).toBeInTheDocument(); + expect(screen.getByText(/CoolApp/)).toBeInTheDocument(); + // Host name should not be rendered for device user page + expect(screen.queryByText(/Test Host/)).not.toBeInTheDocument(); + expect(screen.getByText(/You can retry/)).toBeInTheDocument(); + // Don't show link of not provided + expect( + screen.queryByRole("link", { name: /contact your IT admin/ }) + ).not.toBeInTheDocument(); + }); + + it("on host details page, renders failed install without retry", () => { + render( + + ); + + expect(screen.queryByTestId("error-icon")).toBeInTheDocument(); + expect(screen.getByText(/failed to install/)).toBeInTheDocument(); + expect(screen.getByText(/Test Host/)).toBeInTheDocument(); + expect(screen.queryByText(/You can retry/)).not.toBeInTheDocument(); + }); + + it("on host details page/install activity, renders installed message with timestamp", () => { + render( + + ); + + expect(screen.queryByTestId("success-icon")).toBeInTheDocument(); + expect(screen.getByText(/Fleet installed/)).toBeInTheDocument(); + expect(screen.getByText(/CoolApp/)).toBeInTheDocument(); + expect(screen.getByText(/Test Host/)).toBeInTheDocument(); + expect(screen.getByText(/\(com\.cool\.app\)/)).toBeInTheDocument(); + expect(screen.getByText(/\d+.*ago/)).toBeInTheDocument(); + }); +}); + +describe("SoftwareInstallDetailsModal - ModalButtons component", () => { + it("on device user page, shows Retry/Cancel for failed install and triggers handlers", async () => { + const onCancel = jest.fn(); + const onRetry = jest.fn(); + + const { user } = renderWithSetup( + + ); + expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Retry" })); + expect(onRetry).toHaveBeenCalledWith(99); + expect(onCancel).toHaveBeenCalled(); + + await user.click(screen.getByRole("button", { name: "Cancel" })); + expect(onCancel).toHaveBeenCalledTimes(2); + }); + + it("shows Done button for pending install", () => { + const onCancel = jest.fn(); + render(); + expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "Retry" }) + ).not.toBeInTheDocument(); + }); + + it("on device user page, shows Done button for installed software", () => { + const onCancel = jest.fn(); + render( + + ); + expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument(); + }); +}); diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx index fa2b934964..e3c49c8d3e 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx @@ -16,6 +16,7 @@ import deviceUserAPI from "services/entities/device_user"; import InventoryVersions from "pages/hosts/details/components/InventoryVersions"; import Modal from "components/Modal"; +import ModalFooter from "components/ModalFooter"; import Button from "components/buttons/Button"; import Icon from "components/Icon"; import Textarea from "components/Textarea"; @@ -49,19 +50,21 @@ export const renderContactOption = (url?: string) => ( ); -// TODO - match VppInstallDetailsModal status to this, still accounting for MDM-specific cases -// present there -const StatusMessage = ({ - softwareName, - installResult, - isDUP, - contactUrl, -}: { +interface IInstallStatusMessage { softwareName: string; installResult?: ISoftwareInstallResult; isDUP: boolean; contactUrl?: string; -}) => { +} + +// TODO - match VppInstallDetailsModal status to this, still accounting for MDM-specific cases +// present there +export const StatusMessage = ({ + softwareName, + installResult, + isDUP, + contactUrl, +}: IInstallStatusMessage) => { // the case when software is installed by the user and not by Fleet if (!installResult) { return ( @@ -142,6 +145,51 @@ const StatusMessage = ({ ); }; +interface IModalButtonsProps { + deviceAuthToken?: string; + status?: string; + hostSoftwareId?: number; + onRetry?: (id: number) => void; + onCancel: () => void; +} + +export const ModalButtons = ({ + deviceAuthToken, + status, + hostSoftwareId, + onRetry, + onCancel, +}: IModalButtonsProps) => { + if (deviceAuthToken && status === "failed_install") { + const onClickRetry = () => { + // on DUP, where this is relevant, both will be defined + if (onRetry && hostSoftwareId) { + onRetry(hostSoftwareId); + } + onCancel(); + }; + + return ( + + + + + } + /> + ); + } + + return ( + Done} /> + ); +}; + interface ISoftwareInstallDetailsProps { /** note that details.install_uuid is present in hostSoftware, but since it is always needed for this modal while hostSoftware is not, as in the case of the activity feeds, it is specifically @@ -170,14 +218,6 @@ export const SoftwareInstallDetailsModal = ({ setShowInstallDetails((prev) => !prev); }; - const onClickRetry = () => { - // on DUP, where this is relevant, both will be defined - if (onRetry && hostSoftware?.id) { - onRetry(hostSoftware.id); - } - onCancel(); - }; - const isInstalledByFleet = hostSoftware ? !!hostSoftware.software_package?.last_install : true; // if no hostSoftware passed in, can assume this is the activity feed, meaning this can only refer to a Fleet-handled install @@ -304,26 +344,6 @@ export const SoftwareInstallDetailsModal = ({ ); }; - const renderCta = () => { - if (deviceAuthToken && swInstallResult?.status === "failed_install") { - return ( -
- - -
- ); - } - return ( -
- -
- ); - }; - return ( <> {renderContent()} - {renderCta()} + ); diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tests.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tests.tsx new file mode 100644 index 0000000000..dcef508182 --- /dev/null +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tests.tsx @@ -0,0 +1,155 @@ +import React from "react"; + +import { render, screen } from "@testing-library/react"; +import { renderWithSetup } from "test/test-utils"; +import { createMockHostSoftware } from "__mocks__/hostMock"; +import { IHostSoftwareUiStatus } from "interfaces/software"; + +import { StatusMessage, ModalButtons } from "./SoftwareUninstallDetailsModal"; + +describe("SoftwareUninstallDetailsModal - StatusMessage component", () => { + it("from activity or host details page of offline host, renders pending uninstall message with package name and host", () => { + render( + + ); + + expect(screen.queryByTestId("pending-outline-icon")).toBeInTheDocument(); + expect( + screen.getByText(/is uninstalling or will uninstall/) + ).toBeInTheDocument(); + expect(screen.getByText(/\(com\.cool\.app\)/)).toBeInTheDocument(); // Show package name + expect(screen.getByText(/Offline Host/)).toBeInTheDocument(); // Show host name + expect(screen.getByText(/when it comes online/)).toBeInTheDocument(); // Only reach modal if host offline + }); + + // from device user page, cannot reach pending uninstall modal + + it("from device user page, renders failed uninstall message with retry text", () => { + render( + + ); + + expect(screen.queryByTestId("error-icon")).toBeInTheDocument(); + expect(screen.getByText(/failed to uninstall/)).toBeInTheDocument(); + expect(screen.getByText(/CoolApp/)).toBeInTheDocument(); + expect(screen.queryByText(/Test Host/)).not.toBeInTheDocument(); // Do not render host name + expect(screen.getByText(/You can retry/)).toBeInTheDocument(); // Render retry message + }); + + it("from host details page/failed uninstall activity, renders failed uninstall message for with no retry text", () => { + render( + + ); + + expect(screen.queryByTestId("error-icon")).toBeInTheDocument(); + expect(screen.getByText(/failed to uninstall/)).toBeInTheDocument(); + expect(screen.getByText(/CoolApp/)).toBeInTheDocument(); + expect(screen.getByText(/Test Host/)).toBeInTheDocument(); // Render host name + expect(screen.queryByText(/You can retry/)).not.toBeInTheDocument(); // Do not render retry message + }); + + it("from successful uninstall activity, renders uninstalled message with timestamp", () => { + render( + + ); + + expect(screen.queryByTestId("success-icon")).toBeInTheDocument(); + expect(screen.getByText(/Fleet uninstalled/)).toBeInTheDocument(); + expect(screen.getByText(/CoolApp/)).toBeInTheDocument(); + expect(screen.getByText(/Test Host/)).toBeInTheDocument(); + expect(screen.getByText(/\(com\.cool\.app\)/)).toBeInTheDocument(); + expect(screen.getByText(/\d+.*ago/)).toBeInTheDocument(); // timestamp relative + }); + + // from device user page, cannot reach successful uninstall modal +}); + +describe("SoftwareUninstallDetailsModal - ModalButtons component", () => { + it("from failed uninstall on a device user page, shows Retry/Cancel and calls handlers", async () => { + const onCancel = jest.fn(); + const onRetry = jest.fn(); + const hostSoftware = { + ...createMockHostSoftware({ status: "failed_uninstall" }), + ui_status: "failed_uninstall" as IHostSoftwareUiStatus, + }; + + const { user } = renderWithSetup( + + ); + + expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Retry" })); + expect(onRetry).toHaveBeenCalledWith(hostSoftware); + expect(onCancel).toHaveBeenCalled(); + + await user.click(screen.getByRole("button", { name: "Cancel" })); + expect(onCancel).toHaveBeenCalledTimes(2); // first from retry, second from cancel + }); + + it("from pending uninstall activity or software library of an offline host, shows only Done button", () => { + const onCancel = jest.fn(); + + render( + + ); + + expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "Retry" }) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "Cancel" }) + ).not.toBeInTheDocument(); + }); + + // from device user page, cannot reach pending uninstall modal + + it("from successful uninstall activity, shows Done", () => { + const onCancel = jest.fn(); + + render( + + ); + + expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument(); + }); + + // from device user page, cannot reach successful uninstall modal +}); diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx index 9f9f95f6f9..d6b0829b50 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx @@ -18,6 +18,7 @@ import Button from "components/buttons/Button"; import DataError from "components/DataError"; import Icon from "components/Icon"; import Modal from "components/Modal"; +import ModalFooter from "components/ModalFooter"; import Spinner from "components/Spinner"; import Textarea from "components/Textarea"; import RevealButton from "components/buttons/RevealButton"; @@ -30,7 +31,7 @@ import { renderContactOption } from "../SoftwareInstallDetailsModal/SoftwareInst const baseClass = "software-uninstall-details-modal"; interface IUninstallStatusMessage { - host_display_name: string; + hostDisplayName: string; status: SoftwareUninstallStatus; softwareName: string; softwarePackageName?: string; @@ -39,8 +40,8 @@ interface IUninstallStatusMessage { contactUrl?: string; } -const StatusMessage = ({ - host_display_name, +export const StatusMessage = ({ + hostDisplayName, status, softwareName, softwarePackageName, @@ -48,11 +49,7 @@ const StatusMessage = ({ isDUP, contactUrl, }: IUninstallStatusMessage) => { - const formattedHost = host_display_name ? ( - {host_display_name} - ) : ( - "the host" - ); + const formattedHost = hostDisplayName ? {hostDisplayName} : "the host"; const isPending = isPendingStatus(status); const displayTimeStamp = @@ -100,6 +97,51 @@ const StatusMessage = ({ ); }; +interface IModalButtonsProps { + uninstallStatus: SoftwareUninstallStatus; + deviceAuthToken?: string; + onCancel: () => void; + onRetry?: (s: IHostSoftwareWithUiStatus) => void; + hostSoftware?: IHostSoftwareWithUiStatus; +} + +export const ModalButtons = ({ + uninstallStatus, + deviceAuthToken, + onCancel, + onRetry, + hostSoftware, +}: IModalButtonsProps) => { + const onClickRetry = () => { + if (onRetry && hostSoftware) { + onRetry(hostSoftware); + } + onCancel(); + }; + + if (deviceAuthToken && uninstallStatus === "failed_uninstall") { + return ( + + {" "} + + + + } + /> + ); + } + + return ( + Done} /> + ); +}; + export interface ISWUninstallDetailsParentState { softwareName: string; uninstallStatus: SoftwareUninstallStatus; @@ -142,14 +184,6 @@ const SoftwareUninstallDetailsModal = ({ const toggleDetails = () => setShowDetails((prev) => !prev); - const onClickRetry = () => { - // on DUP, where this is relevant, both will be defined - if (onRetry && hostSoftware) { - onRetry(hostSoftware); - } - onCancel(); - }; - const { data: uninstallResult, isLoading, isError, error } = useQuery< IScriptResultResponse, AxiosError @@ -192,7 +226,7 @@ const SoftwareUninstallDetailsModal = ({ return (
{ - if (deviceAuthToken && uninstallStatus === "failed_uninstall") { - return ( -
- - -
- ); - } - return ( -
- -
- ); - }; - return ( <> {renderContent()} - {renderCta()} + ); diff --git a/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tests.tsx b/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tests.tsx index 0d0156b7d2..95794aa8e1 100644 --- a/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tests.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tests.tsx @@ -1,5 +1,8 @@ +import React from "react"; import { render, screen } from "@testing-library/react"; -import { getStatusMessage } from "./VppInstallDetailsModal"; +import { renderWithSetup } from "test/test-utils"; + +import { getStatusMessage, ModalButtons } from "./VppInstallDetailsModal"; describe("getStatusMessage helper function", () => { it("shows NotNow message when isStatusNotNow is true", () => { @@ -77,6 +80,20 @@ describe("getStatusMessage helper function", () => { ).toBeInTheDocument(); }); + it("shows pending install on host when it comes online", () => { + render( + getStatusMessage({ + displayStatus: "pending_install", + isMDMStatusNotNow: false, + isMDMStatusAcknowledged: false, + appName: "Logic Pro", + hostDisplayName: "Marko's MacBook Pro", + commandUpdatedAt: "2025-07-29T22:49:52Z", + }) + ); + expect(screen.getByText(/when it comes online/i)).toBeInTheDocument(); + }); + it("shows default message for installed status", () => { render( getStatusMessage({ @@ -93,6 +110,22 @@ describe("getStatusMessage helper function", () => { expect(screen.getByText(/Marko's MacBook Pro/i)).toBeInTheDocument(); }); + it("shows manual install message when installed not through Fleet", () => { + render( + getStatusMessage({ + displayStatus: "installed", + isMDMStatusNotNow: false, + isMDMStatusAcknowledged: false, + appName: "Logic Pro", + hostDisplayName: "Marko's MacBook Pro", + commandUpdatedAt: "", // <-- empty + }) + ); + + expect(screen.getByText(/Logic Pro/i)).toBeInTheDocument(); + expect(screen.getByText(/is installed\./i)).toBeInTheDocument(); + }); + it("shows default message with 'the host' if host_display_name is empty", () => { render( getStatusMessage({ @@ -107,4 +140,75 @@ describe("getStatusMessage helper function", () => { expect(screen.getByText(/Fleet installed/i)).toBeInTheDocument(); expect(screen.getByText(/the host/i)).toBeInTheDocument(); }); + + it("shows relative timestamp when available", () => { + render( + getStatusMessage({ + displayStatus: "failed_install", + isMDMStatusNotNow: false, + isMDMStatusAcknowledged: false, + appName: "Logic Pro", + hostDisplayName: "Marko's MacBook Pro", + commandUpdatedAt: new Date().toISOString(), + }) + ); + expect(screen.getByText(/\(.*ago\)/i)).toBeInTheDocument(); + }); + + it("on the device user page, does not show host info", () => { + render( + getStatusMessage({ + isDUP: true, + displayStatus: "installed", + isMDMStatusNotNow: false, + isMDMStatusAcknowledged: false, + appName: "Logic Pro", + hostDisplayName: "Marko's MacBook Pro", + commandUpdatedAt: "2025-07-29T22:49:52Z", + }) + ); + expect(screen.queryByText(/Marko's MacBook Pro/i)).not.toBeInTheDocument(); + }); +}); + +describe("VPP Install Details Modal - ModalButtons component", () => { + it("renders Done button by default", async () => { + const onCancel = jest.fn(); + + const { user } = renderWithSetup( + + ); + + const doneButton = screen.getByRole("button", { name: /done/i }); + expect(doneButton).toBeInTheDocument(); + + await user.click(doneButton); + expect(onCancel).toHaveBeenCalledTimes(1); + }); + + it("renders Cancel + Retry when failed_install with deviceAuthToken", async () => { + const onCancel = jest.fn(); + const onRetry = jest.fn(); + + const { user } = renderWithSetup( + + ); + + const cancelButton = screen.getByRole("button", { name: /cancel/i }); + const retryButton = screen.getByRole("button", { name: /retry/i }); + + expect(cancelButton).toBeInTheDocument(); + expect(retryButton).toBeInTheDocument(); + + // Retry should trigger onRetry + onCancel + await user.click(retryButton); + expect(onRetry).toHaveBeenCalledWith(123); + expect(onCancel).toHaveBeenCalled(); + }); }); diff --git a/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx index d5d58d707a..0ae116f471 100644 --- a/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx @@ -14,6 +14,7 @@ import { IMdmCommandResult } from "interfaces/mdm"; import InventoryVersions from "pages/hosts/details/components/InventoryVersions"; import Modal from "components/Modal"; +import ModalFooter from "components/ModalFooter"; import Button from "components/buttons/Button"; import Icon from "components/Icon"; import Textarea from "components/Textarea"; @@ -148,6 +149,50 @@ export const getStatusMessage = ({ ); }; +interface IModalButtonsProps { + displayStatus: SoftwareInstallStatus | "pending"; + deviceAuthToken?: string; + onCancel: () => void; + onRetry?: (id: number) => void; + hostSoftwareId?: number; +} + +export const ModalButtons = ({ + displayStatus, + deviceAuthToken, + onCancel, + onRetry, + hostSoftwareId, +}: IModalButtonsProps) => { + const onClickRetry = () => { + // on DUP, where this is relevant, both will be defined + if (onRetry && hostSoftwareId) { + onRetry(hostSoftwareId); + } + onCancel(); + }; + + if (deviceAuthToken && displayStatus === "failed_install") { + return ( + + + + + } + /> + ); + } + return ( + Done} /> + ); +}; + const baseClass = "vpp-install-details-modal"; export type IVppInstallDetails = { @@ -206,13 +251,6 @@ export const VppInstallDetailsModal = ({ }; }; - const onClickRetry = () => { - // on DUP, where this is relevant, both will be defined - if (onRetry && hostSoftware?.id) { - onRetry(hostSoftware.id); - } - onCancel(); - }; const { data: vppCommandResult, isLoading: isLoadingVPPCommandResult, @@ -349,26 +387,6 @@ export const VppInstallDetailsModal = ({ ); }; - const renderCta = () => { - if (deviceAuthToken && displayStatus === "failed_install") { - return ( -
- - -
- ); - } - return ( -
- -
- ); - }; - return ( <> {renderContent()} - {renderCta()} + ); diff --git a/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tests.tsx b/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tests.tsx index 66325b34ff..624f067f4f 100644 --- a/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tests.tsx +++ b/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tests.tsx @@ -69,6 +69,8 @@ describe("SoftwareUpdateModal", () => { onUpdate={noop} /> ); + + expect(screen.queryByTestId("pending-outline-icon")).toBeInTheDocument(); expect( screen.getByText(/Fleet is updating or will update/i) ).toBeInTheDocument(); @@ -89,6 +91,8 @@ describe("SoftwareUpdateModal", () => { onUpdate={noop} /> ); + + expect(screen.queryByTestId("pending-outline-icon")).toBeInTheDocument(); expect( screen.getByText(/Fleet is updating or will update/i) ).toBeInTheDocument(); @@ -108,6 +112,7 @@ describe("SoftwareUpdateModal", () => { /> ); + expect(screen.queryByTestId("error-outline-icon")).toBeInTheDocument(); expect(screen.getByText(/New version of/i)).toBeInTheDocument(); expect(screen.getByText(/mock software.app/i)).toBeInTheDocument(); expect(