2025-10-15 13:41:43 +00:00
|
|
|
/** This component is intentionally separate from SoftwareInstallDetailsModal
|
2026-02-11 17:18:32 +00:00
|
|
|
* because it handles script-only package installs (e.g. sh_packages or ps1_packages)
|
2025-10-15 13:41:43 +00:00
|
|
|
*
|
|
|
|
|
* Key differences from SoftwareInstallDetailsModal:
|
|
|
|
|
* - Uses Script/Run/Rerun language in UI instead of Install/Retry.
|
|
|
|
|
* - Omits current versions section (no InventoryVersions display).
|
|
|
|
|
* - Omits post-install script output.
|
|
|
|
|
*
|
|
|
|
|
* Keeping these components and its tests separate improves maintainability and clarity
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
ISoftwareScriptResult,
|
|
|
|
|
ISoftwareInstallResults,
|
|
|
|
|
} from "interfaces/software";
|
|
|
|
|
import softwareAPI from "services/entities/software";
|
|
|
|
|
import deviceUserAPI from "services/entities/device_user";
|
|
|
|
|
|
|
|
|
|
import Modal from "components/Modal";
|
|
|
|
|
import ModalFooter from "components/ModalFooter";
|
|
|
|
|
import Button from "components/buttons/Button";
|
2025-10-24 13:38:59 +00:00
|
|
|
import IconStatusMessage from "components/IconStatusMessage";
|
2025-10-15 13:41:43 +00:00
|
|
|
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 {
|
|
|
|
|
SCRIPT_DETAILS_STATUS_ICONS,
|
|
|
|
|
getScriptDetailsStatusPredicate,
|
|
|
|
|
} from "../constants";
|
|
|
|
|
|
|
|
|
|
const baseClass = "software-script-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 {
|
|
|
|
|
installResult: ISoftwareScriptResult;
|
|
|
|
|
isMyDevicePage: boolean;
|
|
|
|
|
contactUrl?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const StatusMessage = ({
|
|
|
|
|
installResult,
|
|
|
|
|
isMyDevicePage,
|
|
|
|
|
contactUrl,
|
|
|
|
|
}: IInstallStatusMessage) => {
|
|
|
|
|
const {
|
|
|
|
|
host_display_name,
|
|
|
|
|
software_package,
|
|
|
|
|
software_title,
|
|
|
|
|
status,
|
|
|
|
|
updated_at,
|
|
|
|
|
created_at,
|
|
|
|
|
} = installResult;
|
|
|
|
|
|
|
|
|
|
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 {getScriptDetailsStatusPredicate(status)} <b>{software_title}</b>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const middle = isMyDevicePage ? (
|
|
|
|
|
<>
|
|
|
|
|
{" "}
|
|
|
|
|
{displayTimeStamp}
|
|
|
|
|
{status === "failed_install" && (
|
|
|
|
|
<>. You can rerun{renderContactOption(contactUrl)}</>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{" "}
|
|
|
|
|
({software_package}) on {formattedHost}
|
|
|
|
|
{status === "pending_install"
|
|
|
|
|
? " when it comes online"
|
|
|
|
|
: displayTimeStamp}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
return (
|
|
|
|
|
<span>
|
|
|
|
|
{prefix}
|
|
|
|
|
{middle}
|
|
|
|
|
{"."}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2025-10-24 13:38:59 +00:00
|
|
|
<IconStatusMessage
|
|
|
|
|
className={`${baseClass}__status-message`}
|
|
|
|
|
iconName={
|
|
|
|
|
SCRIPT_DETAILS_STATUS_ICONS[status || "pending_install"] ??
|
|
|
|
|
"pending-outline"
|
|
|
|
|
}
|
|
|
|
|
message={renderStatusCopy()}
|
|
|
|
|
/>
|
2025-10-15 13:41:43 +00:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface IModalButtonsProps {
|
|
|
|
|
deviceAuthToken?: string;
|
|
|
|
|
installResultStatus?: string;
|
|
|
|
|
hostSoftwareId?: number;
|
2025-10-17 14:54:00 +00:00
|
|
|
onRerun?: (id: number, isScriptPackage: boolean) => void;
|
2025-10-15 13:41:43 +00:00
|
|
|
onCancel: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const ModalButtons = ({
|
|
|
|
|
deviceAuthToken,
|
|
|
|
|
installResultStatus,
|
|
|
|
|
hostSoftwareId,
|
|
|
|
|
onRerun,
|
|
|
|
|
onCancel,
|
|
|
|
|
}: IModalButtonsProps) => {
|
|
|
|
|
if (!!deviceAuthToken && installResultStatus === "failed_install") {
|
|
|
|
|
const onClickRerun = () => {
|
|
|
|
|
// on My Device Page, where this is relevant, both will be defined
|
|
|
|
|
if (onRerun && hostSoftwareId) {
|
2025-10-17 14:54:00 +00:00
|
|
|
onRerun(hostSoftwareId, true); // isScriptPackage defined for copy changes
|
2025-10-15 13:41:43 +00:00
|
|
|
}
|
|
|
|
|
onCancel();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ModalFooter
|
|
|
|
|
primaryButtons={
|
|
|
|
|
<>
|
|
|
|
|
<Button variant="inverse" onClick={onCancel}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="submit" onClick={onClickRerun}>
|
|
|
|
|
Rerun
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-23 15:59:18 +00:00
|
|
|
<ModalFooter primaryButtons={<Button onClick={onCancel}>Close</Button>} />
|
2025-10-15 13:41:43 +00:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface ISoftwareInstallDetailsProps {
|
|
|
|
|
details: IPackageInstallDetails;
|
|
|
|
|
hostSoftware?: IHostSoftware; // for software name when not Fleet installed (not present on activity feeds)
|
|
|
|
|
deviceAuthToken?: string; // My Device Page only
|
|
|
|
|
onCancel: () => void;
|
2025-10-17 14:54:00 +00:00
|
|
|
onRerun?: (id: number, isScriptPackage?: boolean) => void; // My Device Page only
|
2025-10-15 13:41:43 +00:00
|
|
|
contactUrl?: string; // My Device Page only
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-31 14:55:16 +00:00
|
|
|
export const SoftwareScriptDetailsModal = ({
|
2025-10-15 13:41:43 +00:00
|
|
|
details: detailsFromProps,
|
|
|
|
|
onCancel,
|
|
|
|
|
hostSoftware,
|
|
|
|
|
deviceAuthToken,
|
|
|
|
|
onRerun,
|
|
|
|
|
contactUrl,
|
|
|
|
|
}: ISoftwareInstallDetailsProps) => {
|
|
|
|
|
// will always be present
|
|
|
|
|
const installUUID = detailsFromProps.install_uuid ?? "";
|
|
|
|
|
|
|
|
|
|
const [showInstallDetails, setShowInstallDetails] = useState(false);
|
|
|
|
|
const toggleInstallDetails = () => {
|
|
|
|
|
setShowInstallDetails((prev) => !prev);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const { data: swInstallResult, isLoading, isError, error } = useQuery<
|
|
|
|
|
ISoftwareInstallResults,
|
|
|
|
|
AxiosError,
|
|
|
|
|
ISoftwareScriptResult
|
|
|
|
|
>(
|
|
|
|
|
["softwareInstallResults", installUUID],
|
|
|
|
|
() => {
|
|
|
|
|
return deviceAuthToken
|
|
|
|
|
? deviceUserAPI.getSoftwareInstallResult(deviceAuthToken, installUUID)
|
|
|
|
|
: softwareAPI.getSoftwareInstallResult(installUUID);
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...DEFAULT_USE_QUERY_OPTIONS,
|
|
|
|
|
staleTime: 3000,
|
|
|
|
|
select: (data) => data.results as ISoftwareScriptResult,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const renderScriptDetailsSection = () => {
|
2025-10-29 20:04:44 +00:00
|
|
|
// Only show details button if there's details to display
|
|
|
|
|
const showDetailsButton =
|
|
|
|
|
swInstallResult?.status !== "pending_install" && swInstallResult?.output;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{showDetailsButton && (
|
2025-10-15 13:41:43 +00:00
|
|
|
<RevealButton
|
|
|
|
|
isShowing={showInstallDetails}
|
|
|
|
|
showText="Details"
|
|
|
|
|
hideText="Details"
|
|
|
|
|
caretPosition="after"
|
|
|
|
|
onClick={toggleInstallDetails}
|
|
|
|
|
/>
|
2025-10-29 20:04:44 +00:00
|
|
|
)}
|
|
|
|
|
{showInstallDetails && swInstallResult?.output && (
|
|
|
|
|
<Textarea label="Script output:" variant="code">
|
|
|
|
|
{swInstallResult.output}
|
|
|
|
|
</Textarea>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
2025-10-15 13:41:43 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const hostDisplayname =
|
|
|
|
|
swInstallResult?.host_display_name || detailsFromProps.host_display_name;
|
|
|
|
|
|
|
|
|
|
const installResultWithHostDisplayName = swInstallResult
|
|
|
|
|
? {
|
|
|
|
|
...swInstallResult,
|
|
|
|
|
host_display_name: hostDisplayname,
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
const renderContent = () => {
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return <Spinner />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isError) {
|
|
|
|
|
if (error?.status === 404) {
|
|
|
|
|
return deviceAuthToken ? (
|
|
|
|
|
<DeviceUserError />
|
|
|
|
|
) : (
|
|
|
|
|
<DataError
|
|
|
|
|
description="Couldn't get script details"
|
|
|
|
|
excludeIssueLink
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error?.status === 401) {
|
|
|
|
|
return deviceAuthToken ? (
|
|
|
|
|
<DeviceUserError />
|
|
|
|
|
) : (
|
|
|
|
|
<DataError description="Close this modal and try again." />
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!installResultWithHostDisplayName) {
|
|
|
|
|
return deviceAuthToken ? (
|
|
|
|
|
<DeviceUserError />
|
|
|
|
|
) : (
|
|
|
|
|
<DataError description="No data returned." />
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!["installed", "pending_install", "failed_install"].includes(
|
|
|
|
|
installResultWithHostDisplayName.status
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
return (
|
|
|
|
|
<DataError
|
|
|
|
|
description={`Unexpected software install status ${installResultWithHostDisplayName.status}`}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={`${baseClass}__modal-content`}>
|
|
|
|
|
<StatusMessage
|
|
|
|
|
installResult={installResultWithHostDisplayName}
|
|
|
|
|
isMyDevicePage={!!deviceAuthToken}
|
|
|
|
|
contactUrl={contactUrl}
|
|
|
|
|
/>
|
|
|
|
|
{renderScriptDetailsSection()}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
title="Script details"
|
|
|
|
|
onExit={onCancel}
|
|
|
|
|
onEnter={onCancel}
|
|
|
|
|
className={baseClass}
|
|
|
|
|
>
|
2026-03-10 22:30:55 +00:00
|
|
|
{renderContent()}
|
|
|
|
|
<ModalButtons
|
|
|
|
|
deviceAuthToken={deviceAuthToken}
|
|
|
|
|
installResultStatus={swInstallResult?.status}
|
|
|
|
|
hostSoftwareId={hostSoftware?.id}
|
|
|
|
|
onRerun={onRerun}
|
|
|
|
|
onCancel={onCancel}
|
|
|
|
|
/>
|
2025-10-15 13:41:43 +00:00
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-31 14:55:16 +00:00
|
|
|
export default SoftwareScriptDetailsModal;
|