Fleet UI: IPA custom packages (#34220)
This commit is contained in:
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+3
-1
@@ -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";
|
||||
|
||||
+202
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
+392
@@ -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'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;
|
||||
+25
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { default } from "./SoftwareIpaInstallDetailsModal";
|
||||
+4
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
-1
@@ -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";
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -359,7 +359,7 @@ const EditSoftwareModal = ({
|
||||
)}
|
||||
{!!pendingPackageUpdates.software && isUpdatingSoftware && (
|
||||
<FileProgressModal
|
||||
fileDetails={getFileDetails(pendingPackageUpdates.software)}
|
||||
fileDetails={getFileDetails(pendingPackageUpdates.software, true)}
|
||||
fileProgress={uploadProgress}
|
||||
/>
|
||||
)}
|
||||
|
||||
+1
@@ -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
|
||||
}
|
||||
|
||||
+7
-3
@@ -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`}>
|
||||
|
||||
+5
@@ -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={{
|
||||
|
||||
+3
@@ -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}
|
||||
|
||||
+23
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user