Fleet UI: IPA custom packages (#34220)

This commit is contained in:
RachelElysia
2025-10-28 12:44:17 -04:00
committed by GitHub
parent b35e65455e
commit e1b325130a
27 changed files with 915 additions and 27 deletions
+31
View File
@@ -1,5 +1,6 @@
import { IHostMdmData } from "interfaces/host";
import {
IMdmCommandResult,
IMdmSolution,
IMdmProfile,
IMdmSummaryMdmSolution,
@@ -79,3 +80,33 @@ export const createMockHostMdmData = (
): IHostMdmData => {
return { ...DEFAULT_HOST_MDM_DATA, ...overrides };
};
/**
* Creates a mock of an Apple MDM command result.
* Matches the IMdmCommandResult interface.
*/
export const createMockMdmCommandResult = (
overrides?: Partial<IMdmCommandResult>
): IMdmCommandResult => {
const defaultPayload = `<Command>
<RequestType>InstallApplication</RequestType>
<Identifier>com.example.MockApp</Identifier>
</Command>`;
const defaultResult = `<Result>
<Status>Acknowledged</Status>
<Message>Installation complete</Message>
</Result>`;
return {
host_uuid: "11111111-2222-3333-4444-555555555555",
command_uuid: "mock-command-uuid-1234",
status: "Acknowledged", // or "Error", "NotNow", "200", etc.
updated_at: "2025-08-10T12:05:00Z",
request_type: "InstallApplication",
hostname: "Mock iPhone",
payload: btoa(defaultPayload),
result: btoa(defaultResult),
...overrides,
};
};
@@ -1,5 +1,7 @@
/** For payload-free packages (e.g. software source is sh_packages or ps1_packages)
* we use SoftwareScriptDetailsModal */
* 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 */
import React, { useState } from "react";
import { useQuery } from "react-query";
@@ -0,0 +1,202 @@
import React from "react";
import { screen, waitFor } from "@testing-library/react";
import { createCustomRenderer } from "test/test-utils";
import mockServer from "test/mock-server";
import { getUniversalSoftwareInstallHandler } from "test/handlers/software-handlers";
import { createMockHostSoftware } from "__mocks__/hostMock";
import SoftwareIpaInstallDetailsModal from "./SoftwareIpaInstallDetailsModal";
/**
* Helper for rendering a pre-wired modal component
*/
const renderModal = (
overrides?: Partial<
React.ComponentProps<typeof SoftwareIpaInstallDetailsModal>
>
) => {
const render = createCustomRenderer({ withBackendMock: true });
return render(
<SoftwareIpaInstallDetailsModal
details={{
fleetInstallStatus: "pending_install",
hostDisplayName: "Marko's MacBook Pro",
appName: "Logic Pro",
commandUuid: "uuid-installed",
...overrides?.details,
}}
onCancel={jest.fn()}
{...overrides}
/>
);
};
describe("SoftwareIpaInstallDetailsModal component", () => {
beforeEach(() => {
mockServer.use(getUniversalSoftwareInstallHandler);
});
afterEach(() => {
mockServer.resetHandlers();
});
it("renders NotNow message for an MDM result", async () => {
renderModal({
details: {
commandUuid: "notnow-uuid",
fleetInstallStatus: "pending_install",
hostDisplayName: "Marko's MacBook Pro",
appName: "Logic Pro",
},
});
await waitFor(() => {
expect(screen.getByText(/Fleet tried to install/i)).toBeInTheDocument();
});
expect(
screen.getByText(
/because the host was locked or was running on battery power while in Power Nap/i
)
).toBeInTheDocument();
});
it("renders Acknowledged pending message", async () => {
renderModal({
details: {
commandUuid: "acknowledged-uuid",
fleetInstallStatus: "pending_install",
hostDisplayName: "Marko's MacBook Pro",
appName: "Logic Pro",
},
});
await waitFor(() => {
expect(
screen.getByText(
/was acknowledged but the installation has not been verified/i
)
).toBeInTheDocument();
});
expect(screen.getByText(/Refetch/i)).toBeInTheDocument();
});
it("renders normal software install status for non-MDM case", async () => {
renderModal({
details: {
commandUuid: "uuid-installed",
fleetInstallStatus: "installed",
hostDisplayName: "Marko's MacBook Pro",
appName: "Logic Pro",
},
});
await waitFor(() => {
expect(screen.getByText(/Fleet installed/i)).toBeInTheDocument();
});
expect(screen.getByText(/Logic Pro/i)).toBeInTheDocument();
expect(screen.getByText(/Marko's MacBook Pro/i)).toBeInTheDocument();
});
it("renders manual install message when installed not through Fleet", async () => {
renderModal({
details: {
commandUuid: "",
fleetInstallStatus: "installed",
hostDisplayName: "Marko's MacBook Pro",
appName: "Logic Pro",
},
});
await waitFor(() => {
expect(screen.getByText(/Logic Pro/i)).toBeInTheDocument();
expect(screen.getByText(/is installed\./i)).toBeInTheDocument();
});
});
it("renders host label as 'the host' if host name empty", async () => {
renderModal({
details: {
commandUuid: "uuid-installed",
fleetInstallStatus: "installed",
hostDisplayName: "",
appName: "Logic Pro",
},
});
await waitFor(() => {
expect(screen.getByText(/Fleet installed/i)).toBeInTheDocument();
expect(screen.getByText(/the host/i)).toBeInTheDocument();
});
});
it("renders Done button by default", async () => {
const onCancel = jest.fn();
renderModal({
onCancel,
details: {
commandUuid: "uuid-installed",
fleetInstallStatus: "installed",
hostDisplayName: "Marko's MacBook Pro",
appName: "Logic Pro",
},
});
const doneBtn = await screen.findByRole("button", { name: /done/i });
doneBtn.click();
expect(onCancel).toHaveBeenCalledTimes(1);
});
it("renders Cancel + Retry when failed_install with deviceAuthToken", async () => {
const onRetry = jest.fn();
const onCancel = jest.fn();
renderModal({
onRetry,
onCancel,
deviceAuthToken: "test_token_123",
details: {
commandUuid: "uuid-failed",
fleetInstallStatus: "failed_install",
hostDisplayName: "Marko's MacBook Pro",
appName: "Logic Pro",
},
hostSoftware: createMockHostSoftware({
id: 99,
name: "CoolApp",
installed_versions: [],
}),
});
const cancelButton = await screen.findByRole("button", { name: /cancel/i });
const retryButton = await screen.findByRole("button", { name: /retry/i });
expect(cancelButton).toBeInTheDocument();
expect(retryButton).toBeInTheDocument();
await waitFor(() => {
retryButton.click();
expect(onRetry).toHaveBeenCalledWith(99);
expect(onCancel).toHaveBeenCalled();
});
});
it("renders MDM acknowledged result in details output", async () => {
renderModal({
details: {
commandUuid: "acknowledged-uuid",
fleetInstallStatus: "pending_install",
hostDisplayName: "Marko's MacBook Pro",
appName: "Logic Pro",
},
});
await waitFor(() => {
expect(
screen.getByText(
/acknowledged but the installation has not been verified/i
)
).toBeInTheDocument();
});
});
});
@@ -0,0 +1,392 @@
/** 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 softwareAPI from "services/entities/software";
import deviceUserAPI from "services/entities/device_user";
import {
IHostSoftware,
ISoftwareIpaInstallResults,
SoftwareInstallUninstallStatus,
} from "interfaces/software";
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";
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";
interface IGetStatusMessageProps {
isMyDevicePage?: boolean;
displayStatus: SoftwareInstallUninstallStatus;
isMDMStatusNotNow: boolean;
isMDMStatusAcknowledged: boolean;
appName: string;
hostDisplayName: string;
commandUpdatedAt: string;
}
export const getStatusMessage = ({
isMyDevicePage = false,
displayStatus,
isMDMStatusNotNow,
isMDMStatusAcknowledged,
appName,
hostDisplayName,
commandUpdatedAt,
}: 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;
const isPendingInstall = displayStatus === "pending_install";
// 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}>Done</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 { data: swInstallResult, isLoading, isError, error } = useQuery<
ISoftwareIpaInstallResults,
AxiosError,
IMdmCommandResult
>(
["mdm_command_results", commandUuid],
async () => {
return deviceAuthToken
? deviceUserAPI.getSoftwareInstallResult(deviceAuthToken, commandUuid)
: softwareAPI.getSoftwareInstallResult(commandUuid);
},
{
refetchOnWindowFocus: false,
staleTime: 3000,
enabled: !!commandUuid,
select: (data) => data.results,
}
);
// Fallback to "installed" if no status is provided
const displayStatus = fleetInstallStatus ?? "installed";
const iconName = 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";
const excludeVersions =
!deviceAuthToken &&
["pending_install", "failed_install", "pending"].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: swInstallResult?.updated_at || "",
});
console.log("isMDMStatusNotNow", isMDMStatusNotNow);
const renderInventoryVersionsSection = () => {
if (hostSoftware?.installed_versions?.length) {
return <InventoryVersions hostSoftware={hostSoftware} />;
}
return "If you uninstalled it outside of Fleet it will still show as installed.";
};
const renderInstallDetailsSection = () => {
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`}>
<div className={`${baseClass}__status-message`}>
{!!iconName && <Icon name={iconName} />}
<span>{statusMessage}</span>
</div>
{hostSoftware && !excludeVersions && renderInventoryVersionsSection()}
{!isPendingInstall &&
isInstalledByFleet &&
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;
@@ -0,0 +1,25 @@
.software-ipa-install-details-modal {
overflow-wrap: anywhere; // Prevent long software name overflow
&__modal-content {
display: flex;
flex-direction: column;
gap: $pad-medium;
}
&__status-message {
display: flex;
align-items: center;
gap: $pad-small;
margin: 0;
.icon {
align-self: flex-start;
}
}
.data-set__horizontal {
flex-direction: row;
}
.reveal-button {
width: min-content;
}
}
@@ -0,0 +1 @@
export { default } from "./SoftwareIpaInstallDetailsModal";
@@ -1,3 +1,7 @@
/** This modal is only used for VPP apps and their related installations.
* For iOS/iPadOS packages (e.g. .ipa packages software source is ios_apps or ipados_apps)
* we use SoftwareIpaInstallDetailsModal with the command_uuid. */
import React, { useState } from "react";
import { useQuery } from "react-query";
import { AxiosError } from "axios";
@@ -29,7 +29,7 @@ export type ISupportedGraphicNames = Extract<
interface IFileUploaderProps {
graphicName: ISupportedGraphicNames | ISupportedGraphicNames[];
message: string;
message: React.ReactNode;
title?: string;
additionalInfo?: string;
/** Controls the loading spinner on the upload button */
@@ -67,7 +67,7 @@ interface IFileUploaderProps {
onButtonClick?: () => void;
fileDetails?: {
name: string;
description?: string;
description?: React.ReactNode;
};
/** Indicates that this file uploader deals with an entity that can be managed by GitOps, and so should be disabled when gitops mode is enabled */
gitopsCompatible?: boolean;
+9 -1
View File
@@ -2,21 +2,25 @@ const fleetMaintainedPackageTypes = ["dmg", "zip"] as const;
const unixPackageTypes = ["pkg", "deb", "rpm", "dmg", "zip", "tar.gz"] as const;
const windowsPackageTypes = ["msi", "exe"] as const;
const scriptOnlyPackageTypes = ["sh", "ps1"] as const;
const iosIpadosPackageTypes = ["ipa"] as const;
export const packageTypes = [
...unixPackageTypes,
...windowsPackageTypes,
...scriptOnlyPackageTypes,
...iosIpadosPackageTypes,
] as const;
export type WindowsPackageType = typeof windowsPackageTypes[number];
export type UnixPackageType = typeof unixPackageTypes[number];
export type FleetMaintainedPackageType = typeof fleetMaintainedPackageTypes[number];
export type ScriptOnlyPackageType = typeof scriptOnlyPackageTypes[number];
export type IosIpadosPackageType = typeof iosIpadosPackageTypes[number];
export type PackageType =
| WindowsPackageType
| UnixPackageType
| FleetMaintainedPackageType
| ScriptOnlyPackageType;
| ScriptOnlyPackageType
| IosIpadosPackageType;
export const isWindowsPackageType = (s: any): s is WindowsPackageType => {
return windowsPackageTypes.includes(s);
@@ -32,6 +36,10 @@ export const isFleetMaintainedPackageType = (
return fleetMaintainedPackageTypes.includes(s);
};
export const isIosIpadosPackageType = (s: any): s is IosIpadosPackageType => {
return iosIpadosPackageTypes.includes(s);
};
export const isPackageType = (s: any): s is PackageType => {
return packageTypes.includes(s);
};
+7
View File
@@ -6,6 +6,7 @@ import { IconNames } from "components/icons";
import { HOST_APPLE_PLATFORMS, Platform } from "./platform";
import vulnerabilityInterface from "./vulnerability";
import { ILabelSoftwareTitle } from "./label";
import { IMdmCommandResult } from "./mdm";
export default PropTypes.shape({
type: PropTypes.string,
@@ -452,6 +453,11 @@ export interface ISoftwareInstallResults {
results: ISoftwareInstallResult;
}
/** For Software .ipa installs, we use the install results API to return MDM command results */
export interface ISoftwareIpaInstallResults {
results: IMdmCommandResult;
}
// ISoftwareInstallerType defines the supported installer types for
// software uploaded by the IT admin.
export type ISoftwareInstallerType = "pkg" | "msi" | "deb" | "rpm" | "exe";
@@ -494,6 +500,7 @@ export interface IHostSoftwarePackage {
last_uninstall: ISoftwareLastUninstall | null;
categories?: SoftwareCategory[];
automatic_install_policies?: ISoftwareInstallPolicy[] | null;
platform?: Platform;
}
export interface IHostAppStoreApp {
@@ -26,6 +26,7 @@ import Pagination from "components/Pagination";
import VppInstallDetailsModal from "components/ActivityDetails/InstallDetails/VppInstallDetailsModal";
import { SoftwareInstallDetailsModal } from "components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal";
import SoftwareScriptDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal";
import SoftwareIpaInstallDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareIpaInstallDetailsModal";
import SoftwareUninstallDetailsModal, {
ISWUninstallDetailsParentState,
} from "components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal";
@@ -64,6 +65,10 @@ const ActivityFeed = ({
scriptPackageDetails,
setScriptPackageDetails,
] = useState<IActivityDetails | null>(null);
const [
ipaPackageInstallDetails,
setIpaPackageInstallDetails,
] = useState<IActivityDetails | null>(null);
const [
packageUninstallDetails,
setPackageUninstallDetails,
@@ -146,6 +151,9 @@ const ActivityFeed = ({
} else {
setPackageInstallDetails({ ...details });
}
details?.command_uuid
? setIpaPackageInstallDetails({ ...details })
: setPackageInstallDetails({ ...details });
break;
case ActivityType.UninstalledSoftware:
setPackageUninstallDetails({
@@ -272,6 +280,18 @@ const ActivityFeed = ({
onCancel={() => setScriptPackageDetails(null)}
/>
)}
{ipaPackageInstallDetails && (
<SoftwareIpaInstallDetailsModal
details={{
appName: ipaPackageInstallDetails.software_title || "",
fleetInstallStatus: (ipaPackageInstallDetails.status ||
"pending_install") as SoftwareInstallUninstallStatus,
hostDisplayName: ipaPackageInstallDetails.host_display_name || "",
commandUuid: ipaPackageInstallDetails.command_uuid || "",
}}
onCancel={() => setIpaPackageInstallDetails(null)}
/>
)}
{packageUninstallDetails && (
<SoftwareUninstallDetailsModal
{...packageUninstallDetails}
@@ -1,4 +1,4 @@
import React, { useContext, useState } from "react";
import React from "react";
import FileUploader from "components/FileUploader";
import { getFileDetails } from "utilities/file/fileUtils";
@@ -116,7 +116,7 @@ const SoftwareCustomPackage = ({
return;
}
setUploadDetails(getFileDetails(formData.software));
setUploadDetails(getFileDetails(formData.software, true));
// Note: This TODO is copied to onSaveSoftwareChanges in EditSoftwareModal
// TODO: confirm we are deleting the second sentence (not modifying it) for non-self-service installers
@@ -359,7 +359,7 @@ const EditSoftwareModal = ({
)}
{!!pendingPackageUpdates.software && isUpdatingSoftware && (
<FileProgressModal
fileDetails={getFileDetails(pendingPackageUpdates.software)}
fileDetails={getFileDetails(pendingPackageUpdates.software, true)}
fileProgress={uploadProgress}
/>
)}
@@ -31,6 +31,7 @@ const PKG_TYPE_TO_ID_TEXT = {
exe: "software name",
sh: "package name",
ps1: "package name",
ipa: "software name",
} as const;
const getInstallScriptTooltip = (pkgType: PackageType) => {
@@ -57,6 +57,19 @@ export interface IPackageFormValidation {
customTarget?: { isValid: boolean };
}
const renderFileTypeMessage = () => {
return (
<>
macOS (.pkg), iOS/iPadOS (.ipa),
<br />
Windows (.msi, .exe.,{" "}
<TooltipWrapper tipContent="Payload-free package">.ps1</TooltipWrapper>),
or Linux (.deb, .rpm,{" "}
<TooltipWrapper tipContent="Payload-free package">.sh</TooltipWrapper>)
</>
);
};
interface IPackageFormProps {
labels: ILabelSummary[];
showSchemaButton?: boolean;
@@ -78,7 +91,7 @@ interface IPackageFormProps {
}
// application/gzip is used for .tar.gz files because browsers can't handle double-extensions correctly
const ACCEPTED_EXTENSIONS =
".pkg,.msi,.exe,.deb,.rpm,application/gzip,.tgz,.sh,.ps1";
".pkg,.msi,.exe,.deb,.rpm,application/gzip,.tgz,.sh,.ps1,.ipa";
const PackageForm = ({
labels,
@@ -262,15 +275,16 @@ const PackageForm = ({
const isExePackage = ext === "exe";
const isTarballPackage = ext === "tar.gz";
const isScriptPackage = ext === "sh" || ext === "ps1";
const isIpaPackage = ext === "ipa";
// We currently don't support replacing a tarball package
const canEditFile = isEditingSoftware && !isTarballPackage;
// If a user preselects automatic install and then uploads a .exe
// which automatic install is not supported, the form will default
// back to manual install
// If a user preselects automatic install and then uploads a:
// exe, tarball, script, or ipa which automatic install is not supported,
// the form will default back to manual install
useEffect(() => {
if (
(isExePackage || isTarballPackage || isScriptPackage) &&
(isExePackage || isTarballPackage || isScriptPackage || isIpaPackage) &&
formData.automaticInstall
) {
onToggleAutomaticInstallCheckbox(false);
@@ -280,11 +294,13 @@ const PackageForm = ({
isExePackage,
isTarballPackage,
isScriptPackage,
isIpaPackage,
onToggleAutomaticInstallCheckbox,
]);
// Show advanced options when a package is selected that's not a script
const showAdvancedOptions = formData.software && !isScriptPackage;
// Show advanced options when a package is selected that's not a script or ipa
const showAdvancedOptions =
formData.software && !isScriptPackage && !isIpaPackage;
// GitOps mode hides SoftwareOptionsSelector and TargetLabelSelector
const showOptionsTargetsSelectors = !gitOpsModeEnabled;
@@ -296,13 +312,15 @@ const PackageForm = ({
canEdit={canEditFile}
graphicName="file-pkg"
accept={ACCEPTED_EXTENSIONS}
message=".pkg, .msi, .exe, .deb, .rpm, .tar.gz, .sh, or .ps1"
message={renderFileTypeMessage()}
onFileUpload={onFileSelect}
buttonMessage="Choose file"
buttonType="brand-inverse-icon"
className={`${baseClass}__file-uploader`}
fileDetails={
formData.software ? getFileDetails(formData.software) : undefined
formData.software
? getFileDetails(formData.software, true)
: undefined
}
gitopsCompatible={false}
gitOpsModeEnabled={gitOpsModeEnabled}
@@ -332,6 +350,7 @@ const PackageForm = ({
isExePackage={isExePackage}
isTarballPackage={isTarballPackage}
isScriptPackage={isScriptPackage}
isIpaPackage={isIpaPackage}
onClickPreviewEndUserExperience={
onClickPreviewEndUserExperience
}
@@ -78,6 +78,8 @@ interface ISoftwareOptionsSelector {
isTarballPackage?: boolean;
/** Script only packages do not have ability to select automatic install */
isScriptPackage?: boolean;
/** IPA packages do not have ability to select automatic install or self-service */
isIpaPackage?: boolean;
/** Edit mode does not have ability to change automatic install */
isEditingSoftware?: boolean;
disableOptions?: boolean;
@@ -95,12 +97,14 @@ const SoftwareOptionsSelector = ({
isExePackage,
isTarballPackage,
isScriptPackage,
isIpaPackage,
isEditingSoftware,
disableOptions = false,
}: ISoftwareOptionsSelector) => {
const classNames = classnames(baseClass, className);
const isPlatformIosOrIpados = platform === "ios" || platform === "ipados";
const isPlatformIosOrIpados =
platform === "ios" || platform === "ipados" || isIpaPackage;
const isSelfServiceDisabled = disableOptions || isPlatformIosOrIpados;
const isAutomaticInstallDisabled =
disableOptions ||
@@ -151,8 +155,8 @@ const SoftwareOptionsSelector = ({
{isPlatformIosOrIpados && (
<p>
Currently, self-service and automatic installation are not available
for iOS and iPadOS. Manually install on the <b>Host details</b> page
for each host.
for iOS and iPadOS. Today, you can manually install on the{" "}
<b>Host details</b> page for each host.
</p>
)}
<div className={`${baseClass}__self-service`}>
@@ -247,8 +247,13 @@ export const HostInstallerActionCell = ({
"failed_uninstall",
].includes(ui_status);
const isIpaPackage =
(software.source === "ios_apps" || software.source === "ipados_apps") &&
!!software_package;
const canUninstallSoftware =
!app_store_app &&
!isIpaPackage &&
!!software_package &&
(installedVersionsDetected || installedTgzPackageDetected);
@@ -34,6 +34,7 @@ import Spinner from "components/Spinner";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import SoftwareInstallDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal";
import SoftwareIpaInstallDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareIpaInstallDetailsModal";
import SoftwareScriptDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal";
import VppInstallDetailsModal from "components/ActivityDetails/InstallDetails/VppInstallDetailsModal";
import SoftwareUninstallDetailsModal, {
@@ -143,7 +144,7 @@ const HostSoftwareLibrary = ({
selectedSoftwareUpdates,
setSelectedSoftwareUpdates,
] = useState<IHostSoftware | null>(null);
// these states and modal logic exist at this level intead of the page level to match the similar
// these states and modal logic exist at this level instead of the page level to match the similar
// pattern on
// the device user page, which facilitates manipulating relevant UI states e.g.
// "updating..." when the user clicks "Retry" in the SoftwareInstallDetailsModal
@@ -151,6 +152,10 @@ const HostSoftwareLibrary = ({
selectedHostSWInstallDetails,
setSelectedHostSWInstallDetails,
] = useState<IHostSoftware | null>(null);
const [
selectedHostSWIpaInstallDetails,
setSelectedHostSWIpaInstallDetails,
] = useState<IHostSoftware | null>(null);
const [
selectedHostSWScriptDetails,
setSelectedHostSWScriptDetails,
@@ -382,6 +387,15 @@ const HostSoftwareLibrary = ({
[setSelectedHostSWInstallDetails]
);
const onSetSelectedHostSWIpaInstallDetails = useCallback(
(hostSW?: IHostSoftware) => {
if (hostSW) {
setSelectedHostSWIpaInstallDetails(hostSW);
}
},
[setSelectedHostSWIpaInstallDetails]
);
const onSetSelectedHostSWScriptDetails = useCallback(
(hostSW?: IHostSoftware) => {
if (hostSW) {
@@ -503,6 +517,7 @@ const HostSoftwareLibrary = ({
onShowInventoryVersions,
onShowUpdateDetails,
onSetSelectedHostSWInstallDetails,
onSetSelectedHostSWIpaInstallDetails,
onSetSelectedHostSWScriptDetails,
onSetSelectedHostSWUninstallDetails,
onSetSelectedVPPInstallDetails,
@@ -520,6 +535,7 @@ const HostSoftwareLibrary = ({
onShowInventoryVersions,
onShowUpdateDetails,
onSetSelectedHostSWInstallDetails,
onSetSelectedHostSWIpaInstallDetails,
onSetSelectedHostSWScriptDetails,
onSetSelectedHostSWUninstallDetails,
onSetSelectedVPPInstallDetails,
@@ -592,6 +608,20 @@ const HostSoftwareLibrary = ({
onCancel={() => setSelectedHostSWInstallDetails(null)}
/>
)}
{selectedHostSWIpaInstallDetails && (
<SoftwareIpaInstallDetailsModal
details={{
hostDisplayName,
fleetInstallStatus: selectedHostSWIpaInstallDetails.status,
appName: selectedHostSWIpaInstallDetails.name,
commandUuid:
selectedHostSWIpaInstallDetails.software_package?.last_install
?.install_uuid, // slightly redundant, see explanation in `SoftwareInstallDetailsModal
}}
hostSoftware={selectedHostSWIpaInstallDetails}
onCancel={() => setSelectedHostSWIpaInstallDetails(null)}
/>
)}
{selectedHostSWScriptDetails && (
<SoftwareScriptDetailsModal
details={{
@@ -50,6 +50,7 @@ interface IHostSWLibraryTableHeaders {
onShowInventoryVersions?: (software?: IHostSoftware) => void;
onShowUpdateDetails: (software?: IHostSoftware) => void;
onSetSelectedHostSWInstallDetails: (details?: IHostSoftware) => void;
onSetSelectedHostSWIpaInstallDetails: (details?: IHostSoftware) => void;
onSetSelectedHostSWScriptDetails: (details?: IHostSoftware) => void;
onSetSelectedHostSWUninstallDetails: (
details?: ISWUninstallDetailsParentState
@@ -73,6 +74,7 @@ export const generateHostSWLibraryTableHeaders = ({
onShowInventoryVersions,
onShowUpdateDetails,
onSetSelectedHostSWInstallDetails,
onSetSelectedHostSWIpaInstallDetails,
onSetSelectedHostSWScriptDetails,
onSetSelectedHostSWUninstallDetails,
onSetSelectedVPPInstallDetails,
@@ -136,6 +138,7 @@ export const generateHostSWLibraryTableHeaders = ({
onShowInventoryVersions={onShowInventoryVersions}
onShowUpdateDetails={onShowUpdateDetails}
onShowInstallDetails={onSetSelectedHostSWInstallDetails}
onShowIpaInstallDetails={onSetSelectedHostSWIpaInstallDetails}
onShowScriptDetails={onSetSelectedHostSWScriptDetails}
onShowVPPInstallDetails={onSetSelectedVPPInstallDetails}
onShowUninstallDetails={onSetSelectedHostSWUninstallDetails}
@@ -31,6 +31,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -69,6 +70,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -99,6 +101,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -136,6 +139,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -166,6 +170,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -203,6 +208,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -239,6 +245,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -275,6 +282,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -315,6 +323,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -346,6 +355,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -376,6 +386,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -410,6 +421,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -441,6 +453,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -472,6 +485,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -500,6 +514,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -530,6 +545,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -562,6 +578,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -593,6 +610,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -623,6 +641,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -654,6 +673,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -688,6 +708,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -718,6 +739,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -750,6 +772,7 @@ describe("InstallStatusCell - component", () => {
}}
onShowUpdateDetails={noop}
onShowInstallDetails={noop}
onShowIpaInstallDetails={noop}
onShowScriptDetails={noop}
onShowUninstallDetails={noop}
onShowVPPInstallDetails={noop}
@@ -338,6 +338,7 @@ type IInstallStatusCellProps = {
onShowInventoryVersions?: (software: IHostSoftware) => void;
onShowUpdateDetails: (software: IHostSoftware) => void;
onShowInstallDetails: (hostSoftware: IHostSoftware) => void;
onShowIpaInstallDetails: (hostSoftware: IHostSoftware) => void;
onShowScriptDetails: (hostSoftware: IHostSoftware) => void;
onShowVPPInstallDetails: (s: IVPPHostSoftware) => void;
onShowUninstallDetails: (details: ISWUninstallDetailsParentState) => void;
@@ -385,6 +386,7 @@ const InstallStatusCell = ({
onShowInventoryVersions,
onShowUpdateDetails,
onShowInstallDetails,
onShowIpaInstallDetails,
onShowScriptDetails,
onShowVPPInstallDetails,
onShowUninstallDetails,
@@ -439,6 +441,10 @@ const InstallStatusCell = ({
commandUuid: (lastInstall as IAppLastInstall).command_uuid,
}),
});
}
// TODO: Is this the best way to check for IPA installer?
if (software.source === "ios_apps" || software.source === "ipados_apps") {
onShowIpaInstallDetails(software);
} else {
onShowInstallDetails(software);
}
@@ -31,6 +31,7 @@ import SoftwareUninstallDetailsModal, {
ISWUninstallDetailsParentState,
} from "components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal";
import SoftwareInstallDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal";
import SoftwareIpaInstallDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareIpaInstallDetailsModal";
import SoftwareScriptDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal";
import { VppInstallDetailsModal } from "components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal";
@@ -140,6 +141,10 @@ const SoftwareSelfService = ({
selectedHostSWInstallDetails,
setSelectedHostSWInstallDetails,
] = useState<IHostSoftware | undefined>(undefined);
const [
selectedHostSWIpaInstallDetails,
setSelectedHostSWIpaInstallDetails,
] = useState<IHostSoftware | undefined>(undefined);
const [
selectedHostSWScriptDetails,
setSelectedHostSWScriptDetails,
@@ -474,6 +479,13 @@ const SoftwareSelfService = ({
[setSelectedHostSWInstallDetails]
);
const onShowIpaInstallDetails = useCallback(
(hostSoftware?: IHostSoftware) => {
setSelectedHostSWIpaInstallDetails(hostSoftware);
},
[setSelectedHostSWIpaInstallDetails]
);
const onShowScriptDetails = useCallback(
(hostSoftware?: IHostSoftware) => {
setSelectedHostSWScriptDetails(hostSoftware);
@@ -546,6 +558,7 @@ const SoftwareSelfService = ({
return generateSoftwareTableHeaders({
onShowUpdateDetails,
onShowInstallDetails,
onShowIpaInstallDetails,
onShowScriptDetails,
onShowVPPInstallDetails,
onShowUninstallDetails,
@@ -556,6 +569,7 @@ const SoftwareSelfService = ({
}, [
onShowUpdateDetails,
onShowInstallDetails,
onShowIpaInstallDetails,
onShowScriptDetails,
onShowVPPInstallDetails,
onShowUninstallDetails,
@@ -621,9 +635,25 @@ const SoftwareSelfService = ({
contactUrl={contactUrl}
/>
)}
{selectedHostSWIpaInstallDetails && (
<SoftwareIpaInstallDetailsModal
hostSoftware={selectedHostSWIpaInstallDetails}
details={{
hostDisplayName,
fleetInstallStatus: selectedHostSWIpaInstallDetails.status,
appName: selectedHostSWIpaInstallDetails.name,
commandUuid:
selectedHostSWIpaInstallDetails.software_package?.last_install
?.install_uuid, // slightly redundant, see explanation in `SoftwareInstallDetailsModal
}}
onRetry={onClickInstallAction}
onCancel={() => setSelectedHostSWIpaInstallDetails(undefined)}
deviceAuthToken={deviceToken}
/>
)}
{selectedHostSWScriptDetails && (
<SoftwareScriptDetailsModal
hostSoftware={selectedHostSWInstallDetails}
hostSoftware={selectedHostSWScriptDetails}
details={{
host_display_name: hostDisplayName,
install_uuid:
@@ -41,6 +41,7 @@ export const generateSoftwareTableData = (
interface ISelfServiceTableHeaders {
onShowUpdateDetails: (software: IDeviceSoftware) => void;
onShowInstallDetails: (hostSoftware: IHostSoftware) => void;
onShowIpaInstallDetails: (hostSoftware: IHostSoftware) => void;
onShowScriptDetails: (hostSoftware: IHostSoftware) => void;
onShowVPPInstallDetails: (hostSoftware: IVPPHostSoftware) => void;
onShowUninstallDetails: (
@@ -56,6 +57,7 @@ interface ISelfServiceTableHeaders {
export const generateSoftwareTableHeaders = ({
onShowUpdateDetails,
onShowInstallDetails,
onShowIpaInstallDetails,
onShowScriptDetails,
onShowVPPInstallDetails,
onShowUninstallDetails,
@@ -101,6 +103,7 @@ export const generateSoftwareTableHeaders = ({
software={cellProps.row.original}
onShowUpdateDetails={onShowUpdateDetails}
onShowInstallDetails={onShowInstallDetails}
onShowIpaInstallDetails={onShowIpaInstallDetails}
onShowScriptDetails={onShowScriptDetails}
onShowVPPInstallDetails={onShowVPPInstallDetails}
onShowUninstallDetails={onShowUninstallDetails}
@@ -1,6 +1,7 @@
import { http, HttpResponse } from "msw";
import { baseUrl } from "test/test-utils";
import { createMockSoftwareInstallResult } from "__mocks__/softwareMock";
import { createMockMdmCommandResult } from "__mocks__/mdmMock";
// Installed with outputs
export const getDefaultSoftwareInstallHandler = http.get(
@@ -46,3 +47,48 @@ export const getSoftwareInstallHandlerOnlyInstallOutput = http.get(
});
}
);
/**
* Generic handler for /software/install/:install_uuid/results
* Returns either a 'SoftwareInstallResult' or an MdmCommandResult[]
* depending on the install_uuid/command_uuid supplied.
*/
export const getUniversalSoftwareInstallHandler = http.get(
baseUrl("/software/install/:install_uuid/results"),
({ params }) => {
const installUuid = params.install_uuid as string;
if (
installUuid.startsWith("mdm-") ||
installUuid === "notnow-uuid" ||
installUuid === "acknowledged-uuid"
) {
const statusMap: Record<string, string> = {
"notnow-uuid": "NotNow",
"acknowledged-uuid": "Acknowledged",
};
const status = statusMap[installUuid] || "Acknowledged";
const mdmCommand = createMockMdmCommandResult({
command_uuid: installUuid,
status,
});
// Return what Fleet API actually returns
return HttpResponse.json({
results: mdmCommand,
});
}
// Normal fleet install
return HttpResponse.json({
results: createMockSoftwareInstallResult({
install_uuid: installUuid,
status: "installed",
output: "Install script ran",
post_install_script_output: "Post-install success",
}),
});
}
);
@@ -1,6 +1,13 @@
import React from "react";
import { PackageType } from "interfaces/package_type";
import TooltipWrapper from "components/TooltipWrapper";
type IPlatformDisplayName = "macOS" | "Windows" | "Linux" | "macOS & Linux";
type IPlatformDisplayName =
| "macOS"
| "Windows"
| "Linux"
| "iOS/iPadOS"
| "macOS & Linux";
export const FILE_EXTENSIONS_TO_PLATFORM_DISPLAY_NAME: Record<
string,
@@ -17,6 +24,7 @@ export const FILE_EXTENSIONS_TO_PLATFORM_DISPLAY_NAME: Record<
"tar.gz": "Linux",
sh: "macOS & Linux",
ps1: "Windows",
ipa: "iOS/iPadOS",
};
/** Currently only using tar.gz, but keeping the others for future use
@@ -61,24 +69,42 @@ export const getExtensionFromFileName = (fileName: string) => {
return ext as PackageType | undefined;
};
/** This gets the platform display name from the file. */
export const getPlatformDisplayName = (file: File) => {
/** This gets the platform display name from the file.
* Includes nuance for .sh software installers only supported on Linux
*/
export const getPlatformDisplayName = (
file: File,
isSoftwareInstaller = false
) => {
const fileExt = getExtensionFromFileName(file.name);
if (!fileExt) {
return undefined;
}
if (fileExt === "ipa") {
return (
<TooltipWrapper tipContent="Software will be added for both platforms.">
{FILE_EXTENSIONS_TO_PLATFORM_DISPLAY_NAME[fileExt]}
</TooltipWrapper>
);
}
if (fileExt === "sh" && isSoftwareInstaller) {
// Currently, .sh files for software installers are only supported for Linux
return "Linux";
}
return FILE_EXTENSIONS_TO_PLATFORM_DISPLAY_NAME[fileExt];
};
/** This gets the file details from the file. */
export const getFileDetails = (file: File) => {
export const getFileDetails = (file: File, isSoftwareInstaller = false) => {
return {
name: file.name,
description: getPlatformDisplayName(file),
description: getPlatformDisplayName(file, isSoftwareInstaller),
};
};
export interface IFileDetails {
name: string;
description?: string;
description?: React.ReactNode;
}