mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
## 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
470 lines
13 KiB
TypeScript
470 lines
13 KiB
TypeScript
/** For script-only packages (e.g. software source is sh_packages or ps1_packages)
|
|
* we use SoftwareScriptDetailsModal
|
|
* For iOS/iPadOS packages (e.g. .ipa packages software source is ios_apps or ipados_apps)
|
|
* we use SoftwareIpaInstallDetailsModal with the command_uuid
|
|
* For VPP iOS/iPadOS packages, we use VppInstallDetailsModal
|
|
* For Android Google Play Store apps, we also use THIS modal
|
|
* For all other apps, we use THIS modal */
|
|
|
|
import React, { useState } from "react";
|
|
import { useQuery } from "react-query";
|
|
import { formatDistanceToNow } from "date-fns";
|
|
import { AxiosError } from "axios";
|
|
|
|
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
|
|
|
|
import {
|
|
IHostSoftware,
|
|
ISoftwareInstallResult,
|
|
ISoftwareInstallResults,
|
|
} from "interfaces/software";
|
|
import softwareAPI from "services/entities/software";
|
|
import deviceUserAPI from "services/entities/device_user";
|
|
|
|
import InventoryVersions from "pages/hosts/details/components/InventoryVersions";
|
|
import { getDisplayedSoftwareName } from "pages/SoftwarePage/helpers";
|
|
|
|
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 CustomLink from "components/CustomLink";
|
|
|
|
import {
|
|
INSTALL_DETAILS_STATUS_ICONS,
|
|
getInstallDetailsStatusPredicate,
|
|
} from "../constants";
|
|
|
|
const baseClass = "software-install-details-modal";
|
|
|
|
export type IPackageInstallDetails = {
|
|
host_display_name?: string;
|
|
install_uuid?: string; // not actually optional
|
|
};
|
|
|
|
export const renderContactOption = (url?: string) => (
|
|
<>
|
|
{" "}
|
|
or{" "}
|
|
{url ? (
|
|
<CustomLink url={url} text="contact your IT admin" newTab />
|
|
) : (
|
|
"contact your IT admin"
|
|
)}
|
|
</>
|
|
);
|
|
|
|
interface IInstallStatusMessage {
|
|
softwareName: string;
|
|
installResult?: ISoftwareInstallResult;
|
|
isMyDevicePage: boolean;
|
|
contactUrl?: 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/SoftwareIpaInstallDetailsModal */
|
|
canOverrideFailureWithInstalled?: boolean;
|
|
}
|
|
|
|
// TODO - match VppInstallDetailsModal status to this, still accounting for MDM-specific cases
|
|
// present there
|
|
export const StatusMessage = ({
|
|
softwareName,
|
|
installResult,
|
|
isMyDevicePage,
|
|
contactUrl,
|
|
canOverrideFailureWithInstalled = false,
|
|
}: IInstallStatusMessage) => {
|
|
// the case when software is installed by the user and not by Fleet
|
|
if (!installResult) {
|
|
return (
|
|
<IconStatusMessage
|
|
className={`${baseClass}__status-message`}
|
|
iconName="success"
|
|
message={
|
|
<span>
|
|
<b>{softwareName}</b> is installed.
|
|
</span>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const {
|
|
host_display_name,
|
|
software_package,
|
|
software_title,
|
|
status,
|
|
updated_at,
|
|
created_at,
|
|
} = installResult;
|
|
|
|
// 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(status || "");
|
|
|
|
if (overrideFailureWithInstalled) {
|
|
return (
|
|
<IconStatusMessage
|
|
className={`${baseClass}__status-message`}
|
|
iconName="success"
|
|
message={
|
|
<span>
|
|
<b>{softwareName}</b> is installed.
|
|
</span>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const formattedHost = host_display_name ? (
|
|
<b>{host_display_name}</b>
|
|
) : (
|
|
"the host"
|
|
);
|
|
|
|
const displayTimeStamp = ["failed_install", "installed"].includes(
|
|
status || ""
|
|
)
|
|
? ` (${formatDistanceToNow(new Date(updated_at || created_at), {
|
|
includeSeconds: true,
|
|
addSuffix: true,
|
|
})})`
|
|
: "";
|
|
|
|
const renderStatusCopy = () => {
|
|
const prefix = (
|
|
<>
|
|
Fleet {getInstallDetailsStatusPredicate(status)} <b>{software_title}</b>
|
|
</>
|
|
);
|
|
|
|
const middle = isMyDevicePage ? (
|
|
<>
|
|
{" "}
|
|
{displayTimeStamp}
|
|
{status === "failed_install" && (
|
|
<>. You can retry{renderContactOption(contactUrl)}</>
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
{" "}
|
|
({software_package}) on {formattedHost}
|
|
{status === "pending_install"
|
|
? " when it comes online"
|
|
: displayTimeStamp}
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<span>
|
|
{prefix}
|
|
{middle}
|
|
{"."}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<IconStatusMessage
|
|
className={`${baseClass}__status-message`}
|
|
iconName={
|
|
INSTALL_DETAILS_STATUS_ICONS[status || "pending_install"] ??
|
|
"pending-outline"
|
|
}
|
|
message={renderStatusCopy()}
|
|
/>
|
|
);
|
|
};
|
|
|
|
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 My Device Page, where this is relevant, both will be defined
|
|
if (onRetry && hostSoftwareId) {
|
|
onRetry(hostSoftwareId);
|
|
}
|
|
onCancel();
|
|
};
|
|
|
|
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>} />
|
|
);
|
|
};
|
|
|
|
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
|
|
necessary in the details prop */
|
|
details: IPackageInstallDetails;
|
|
hostSoftware?: IHostSoftware; // for inventory versions, and software name when not Fleet installed (not present on activity feeds)
|
|
deviceAuthToken?: string; // My Device Page only
|
|
onCancel: () => void;
|
|
onRetry?: (id: number) => void; // My Device Page only
|
|
contactUrl?: string; // My Device Page only
|
|
}
|
|
|
|
export const SoftwareInstallDetailsModal = ({
|
|
details: detailsFromProps,
|
|
onCancel,
|
|
hostSoftware,
|
|
deviceAuthToken,
|
|
onRetry,
|
|
contactUrl,
|
|
}: ISoftwareInstallDetailsProps) => {
|
|
// will always be present
|
|
const installUUID = detailsFromProps.install_uuid ?? "";
|
|
|
|
const [showInstallDetails, setShowInstallDetails] = useState(false);
|
|
const toggleInstallDetails = () => {
|
|
setShowInstallDetails((prev) => !prev);
|
|
};
|
|
|
|
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
|
|
|
|
const { data: swInstallResult, isLoading, isError, error } = useQuery<
|
|
ISoftwareInstallResults,
|
|
AxiosError,
|
|
ISoftwareInstallResult
|
|
>(
|
|
["softwareInstallResults", installUUID],
|
|
() => {
|
|
return deviceAuthToken
|
|
? deviceUserAPI.getSoftwareInstallResult(deviceAuthToken, installUUID)
|
|
: softwareAPI.getSoftwareInstallResult(installUUID);
|
|
},
|
|
{
|
|
enabled: !!isInstalledByFleet,
|
|
...DEFAULT_USE_QUERY_OPTIONS,
|
|
staleTime: 3000,
|
|
select: (data) => data.results,
|
|
}
|
|
);
|
|
|
|
const renderInventoryVersionsSection = () => {
|
|
if (hostSoftware?.installed_versions?.length) {
|
|
return <InventoryVersions hostSoftware={hostSoftware} />;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const renderInstallDetailsSection = () => {
|
|
const outputs = [
|
|
{
|
|
label: "Pre-install query output:",
|
|
value: swInstallResult?.pre_install_query_output,
|
|
},
|
|
{
|
|
label: "Install script output:",
|
|
value: swInstallResult?.output,
|
|
},
|
|
{
|
|
label: "Post-install script output:",
|
|
value: swInstallResult?.post_install_script_output,
|
|
},
|
|
];
|
|
|
|
// Only show details button if there's details to display
|
|
const showDetailsButton =
|
|
(!!swInstallResult?.post_install_script_output ||
|
|
!!swInstallResult?.output ||
|
|
!!swInstallResult?.pre_install_query_output) &&
|
|
swInstallResult?.status !== "pending_install";
|
|
|
|
return (
|
|
<>
|
|
{showDetailsButton && (
|
|
<RevealButton
|
|
isShowing={showInstallDetails}
|
|
showText="Details"
|
|
hideText="Details"
|
|
caretPosition="after"
|
|
onClick={toggleInstallDetails}
|
|
/>
|
|
)}
|
|
{showInstallDetails &&
|
|
outputs.map(
|
|
({ label, value }) =>
|
|
value && (
|
|
<Textarea key={label} label={label} variant="code">
|
|
{value}
|
|
</Textarea>
|
|
)
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const hostDisplayname =
|
|
swInstallResult?.host_display_name || detailsFromProps.host_display_name;
|
|
|
|
const installResultWithHostDisplayName = swInstallResult
|
|
? {
|
|
...swInstallResult,
|
|
host_display_name: hostDisplayname,
|
|
}
|
|
: undefined;
|
|
|
|
// 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 install 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;
|
|
|
|
// Treat failed_install / failed_uninstall with installed versions as installed
|
|
const overrideFailedMessageWithInstalledMessage =
|
|
canOverrideFailureWithInstalled &&
|
|
["failed_install", "failed_uninstall"].includes(
|
|
swInstallResult?.status || "" || ""
|
|
);
|
|
|
|
// 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(swInstallResult?.status || "")) ||
|
|
overrideFailedMessageWithInstalledMessage;
|
|
|
|
const renderContent = () => {
|
|
if (isInstalledByFleet) {
|
|
if (isLoading) {
|
|
return <Spinner />;
|
|
}
|
|
|
|
if (isError) {
|
|
if (error?.status === 404) {
|
|
return deviceAuthToken ? (
|
|
<DeviceUserError />
|
|
) : (
|
|
<DataError
|
|
description="Couldn't get install details"
|
|
excludeIssueLink
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (error?.status === 401) {
|
|
return deviceAuthToken ? (
|
|
<DeviceUserError />
|
|
) : (
|
|
<DataError description="Close this modal and try again." />
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!swInstallResult) {
|
|
return deviceAuthToken ? (
|
|
<DeviceUserError />
|
|
) : (
|
|
<DataError description="No data returned." />
|
|
);
|
|
}
|
|
|
|
if (
|
|
!["installed", "pending_install", "failed_install"].includes(
|
|
swInstallResult.status
|
|
)
|
|
) {
|
|
return (
|
|
<DataError
|
|
description={`Unexpected software install status ${swInstallResult.status}`}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className={`${baseClass}__modal-content`}>
|
|
<StatusMessage
|
|
installResult={installResultWithHostDisplayName}
|
|
softwareName={getDisplayedSoftwareName(
|
|
hostSoftware?.name,
|
|
hostSoftware?.display_name
|
|
)}
|
|
isMyDevicePage={!!deviceAuthToken}
|
|
contactUrl={contactUrl}
|
|
canOverrideFailureWithInstalled={canOverrideFailureWithInstalled}
|
|
/>
|
|
|
|
{shouldShowInventoryVersions && renderInventoryVersionsSection()}
|
|
{isInstalledByFleet &&
|
|
!overrideFailedMessageWithInstalledMessage &&
|
|
renderInstallDetailsSection()}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
title="Install details"
|
|
onExit={onCancel}
|
|
onEnter={onCancel}
|
|
className={baseClass}
|
|
>
|
|
{renderContent()}
|
|
<ModalButtons
|
|
deviceAuthToken={deviceAuthToken}
|
|
status={swInstallResult?.status}
|
|
hostSoftwareId={hostSoftware?.id}
|
|
onRetry={onRetry}
|
|
onCancel={onCancel}
|
|
/>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default SoftwareInstallDetailsModal;
|