fleet/frontend/components/ActivityDetails/InstallDetails/SoftwareIpaInstallDetailsModal/SoftwareIpaInstallDetailsModal.tsx
RachelElysia 9bfef5dec3
Fleet UI: Remove incorrect copy (#42586)
## Issue
Closes #40683 

## Description
- Removed from 2 modals that were rendering it

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
2026-03-28 15:27:28 -05:00

494 lines
16 KiB
TypeScript

/** Similar look and feel to the VppInstallDetailsModal, but this modal
* is rendered instead of the SoftwareInstallDetailsModal when the package is
* an .ipa for iOS/iPadOS */
import React, { useState } from "react";
import { useQuery } from "react-query";
import { AxiosError } from "axios";
import { formatDistanceToNow } from "date-fns";
import commandAPI, {
IGetCommandResultsResponse,
} from "services/entities/command";
import deviceUserAPI, {
IGetVppInstallCommandResultsResponse,
} from "services/entities/device_user";
import {
IHostSoftware,
SoftwareInstallUninstallStatus,
} from "interfaces/software";
import { ICommandResult } from "interfaces/command";
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 IconStatusMessage from "components/IconStatusMessage";
import Textarea from "components/Textarea";
import DataError from "components/DataError/DataError";
import DeviceUserError from "components/DeviceUserError";
import Spinner from "components/Spinner/Spinner";
import RevealButton from "components/buttons/RevealButton";
import {
getInstallDetailsStatusPredicate,
INSTALL_DETAILS_STATUS_ICONS,
} from "../constants";
import decodeBase64Utf8 from "../helpers";
interface IGetStatusMessageProps {
isMyDevicePage?: boolean;
/** "pending" is an edge case here where IPA install activities that were added to the feed
* prior to v4.57 will list the status as "pending" rather than "pending_install" */
displayStatus: SoftwareInstallUninstallStatus | "pending";
isMDMStatusNotNow: boolean;
isMDMStatusAcknowledged: boolean;
appName: string;
hostDisplayName: string;
commandUpdatedAt: string;
/** Used only for overriding failed_install/failed_uninstall -> "is installed."
- From Host -> Software: override based on inventory.
- From Activity feed: never override (always show the failure).
Parity with VPPInstallDetailsModal/SoftwareInstallDetailsModal */
canOverrideFailureWithInstalled?: boolean;
}
export const getStatusMessage = ({
isMyDevicePage = false,
displayStatus,
isMDMStatusNotNow,
isMDMStatusAcknowledged,
appName,
hostDisplayName,
commandUpdatedAt,
canOverrideFailureWithInstalled = false,
}: IGetStatusMessageProps) => {
const formattedHost = hostDisplayName ? <b>{hostDisplayName}</b> : "the host";
const displayTimestamp =
["failed_install", "installed"].includes(displayStatus || "") &&
commandUpdatedAt
? ` (${formatDistanceToNow(new Date(commandUpdatedAt), {
includeSeconds: true,
addSuffix: true,
})})`
: null;
// Handles "pending" value prior to 4.57
const isPendingInstall = ["pending_install", "pending"].includes(
displayStatus
);
// Treat failed_install / failed_uninstall with installed versions as installed
// as the host still reports installed versions (4.82 #31663)
const overrideFailureWithInstalled =
canOverrideFailureWithInstalled &&
["failed_install", "failed_uninstall"].includes(displayStatus || "");
if (overrideFailureWithInstalled) {
return (
<>
<b>{appName}</b> is installed.
</>
);
}
// Handles the case where software is installed manually by the user and not through Fleet
// This IPA software_packages modal matches app_store_app modal and software_packages modal
// for software installed manually shown with VppInstallDetailsModal and SoftwareInstallDetailsModal
if (displayStatus === "installed" && !commandUpdatedAt) {
return (
<>
<b>{appName}</b> is installed.
</>
);
}
// Handle NotNow case separately
if (isMDMStatusNotNow) {
return (
<>
Fleet tried to install <b>{appName}</b>
{!isMyDevicePage && (
<>
{" "}
on {formattedHost} but couldn&apos;t because the host was locked or
was running on battery power while in Power Nap
</>
)}
{displayTimestamp && <> {displayTimestamp}</>}. Fleet will try again.
</>
);
}
// IPA Verify command pending state
if (isPendingInstall && isMDMStatusAcknowledged) {
return (
<>
The MDM command (request) to install <b>{appName}</b>
{!isMyDevicePage && <> on {formattedHost}</>} was acknowledged but the
installation has not been verified. To re-check, select <b>Refetch</b>
{!isMyDevicePage && " for this host"}.
</>
);
}
// Verification failed (timeout)
if (displayStatus === "failed_install" && isMDMStatusAcknowledged) {
return (
<>
The MDM command (request) to install <b>{appName}</b>
{!isMyDevicePage && <> on {formattedHost}</>} was acknowledged but the
installation has not been verified. Please re-attempt this installation.
</>
);
}
// Install command failed
if (displayStatus === "failed_install") {
return (
<>
The MDM command (request) to install <b>{appName}</b>
{!isMyDevicePage && <> on {formattedHost}</>} failed
{displayTimestamp && <> {displayTimestamp}</>}. Please re-attempt this
installation.
</>
);
}
const renderSuffix = () => {
if (isMyDevicePage) {
return <> {displayTimestamp && <> {displayTimestamp}</>}</>;
}
return (
<>
{" "}
on {formattedHost}
{isPendingInstall && " when it comes online"}
{displayTimestamp && <> {displayTimestamp}</>}
</>
);
};
// Create predicate and subordinate for other statuses
return (
<>
Fleet {getInstallDetailsStatusPredicate(displayStatus)} <b>{appName}</b>
{renderSuffix()}.
</>
);
};
interface IModalButtonsProps {
displayStatus: SoftwareInstallUninstallStatus | "pending";
deviceAuthToken?: string;
onCancel: () => void;
onRetry?: (id: number) => void;
hostSoftwareId?: number;
}
export const ModalButtons = ({
displayStatus,
deviceAuthToken,
onCancel,
onRetry,
hostSoftwareId,
}: IModalButtonsProps) => {
const onClickRetry = () => {
// on My Device Page, where this is relevant, both will be defined
if (onRetry && hostSoftwareId) {
onRetry(hostSoftwareId);
}
onCancel();
};
if (deviceAuthToken && displayStatus === "failed_install") {
return (
<ModalFooter
primaryButtons={
<>
<Button variant="inverse" onClick={onCancel}>
Cancel
</Button>
<Button type="submit" onClick={onClickRetry}>
Retry
</Button>
</>
}
/>
);
}
return (
<ModalFooter primaryButtons={<Button onClick={onCancel}>Close</Button>} />
);
};
const baseClass = "software-ipa-install-details-modal";
export type ISoftwareIpaInstallDetails = {
/** Status: null when a host manually installed not using Fleet */
fleetInstallStatus: SoftwareInstallUninstallStatus | null;
hostDisplayName: string;
appName: string;
commandUuid?: string;
};
interface ISoftwareIpaInstallDetailsModal {
details: ISoftwareIpaInstallDetails;
/** for inventory versions, not present on activity feeds */
hostSoftware?: IHostSoftware;
/** My Device Page only */
deviceAuthToken?: string;
onCancel: () => void;
/** My Device Page only */
onRetry?: (id: number) => void;
}
export const SoftwareIpaInstallDetailsModal = ({
details,
onCancel,
deviceAuthToken,
hostSoftware,
onRetry,
}: ISoftwareIpaInstallDetailsModal) => {
const {
fleetInstallStatus,
commandUuid = "",
hostDisplayName = "",
appName = "",
} = details;
const [showInstallDetails, setShowInstallDetails] = useState(false);
const toggleInstallDetails = () => {
setShowInstallDetails((prev) => !prev);
};
const responseHandler = (
response: IGetVppInstallCommandResultsResponse | IGetCommandResultsResponse
) => {
const results = response.results?.[0];
if (!results) {
// FIXME: It's currently possible that the command results API response is empty for pending
// commands. As a temporary workaround to handle this case, we'll ignore the empty response and
// display some minimal pending UI. This should be removed once the API response is fixed.
return {} as ICommandResult;
}
return {
...results,
payload: results.payload ? decodeBase64Utf8(results.payload) : "",
result: results.result ? decodeBase64Utf8(results.result) : "",
};
};
const { data: swInstallResult, isLoading, isError, error } = useQuery<
ICommandResult,
AxiosError
>(
["mdm_command_results", commandUuid],
async () => {
return deviceAuthToken
? deviceUserAPI
.getVppCommandResult(deviceAuthToken, commandUuid)
.then(responseHandler)
: commandAPI.getCommandResults(commandUuid).then(responseHandler);
},
{
refetchOnWindowFocus: false,
staleTime: 3000,
enabled: !!commandUuid,
}
);
// Reconcile "installed" state from inventory vs command results.
// True when host inventory reports at least one installed version for this app.
const inventoryReportsInstalled = !!hostSoftware?.installed_versions?.length;
// This modal is opened in two contexts:
// - From Host -> Software: hostSoftware is defined (we trust inventory to override failures).
// - From the Activity feed: hostSoftware is undefined (we trust command result status).
const openedFromHostSoftwarePage = !!hostSoftware;
// Used only for overriding failed_install/failed_uninstall -> "is installed."
// - From Host -> Software: override based on inventory.
// - From Activity feed: never override (always show the failure).
const canOverrideFailureWithInstalled = openedFromHostSoftwarePage
? inventoryReportsInstalled
: false;
// Fallback to "installed" if no status is provided
const displayStatus = fleetInstallStatus ?? "installed";
// Treat failed_install / failed_uninstall with installed versions as installed
const overrideFailedMessageWithInstalledMessage =
canOverrideFailureWithInstalled &&
["failed_install", "failed_uninstall"].includes(displayStatus || "");
const commandUpdatedAt = swInstallResult?.updated_at;
// Handles the case where software is installed manually by the user and not through Fleet
const isInstalledManual = displayStatus === "installed" && !commandUpdatedAt;
// Use success icon when we show “is installed”
const iconName =
overrideFailedMessageWithInstalledMessage || isInstalledManual
? "success"
: INSTALL_DETAILS_STATUS_ICONS[displayStatus];
// Handles "pending" value prior to 4.57 AND never shows error state on pending_install
// as some cases have command results not available for pending_installs
// which we don't want to show a UI error state for
const isPendingInstall = ["pending_install", "pending"].includes(
displayStatus
);
// Note: We need to reconcile status values from two different sources. From props, we
// get the status of the Fleet install operation (which can be "failed", "pending", or
// "installed"). From the command results API response, we also receive the raw status
// from the MDM protocol, e.g., "NotNow" or "Acknowledged". We need to display some special
// messaging for the "NotNow" status, which otherwise would be treated as "pending".
const isMDMStatusNotNow = swInstallResult?.status === "NotNow";
const isMDMStatusAcknowledged = swInstallResult?.status === "Acknowledged";
// Hide version section from pending installs or failures that aren't overridden to installed (4.82 #31663)
const shouldShowInventoryVersions =
(!!hostSoftware &&
deviceAuthToken &&
![
"pending_install",
"failed_install",
"failed_uninstall",
"pending",
].includes(displayStatus)) ||
overrideFailedMessageWithInstalledMessage;
// Hide failed details if host shows installed versions (4.82 #31663)
// Note: Currently no uninstall IPA but added for symmetry with SoftwareInstallDetailsModal
const excludeInstallDetails =
canOverrideFailureWithInstalled &&
[
"failed_install_installed",
"failed_uninstall_installed",
"failed_install",
"failed_uninstall",
].includes(displayStatus || "");
const isInstalledByFleet = hostSoftware
? !!hostSoftware.app_store_app?.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
const statusMessage = getStatusMessage({
isMyDevicePage: !!deviceAuthToken,
displayStatus,
isMDMStatusNotNow,
isMDMStatusAcknowledged,
appName,
hostDisplayName,
commandUpdatedAt: commandUpdatedAt || "",
canOverrideFailureWithInstalled,
});
const renderInventoryVersionsSection = () => {
if (hostSoftware?.installed_versions?.length) {
return <InventoryVersions hostSoftware={hostSoftware} />;
}
return null;
};
const renderInstallDetailsSection = () => {
// Hide section if there's no details to display
if (!swInstallResult?.result && !swInstallResult?.payload) {
return null;
}
return (
<>
<RevealButton
isShowing={showInstallDetails}
showText="Details"
hideText="Details"
caretPosition="after"
onClick={toggleInstallDetails}
/>
{showInstallDetails && (
<>
{swInstallResult?.result && (
<Textarea label="MDM command output:" variant="code">
{swInstallResult.result}
</Textarea>
)}
{swInstallResult?.payload && (
<Textarea label="MDM command:" variant="code">
{swInstallResult.payload}
</Textarea>
)}
</>
)}
</>
);
};
const renderContent = () => {
if (isLoading) {
return <Spinner />;
}
if (isError && !isPendingInstall) {
if (error?.status === 404) {
return deviceAuthToken ? (
<DeviceUserError />
) : (
<DataError
description="Install details are no longer available for this activity."
excludeIssueLink
/>
);
}
if (error?.status === 401) {
return deviceAuthToken ? (
<DeviceUserError />
) : (
<DataError description="Close this modal and try again." />
);
}
} else if (!swInstallResult) {
// FIXME: It's currently possible that the command results API response is empty for pending
// commands. As a temporary workaround to handle this case, we'll ignore the empty response and
// display some minimal pending UI. This should be updated once the API response is fixed.
}
return (
<div className={`${baseClass}__modal-content`}>
<IconStatusMessage
className={`${baseClass}__status-message`}
iconName={iconName}
message={<span>{statusMessage}</span>}
/>
{shouldShowInventoryVersions && renderInventoryVersionsSection()}
{!isPendingInstall &&
isInstalledByFleet &&
!excludeInstallDetails &&
renderInstallDetailsSection()}
</div>
);
};
return (
<Modal
title="Install details"
onExit={onCancel}
onEnter={onCancel}
className={baseClass}
>
{renderContent()}
<ModalButtons
deviceAuthToken={deviceAuthToken}
hostSoftwareId={hostSoftware?.id}
onRetry={onRetry}
onCancel={onCancel}
displayStatus={displayStatus}
/>
</Modal>
);
};
export default SoftwareIpaInstallDetailsModal;