From 5619f94da3f5c111d3468750a72feb04b45adb1e Mon Sep 17 00:00:00 2001 From: RachelElysia Date: Tue, 7 Oct 2025 12:01:55 -0400 Subject: [PATCH 1/9] Bulk of work getting the modals up, typed correctly, wired throughout pages --- .../SoftwareScriptDetailsModal.tests.tsx | 166 +++++++++ .../SoftwareScriptDetailsModal.tsx | 340 ++++++++++++++++++ .../SoftwareScriptDetailsModal/_styles.scss | 27 ++ .../SoftwareScriptDetailsModal/index.ts | 1 + .../SoftwareUninstallDetailsModal.tsx | 4 +- .../VppInstallDetailsModal.tsx | 11 +- .../InstallDetails/constants.ts | 39 +- frontend/interfaces/package_type.ts | 6 +- frontend/interfaces/software.ts | 57 ++- .../cards/ActivityFeed/ActivityFeed.tsx | 4 +- .../InstallerStatusTable.tsx | 4 + .../InstallerStatusTableConfig.tsx | 4 +- .../SoftwareInstallerCard.tsx | 3 + .../SoftwareSummaryCard.tsx | 7 +- .../SoftwareTitleDetailsPage.tsx | 2 + .../SoftwareTitleDetailsPage/helpers.ts | 2 + .../SoftwareTitlesTableConfig.tsx | 8 +- .../SoftwareDetailsSummary.tsx | 8 +- .../PackageAdvancedOptions.tsx | 2 + .../forms/PackageForm/PackageForm.tsx | 47 ++- .../SoftwareOptionsSelector.tsx | 22 +- .../HostDetailsPage/HostDetailsPage.tsx | 4 +- .../HostInstallerActionCell.tsx | 4 +- .../HostSoftwareLibrary.tsx | 28 ++ .../HostSoftwareLibraryTableConfig.tsx | 3 + .../InstallStatusCell.tests.tsx | 18 + .../InstallStatusCell/InstallStatusCell.tsx | 106 +++++- .../Software/SelfService/SelfService.tsx | 29 ++ .../SelfService/SelfServiceTableConfig.tsx | 3 + .../UpdateSoftwareItem/UpdateSoftwareItem.tsx | 4 +- .../SoftwareUpdateModal.tsx | 7 +- .../details/cards/Software/helpers.tests.ts | 42 +++ .../hosts/details/cards/Software/helpers.tsx | 34 +- frontend/utilities/file/fileUtils.ts | 2 + 34 files changed, 973 insertions(+), 75 deletions(-) create mode 100644 frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx create mode 100644 frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx create mode 100644 frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/_styles.scss create mode 100644 frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/index.ts diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx new file mode 100644 index 00000000000..ed1b9844507 --- /dev/null +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx @@ -0,0 +1,166 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { renderWithSetup } from "test/test-utils"; +import { createMockSoftwareInstallResult } from "__mocks__/softwareMock"; +import { ISoftwareScriptResult } from "interfaces/software"; +import { StatusMessage, ModalButtons } from "./SoftwareScriptDetailsModal"; + +describe("SoftwareScriptDetailsModal - StatusMessage component", () => { + it("on software library page/pending activity, renders pending install message with host and package name", () => { + render( + + ); + + expect(screen.queryByTestId("pending-outline-icon")).toBeInTheDocument(); + expect(screen.getByText(/is running or will run/)).toBeInTheDocument(); + expect(screen.getByText(/\(com\.cool\.app\)/)).toBeInTheDocument(); + expect(screen.getByText(/Test Host/)).toBeInTheDocument(); + expect(screen.getByText(/when it comes online/)).toBeInTheDocument(); + }); + + it("on device user page, renders failed run with rerun option with contact link", () => { + render( + + ); + + expect(screen.queryByTestId("error-icon")).toBeInTheDocument(); + expect(screen.getByText(/failed to run/)).toBeInTheDocument(); + expect(screen.getByText(/CoolApp/)).toBeInTheDocument(); + // Host name should not be rendered for device user page + expect(screen.queryByText(/Test Host/)).not.toBeInTheDocument(); + expect(screen.getByText(/You can rerun/)).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: /contact your IT admin/ }) + ).toHaveAttribute("href", "http://support"); + }); + + it("on device user page, renders failed install with retry option without contact link", () => { + render( + + ); + + expect(screen.queryByTestId("error-icon")).toBeInTheDocument(); + expect(screen.getByText(/failed to run/)).toBeInTheDocument(); + expect(screen.getByText(/CoolApp/)).toBeInTheDocument(); + // Host name should not be rendered for device user page + expect(screen.queryByText(/Test Host/)).not.toBeInTheDocument(); + expect(screen.getByText(/You can rerun/)).toBeInTheDocument(); + // Don't show link of not provided + expect( + screen.queryByRole("link", { name: /contact your IT admin/ }) + ).not.toBeInTheDocument(); + }); + + it("on host details page, renders failed script without rerun", () => { + render( + + ); + + expect(screen.queryByTestId("error-icon")).toBeInTheDocument(); + expect(screen.getByText(/failed to run/)).toBeInTheDocument(); + expect(screen.getByText(/Test Host/)).toBeInTheDocument(); + expect(screen.queryByText(/You can rerun/)).not.toBeInTheDocument(); + }); + + it("on host details page/install activity, renders ran message with timestamp", () => { + render( + + ); + + expect(screen.queryByTestId("success-icon")).toBeInTheDocument(); + expect(screen.getByText(/Fleet ran/)).toBeInTheDocument(); + expect(screen.getByText(/CoolApp/)).toBeInTheDocument(); + expect(screen.getByText(/Test Host/)).toBeInTheDocument(); + expect(screen.getByText(/\(com\.cool\.app\)/)).toBeInTheDocument(); + expect(screen.getByText(/\d+.*ago/)).toBeInTheDocument(); + }); +}); + +describe("SoftwareScriptDetailsModal - ModalButtons component", () => { + it("on device user page, shows Rerun/Cancel for failed install and triggers handlers", async () => { + const onCancel = jest.fn(); + const onRerun = jest.fn(); + + const { user } = renderWithSetup( + + ); + expect(screen.getByRole("button", { name: "Rerun" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Rerun" })); + expect(onRerun).toHaveBeenCalledWith(99); + expect(onCancel).toHaveBeenCalled(); + + await user.click(screen.getByRole("button", { name: "Cancel" })); + expect(onCancel).toHaveBeenCalledTimes(2); + }); + + it("shows Done button for pending run", () => { + const onCancel = jest.fn(); + render(); + expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "Rerun" }) + ).not.toBeInTheDocument(); + }); + + it("on device user page, shows Done button for ran payload-free software", () => { + const onCancel = jest.fn(); + render( + + ); + expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument(); + }); +}); diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx new file mode 100644 index 00000000000..e1a2db59bd5 --- /dev/null +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx @@ -0,0 +1,340 @@ +import React, { useState } from "react"; +import { useQuery } from "react-query"; +import { formatDistanceToNow } from "date-fns"; +import { AxiosError } from "axios"; + +import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; + +import { + IHostSoftware, + ISoftwareScriptResult, + ISoftwareInstallResults, +} from "interfaces/software"; +import softwareAPI from "services/entities/software"; +import deviceUserAPI from "services/entities/device_user"; + +import Modal from "components/Modal"; +import ModalFooter from "components/ModalFooter"; +import Button from "components/buttons/Button"; +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 CustomLink from "components/CustomLink"; + +import { + SCRIPT_DETAILS_STATUS_ICONS, + getScriptDetailsStatusPredicate, +} from "../constants"; + +const baseClass = "software-script-details-modal"; + +export type IPackageInstallDetails = { + host_display_name?: string; + install_uuid?: string; // not actually optional +}; + +export const renderContactOption = (url?: string) => ( + <> + {" "} + or{" "} + {url ? ( + + ) : ( + "contact your IT admin" + )} + +); + +interface IInstallStatusMessage { + softwareName: string; + installResult: ISoftwareScriptResult; + isDUP: boolean; + contactUrl?: string; +} + +// TODO - match VppInstallDetailsModal status to this, still accounting for MDM-specific cases +// present there +export const StatusMessage = ({ + softwareName, + installResult, + isDUP, + contactUrl, +}: IInstallStatusMessage) => { + const { + host_display_name, + software_package, + software_title, + status, + updated_at, + created_at, + } = installResult; + + const formattedHost = host_display_name ? ( + {host_display_name} + ) : ( + "the host" + ); + + const displayTimeStamp = ["failed_install", "installed"].includes( + status || "" + ) + ? ` (${formatDistanceToNow(new Date(updated_at || created_at), { + includeSeconds: true, + addSuffix: true, + })})` + : ""; + + const renderStatusCopy = () => { + const prefix = ( + <> + Fleet {getScriptDetailsStatusPredicate(status)} {software_title} + + ); + let middle = null; + if (isDUP) { + if (status === "failed_install") { + middle = <>. You can retry{renderContactOption(contactUrl)}; + } + } else { + // host details page + middle = ( + <> + {" "} + ({software_package}) on {formattedHost} + {status === "pending_install" ? " when it comes online" : ""} + {displayTimeStamp} + + ); + } + return ( + + {prefix} + {middle} + {"."} + + ); + }; + + return ( +
+ + {renderStatusCopy()} +
+ ); +}; + +interface IModalButtonsProps { + deviceAuthToken?: string; + status?: string; + hostSoftwareId?: number; + onRerun?: (id: number) => void; + onCancel: () => void; +} + +export const ModalButtons = ({ + deviceAuthToken, + status, + hostSoftwareId, + onRerun, + onCancel, +}: IModalButtonsProps) => { + if (deviceAuthToken && status === "failed_install") { + const onClickRerun = () => { + // on DUP, where this is relevant, both will be defined + if (onRerun && hostSoftwareId) { + onRerun(hostSoftwareId); + } + onCancel(); + }; + + return ( + + + + + } + /> + ); + } + + return ( + Done} /> + ); +}; + +interface ISoftwareInstallDetailsProps { + /** note that details.install_uuid is present in hostSoftware, but since it is always needed for + this modal while hostSoftware is not, as in the case of the activity feeds, it is specifically + necessary in the details prop */ + details: IPackageInstallDetails; + hostSoftware?: IHostSoftware; // for inventory versions, and software name when not Fleet installed (not present on activity feeds) + deviceAuthToken?: string; // DUP only + onCancel: () => void; + onRerun?: (id: number) => void; // DUP only + contactUrl?: string; // DUP only +} + +export const SoftwareInstallDetailsModal = ({ + details: detailsFromProps, + onCancel, + hostSoftware, + deviceAuthToken, + onRerun, + contactUrl, +}: ISoftwareInstallDetailsProps) => { + // will always be present + const installUUID = detailsFromProps.install_uuid ?? ""; + + const [showInstallDetails, setShowInstallDetails] = useState(false); + const toggleInstallDetails = () => { + setShowInstallDetails((prev) => !prev); + }; + + const isInstalledByFleet = hostSoftware + ? !!hostSoftware.software_package?.last_install + : true; // if no hostSoftware passed in, can assume this is the activity feed, meaning this can only refer to a Fleet-handled install + + const { data: swInstallResult, isLoading, isError, error } = useQuery< + ISoftwareInstallResults, + AxiosError, + ISoftwareScriptResult + >( + ["softwareInstallResults", installUUID], + () => { + return deviceAuthToken + ? deviceUserAPI.getSoftwareInstallResult(deviceAuthToken, installUUID) + : softwareAPI.getSoftwareInstallResult(installUUID); + }, + { + enabled: !!isInstalledByFleet, + ...DEFAULT_USE_QUERY_OPTIONS, + staleTime: 3000, + select: (data) => data.results as ISoftwareScriptResult, + } + ); + + const renderScriptDetailsSection = () => ( + <> + + {showInstallDetails && swInstallResult?.output && ( + + )} + + ); + + const hostDisplayname = + swInstallResult?.host_display_name || detailsFromProps.host_display_name; + + const installResultWithHostDisplayName = swInstallResult + ? { + ...swInstallResult, + host_display_name: hostDisplayname, + } + : undefined; + + const renderContent = () => { + if (isInstalledByFleet) { + if (isLoading) { + return ; + } + + if (isError) { + if (error?.status === 404) { + return deviceAuthToken ? ( + + ) : ( + + ); + } + + if (error?.status === 401) { + return deviceAuthToken ? ( + + ) : ( + + ); + } + } + + if (!swInstallResult) { + return deviceAuthToken ? ( + + ) : ( + + ); + } + + if ( + !["installed", "pending_install", "failed_install"].includes( + swInstallResult.status + ) + ) { + return ( + + ); + } + } + + if (installResultWithHostDisplayName) { + return ( +
+ +
+ ); + } + }; + + return ( + + <> + {renderContent()} + + + + ); +}; + +export default SoftwareInstallDetailsModal; diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/_styles.scss b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/_styles.scss new file mode 100644 index 00000000000..74e08bf52bb --- /dev/null +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/_styles.scss @@ -0,0 +1,27 @@ +// TODO: Confirm all styling + +.software-script-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; + } +} diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/index.ts b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/index.ts new file mode 100644 index 00000000000..0d8cdac1514 --- /dev/null +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/index.ts @@ -0,0 +1 @@ +export { default } from "./SoftwareScriptDetailsModal"; diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx index 113c3e2d395..31fdd95e393 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx @@ -23,7 +23,7 @@ import Spinner from "components/Spinner"; import Textarea from "components/Textarea"; import RevealButton from "components/buttons/RevealButton"; import { - getInstallDetailsStatusPredicate, + getScriptDetailsStatusPredicate, INSTALL_DETAILS_STATUS_ICONS, } from "../constants"; import { renderContactOption } from "../SoftwareInstallDetailsModal/SoftwareInstallDetailsModal"; @@ -63,7 +63,7 @@ export const StatusMessage = ({ const renderStatusCopy = () => { const prefix = ( <> - Fleet {getInstallDetailsStatusPredicate(status)} {softwareName} + Fleet {getScriptDetailsStatusPredicate(status)} {softwareName} ); let suffix = null; diff --git a/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx index 8644ec313f3..0384be11f45 100644 --- a/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx @@ -8,7 +8,10 @@ import deviceUserAPI, { IGetVppInstallCommandResultsResponse, } from "services/entities/device_user"; -import { IHostSoftware, SoftwareInstallStatus } from "interfaces/software"; +import { + IHostSoftware, + SoftwareInstallUninstallStatus, +} from "interfaces/software"; import { IMdmCommandResult } from "interfaces/mdm"; import InventoryVersions from "pages/hosts/details/components/InventoryVersions"; @@ -32,7 +35,7 @@ interface IGetStatusMessageProps { isMyDevicePage?: boolean; /** "pending" is an edge case here where VPP install activities that were added to the feed prior to v4.57 * (when we split pending into pending_install/pending_uninstall) will list the status as "pending" rather than "pending_install" */ - displayStatus: SoftwareInstallStatus | "pending"; + displayStatus: SoftwareInstallUninstallStatus | "pending"; isMDMStatusNotNow: boolean; isMDMStatusAcknowledged: boolean; appName: string; @@ -149,7 +152,7 @@ export const getStatusMessage = ({ }; interface IModalButtonsProps { - displayStatus: SoftwareInstallStatus | "pending"; + displayStatus: SoftwareInstallUninstallStatus | "pending"; deviceAuthToken?: string; onCancel: () => void; onRetry?: (id: number) => void; @@ -196,7 +199,7 @@ const baseClass = "vpp-install-details-modal"; export type IVppInstallDetails = { /** Status: null when a host manually installed not using Fleet */ - fleetInstallStatus: SoftwareInstallStatus | null; + fleetInstallStatus: SoftwareInstallUninstallStatus | null; hostDisplayName: string; appName: string; commandUuid?: string; diff --git a/frontend/components/ActivityDetails/InstallDetails/constants.ts b/frontend/components/ActivityDetails/InstallDetails/constants.ts index e04b08b07db..49f6bf63b0f 100644 --- a/frontend/components/ActivityDetails/InstallDetails/constants.ts +++ b/frontend/components/ActivityDetails/InstallDetails/constants.ts @@ -1,11 +1,14 @@ import { IconNames } from "components/icons"; import { + SoftwareInstallUninstallStatus, SoftwareInstallStatus, SoftwareUninstallStatus, } from "interfaces/software"; +// Install/Uninstall helpers + export const INSTALL_DETAILS_STATUS_ICONS: Record< - SoftwareInstallStatus | SoftwareUninstallStatus, // former is superset of latter, latter included in union for type system + SoftwareInstallUninstallStatus | SoftwareUninstallStatus, // former is superset of latter, latter included in union for type system IconNames > = { pending_install: "pending-outline", @@ -17,7 +20,7 @@ export const INSTALL_DETAILS_STATUS_ICONS: Record< } as const; const INSTALL_DETAILS_STATUS_PREDICATES: Record< - SoftwareInstallStatus | SoftwareUninstallStatus, // former is superset of latter, latter included in union for type system + SoftwareInstallUninstallStatus | SoftwareUninstallStatus, // former is superset of latter, latter included in union for type system string > = { pending_install: "is installing or will install", @@ -36,7 +39,37 @@ export const getInstallDetailsStatusPredicate = ( } return ( INSTALL_DETAILS_STATUS_PREDICATES[ - status.toLowerCase() as SoftwareInstallStatus + status.toLowerCase() as SoftwareInstallUninstallStatus ] || INSTALL_DETAILS_STATUS_PREDICATES.pending_install ); }; + +// Script helpers +export const SCRIPT_DETAILS_STATUS_ICONS: Record< + SoftwareInstallStatus, + IconNames +> = { + pending_install: "pending-outline", + installed: "success", + failed_install: "error", +} as const; + +const SCRIPT_DETAILS_STATUS_PREDICATES: Record< + SoftwareInstallStatus, + string +> = { + pending_install: "is running or will run", + installed: "ran", + failed_install: "failed to run", +} as const; + +export const getScriptDetailsStatusPredicate = (status: string | undefined) => { + if (!status) { + return SCRIPT_DETAILS_STATUS_PREDICATES.pending_install; + } + return ( + SCRIPT_DETAILS_STATUS_PREDICATES[ + status.toLowerCase() as SoftwareInstallStatus + ] || SCRIPT_DETAILS_STATUS_PREDICATES.pending_install + ); +}; diff --git a/frontend/interfaces/package_type.ts b/frontend/interfaces/package_type.ts index 672660b4d12..cd0069311cd 100644 --- a/frontend/interfaces/package_type.ts +++ b/frontend/interfaces/package_type.ts @@ -1,18 +1,22 @@ 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; export const packageTypes = [ ...unixPackageTypes, ...windowsPackageTypes, + ...scriptOnlyPackageTypes, ] 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 PackageType = | WindowsPackageType | UnixPackageType - | FleetMaintainedPackageType; + | FleetMaintainedPackageType + | ScriptOnlyPackageType; export const isWindowsPackageType = (s: any): s is WindowsPackageType => { return windowsPackageTypes.includes(s); diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts index cc943d777d3..17c6c3d711e 100644 --- a/frontend/interfaces/software.ts +++ b/frontend/interfaces/software.ts @@ -223,6 +223,8 @@ export const SOURCE_TYPE_CONVERSION = { chocolatey_packages: "Package (Chocolatey)", pkg_packages: "Package (pkg)", vscode_extensions: "IDE extension", // vscode_extensions can include any vscode-based editor (e.g., Cursor, Trae, Windsurf), so we rely instead on the `extension_for` field computed by Fleet server and fallback to this value if it is not present. + sh_packages: "Payload-free (Linux)", + ps1_packages: "Payload-free (Windows)", } as const; export type SoftwareSource = keyof typeof SOURCE_TYPE_CONVERSION; @@ -252,8 +254,18 @@ export const INSTALLABLE_SOURCE_PLATFORM_CONVERSION = { chocolatey_packages: "windows", pkg_packages: "darwin", vscode_extensions: null, + sh_packages: "linux", + ps1_packages: "windows", } as const; +export const NoVersionOrHostDataAvailable = [ + "tgz_packages", + "sh_packages", + "ps1_packages", +]; + +export const ScriptPackage = ["sh_packages", "ps1_packages"]; + export type InstallableSoftwareSource = keyof typeof INSTALLABLE_SOURCE_PLATFORM_CONVERSION; const EXTENSION_FOR_TYPE_CONVERSION = { @@ -311,18 +323,27 @@ export const SOFTWARE_INSTALL_STATUSES = [ "installed", "pending_install", "failed_install", +] as const; + +export type SoftwareInstallStatus = typeof SOFTWARE_INSTALL_STATUSES[number]; + +export const SOFTWARE_INSTALL_UNINSTALL_STATUSES = [ + ...SOFTWARE_INSTALL_STATUSES, ...SOFTWARE_UNINSTALL_STATUSES, ] as const; /* - * SoftwareInstallStatus represents the possible states of software install operations. + * SoftwareInstallUninstallStatus represents the possible states of software install operations. */ -export type SoftwareInstallStatus = typeof SOFTWARE_INSTALL_STATUSES[number]; +export type SoftwareInstallUninstallStatus = typeof SOFTWARE_INSTALL_UNINSTALL_STATUSES[number]; -export const isValidSoftwareInstallStatus = ( +export const isValidSoftwareInstallUninstallStatus = ( s: string | undefined | null -): s is SoftwareInstallStatus => - !!s && SOFTWARE_INSTALL_STATUSES.includes(s as SoftwareInstallStatus); +): s is SoftwareInstallUninstallStatus => + !!s && + SOFTWARE_INSTALL_UNINSTALL_STATUSES.includes( + s as SoftwareInstallUninstallStatus + ); export const SOFTWARE_AGGREGATE_STATUSES = [ "installed", @@ -376,7 +397,7 @@ export interface ISoftwareInstallResult { software_title_id: number; software_package: string; host_id: number; - status: SoftwareInstallStatus; + status: SoftwareInstallUninstallStatus; detail: string; output: string; pre_install_query_output: string; @@ -386,6 +407,11 @@ export interface ISoftwareInstallResult { self_service: boolean; } +// Script results are only install results, never uninstall +export type ISoftwareScriptResult = Omit & { + status: SoftwareInstallStatus; +}; + export interface ISoftwareInstallResults { results: ISoftwareInstallResult; } @@ -453,7 +479,7 @@ export interface IHostSoftware { source: SoftwareSource; extension_for?: SoftwareExtensionFor; bundle_identifier?: string; - status: Exclude | null; + status: Exclude | null; installed_versions: ISoftwareInstallVersion[] | null; } @@ -483,7 +509,13 @@ export type IHostSoftwareUiStatus = | "failed_install_update_available" // Install/update failed; newer installer version available | "failed_uninstall" // Uninstall attempt failed | "failed_uninstall_update_available" // Uninstall/update failed; newer installer version available - | "update_available"; // In inventory, but newer fleet installer version is available + | "update_available" // In inventory, but newer fleet installer version is available + // Script UI statuses + | "ran_script" // Script package ran successfully + | "failed_script" // Script package failed to run + | "running_script" // ONLINE; fleet-initiated script run in progress + | "pending_script" // OFFLINE; fleet-initiated script run scheduled + | "never_ran_script"; // Script package never ran before /** * Extends IHostSoftware with a computed `ui_status` field. @@ -513,7 +545,7 @@ export type IDeviceSoftware = IHostSoftware; export type IDeviceSoftwareWithUiStatus = IHostSoftwareWithUiStatus; const INSTALL_STATUS_PREDICATES: Record< - SoftwareInstallStatus | "pending", + SoftwareInstallUninstallStatus | "pending", string > = { pending: "pending", @@ -530,8 +562,9 @@ export const getInstallStatusPredicate = (status: string | undefined) => { return INSTALL_STATUS_PREDICATES.pending; } return ( - INSTALL_STATUS_PREDICATES[status.toLowerCase() as SoftwareInstallStatus] || - INSTALL_STATUS_PREDICATES.pending + INSTALL_STATUS_PREDICATES[ + status.toLowerCase() as SoftwareInstallUninstallStatus + ] || INSTALL_STATUS_PREDICATES.pending ); }; @@ -544,7 +577,7 @@ export const aggregateInstallStatusCounts = ( }); export const INSTALL_STATUS_ICONS: Record< - SoftwareInstallStatus | "pending" | "failed", + SoftwareInstallUninstallStatus | "pending" | "failed", IconNames > = { pending: "pending-outline", diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx index c35e359cbc8..9a460388b0f 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx @@ -11,7 +11,7 @@ import activitiesAPI, { import { resolveUninstallStatus, - SoftwareInstallStatus, + SoftwareInstallUninstallStatus, } from "interfaces/software"; import { ActivityType, IActivityDetails } from "interfaces/activity"; @@ -268,7 +268,7 @@ const ActivityFeed = ({ details={{ appName: vppInstallDetails.software_title || "", fleetInstallStatus: (vppInstallDetails.status || - "pending_install") as SoftwareInstallStatus, + "pending_install") as SoftwareInstallUninstallStatus, hostDisplayName: vppInstallDetails.host_display_name || "", commandUuid: vppInstallDetails.command_uuid || "", }} diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx index 81684d8ab43..2807bf15f4f 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx @@ -16,13 +16,16 @@ interface IInstallerStatusTableProps { teamId?: number; status: ISoftwarePackageStatus | ISoftwareAppStoreAppStatus; isLoading?: boolean; + isScriptPackage?: boolean; } + const InstallerStatusTable = ({ className, softwareId, teamId, status, isLoading = false, + isScriptPackage = false, }: IInstallerStatusTableProps) => { const classNames = classnames(baseClass, className); @@ -30,6 +33,7 @@ const InstallerStatusTable = ({ baseClass: classNames, softwareId, teamId, + isScriptPackage, }); return ( diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx index 022ea46b961..67e94c35d2a 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx @@ -14,6 +14,7 @@ interface ISoftwareTitleDetailsTableConfigProps { softwareId?: number; teamId?: number; baseClass?: string; + isScriptPackage?: boolean; } interface ICellProps { cell: { @@ -78,12 +79,13 @@ const generateSoftwareTitleDetailsTableConfig = ({ softwareId, teamId, baseClass, + isScriptPackage, }: ISoftwareTitleDetailsTableConfigProps) => { const tableHeaders = [ { accessor: "installed", disableSortBy: true, - title: "Installed", + title: isScriptPackage ? "Ran" : "Installed", Header: () => { const displayData = STATUS_DISPLAY_OPTIONS.installed; const titleWithTooltip = ( diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx index 109a5f1ab61..993b4516488 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx @@ -149,6 +149,7 @@ export const SoftwareActionButtons = ({ interface ISoftwareInstallerCardProps { softwareTitleName: string; + isScriptPackage?: boolean; name: string; version: string | null; addedTimestamp: string; @@ -175,6 +176,7 @@ interface ISoftwareInstallerCardProps { // of packages we should consider refactoring this to be more dynamic. const SoftwareInstallerCard = ({ softwareTitleName, + isScriptPackage = false, name, version, addedTimestamp, @@ -362,6 +364,7 @@ const SoftwareInstallerCard = ({
{ + const { source } = title; + const { isGlobalAdmin, isGlobalMaintainer, @@ -50,8 +53,8 @@ const SoftwareSummaryCard = ({ const [iconUploadedAt, setIconUploadedAt] = useState(""); - // Hide versions table for tgz_packages only - const showVersionsTable = title.source !== "tgz_packages"; + // Hide versions table for tgz_packages, sh_packages, & ps1_packages only + const showVersionsTable = !NoVersionOrHostDataAvailable.includes(source); const hasEditPermissions = isGlobalAdmin || isGlobalMaintainer || isTeamMaintainerOrTeamAdmin; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx index def1e188a62..18870604396 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx @@ -145,11 +145,13 @@ const SoftwareTitleDetailsPage = ({ addedTimestamp, status, isSelfService, + isScriptPackage, } = getInstallerCardInfo(title); return ( { ? aggregateInstallStatusCounts(installerData.status) : installerData.status, isSelfService: installerData.self_service, + isScriptPackage: ScriptPackage.includes(softwareTitle.source) || false, }; }; diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx index d66732cdcc1..ca4339407c3 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx @@ -4,6 +4,7 @@ import { InjectedRouter } from "react-router"; import { ISoftwareTitle, + NoVersionOrHostDataAvailable, formatSoftwareType, isIpadOrIphoneSoftwareSource, } from "interfaces/software"; @@ -186,8 +187,11 @@ const generateTableHeaders = ( id: "view-all-hosts", disableSortBy: true, Cell: (cellProps: IViewAllHostsLinkProps) => { - const hostCountNotSupported = - cellProps.row.original.source === "tgz_packages"; + const { source } = cellProps.row.original; + + const hostCountNotSupported = NoVersionOrHostDataAvailable.includes( + source + ); if (hostCountNotSupported) return null; diff --git a/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx b/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx index f408e124352..394429d1b0a 100644 --- a/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx +++ b/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx @@ -8,6 +8,7 @@ import React from "react"; import { getPathWithQueryParams, QueryParams } from "utilities/url"; import paths from "router/paths"; +import { NoVersionOrHostDataAvailable } from "interfaces/software"; import DataSet from "components/DataSet"; import LastUpdatedHostCount from "components/LastUpdatedHostCount"; @@ -64,9 +65,12 @@ const SoftwareDetailsSummary = ({ }: ISoftwareDetailsSummaryProps) => { const hostCountPath = getPathWithQueryParams(paths.MANAGE_HOSTS, queryParams); - // Remove host count for tgz_packages only and if viewing details summary from edit icon preview modal + // Remove host count for tgz_packages, sh_packages, and ps1_packages only + // or if viewing details summary from edit icon preview modal const showHostCount = - source !== "tgz_packages" && iconPreviewUrl === undefined; + source && + !NoVersionOrHostDataAvailable.includes(source) && + iconPreviewUrl === undefined; const renderSoftwareIcon = () => { if ( diff --git a/frontend/pages/SoftwarePage/components/forms/PackageAdvancedOptions/PackageAdvancedOptions.tsx b/frontend/pages/SoftwarePage/components/forms/PackageAdvancedOptions/PackageAdvancedOptions.tsx index d028f826214..051e0e54c9f 100644 --- a/frontend/pages/SoftwarePage/components/forms/PackageAdvancedOptions/PackageAdvancedOptions.tsx +++ b/frontend/pages/SoftwarePage/components/forms/PackageAdvancedOptions/PackageAdvancedOptions.tsx @@ -29,6 +29,8 @@ const PKG_TYPE_TO_ID_TEXT = { rpm: "package name", msi: "product code", exe: "software name", + sh: "package name", + ps1: "package name", } as const; const getInstallScriptTooltip = (pkgType: PackageType) => { diff --git a/frontend/pages/SoftwarePage/components/forms/PackageForm/PackageForm.tsx b/frontend/pages/SoftwarePage/components/forms/PackageForm/PackageForm.tsx index c884ecc5014..9de30aacaa4 100644 --- a/frontend/pages/SoftwarePage/components/forms/PackageForm/PackageForm.tsx +++ b/frontend/pages/SoftwarePage/components/forms/PackageForm/PackageForm.tsx @@ -260,6 +260,7 @@ const PackageForm = ({ const ext = getExtensionFromFileName(formData?.software?.name || ""); const isExePackage = ext === "exe"; const isTarballPackage = ext === "tar.gz"; + const isScriptPackage = ext === "sh" || ext === "ps1"; // We currently don't support replacing a tarball package const canEditFile = isEditingSoftware && !isTarballPackage; @@ -267,16 +268,23 @@ const PackageForm = ({ // which automatic install is not supported, the form will default // back to manual install useEffect(() => { - if ((isExePackage || isTarballPackage) && formData.automaticInstall) { + if ( + (isExePackage || isTarballPackage || isScriptPackage) && + formData.automaticInstall + ) { onToggleAutomaticInstallCheckbox(false); } }, [ formData.automaticInstall, isExePackage, isTarballPackage, + isScriptPackage, onToggleAutomaticInstallCheckbox, ]); + // Show advanced options when a package is selected that's not a script + const showAdvancedOptions = formData.software && !isScriptPackage; + // GitOps mode hides SoftwareOptionsSelector and TargetLabelSelector const showOptionsTargetsSelectors = !gitOpsModeEnabled; @@ -287,7 +295,7 @@ const PackageForm = ({ canEdit={canEditFile} graphicName="file-pkg" accept={ACCEPTED_EXTENSIONS} - message=".pkg, .msi, .exe, .deb, .rpm, or .tar.gz" + message=".pkg, .msi, .exe, .deb, .rpm, .tar.gz, .sh, or .ps1" onFileUpload={onFileSelect} buttonMessage="Choose file" buttonType="brand-inverse-icon" @@ -322,6 +330,7 @@ const PackageForm = ({ isEditingSoftware={isEditingSoftware} isExePackage={isExePackage} isTarballPackage={isTarballPackage} + isScriptPackage={isScriptPackage} onClickPreviewEndUserExperience={ onClickPreviewEndUserExperience } @@ -353,22 +362,24 @@ const PackageForm = ({
)} - + {showAdvancedOptions && ( + + )}
{submitTooltipContent ? ( { @@ -102,9 +105,12 @@ const SoftwareOptionsSelector = ({ const isAutomaticInstallDisabled = disableOptions || isPlatformIosOrIpados || isExePackage || isTarballPackage; - /** Tooltip only shows when enabled or for exe/tar.gz packages */ + /** Tooltip only shows when enabled or for exe/tar.gz/sh/ps1 packages */ const showAutomaticInstallTooltip = - !isAutomaticInstallDisabled || isExePackage || isTarballPackage; + !isAutomaticInstallDisabled || + isExePackage || + isTarballPackage || + isScriptPackage; const getAutomaticInstallTooltip = (): JSX.Element => { if (isExePackage || isTarballPackage) { return ( @@ -118,6 +124,18 @@ const SoftwareOptionsSelector = ({ ); } + + // TODO: Confirm with PM/Design if we want tooltip messaging for script packages + if (isScriptPackage) { + return ( + <> + Fleet can't create a policy to detect existing installations of + software installed by scripts. To automatically install script-based + packages, add a custom policy and enable the install software + automation on the Policies page. + + ); + } return <>Automatically install only on hosts missing this software.; }; diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 9bfcbf0bbf1..5a8a56a3bfc 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -34,7 +34,7 @@ import { IQueryStats } from "interfaces/query_stats"; import { IHostSoftware, resolveUninstallStatus, - SoftwareInstallStatus, + SoftwareInstallUninstallStatus, } from "interfaces/software"; import { ITeam } from "interfaces/team"; import { ActivityType, IHostUpcomingActivity } from "interfaces/activity"; @@ -704,7 +704,7 @@ const HostDetailsPage = ({ setActivityVPPInstallDetails({ appName: details?.software_title || "", fleetInstallStatus: (details?.status || - "pending_install") as SoftwareInstallStatus, + "pending_install") as SoftwareInstallUninstallStatus, commandUuid: details?.command_uuid || "", // FIXME: It seems like the backend is not using the correct display name when it returns // upcoming install activities. As a workaround, we'll prefer the display name from diff --git a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx index 112b3fef3cc..454ac3d89d1 100644 --- a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx +++ b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx @@ -15,7 +15,7 @@ import { IDropdownOption } from "interfaces/dropdownOption"; import { IHostSoftwarePackage, IHostAppStoreApp, - SoftwareInstallStatus, + SoftwareInstallUninstallStatus, IHostSoftwareWithUiStatus, } from "interfaces/software"; import { IconNames } from "components/icons"; @@ -35,7 +35,7 @@ interface IActionButtonState { export interface IGetActionButtonStateProps { hostScriptsEnabled: boolean; softwareId: number; - status: SoftwareInstallStatus | null; + status: SoftwareInstallUninstallStatus | null; softwarePackage: IHostSoftwarePackage | null; appStoreApp: IHostAppStoreApp | null; hostMDMEnrolled?: boolean; diff --git a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibrary.tsx b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibrary.tsx index d15bb0aa234..3bf4650cd51 100644 --- a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibrary.tsx +++ b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibrary.tsx @@ -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 SoftwareScriptDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal"; import VppInstallDetailsModal from "components/ActivityDetails/InstallDetails/VppInstallDetailsModal"; import SoftwareUninstallDetailsModal, { ISWUninstallDetailsParentState, @@ -150,6 +151,10 @@ const HostSoftwareLibrary = ({ selectedHostSWInstallDetails, setSelectedHostSWInstallDetails, ] = useState(null); + const [ + selectedHostSWScriptDetails, + setSelectedHostSWScriptDetails, + ] = useState(null); const [ selectedHostSWUninstallDetails, setSelectedHostSWUninstallDetails, @@ -377,6 +382,15 @@ const HostSoftwareLibrary = ({ [setSelectedHostSWInstallDetails] ); + const onSetSelectedHostSWScriptDetails = useCallback( + (hostSW?: IHostSoftware) => { + if (hostSW) { + setSelectedHostSWScriptDetails(hostSW); + } + }, + [setSelectedHostSWScriptDetails] + ); + const onSetSelectedHostSWUninstallDetails = useCallback( (uninstallDetails?: ISWUninstallDetailsParentState) => { if (uninstallDetails) { @@ -479,6 +493,7 @@ const HostSoftwareLibrary = ({ onShowInventoryVersions, onShowUpdateDetails, onSetSelectedHostSWInstallDetails, + onSetSelectedHostSWScriptDetails, onSetSelectedHostSWUninstallDetails, onSetSelectedVPPInstallDetails, onClickInstallAction, @@ -495,6 +510,7 @@ const HostSoftwareLibrary = ({ onShowInventoryVersions, onShowUpdateDetails, onSetSelectedHostSWInstallDetails, + onSetSelectedHostSWScriptDetails, onSetSelectedHostSWUninstallDetails, onSetSelectedVPPInstallDetails, onClickInstallAction, @@ -566,6 +582,18 @@ const HostSoftwareLibrary = ({ onCancel={() => setSelectedHostSWInstallDetails(null)} /> )} + {selectedHostSWScriptDetails && ( + setSelectedHostSWScriptDetails(null)} + /> + )} {selectedHostSWUninstallDetails && ( void; onShowUpdateDetails: (software?: IHostSoftware) => void; onSetSelectedHostSWInstallDetails: (details?: IHostSoftware) => void; + onSetSelectedHostSWScriptDetails: (details?: IHostSoftware) => void; onSetSelectedHostSWUninstallDetails: ( details?: ISWUninstallDetailsParentState ) => void; @@ -72,6 +73,7 @@ export const generateHostSWLibraryTableHeaders = ({ onShowInventoryVersions, onShowUpdateDetails, onSetSelectedHostSWInstallDetails, + onSetSelectedHostSWScriptDetails, onSetSelectedHostSWUninstallDetails, onSetSelectedVPPInstallDetails, onClickInstallAction, @@ -134,6 +136,7 @@ export const generateHostSWLibraryTableHeaders = ({ onShowInventoryVersions={onShowInventoryVersions} onShowUpdateDetails={onShowUpdateDetails} onShowInstallDetails={onSetSelectedHostSWInstallDetails} + onShowScriptDetails={onSetSelectedHostSWScriptDetails} onShowVPPInstallDetails={onSetSelectedVPPInstallDetails} onShowUninstallDetails={onSetSelectedHostSWUninstallDetails} isHostOnline={isHostOnline} diff --git a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tests.tsx b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tests.tsx index 4eada43992a..98fb7286e98 100644 --- a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tests.tsx +++ b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tests.tsx @@ -31,6 +31,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -68,6 +69,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -103,6 +105,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} isHostOnline @@ -137,6 +140,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -172,6 +176,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} isHostOnline @@ -211,6 +216,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -241,6 +247,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -274,6 +281,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -304,6 +312,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -334,6 +343,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -361,6 +371,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -390,6 +401,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} isHostOnline @@ -421,6 +433,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -451,6 +464,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -479,6 +493,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -511,6 +526,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -539,6 +555,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> @@ -570,6 +587,7 @@ describe("InstallStatusCell - component", () => { }} onShowUpdateDetails={noop} onShowInstallDetails={noop} + onShowScriptDetails={noop} onShowUninstallDetails={noop} onShowVPPInstallDetails={noop} /> diff --git a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx index 6eac9f75225..414bd69cd35 100644 --- a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx +++ b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx @@ -5,7 +5,7 @@ import { IHostSoftware, IHostSoftwareWithUiStatus, IHostSoftwareUiStatus, - SoftwareInstallStatus, + SoftwareInstallUninstallStatus, IVPPHostSoftware, SoftwareUninstallStatus, IAppLastInstall, @@ -28,7 +28,7 @@ const baseClass = "install-status-cell"; interface CommandUuid { command_uuid: string; software_title?: string; - status?: SoftwareInstallStatus; + status?: SoftwareInstallUninstallStatus; } interface InstallUuid { @@ -68,12 +68,15 @@ export const RECENT_SUCCESS_ACTION_MESSAGE = ( // Similar to SelfServiceTableConfig STATUS_CONFIG export const INSTALL_STATUS_DISPLAY_OPTIONS: Record< - Exclude, // Uninstalled/recently uninstalled is handled separately with empty cell + Exclude< + IHostSoftwareUiStatus, + "uninstalled" | "recently_uninstalled" | "never_ran_script" + >, // Uninstalled/recently uninstalled/ never ran script is handled separately with empty cell IStatusDisplayConfig > = { installed: { iconName: "success", - displayText: "Installed", + displayText: "Installed", // TODO: "Ran" for script packages tooltip: () => undefined, // No tooltip for installed state }, recently_updated: { @@ -107,7 +110,7 @@ export const INSTALL_STATUS_DISPLAY_OPTIONS: Record< pending_install: { iconName: "pending-outline", displayText: ({ isSelfService, isHostOnline }) => - isSelfService || isHostOnline ? "Installing..." : "Install (pending)", + isSelfService || isHostOnline ? "Installing..." : "Install (pending)", // TODO: "Running..." for script packages / "Run (pending)" tooltip: ({ isSelfService, isHostOnline }) => isSelfService || isHostOnline ? ( "Fleet is installing software." @@ -278,6 +281,57 @@ export const INSTALL_STATUS_DISPLAY_OPTIONS: Record< ), }, + // Script package statuses + ran_script: { + iconName: "success", + displayText: "Ran", + tooltip: ({ lastInstalledAt }) => + lastInstalledAt ? ( + <> + Payload-free software script ran (finished with exit code 0){" "} + {dateAgo(lastInstalledAt)}. + + ) : undefined, + }, + failed_script: { + iconName: "error", + displayText: "Failed run", + tooltip: ({ lastInstalledAt, isSelfService }) => ( + <> + Payload-free software script failed to run + {lastInstalledAt ? ` (${dateAgo(lastInstalledAt)})` : ""}.{" "} + {isSelfService ? ( + <> + Select Retry to run again, or contact your IT department. + + ) : ( + !lastInstalledAt && ( + <> + Select Details > Activity to view errors. + + ) + )} + + ), + }, + running_script: { + iconName: "pending-outline", + displayText: "Run (pending)", + tooltip: () => "Fleet is running the payload-free software script.", + }, + pending_script: { + iconName: "pending-outline", + displayText: "Run (pending)", + tooltip: ({ isSelfService, isHostOnline }) => + isSelfService || isHostOnline ? ( + "Fleet is running the payload-free software script." + ) : ( + <> + Fleet will run the payload-free software +
script when the host comes online. + + ), + }, }; type IInstallStatusCellProps = { @@ -285,6 +339,7 @@ type IInstallStatusCellProps = { onShowInventoryVersions?: (software: IHostSoftware) => void; onShowUpdateDetails: (software: IHostSoftware) => void; onShowInstallDetails: (hostSoftware: IHostSoftware) => void; + onShowScriptDetails: (hostSoftware: IHostSoftware) => void; onShowVPPInstallDetails: (s: IVPPHostSoftware) => void; onShowUninstallDetails: (details: ISWUninstallDetailsParentState) => void; isSelfService?: boolean; @@ -303,25 +358,35 @@ const resolveDisplayText = ( ? displayText({ isSelfService, isHostOnline }) : displayText; -const getEmptyCellTooltip = (isAppStoreApp: boolean, softwareName?: string) => - isAppStoreApp ? ( - <> - App Store app can be installed on the host.
- Select Actions > Install to install. - - ) : ( +const getEmptyCellTooltip = ( + isAppStoreApp: boolean, + isScriptPackage: boolean, + softwareName?: string +) => { + if (isAppStoreApp) { + return ( + <> + App Store app can be installed on the host.
+ Select Actions > Install to install. + + ); + } + + return ( <> - {softwareName ? {softwareName} : "Software"} can be installed on - the host. + {softwareName ? {softwareName} : "Software"} can be + {isScriptPackage ? "ran" : "installed"} on the host.
Select Actions > Install to install. ); +}; const InstallStatusCell = ({ software, onShowInventoryVersions, onShowUpdateDetails, onShowInstallDetails, + onShowScriptDetails, onShowVPPInstallDetails, onShowUninstallDetails, isSelfService = false, @@ -333,13 +398,14 @@ const InstallStatusCell = ({ const softwarePackageName = getSoftwarePackageName(software); // @RachelElysia I renamed this function and the variable name its return value is set to here because it is looking at the software_package.name, which has a suffix like ".pkg". software.name has the more human-readable version. Not sure how else this data is being used so I am not going to refactor anything. Please update if needed. const displayStatus = software.ui_status; - if (displayStatus === "uninstalled") { + if (displayStatus === "uninstalled" || displayStatus === "never_ran_script") { return ( @@ -358,6 +424,11 @@ const InstallStatusCell = ({ const displayConfig = INSTALL_STATUS_DISPLAY_OPTIONS[displayStatus]; + // This is only called for script packages (payload-free installers: .sh, .ps1) + const onClickScriptStatus = () => { + onShowScriptDetails(software); + }; + // This is never called for App Store app missing 'last_install' info for // successful and failed installs (Old clients <4.72 bug) See shouldOnClickBeDisabled const onClickInstallStatus = () => { @@ -423,6 +494,11 @@ const InstallStatusCell = ({ // Status groups and their click handlers const displayStatusConfig = [ + { + condition: true, // Allow click even if no last install to see details modal + statuses: ["Failed run", "Run (pending)", "Ran"], + onClick: onClickScriptStatus, + }, { condition: true, // Allow click even if no last install to see details modal statuses: ["Failed", "Install (pending)", "Installed"], diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tsx index 68d2fdfde93..99412120ccb 100644 --- a/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tsx +++ b/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tsx @@ -31,6 +31,7 @@ import SoftwareUninstallDetailsModal, { ISWUninstallDetailsParentState, } from "components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal"; import SoftwareInstallDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal"; +import SoftwareScriptDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal"; import { VppInstallDetailsModal } from "components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal"; import UpdatesCard from "./UpdatesCard/UpdatesCard"; @@ -139,6 +140,10 @@ const SoftwareSelfService = ({ selectedHostSWInstallDetails, setSelectedHostSWInstallDetails, ] = useState(undefined); + const [ + selectedHostSWScriptDetails, + setSelectedHostSWScriptDetails, + ] = useState(undefined); const [ selectedVPPInstallDetails, setSelectedVPPInstallDetails, @@ -466,6 +471,13 @@ const SoftwareSelfService = ({ [setSelectedHostSWInstallDetails] ); + const onShowScriptDetails = useCallback( + (hostSoftware?: IHostSoftware) => { + setSelectedHostSWScriptDetails(hostSoftware); + }, + [setSelectedHostSWScriptDetails] + ); + const onShowVPPInstallDetails = useCallback( (s: IVPPHostSoftware) => { setSelectedVPPInstallDetails(s); @@ -531,6 +543,7 @@ const SoftwareSelfService = ({ return generateSoftwareTableHeaders({ onShowUpdateDetails, onShowInstallDetails, + onShowScriptDetails, onShowVPPInstallDetails, onShowUninstallDetails, onClickInstallAction, @@ -540,6 +553,7 @@ const SoftwareSelfService = ({ }, [ onShowUpdateDetails, onShowInstallDetails, + onShowScriptDetails, onShowVPPInstallDetails, onShowUninstallDetails, onClickInstallAction, @@ -604,6 +618,21 @@ const SoftwareSelfService = ({ contactUrl={contactUrl} /> )} + {selectedHostSWScriptDetails && ( + setSelectedHostSWScriptDetails(undefined)} + deviceAuthToken={deviceToken} + contactUrl={contactUrl} + /> + )} {selectedVPPInstallDetails && ( void; onShowInstallDetails: (hostSoftware: IHostSoftware) => void; + onShowScriptDetails: (hostSoftware: IHostSoftware) => void; onShowVPPInstallDetails: (hostSoftware: IVPPHostSoftware) => void; onShowUninstallDetails: ( uninstallDetails: ISWUninstallDetailsParentState @@ -55,6 +56,7 @@ interface ISelfServiceTableHeaders { export const generateSoftwareTableHeaders = ({ onShowUpdateDetails, onShowInstallDetails, + onShowScriptDetails, onShowVPPInstallDetails, onShowUninstallDetails, onClickInstallAction, @@ -99,6 +101,7 @@ export const generateSoftwareTableHeaders = ({ software={cellProps.row.original} onShowUpdateDetails={onShowUpdateDetails} onShowInstallDetails={onShowInstallDetails} + onShowScriptDetails={onShowScriptDetails} onShowVPPInstallDetails={onShowVPPInstallDetails} onShowUninstallDetails={onShowUninstallDetails} isSelfService diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/UpdatesCard/UpdateSoftwareItem/UpdateSoftwareItem.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/UpdatesCard/UpdateSoftwareItem/UpdateSoftwareItem.tsx index 24f82b9257f..bbd77c2417f 100644 --- a/frontend/pages/hosts/details/cards/Software/SelfService/UpdatesCard/UpdateSoftwareItem/UpdateSoftwareItem.tsx +++ b/frontend/pages/hosts/details/cards/Software/SelfService/UpdatesCard/UpdateSoftwareItem/UpdateSoftwareItem.tsx @@ -8,7 +8,7 @@ import { IHostSoftware, IHostSoftwareWithUiStatus, ISoftwareLastInstall, - SoftwareInstallStatus, + SoftwareInstallUninstallStatus, } from "interfaces/software"; import { dateAgo } from "utilities/date_format"; @@ -31,7 +31,7 @@ const baseClass = "update-software-item"; const STATUS_CONFIG: Record< Exclude< - SoftwareInstallStatus, + SoftwareInstallUninstallStatus, "pending_uninstall" | "failed_uninstall" | "uninstalled" >, IStatusDisplayConfig diff --git a/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tsx b/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tsx index 4aee7649e7b..a1bd3465089 100644 --- a/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tsx +++ b/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tsx @@ -1,6 +1,9 @@ import React from "react"; -import { IHostSoftware, SoftwareInstallStatus } from "interfaces/software"; +import { + IHostSoftware, + SoftwareInstallUninstallStatus, +} from "interfaces/software"; import Button from "components/buttons/Button"; import Modal from "components/Modal"; import Icon from "components/Icon"; @@ -13,7 +16,7 @@ const baseClass = "software-update-modal"; interface IStatusMessageProps { hostDisplayName: string; isDeviceUser: boolean; - softwareStatus: SoftwareInstallStatus | null; + softwareStatus: SoftwareInstallUninstallStatus | null; softwareName: string; installerName: string; installerVersion?: string; diff --git a/frontend/pages/hosts/details/cards/Software/helpers.tests.ts b/frontend/pages/hosts/details/cards/Software/helpers.tests.ts index 10d3c7fb586..29109f2b162 100644 --- a/frontend/pages/hosts/details/cards/Software/helpers.tests.ts +++ b/frontend/pages/hosts/details/cards/Software/helpers.tests.ts @@ -325,6 +325,48 @@ describe("getUiStatus", () => { }); expect(getUiStatus(sw, true)).toBe("uninstalled"); }); + + describe("Script packages UI statuses", () => { + it("returns 'failed_run' when status is failed_install and isScriptPackage", () => { + const sw = createMockHostSoftware({ + status: "failed_install", + source: "sh_packages", + }); + expect(getUiStatus(sw, true)).toBe("failed_run"); + }); + + it("returns 'running_script' when status is pending_install, isScriptPackage and host online", () => { + const sw = createMockHostSoftware({ + status: "pending_install", + source: "sh_packages", + }); + expect(getUiStatus(sw, true)).toBe("running_script"); + }); + + it("returns 'pending_script' when status is pending_install, isScriptPackage and host offline", () => { + const sw = createMockHostSoftware({ + status: "pending_install", + source: "sh_packages", + }); + expect(getUiStatus(sw, false)).toBe("pending_script"); + }); + + it("returns 'ran_script' when status is installed and isScriptPackage", () => { + const sw = createMockHostSoftware({ + status: "installed", + source: "sh_packages", + }); + expect(getUiStatus(sw, true)).toBe("ran_script"); + }); + + it("returns 'never_ran_script' when status is null and isScriptPackage", () => { + const sw = createMockHostSoftware({ + status: null, + source: "sh_packages", + }); + expect(getUiStatus(sw, true)).toBe("never_ran_script"); + }); + }); }); describe("getSoftwareSubheader", () => { diff --git a/frontend/pages/hosts/details/cards/Software/helpers.tsx b/frontend/pages/hosts/details/cards/Software/helpers.tsx index e0a91a604ab..b6f2b8bfd40 100644 --- a/frontend/pages/hosts/details/cards/Software/helpers.tsx +++ b/frontend/pages/hosts/details/cards/Software/helpers.tsx @@ -7,6 +7,7 @@ import { IHostSoftware, IHostSoftwareUiStatus, IHostSoftwareWithUiStatus, + ScriptPackage, } from "interfaces/software"; import { IconNames } from "components/icons"; import { @@ -172,11 +173,26 @@ export const getUiStatus = ( isHostOnline: boolean, hostSoftwareUpdatedAt?: string | null ): IHostSoftwareUiStatus => { - const { status, installed_versions } = software; + const { status, installed_versions, source } = software; const lastInstallDate = getLastInstall(software)?.installed_at; const lastUninstallDate = getLastUninstall(software)?.uninstalled_at; const installerVersion = getInstallerVersion(software); + const isScriptPackage = ScriptPackage.includes(source); + + // 0. Script Packages states + if (isScriptPackage) { + if (status === "failed_install") { + return "failed_script"; + } + if (status === "pending_install") { + return isHostOnline ? "running_script" : "pending_script"; + } + if (status === "installed") { + return "ran_script"; + } + return "never_ran_script"; + } // 1. Failed install states if (status === "failed_install") { @@ -299,12 +315,23 @@ interface IButtonConfig { /** Display text and icon are shared across self-service and * host details > library action buttons */ +// TODO: "Retry" and "Reinstall" should be "Rerun" for script packages export const getInstallerActionButtonConfig = ( type: ButtonType, status: IHostSoftwareUiStatus ): IButtonConfig => { if (type === "install") { switch (status) { + // Script statuses + case "failed_script": + return { text: "Retry", icon: "refresh" }; + case "ran_script": + return { text: "Rerun", icon: "refresh" }; + case "running_script": + case "pending_script": + case "never_ran_script": + return { text: "Run", icon: "install" }; + // Normal install statuses case "failed_install": case "failed_install_update_available": return { text: "Retry", icon: "refresh" }; @@ -338,17 +365,22 @@ export const getInstallerActionButtonConfig = ( const INSTALL_STATUS_SORT_ORDER: IHostSoftwareUiStatus[] = [ "failed_install", // Failed "failed_install_update_available", // Failed install with update available + "failed_script", // Failed to run (for script packages) "failed_uninstall", // Failed uninstall "failed_uninstall_update_available", // Failed uninstall with update available "update_available", // Update available "updating", // Updating... "pending_update", // Update (pending) + "running_script", // Running... (for script packages) + "pending_script", // Run (pending) (for script packages) "installing", // Installing... "pending_install", // Install (pending) "uninstalling", // Uninstalling... "pending_uninstall", // Uninstall (pending) + "ran_script", // Ran (for script packages) "installed", // Installed "uninstalled", // Empty (---) + "never_ran_script", // Empty (---) for script packages ]; /** Status column custom sortType */ diff --git a/frontend/utilities/file/fileUtils.ts b/frontend/utilities/file/fileUtils.ts index 77089429eca..db7006347c6 100644 --- a/frontend/utilities/file/fileUtils.ts +++ b/frontend/utilities/file/fileUtils.ts @@ -15,6 +15,8 @@ export const FILE_EXTENSIONS_TO_PLATFORM_DISPLAY_NAME: Record< deb: "Linux", rpm: "Linux", "tar.gz": "Linux", + sh: "Linux", + ps1: "Windows", }; /** Currently only using tar.gz, but keeping the others for future use From b93dfa13919f0f9ef0572deb0b7575c14d20adb3 Mon Sep 17 00:00:00 2001 From: RachelElysia Date: Wed, 8 Oct 2025 16:44:18 -0400 Subject: [PATCH 2/9] Update to api returned status for modal nuance --- .../SoftwareScriptDetailsModal.tests.tsx | 8 +++++--- .../SoftwareScriptDetailsModal.tsx | 10 +++++----- .../hosts/details/cards/Software/helpers.tests.ts | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx index ed1b9844507..271c653375c 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx @@ -126,7 +126,7 @@ describe("SoftwareScriptDetailsModal - ModalButtons component", () => { const { user } = renderWithSetup( { it("shows Done button for pending run", () => { const onCancel = jest.fn(); - render(); + render( + + ); expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument(); expect( screen.queryByRole("button", { name: "Rerun" }) @@ -157,7 +159,7 @@ describe("SoftwareScriptDetailsModal - ModalButtons component", () => { render( ); diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx index e1a2db59bd5..08cce823339 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx @@ -96,7 +96,7 @@ export const StatusMessage = ({ let middle = null; if (isDUP) { if (status === "failed_install") { - middle = <>. You can retry{renderContactOption(contactUrl)}; + middle = <>. You can rerun{renderContactOption(contactUrl)}; } } else { // host details page @@ -133,7 +133,7 @@ export const StatusMessage = ({ interface IModalButtonsProps { deviceAuthToken?: string; - status?: string; + installResultStatus?: string; hostSoftwareId?: number; onRerun?: (id: number) => void; onCancel: () => void; @@ -141,12 +141,12 @@ interface IModalButtonsProps { export const ModalButtons = ({ deviceAuthToken, - status, + installResultStatus, hostSoftwareId, onRerun, onCancel, }: IModalButtonsProps) => { - if (deviceAuthToken && status === "failed_install") { + if (!!deviceAuthToken && installResultStatus === "failed_install") { const onClickRerun = () => { // on DUP, where this is relevant, both will be defined if (onRerun && hostSoftwareId) { @@ -327,7 +327,7 @@ export const SoftwareInstallDetailsModal = ({ {renderContent()} { }); describe("Script packages UI statuses", () => { - it("returns 'failed_run' when status is failed_install and isScriptPackage", () => { + it("returns 'failed_script' when status is failed_install and isScriptPackage", () => { const sw = createMockHostSoftware({ status: "failed_install", source: "sh_packages", }); - expect(getUiStatus(sw, true)).toBe("failed_run"); + expect(getUiStatus(sw, true)).toBe("failed_script"); }); it("returns 'running_script' when status is pending_install, isScriptPackage and host online", () => { From df02852ed69272ec2c2f8ac094cb6ce6c895b555 Mon Sep 17 00:00:00 2001 From: RachelElysia Date: Thu, 9 Oct 2025 11:00:49 -0400 Subject: [PATCH 3/9] Lots of UI changes to ran/running/will run --- .../SoftwareInstallDetailsModal.tsx | 8 +- .../SoftwareScriptDetailsModal.tests.tsx | 10 +-- .../SoftwareScriptDetailsModal.tsx | 49 ++++++------ .../SoftwareUninstallDetailsModal.tsx | 4 +- .../VppInstallDetailsModal.tsx | 14 ++-- .../InstallDetails/constants.ts | 13 +-- frontend/interfaces/activity.ts | 2 + frontend/interfaces/setup.ts | 6 +- frontend/interfaces/software.ts | 80 ++++++++++++++----- .../cards/ActivityFeed/ActivityFeed.tsx | 4 +- .../GlobalActivityItem.tests.tsx | 54 +++++++++++++ .../GlobalActivityItem/GlobalActivityItem.tsx | 13 ++- .../SoftwareSummaryCard.tsx | 6 +- .../SoftwareTitleDetailsPage/helpers.ts | 5 +- .../SoftwareTitlesTableConfig.tsx | 4 +- .../SoftwareDetailsSummary.tsx | 4 +- .../HostDetailsPage/HostDetailsPage.tsx | 4 +- .../InstalledSoftwareActivityItem.tsx | 13 ++- .../HostInstallerActionCell.tsx | 4 +- .../details/cards/Software/HostSoftware.tsx | 2 +- .../InstallStatusCell/InstallStatusCell.tsx | 4 +- .../UpdateSoftwareItem/UpdateSoftwareItem.tsx | 10 +-- .../SoftwareUpdateModal.tsx | 4 +- .../hosts/details/cards/Software/helpers.tsx | 4 +- 24 files changed, 220 insertions(+), 101 deletions(-) diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx index d7c490c53c9..6b125ed5655 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx @@ -165,7 +165,7 @@ export const ModalButtons = ({ }: IModalButtonsProps) => { if (deviceAuthToken && status === "failed_install") { const onClickRetry = () => { - // on DUP, where this is relevant, both will be defined + // on My Device Page, where this is relevant, both will be defined if (onRetry && hostSoftwareId) { onRetry(hostSoftwareId); } @@ -199,10 +199,10 @@ interface ISoftwareInstallDetailsProps { necessary in the details prop */ details: IPackageInstallDetails; hostSoftware?: IHostSoftware; // for inventory versions, and software name when not Fleet installed (not present on activity feeds) - deviceAuthToken?: string; // DUP only + deviceAuthToken?: string; // My Device Page only onCancel: () => void; - onRetry?: (id: number) => void; // DUP only - contactUrl?: string; // DUP only + onRetry?: (id: number) => void; // My Device Page only + contactUrl?: string; // My Device Page only } export const SoftwareInstallDetailsModal = ({ diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx index 271c653375c..78bd0d9ce59 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx @@ -15,7 +15,7 @@ describe("SoftwareScriptDetailsModal - StatusMessage component", () => { status: "pending_install", }) as ISoftwareScriptResult } - isDUP={false} + isMyDevicePage={false} /> ); @@ -35,7 +35,7 @@ describe("SoftwareScriptDetailsModal - StatusMessage component", () => { status: "failed_install", }) as ISoftwareScriptResult } - isDUP + isMyDevicePage contactUrl="http://support" /> ); @@ -60,7 +60,7 @@ describe("SoftwareScriptDetailsModal - StatusMessage component", () => { status: "failed_install", }) as ISoftwareScriptResult } - isDUP + isMyDevicePage /> ); @@ -85,7 +85,7 @@ describe("SoftwareScriptDetailsModal - StatusMessage component", () => { status: "failed_install", }) as ISoftwareScriptResult } - isDUP={false} + isMyDevicePage={false} contactUrl="http://support" /> ); @@ -105,7 +105,7 @@ describe("SoftwareScriptDetailsModal - StatusMessage component", () => { status: "installed", }) as ISoftwareScriptResult } - isDUP={false} + isMyDevicePage={false} /> ); diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx index 08cce823339..3f452b67584 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx @@ -51,7 +51,7 @@ export const renderContactOption = (url?: string) => ( interface IInstallStatusMessage { softwareName: string; installResult: ISoftwareScriptResult; - isDUP: boolean; + isMyDevicePage: boolean; contactUrl?: string; } @@ -60,7 +60,7 @@ interface IInstallStatusMessage { export const StatusMessage = ({ softwareName, installResult, - isDUP, + isMyDevicePage, contactUrl, }: IInstallStatusMessage) => { const { @@ -93,22 +93,24 @@ export const StatusMessage = ({ Fleet {getScriptDetailsStatusPredicate(status)} {software_title} ); - let middle = null; - if (isDUP) { - if (status === "failed_install") { - middle = <>. You can rerun{renderContactOption(contactUrl)}; - } - } else { - // host details page - middle = ( - <> - {" "} - ({software_package}) on {formattedHost} - {status === "pending_install" ? " when it comes online" : ""} - {displayTimeStamp} - - ); - } + + const middle = isMyDevicePage ? ( + <> + {" "} + {displayTimeStamp} + {status === "failed_install" && ( + <>. You can rerun{renderContactOption(contactUrl)} + )} + + ) : ( + <> + {" "} + ({software_package}) on {formattedHost} + {status === "pending_install" + ? " when it comes online" + : displayTimeStamp} + + ); return ( {prefix} @@ -148,7 +150,7 @@ export const ModalButtons = ({ }: IModalButtonsProps) => { if (!!deviceAuthToken && installResultStatus === "failed_install") { const onClickRerun = () => { - // on DUP, where this is relevant, both will be defined + // on My Device Page, where this is relevant, both will be defined if (onRerun && hostSoftwareId) { onRerun(hostSoftwareId); } @@ -182,10 +184,10 @@ interface ISoftwareInstallDetailsProps { necessary in the details prop */ details: IPackageInstallDetails; hostSoftware?: IHostSoftware; // for inventory versions, and software name when not Fleet installed (not present on activity feeds) - deviceAuthToken?: string; // DUP only + deviceAuthToken?: string; // My Device Page only onCancel: () => void; - onRerun?: (id: number) => void; // DUP only - contactUrl?: string; // DUP only + onRerun?: (id: number) => void; // My Device Page only + contactUrl?: string; // My Device Page only } export const SoftwareInstallDetailsModal = ({ @@ -308,9 +310,10 @@ export const SoftwareInstallDetailsModal = ({ + {renderScriptDetailsSection()}
); } diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx index 31fdd95e393..d59b9a3004c 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx @@ -149,7 +149,7 @@ export interface ISWUninstallDetailsParentState { /** Optional since may come from dedicated state, may come from elsewhere */ hostDisplayName?: string; - /** Optional since DUP only */ + /** Optional since My Device Page only */ hostSoftware?: IHostSoftwareWithUiStatus; // UI status not necessary in this modal, but type aligns with onRetry argument } export interface ISoftwareUninstallDetailsModalProps { @@ -160,7 +160,7 @@ export interface ISoftwareUninstallDetailsModalProps { onCancel: () => void; softwarePackageName?: string; - /** DUP only */ + /** My Device Page only */ onRetry?: (s: IHostSoftwareWithUiStatus) => void; hostSoftware?: IHostSoftwareWithUiStatus; // UI status not necessary in this modal, but type aligns with onRetry argument deviceAuthToken?: string; diff --git a/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx index 0384be11f45..e66a767b024 100644 --- a/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx @@ -10,7 +10,7 @@ import deviceUserAPI, { import { IHostSoftware, - SoftwareInstallUninstallStatus, + SoftwareInstallUninstallApiStatus, } from "interfaces/software"; import { IMdmCommandResult } from "interfaces/mdm"; @@ -35,7 +35,7 @@ interface IGetStatusMessageProps { isMyDevicePage?: boolean; /** "pending" is an edge case here where VPP install activities that were added to the feed prior to v4.57 * (when we split pending into pending_install/pending_uninstall) will list the status as "pending" rather than "pending_install" */ - displayStatus: SoftwareInstallUninstallStatus | "pending"; + displayStatus: SoftwareInstallUninstallApiStatus | "pending"; isMDMStatusNotNow: boolean; isMDMStatusAcknowledged: boolean; appName: string; @@ -152,7 +152,7 @@ export const getStatusMessage = ({ }; interface IModalButtonsProps { - displayStatus: SoftwareInstallUninstallStatus | "pending"; + displayStatus: SoftwareInstallUninstallApiStatus | "pending"; deviceAuthToken?: string; onCancel: () => void; onRetry?: (id: number) => void; @@ -167,7 +167,7 @@ export const ModalButtons = ({ hostSoftwareId, }: IModalButtonsProps) => { const onClickRetry = () => { - // on DUP, where this is relevant, both will be defined + // on My Device Page, where this is relevant, both will be defined if (onRetry && hostSoftwareId) { onRetry(hostSoftwareId); } @@ -199,7 +199,7 @@ const baseClass = "vpp-install-details-modal"; export type IVppInstallDetails = { /** Status: null when a host manually installed not using Fleet */ - fleetInstallStatus: SoftwareInstallUninstallStatus | null; + fleetInstallStatus: SoftwareInstallUninstallApiStatus | null; hostDisplayName: string; appName: string; commandUuid?: string; @@ -209,10 +209,10 @@ interface IVPPInstallDetailsModalProps { details: IVppInstallDetails; /** for inventory versions, not present on activity feeds */ hostSoftware?: IHostSoftware; - /** DUP only */ + /** My Device Page only */ deviceAuthToken?: string; onCancel: () => void; - /** DUP only */ + /** My Device Page only */ onRetry?: (id: number) => void; } export const VppInstallDetailsModal = ({ diff --git a/frontend/components/ActivityDetails/InstallDetails/constants.ts b/frontend/components/ActivityDetails/InstallDetails/constants.ts index 49f6bf63b0f..044aac2f757 100644 --- a/frontend/components/ActivityDetails/InstallDetails/constants.ts +++ b/frontend/components/ActivityDetails/InstallDetails/constants.ts @@ -1,14 +1,14 @@ import { IconNames } from "components/icons"; import { - SoftwareInstallUninstallStatus, + SoftwareInstallUninstallApiStatus, + SoftwareInstallUninstallUiStatus, SoftwareInstallStatus, - SoftwareUninstallStatus, } from "interfaces/software"; // Install/Uninstall helpers export const INSTALL_DETAILS_STATUS_ICONS: Record< - SoftwareInstallUninstallStatus | SoftwareUninstallStatus, // former is superset of latter, latter included in union for type system + SoftwareInstallUninstallApiStatus, // former is superset of latter, latter included in union for type system IconNames > = { pending_install: "pending-outline", @@ -20,7 +20,7 @@ export const INSTALL_DETAILS_STATUS_ICONS: Record< } as const; const INSTALL_DETAILS_STATUS_PREDICATES: Record< - SoftwareInstallUninstallStatus | SoftwareUninstallStatus, // former is superset of latter, latter included in union for type system + SoftwareInstallUninstallUiStatus, string > = { pending_install: "is installing or will install", @@ -29,6 +29,9 @@ const INSTALL_DETAILS_STATUS_PREDICATES: Record< failed_install: "failed to install", pending_uninstall: "is uninstalling or will uninstall", failed_uninstall: "failed to uninstall", + pending_script: "is running or will run", + failed_script: "failed to run", + ran_script: "ran", } as const; export const getInstallDetailsStatusPredicate = ( @@ -39,7 +42,7 @@ export const getInstallDetailsStatusPredicate = ( } return ( INSTALL_DETAILS_STATUS_PREDICATES[ - status.toLowerCase() as SoftwareInstallUninstallStatus + status.toLowerCase() as SoftwareInstallUninstallUiStatus ] || INSTALL_DETAILS_STATUS_PREDICATES.pending_install ); }; diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index 5ac77e5b10f..2f9eb34dab5 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -3,6 +3,7 @@ import { Platform } from "./platform"; import { IPolicy } from "./policy"; import { IQuery } from "./query"; import { ISchedulableQueryStats } from "./schedulable_query"; +import { SoftwareSource } from "./software"; import { ITeamSummary } from "./team"; import { UserRole } from "./user"; @@ -235,6 +236,7 @@ export interface IActivityDetails { software_package?: string; software_title_id?: number; software_title?: string; + source?: SoftwareSource; specs?: IQuery[] | IPolicy[]; stats?: ISchedulableQueryStats; status?: string; diff --git a/frontend/interfaces/setup.ts b/frontend/interfaces/setup.ts index 9422cd33868..381727505f9 100644 --- a/frontend/interfaces/setup.ts +++ b/frontend/interfaces/setup.ts @@ -8,7 +8,11 @@ export const SETUP_STEP_STATUSES = [ export type SetupStepStatus = typeof SETUP_STEP_STATUSES[number]; -export const SETUP_STEP_TYPES = ["software_install", "script_run"]; +export const SETUP_STEP_TYPES = [ + "software_install", + "script_run", + "software_script_run", +]; export type SetupStepType = typeof SETUP_STEP_TYPES[number]; diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts index 17c6c3d711e..c8391bb5ab3 100644 --- a/frontend/interfaces/software.ts +++ b/frontend/interfaces/software.ts @@ -254,18 +254,17 @@ export const INSTALLABLE_SOURCE_PLATFORM_CONVERSION = { chocolatey_packages: "windows", pkg_packages: "darwin", vscode_extensions: null, - sh_packages: "linux", + sh_packages: "linux", // 4.76 Added support for Linux hosts only ps1_packages: "windows", } as const; -export const NoVersionOrHostDataAvailable = [ +export const SCRIPT_PACKAGE_SOURCES = ["sh_packages", "ps1_packages"]; + +export const NO_VERSION_OR_HOST_DATA_AVAIL_SOURCES = [ "tgz_packages", - "sh_packages", - "ps1_packages", + ...SCRIPT_PACKAGE_SOURCES, ]; -export const ScriptPackage = ["sh_packages", "ps1_packages"]; - export type InstallableSoftwareSource = keyof typeof INSTALLABLE_SOURCE_PLATFORM_CONVERSION; const EXTENSION_FOR_TYPE_CONVERSION = { @@ -325,24 +324,44 @@ export const SOFTWARE_INSTALL_STATUSES = [ "failed_install", ] as const; +// Payload-free (script) software statuses +export const SOFTWARE_SCRIPT_STATUSES = [ + "ran_script", + "pending_script", + "failed_script", +] as const; + export type SoftwareInstallStatus = typeof SOFTWARE_INSTALL_STATUSES[number]; -export const SOFTWARE_INSTALL_UNINSTALL_STATUSES = [ +export const SOFTWARE_INSTALL_UNINSTALL_API_STATUSES = [ + ...SOFTWARE_INSTALL_STATUSES, + ...SOFTWARE_UNINSTALL_STATUSES, + // Payload-free (script) software statuses use API's SOFTWARE_INSTALL_STATUSES +] as const; + +/* + * SoftwareInstallUninstallApiStatus represents the possible states of software install operations. + */ +export type SoftwareInstallUninstallApiStatus = typeof SOFTWARE_INSTALL_UNINSTALL_API_STATUSES[number]; + +/** Include payload-free statuses */ +export const SOFTWARE_INSTALL_UNINSTALL_UI_STATUSES = [ ...SOFTWARE_INSTALL_STATUSES, ...SOFTWARE_UNINSTALL_STATUSES, + ...SOFTWARE_SCRIPT_STATUSES, // Payload-free (script) software ] as const; /* - * SoftwareInstallUninstallStatus represents the possible states of software install operations. + * SoftwareInstallUninstallUiStatus represents the possible states of software install operations including payload-free. */ -export type SoftwareInstallUninstallStatus = typeof SOFTWARE_INSTALL_UNINSTALL_STATUSES[number]; +export type SoftwareInstallUninstallUiStatus = typeof SOFTWARE_INSTALL_UNINSTALL_UI_STATUSES[number]; export const isValidSoftwareInstallUninstallStatus = ( s: string | undefined | null -): s is SoftwareInstallUninstallStatus => +): s is SoftwareInstallUninstallUiStatus => !!s && - SOFTWARE_INSTALL_UNINSTALL_STATUSES.includes( - s as SoftwareInstallUninstallStatus + SOFTWARE_INSTALL_UNINSTALL_UI_STATUSES.includes( + s as SoftwareInstallUninstallUiStatus ); export const SOFTWARE_AGGREGATE_STATUSES = [ @@ -397,7 +416,7 @@ export interface ISoftwareInstallResult { software_title_id: number; software_package: string; host_id: number; - status: SoftwareInstallUninstallStatus; + status: SoftwareInstallUninstallApiStatus; detail: string; output: string; pre_install_query_output: string; @@ -479,7 +498,7 @@ export interface IHostSoftware { source: SoftwareSource; extension_for?: SoftwareExtensionFor; bundle_identifier?: string; - status: Exclude | null; + status: Exclude | null; installed_versions: ISoftwareInstallVersion[] | null; } @@ -545,7 +564,7 @@ export type IDeviceSoftware = IHostSoftware; export type IDeviceSoftwareWithUiStatus = IHostSoftwareWithUiStatus; const INSTALL_STATUS_PREDICATES: Record< - SoftwareInstallUninstallStatus | "pending", + SoftwareInstallUninstallUiStatus | "pending", string > = { pending: "pending", @@ -555,15 +574,37 @@ const INSTALL_STATUS_PREDICATES: Record< failed_install: "failed to install", pending_uninstall: "told Fleet to uninstall", failed_uninstall: "failed to uninstall", + ran_script: "ran", // Payload-free (script) software + failed_script: "failed to run", // Payload-free (script) software + pending_script: "told Fleet to run", // Payload-free (script) software } as const; -export const getInstallStatusPredicate = (status: string | undefined) => { +export const getInstallUninstallStatusPredicate = ( + status: string | undefined, + isScriptPackage = false +) => { if (!status) { return INSTALL_STATUS_PREDICATES.pending; } + + // If it is a script package, map install statuses to script-specific predicates + if (isScriptPackage) { + switch (status.toLowerCase()) { + case "installed": + return INSTALL_STATUS_PREDICATES.ran_script; + case "pending_install": + return INSTALL_STATUS_PREDICATES.pending_script; + case "failed_install": + return INSTALL_STATUS_PREDICATES.failed_script; + default: + break; + } + } + + // For all other cases, return the matching predicate or default to pending return ( INSTALL_STATUS_PREDICATES[ - status.toLowerCase() as SoftwareInstallUninstallStatus + status.toLowerCase() as keyof typeof INSTALL_STATUS_PREDICATES ] || INSTALL_STATUS_PREDICATES.pending ); }; @@ -577,7 +618,7 @@ export const aggregateInstallStatusCounts = ( }); export const INSTALL_STATUS_ICONS: Record< - SoftwareInstallUninstallStatus | "pending" | "failed", + SoftwareInstallUninstallUiStatus | "pending" | "failed", IconNames > = { pending: "pending-outline", @@ -588,6 +629,9 @@ export const INSTALL_STATUS_ICONS: Record< failed_install: "error-outline", pending_uninstall: "pending-outline", failed_uninstall: "error-outline", + ran_script: "success-outline", // Payload-free (script) software + failed_script: "error-outline", // Payload-free (script) software + pending_script: "pending-outline", // Payload-free (script) software } as const; type IHostSoftwarePackageWithLastInstall = IHostSoftwarePackage & { diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx index 9a460388b0f..c13a9828d5f 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx @@ -11,7 +11,7 @@ import activitiesAPI, { import { resolveUninstallStatus, - SoftwareInstallUninstallStatus, + SoftwareInstallUninstallApiStatus, } from "interfaces/software"; import { ActivityType, IActivityDetails } from "interfaces/activity"; @@ -268,7 +268,7 @@ const ActivityFeed = ({ details={{ appName: vppInstallDetails.software_title || "", fleetInstallStatus: (vppInstallDetails.status || - "pending_install") as SoftwareInstallUninstallStatus, + "pending_install") as SoftwareInstallUninstallApiStatus, hostDisplayName: vppInstallDetails.host_display_name || "", commandUuid: vppInstallDetails.command_uuid || "", }} diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx index b01219b352e..1b7f1a15cc6 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx @@ -1398,6 +1398,60 @@ describe("Activity Feed", () => { expect(screen.getByText("An end user")).toBeInTheDocument(); }); + it("renders script package ran status in InstalledSoftware activity", () => { + const activity = createMockActivity({ + type: ActivityType.InstalledSoftware, + actor_full_name: "Script Admin", + details: { + software_title: "Payload-free Script", + source: "sh_packages", + status: "installed", + software_package: "myscript.sh", + host_display_name: "Example Host", + }, + }); + + render(); + expect(screen.getByText(/ran/i)).toBeInTheDocument(); // For status: "installed" + expect(screen.getByText("Payload-free Script")).toBeInTheDocument(); + }); + + it("renders script package pending run status in InstalledSoftware activity", () => { + const activity = createMockActivity({ + type: ActivityType.InstalledSoftware, + actor_full_name: "Script Admin", + details: { + software_title: "Payload-free Script", + source: "sh_packages", + status: "pending_install", + software_package: "myscript.sh", + host_display_name: "Example Host", + }, + }); + + render(); + expect(screen.getByText(/told Fleet to run/i)).toBeInTheDocument(); // For status: "pending_install" + expect(screen.getByText("Payload-free Script")).toBeInTheDocument(); + }); + + it("renders script package failed run status in InstalledSoftware activity", () => { + const activity = createMockActivity({ + type: ActivityType.InstalledSoftware, + actor_full_name: "Script Admin", + details: { + software_title: "Payload-free Script", + source: "ps1_packages", // Other script package source + status: "failed_install", + software_package: "myscript.ps1", + host_display_name: "Example Host", + }, + }); + + render(); + expect(screen.getByText(/failed to run/i)).toBeInTheDocument(); // For status: "failed_install" + expect(screen.getByText("Payload-free Script")).toBeInTheDocument(); + }); + it("renders addedNdesScepProxy activity correctly", () => { const activity = createMockActivity({ type: ActivityType.AddedNdesScepProxy, diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx index 307df022d68..c6924cbbd2a 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx @@ -8,7 +8,10 @@ import { isIPadOrIPhone, PLATFORM_DISPLAY_NAMES, } from "interfaces/platform"; -import { getInstallStatusPredicate } from "interfaces/software"; +import { + getInstallUninstallStatusPredicate, + SCRIPT_PACKAGE_SOURCES, +} from "interfaces/software"; import { formatScriptNameForActivityItem, getPerformanceImpactDescription, @@ -1136,16 +1139,18 @@ const TAGGED_TEMPLATES = { host_display_name: hostName, software_title: title, status, + source, } = details; const showSoftwarePackage = !!details.software_package && activity.type === ActivityType.InstalledSoftware; - + const isScriptPackageSource = SCRIPT_PACKAGE_SOURCES.includes(source || ""); return ( <> {" "} - {getInstallStatusPredicate(status)} {title} + {getInstallUninstallStatusPredicate(status, isScriptPackageSource)}{" "} + {title} {showSoftwarePackage && ` (${details.software_package})`} on{" "} {hostName}. @@ -1168,7 +1173,7 @@ const TAGGED_TEMPLATES = { return ( <> {" "} - {getInstallStatusPredicate(status)} software {title} + {getInstallUninstallStatusPredicate(status)} software {title} {showSoftwarePackage && ` (${details.software_package})`} from{" "} {hostName}. diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx index 791e29a5332..6aa4c160ce3 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx @@ -12,7 +12,7 @@ import { isSoftwarePackage, ISoftwarePackage, IAppStoreApp, - NoVersionOrHostDataAvailable, + NO_VERSION_OR_HOST_DATA_AVAIL_SOURCES, } from "interfaces/software"; import Card from "components/Card"; @@ -54,7 +54,9 @@ const SoftwareSummaryCard = ({ const [iconUploadedAt, setIconUploadedAt] = useState(""); // Hide versions table for tgz_packages, sh_packages, & ps1_packages only - const showVersionsTable = !NoVersionOrHostDataAvailable.includes(source); + const showVersionsTable = !NO_VERSION_OR_HOST_DATA_AVAIL_SOURCES.includes( + source + ); const hasEditPermissions = isGlobalAdmin || isGlobalMaintainer || isTeamMaintainerOrTeamAdmin; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.ts index 1bafc9f8d6d..0013e9c5af2 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.ts +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.ts @@ -3,7 +3,7 @@ import { ISoftwareTitleDetails, isSoftwarePackage, aggregateInstallStatusCounts, - ScriptPackage, + SCRIPT_PACKAGE_SOURCES, } from "interfaces/software"; /** @@ -38,6 +38,7 @@ export const getInstallerCardInfo = (softwareTitle: ISoftwareTitleDetails) => { ? aggregateInstallStatusCounts(installerData.status) : installerData.status, isSelfService: installerData.self_service, - isScriptPackage: ScriptPackage.includes(softwareTitle.source) || false, + isScriptPackage: + SCRIPT_PACKAGE_SOURCES.includes(softwareTitle.source) || false, }; }; diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx index ca4339407c3..76b38bd51f6 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx @@ -4,7 +4,7 @@ import { InjectedRouter } from "react-router"; import { ISoftwareTitle, - NoVersionOrHostDataAvailable, + NO_VERSION_OR_HOST_DATA_AVAIL_SOURCES, formatSoftwareType, isIpadOrIphoneSoftwareSource, } from "interfaces/software"; @@ -189,7 +189,7 @@ const generateTableHeaders = ( Cell: (cellProps: IViewAllHostsLinkProps) => { const { source } = cellProps.row.original; - const hostCountNotSupported = NoVersionOrHostDataAvailable.includes( + const hostCountNotSupported = NO_VERSION_OR_HOST_DATA_AVAIL_SOURCES.includes( source ); diff --git a/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx b/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx index 394429d1b0a..e49fe99a121 100644 --- a/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx +++ b/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx @@ -8,7 +8,7 @@ import React from "react"; import { getPathWithQueryParams, QueryParams } from "utilities/url"; import paths from "router/paths"; -import { NoVersionOrHostDataAvailable } from "interfaces/software"; +import { NO_VERSION_OR_HOST_DATA_AVAIL_SOURCES } from "interfaces/software"; import DataSet from "components/DataSet"; import LastUpdatedHostCount from "components/LastUpdatedHostCount"; @@ -69,7 +69,7 @@ const SoftwareDetailsSummary = ({ // or if viewing details summary from edit icon preview modal const showHostCount = source && - !NoVersionOrHostDataAvailable.includes(source) && + !NO_VERSION_OR_HOST_DATA_AVAIL_SOURCES.includes(source) && iconPreviewUrl === undefined; const renderSoftwareIcon = () => { diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 5a8a56a3bfc..20a6a22edf1 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -34,7 +34,7 @@ import { IQueryStats } from "interfaces/query_stats"; import { IHostSoftware, resolveUninstallStatus, - SoftwareInstallUninstallStatus, + SoftwareInstallUninstallApiStatus, } from "interfaces/software"; import { ITeam } from "interfaces/team"; import { ActivityType, IHostUpcomingActivity } from "interfaces/activity"; @@ -704,7 +704,7 @@ const HostDetailsPage = ({ setActivityVPPInstallDetails({ appName: details?.software_title || "", fleetInstallStatus: (details?.status || - "pending_install") as SoftwareInstallUninstallStatus, + "pending_install") as SoftwareInstallUninstallApiStatus, commandUuid: details?.command_uuid || "", // FIXME: It seems like the backend is not using the correct display name when it returns // upcoming install activities. As a workaround, we'll prefer the display name from diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/InstalledSoftwareActivityItem/InstalledSoftwareActivityItem.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/InstalledSoftwareActivityItem/InstalledSoftwareActivityItem.tsx index 54db8e7f948..12a5eabaced 100644 --- a/frontend/pages/hosts/details/cards/Activity/ActivityItems/InstalledSoftwareActivityItem/InstalledSoftwareActivityItem.tsx +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/InstalledSoftwareActivityItem/InstalledSoftwareActivityItem.tsx @@ -1,6 +1,9 @@ import React from "react"; -import { getInstallStatusPredicate } from "interfaces/software"; +import { + getInstallUninstallStatusPredicate, + SCRIPT_PACKAGE_SOURCES, +} from "interfaces/software"; import ActivityItem from "components/ActivityItem"; @@ -17,9 +20,10 @@ const InstalledSoftwareActivityItem = ({ isSoloActivity, }: IHostActivityItemComponentPropsWithShowDetails) => { const { actor_full_name: actorName, details } = activity; - const { self_service, software_title: title } = details; + const { self_service, software_title: title, source } = details; const status = details.status === "failed" ? "failed_uninstall" : details.status; + const isScriptPackageSource = SCRIPT_PACKAGE_SOURCES.includes(source || ""); const actorDisplayName = self_service ? ( End user @@ -27,7 +31,10 @@ const InstalledSoftwareActivityItem = ({ {actorName ?? "Fleet"} ); - let installedSoftwarePrefix = getInstallStatusPredicate(status); + let installedSoftwarePrefix = getInstallUninstallStatusPredicate( + status, + isScriptPackageSource + ); if (tab !== "past" && activity.fleet_initiated) { installedSoftwarePrefix = status === "pending_uninstall" ? "will uninstall" : "will install"; diff --git a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx index 454ac3d89d1..530271b559f 100644 --- a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx +++ b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx @@ -15,7 +15,7 @@ import { IDropdownOption } from "interfaces/dropdownOption"; import { IHostSoftwarePackage, IHostAppStoreApp, - SoftwareInstallUninstallStatus, + SoftwareInstallUninstallUiStatus, IHostSoftwareWithUiStatus, } from "interfaces/software"; import { IconNames } from "components/icons"; @@ -35,7 +35,7 @@ interface IActionButtonState { export interface IGetActionButtonStateProps { hostScriptsEnabled: boolean; softwareId: number; - status: SoftwareInstallUninstallStatus | null; + status: SoftwareInstallUninstallUiStatus | null; softwarePackage: IHostSoftwarePackage | null; appStoreApp: IHostAppStoreApp | null; hostMDMEnrolled?: boolean; diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx index 030361fa249..8a7aecd4132 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx @@ -186,7 +186,7 @@ const HostSoftware = ({ ({ queryKey }) => deviceAPI.getDeviceSoftware(queryKey[0]), { ...DEFAULT_USE_QUERY_OPTIONS, - enabled: isSoftwareEnabled && isMyDevicePage, // if disabled, we'll always show a generic "No software detected" message. No DUP for iPad/iPhone + enabled: isSoftwareEnabled && isMyDevicePage, // if disabled, we'll always show a generic "No software detected" message. No My Device Page for iPad/iPhone keepPreviousData: true, staleTime: 7000, } diff --git a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx index 414bd69cd35..f7927132045 100644 --- a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx +++ b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx @@ -5,7 +5,7 @@ import { IHostSoftware, IHostSoftwareWithUiStatus, IHostSoftwareUiStatus, - SoftwareInstallUninstallStatus, + SoftwareInstallUninstallApiStatus, IVPPHostSoftware, SoftwareUninstallStatus, IAppLastInstall, @@ -28,7 +28,7 @@ const baseClass = "install-status-cell"; interface CommandUuid { command_uuid: string; software_title?: string; - status?: SoftwareInstallUninstallStatus; + status?: SoftwareInstallUninstallApiStatus; } interface InstallUuid { diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/UpdatesCard/UpdateSoftwareItem/UpdateSoftwareItem.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/UpdatesCard/UpdateSoftwareItem/UpdateSoftwareItem.tsx index bbd77c2417f..f45cfcb97d5 100644 --- a/frontend/pages/hosts/details/cards/Software/SelfService/UpdatesCard/UpdateSoftwareItem/UpdateSoftwareItem.tsx +++ b/frontend/pages/hosts/details/cards/Software/SelfService/UpdatesCard/UpdateSoftwareItem/UpdateSoftwareItem.tsx @@ -8,7 +8,7 @@ import { IHostSoftware, IHostSoftwareWithUiStatus, ISoftwareLastInstall, - SoftwareInstallUninstallStatus, + SoftwareInstallStatus, } from "interfaces/software"; import { dateAgo } from "utilities/date_format"; @@ -29,13 +29,7 @@ import { const baseClass = "update-software-item"; -const STATUS_CONFIG: Record< - Exclude< - SoftwareInstallUninstallStatus, - "pending_uninstall" | "failed_uninstall" | "uninstalled" - >, - IStatusDisplayConfig -> = { +const STATUS_CONFIG: Record = { installed: { iconName: "success", displayText: "Installed", diff --git a/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tsx b/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tsx index a1bd3465089..de204a0fc24 100644 --- a/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tsx +++ b/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tsx @@ -2,7 +2,7 @@ import React from "react"; import { IHostSoftware, - SoftwareInstallUninstallStatus, + SoftwareInstallUninstallApiStatus, } from "interfaces/software"; import Button from "components/buttons/Button"; import Modal from "components/Modal"; @@ -16,7 +16,7 @@ const baseClass = "software-update-modal"; interface IStatusMessageProps { hostDisplayName: string; isDeviceUser: boolean; - softwareStatus: SoftwareInstallUninstallStatus | null; + softwareStatus: SoftwareInstallUninstallApiStatus | null; softwareName: string; installerName: string; installerVersion?: string; diff --git a/frontend/pages/hosts/details/cards/Software/helpers.tsx b/frontend/pages/hosts/details/cards/Software/helpers.tsx index b6f2b8bfd40..9eb233d0fa4 100644 --- a/frontend/pages/hosts/details/cards/Software/helpers.tsx +++ b/frontend/pages/hosts/details/cards/Software/helpers.tsx @@ -7,7 +7,7 @@ import { IHostSoftware, IHostSoftwareUiStatus, IHostSoftwareWithUiStatus, - ScriptPackage, + SCRIPT_PACKAGE_SOURCES, } from "interfaces/software"; import { IconNames } from "components/icons"; import { @@ -178,7 +178,7 @@ export const getUiStatus = ( const lastInstallDate = getLastInstall(software)?.installed_at; const lastUninstallDate = getLastUninstall(software)?.uninstalled_at; const installerVersion = getInstallerVersion(software); - const isScriptPackage = ScriptPackage.includes(source); + const isScriptPackage = SCRIPT_PACKAGE_SOURCES.includes(source); // 0. Script Packages states if (isScriptPackage) { From 252b41f87a58d8352c6ffd1157d015cbab91cb64 Mon Sep 17 00:00:00 2001 From: RachelElysia Date: Thu, 9 Oct 2025 14:21:54 -0400 Subject: [PATCH 4/9] Setup experience work --- frontend/__mocks__/deviceUserMock.ts | 1 - frontend/interfaces/setup.ts | 12 ++++++++---- .../DeviceUserPage/DeviceUserPage.tests.tsx | 12 +++++------- .../details/DeviceUserPage/DeviceUserPage.tsx | 18 ++++++++++++------ .../SettingUpYourDevice.tsx | 4 ++-- .../SetupStatusTable/SetupStatusTable.tsx | 18 +++++++----------- .../SetupStatusTableConfig.tsx | 10 +++++----- .../hosts/details/DeviceUserPage/helpers.ts | 9 +++++++++ frontend/services/entities/device_user.ts | 5 ++++- 9 files changed, 52 insertions(+), 37 deletions(-) diff --git a/frontend/__mocks__/deviceUserMock.ts b/frontend/__mocks__/deviceUserMock.ts index 384c89b70f9..b950586caaf 100644 --- a/frontend/__mocks__/deviceUserMock.ts +++ b/frontend/__mocks__/deviceUserMock.ts @@ -57,7 +57,6 @@ export const createMockDeviceSoftwareResponse = ( const DEFAULT_SETUP_STEP_STATUS_MOCK: ISetupStep = { name: "Slack", status: "pending", - type: "", }; export const createMockSetupStepStatus = ( diff --git a/frontend/interfaces/setup.ts b/frontend/interfaces/setup.ts index 381727505f9..1a33a7f2133 100644 --- a/frontend/interfaces/setup.ts +++ b/frontend/interfaces/setup.ts @@ -8,10 +8,11 @@ export const SETUP_STEP_STATUSES = [ export type SetupStepStatus = typeof SETUP_STEP_STATUSES[number]; +/** These type extends onto API returned software steps */ export const SETUP_STEP_TYPES = [ - "software_install", - "script_run", - "software_script_run", + "software_install", // API key: software + "software_script_run", // API key: software, key: name ending in .sh or .ps1 for now + "script_run", // API key: scripts ]; export type SetupStepType = typeof SETUP_STEP_TYPES[number]; @@ -19,5 +20,8 @@ export type SetupStepType = typeof SETUP_STEP_TYPES[number]; export interface ISetupStep { name: string | null; status: SetupStepStatus; - type?: SetupStepType; +} + +export interface IEnhancedSetupStep extends ISetupStep { + type: SetupStepType; } diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx index 0ba6c9edeb6..9a30e42726a 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx @@ -136,9 +136,9 @@ describe("Device User Page", () => { }); describe("Setup experience software installation", () => { - const REGULAR_DUP_MATCHER = /Last fetched/; - const SETTING_UP_YOUR_DEVICE_MATCHER = /Setting up your device/; - const CONFIG_COMPLETE_MATCHER = /Configuration complete/; + const REGULAR_DUP_MATCHER = /Last fetched/i; + const SETTING_UP_YOUR_DEVICE_MATCHER = /Setting up your device/i; + const CONFIG_COMPLETE_MATCHER = /Configuration complete/i; const setupTest = async ( deviceUserResponseOverrides?: Partial, @@ -232,10 +232,8 @@ describe("Device User Page", () => { { host }, { setup_experience_results: { - software: [ - { type: "software_installer", name: "step 1", status: "success" }, - ], - scripts: [{ type: "script", name: "step 2", status: "failure" }], + software: [{ name: "step 1.sh", status: "success" }], + scripts: [{ name: "step 2.sh", status: "failure" }], }, }, { query: { setup_only: "1" } } diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index 8a37441fed9..64d82ab0deb 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -27,7 +27,7 @@ import { } from "interfaces/certificates"; import { isAppleDevice, isLinuxLike } from "interfaces/platform"; import { IHostSoftware } from "interfaces/software"; -import { ISetupStep } from "interfaces/setup"; +import { IEnhancedSetupStep } from "interfaces/setup"; import DeviceUserError from "components/DeviceUserError"; // @ts-ignore @@ -54,7 +54,11 @@ import AboutCard from "../cards/About"; import SoftwareCard from "../cards/Software"; import PoliciesCard from "../cards/Policies"; import InfoModal from "./InfoModal"; -import { getErrorMessage, hasRemainingSetupSteps } from "./helpers"; +import { + getErrorMessage, + hasRemainingSetupSteps, + isSoftwareScriptSetup, +} from "./helpers"; import FleetIcon from "../../../../../assets/images/fleet-avatar-24x24@2x.png"; import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal"; @@ -336,7 +340,7 @@ const DeviceUserPage = ({ } = useQuery< IGetSetupExperienceStatusesResponse, Error, - ISetupStep[] | null | undefined + IEnhancedSetupStep[] | null | undefined >( ["software-setup-statuses", deviceAuthToken], () => deviceUserAPI.getSetupExperienceStatuses({ token: deviceAuthToken }), @@ -347,15 +351,17 @@ const DeviceUserPage = ({ refetchIntervalInBackground: true, select: (response) => { // Marshal the response to include a `type` property so we can differentiate - // between software and script setup steps in the UI. + // between software, payload-free software, and script setup steps in the UI. return [ ...(response.setup_experience_results.software ?? []).map((s) => ({ ...s, - type: "software" as const, + type: isSoftwareScriptSetup(s) + ? "software_script_run" // used for payload-free software + : "software_install", })), ...(response.setup_experience_results.scripts ?? []).map((s) => ({ ...s, - type: "script" as const, + type: "script_run" as const, })), ]; }, diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SettingUpYourDevice.tsx b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SettingUpYourDevice.tsx index 7095d69bf9d..2f359f7e9c4 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SettingUpYourDevice.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SettingUpYourDevice.tsx @@ -1,5 +1,5 @@ import Card from "components/Card"; -import { ISetupStep } from "interfaces/setup"; +import { IEnhancedSetupStep } from "interfaces/setup"; import React from "react"; import InfoButton from "../InfoButton"; import SetupStatusTable from "./SetupStatusTable"; @@ -9,7 +9,7 @@ import { hasRemainingSetupSteps } from "../../helpers"; const baseClass = "setting-up-your-device"; interface ISettingUpYourDevice { - setupSteps: ISetupStep[]; + setupSteps: IEnhancedSetupStep[]; toggleInfoModal: () => void; } diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTable.tsx b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTable.tsx index 0ad5ae690a4..63e0fb92697 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTable.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTable.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { ISetupStep } from "interfaces/setup"; +import { IEnhancedSetupStep } from "interfaces/setup"; import TableContainer from "components/TableContainer"; import EmptyTable from "components/EmptyTable"; @@ -10,21 +10,17 @@ import generateColumnConfigs from "./SetupStatusTableConfig"; const baseClass = "setup-status-table"; interface ISetupStatusTableProps { - statuses: ISetupStep[]; + statuses: IEnhancedSetupStep[]; } const SetupStatusTable = ({ statuses }: ISetupStatusTableProps) => { const columnConfigs = generateColumnConfigs(); - // Sort the statuses so that scripts are always at the bottom. - statuses.sort((a, b) => { - if (a.type === b.type) { - return 0; - } - if (a.type === "script") { - return 1; - } - return -1; + // Sort the statuses so that it's status of software, then software scripts, then scripts + const order = ["software_install", "software_script_run", "script_run"]; + + statuses.sort((a: IEnhancedSetupStep, b: IEnhancedSetupStep) => { + return order.indexOf(a.type) - order.indexOf(b.type); }); return ( diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTableConfig.tsx b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTableConfig.tsx index 09567c67d37..1fa5dce8bdf 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTableConfig.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTableConfig.tsx @@ -2,15 +2,15 @@ import React from "react"; import { CellProps, Column } from "react-table"; -import { ISetupStep } from "interfaces/setup"; +import { IEnhancedSetupStep } from "interfaces/setup"; import SetupSoftwareProcessCell from "components/TableContainer/DataTable/SetupSoftwareProcessCell"; import SetupSoftwareStatusCell from "components/TableContainer/DataTable/SetupSoftwareStatusCell"; import SetupScriptProcessCell from "components/TableContainer/DataTable/SetupScriptProcessCell"; import SetupScriptStatusCell from "components/TableContainer/DataTable/SetupScriptStatusCell"; -type ISetupStatusTableConfig = Column; -type ITableCellProps = CellProps; +type ISetupStatusTableConfig = Column; +type ITableCellProps = CellProps; const generateColumnConfigs = (): ISetupStatusTableConfig[] => [ { @@ -19,10 +19,10 @@ const generateColumnConfigs = (): ISetupStatusTableConfig[] => [ disableSortBy: true, Cell: (cellProps: ITableCellProps) => { const { name, type } = cellProps.row.original; - if (type === "software") { + if (type === "software_install" || type === "software_script_run") { return ; } - if (type === "script") { + if (type === "script_run") { return ; } return null; diff --git a/frontend/pages/hosts/details/DeviceUserPage/helpers.ts b/frontend/pages/hosts/details/DeviceUserPage/helpers.ts index d0ba6974e56..ca8955990e8 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/helpers.ts +++ b/frontend/pages/hosts/details/DeviceUserPage/helpers.ts @@ -17,3 +17,12 @@ export const hasRemainingSetupSteps = ( return statuses.some((s) => ["pending", "running"].includes(s.status)); }; + +/** Checks if name value ends with .sh or .ps1 as + * there's no other key to identify payload-free software + * Update if/when API adds better identifier */ +export const isSoftwareScriptSetup = (s: ISetupStep) => { + if (!s.name) return false; + + return s.name.endsWith(".sh") || s.name.endsWith(".ps1"); +}; diff --git a/frontend/services/entities/device_user.ts b/frontend/services/entities/device_user.ts index 7509272f780..826fb62e342 100644 --- a/frontend/services/entities/device_user.ts +++ b/frontend/services/entities/device_user.ts @@ -53,7 +53,10 @@ export interface IGetVppInstallCommandResultsResponse { results: IMdmCommandResult[]; } export interface IGetSetupExperienceStatusesResponse { - setup_experience_results: { software: ISetupStep[]; scripts: ISetupStep[] }; + setup_experience_results: { + software: ISetupStep[]; + scripts: ISetupStep[]; + }; } export interface IGetSetupExperienceStatusesParams { From 2cb2296e6349bb7191166f4fde6d46169a7c2223 Mon Sep 17 00:00:00 2001 From: RachelElysia Date: Thu, 9 Oct 2025 16:15:18 -0400 Subject: [PATCH 5/9] Cleaner code, update tooltips, fix some tests --- .../SoftwareInstallDetailsModal.tsx | 3 ++ .../SoftwareScriptDetailsModal.tests.tsx | 5 -- .../SoftwareScriptDetailsModal.tsx | 16 +++++-- .../SoftwareUninstallDetailsModal.tsx | 4 +- .../VppInstallDetailsModal.tsx | 8 ++-- .../InstallDetails/constants.ts | 10 ++-- frontend/interfaces/software.ts | 28 +++++------ .../cards/ActivityFeed/ActivityFeed.tsx | 4 +- .../InstallerStatusTableConfig.tsx | 48 +++++++++++++++++-- .../SoftwareSummaryCard.tsx | 6 +-- .../SoftwareTitleDetailsPage/helpers.tests.ts | 2 + .../SoftwareTitlesTableConfig.tsx | 4 +- .../SoftwareDetailsSummary.tsx | 5 +- .../SoftwareOptionsSelector.tsx | 7 ++- .../HostDetailsPage/HostDetailsPage.tsx | 4 +- .../HostInstallerActionCell.tsx | 4 +- .../InstallStatusCell/InstallStatusCell.tsx | 10 ++-- .../SoftwareUpdateModal.tsx | 4 +- 18 files changed, 109 insertions(+), 63 deletions(-) diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx index 6b125ed5655..8a221f320fa 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx @@ -1,3 +1,6 @@ +/** For payload-free packages (e.g. software source is sh_packages or ps1_packages) + * we use SoftwareIScriptDetailsModal */ + import React, { useState } from "react"; import { useQuery } from "react-query"; import { formatDistanceToNow } from "date-fns"; diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx index 78bd0d9ce59..39567d200ba 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx @@ -9,7 +9,6 @@ describe("SoftwareScriptDetailsModal - StatusMessage component", () => { it("on software library page/pending activity, renders pending install message with host and package name", () => { render( { it("on device user page, renders failed run with rerun option with contact link", () => { render( { it("on device user page, renders failed install with retry option without contact link", () => { render( { it("on host details page, renders failed script without rerun", () => { render( { it("on host details page/install activity, renders ran message with timestamp", () => { render( ( ); interface IInstallStatusMessage { - softwareName: string; installResult: ISoftwareScriptResult; isMyDevicePage: boolean; contactUrl?: string; } -// TODO - match VppInstallDetailsModal status to this, still accounting for MDM-specific cases -// present there export const StatusMessage = ({ - softwareName, installResult, isMyDevicePage, contactUrl, @@ -309,7 +316,6 @@ export const SoftwareInstallDetailsModal = ({
diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx index d59b9a3004c..70d37d099a6 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal.tsx @@ -23,7 +23,7 @@ import Spinner from "components/Spinner"; import Textarea from "components/Textarea"; import RevealButton from "components/buttons/RevealButton"; import { - getScriptDetailsStatusPredicate, + getInstallDetailsStatusPredicate, INSTALL_DETAILS_STATUS_ICONS, } from "../constants"; import { renderContactOption } from "../SoftwareInstallDetailsModal/SoftwareInstallDetailsModal"; @@ -63,7 +63,7 @@ export const StatusMessage = ({ const renderStatusCopy = () => { const prefix = ( <> - Fleet {getScriptDetailsStatusPredicate(status)} {softwareName} + Fleet {getInstallDetailsStatusPredicate(status)} {softwareName} ); let suffix = null; diff --git a/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx index e66a767b024..c853f3f4138 100644 --- a/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/VppInstallDetailsModal/VppInstallDetailsModal.tsx @@ -10,7 +10,7 @@ import deviceUserAPI, { import { IHostSoftware, - SoftwareInstallUninstallApiStatus, + SoftwareInstallUninstallStatus, } from "interfaces/software"; import { IMdmCommandResult } from "interfaces/mdm"; @@ -35,7 +35,7 @@ interface IGetStatusMessageProps { isMyDevicePage?: boolean; /** "pending" is an edge case here where VPP install activities that were added to the feed prior to v4.57 * (when we split pending into pending_install/pending_uninstall) will list the status as "pending" rather than "pending_install" */ - displayStatus: SoftwareInstallUninstallApiStatus | "pending"; + displayStatus: SoftwareInstallUninstallStatus | "pending"; isMDMStatusNotNow: boolean; isMDMStatusAcknowledged: boolean; appName: string; @@ -152,7 +152,7 @@ export const getStatusMessage = ({ }; interface IModalButtonsProps { - displayStatus: SoftwareInstallUninstallApiStatus | "pending"; + displayStatus: SoftwareInstallUninstallStatus | "pending"; deviceAuthToken?: string; onCancel: () => void; onRetry?: (id: number) => void; @@ -199,7 +199,7 @@ const baseClass = "vpp-install-details-modal"; export type IVppInstallDetails = { /** Status: null when a host manually installed not using Fleet */ - fleetInstallStatus: SoftwareInstallUninstallApiStatus | null; + fleetInstallStatus: SoftwareInstallUninstallStatus | null; hostDisplayName: string; appName: string; commandUuid?: string; diff --git a/frontend/components/ActivityDetails/InstallDetails/constants.ts b/frontend/components/ActivityDetails/InstallDetails/constants.ts index 044aac2f757..c3b9ebffda5 100644 --- a/frontend/components/ActivityDetails/InstallDetails/constants.ts +++ b/frontend/components/ActivityDetails/InstallDetails/constants.ts @@ -1,14 +1,14 @@ import { IconNames } from "components/icons"; import { - SoftwareInstallUninstallApiStatus, - SoftwareInstallUninstallUiStatus, + SoftwareInstallUninstallStatus, + EnhancedSoftwareInstallUninstallStatus, SoftwareInstallStatus, } from "interfaces/software"; // Install/Uninstall helpers export const INSTALL_DETAILS_STATUS_ICONS: Record< - SoftwareInstallUninstallApiStatus, // former is superset of latter, latter included in union for type system + SoftwareInstallUninstallStatus, // former is superset of latter, latter included in union for type system IconNames > = { pending_install: "pending-outline", @@ -20,7 +20,7 @@ export const INSTALL_DETAILS_STATUS_ICONS: Record< } as const; const INSTALL_DETAILS_STATUS_PREDICATES: Record< - SoftwareInstallUninstallUiStatus, + EnhancedSoftwareInstallUninstallStatus, string > = { pending_install: "is installing or will install", @@ -42,7 +42,7 @@ export const getInstallDetailsStatusPredicate = ( } return ( INSTALL_DETAILS_STATUS_PREDICATES[ - status.toLowerCase() as SoftwareInstallUninstallUiStatus + status.toLowerCase() as EnhancedSoftwareInstallUninstallStatus ] || INSTALL_DETAILS_STATUS_PREDICATES.pending_install ); }; diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts index c8391bb5ab3..3f6b377963f 100644 --- a/frontend/interfaces/software.ts +++ b/frontend/interfaces/software.ts @@ -260,7 +260,7 @@ export const INSTALLABLE_SOURCE_PLATFORM_CONVERSION = { export const SCRIPT_PACKAGE_SOURCES = ["sh_packages", "ps1_packages"]; -export const NO_VERSION_OR_HOST_DATA_AVAIL_SOURCES = [ +export const NO_VERSION_OR_HOST_DATA_SOURCES = [ "tgz_packages", ...SCRIPT_PACKAGE_SOURCES, ]; @@ -333,35 +333,35 @@ export const SOFTWARE_SCRIPT_STATUSES = [ export type SoftwareInstallStatus = typeof SOFTWARE_INSTALL_STATUSES[number]; -export const SOFTWARE_INSTALL_UNINSTALL_API_STATUSES = [ +export const SOFTWARE_INSTALL_UNINSTALL_STATUSES = [ ...SOFTWARE_INSTALL_STATUSES, ...SOFTWARE_UNINSTALL_STATUSES, // Payload-free (script) software statuses use API's SOFTWARE_INSTALL_STATUSES ] as const; /* - * SoftwareInstallUninstallApiStatus represents the possible states of software install operations. + * SoftwareInstallUninstallStatus represents the possible states of software install operations. */ -export type SoftwareInstallUninstallApiStatus = typeof SOFTWARE_INSTALL_UNINSTALL_API_STATUSES[number]; +export type SoftwareInstallUninstallStatus = typeof SOFTWARE_INSTALL_UNINSTALL_STATUSES[number]; /** Include payload-free statuses */ -export const SOFTWARE_INSTALL_UNINSTALL_UI_STATUSES = [ +export const ENAHNCED_SOFTWARE_INSTALL_UNINSTALL_STATUSES = [ ...SOFTWARE_INSTALL_STATUSES, ...SOFTWARE_UNINSTALL_STATUSES, ...SOFTWARE_SCRIPT_STATUSES, // Payload-free (script) software ] as const; /* - * SoftwareInstallUninstallUiStatus represents the possible states of software install operations including payload-free. + * EnhancedSoftwareInstallUninstallStatus represents the possible states of software install operations including payload-free used in the UI. */ -export type SoftwareInstallUninstallUiStatus = typeof SOFTWARE_INSTALL_UNINSTALL_UI_STATUSES[number]; +export type EnhancedSoftwareInstallUninstallStatus = typeof ENAHNCED_SOFTWARE_INSTALL_UNINSTALL_STATUSES[number]; export const isValidSoftwareInstallUninstallStatus = ( s: string | undefined | null -): s is SoftwareInstallUninstallUiStatus => +): s is EnhancedSoftwareInstallUninstallStatus => !!s && - SOFTWARE_INSTALL_UNINSTALL_UI_STATUSES.includes( - s as SoftwareInstallUninstallUiStatus + ENAHNCED_SOFTWARE_INSTALL_UNINSTALL_STATUSES.includes( + s as EnhancedSoftwareInstallUninstallStatus ); export const SOFTWARE_AGGREGATE_STATUSES = [ @@ -416,7 +416,7 @@ export interface ISoftwareInstallResult { software_title_id: number; software_package: string; host_id: number; - status: SoftwareInstallUninstallApiStatus; + status: SoftwareInstallUninstallStatus; detail: string; output: string; pre_install_query_output: string; @@ -498,7 +498,7 @@ export interface IHostSoftware { source: SoftwareSource; extension_for?: SoftwareExtensionFor; bundle_identifier?: string; - status: Exclude | null; + status: Exclude | null; installed_versions: ISoftwareInstallVersion[] | null; } @@ -564,7 +564,7 @@ export type IDeviceSoftware = IHostSoftware; export type IDeviceSoftwareWithUiStatus = IHostSoftwareWithUiStatus; const INSTALL_STATUS_PREDICATES: Record< - SoftwareInstallUninstallUiStatus | "pending", + EnhancedSoftwareInstallUninstallStatus | "pending", string > = { pending: "pending", @@ -618,7 +618,7 @@ export const aggregateInstallStatusCounts = ( }); export const INSTALL_STATUS_ICONS: Record< - SoftwareInstallUninstallUiStatus | "pending" | "failed", + EnhancedSoftwareInstallUninstallStatus | "pending" | "failed", IconNames > = { pending: "pending-outline", diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx index c13a9828d5f..9a460388b0f 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx @@ -11,7 +11,7 @@ import activitiesAPI, { import { resolveUninstallStatus, - SoftwareInstallUninstallApiStatus, + SoftwareInstallUninstallStatus, } from "interfaces/software"; import { ActivityType, IActivityDetails } from "interfaces/activity"; @@ -268,7 +268,7 @@ const ActivityFeed = ({ details={{ appName: vppInstallDetails.software_title || "", fleetInstallStatus: (vppInstallDetails.status || - "pending_install") as SoftwareInstallUninstallApiStatus, + "pending_install") as SoftwareInstallUninstallStatus, hostDisplayName: vppInstallDetails.host_display_name || "", commandUuid: vppInstallDetails.command_uuid || "", }} diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx index 67e94c35d2a..c7727292052 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx @@ -33,9 +33,13 @@ interface IStatusDisplayOption { // "pending" and "failed" each encompass both "_install" and "_uninstall" sub-statuses type SoftwareInstallDisplayStatus = "installed" | "pending" | "failed"; +type SoftwareScriptDisplayStatus = + | "ran_script" + | "pending_script" + | "failed_script"; const STATUS_DISPLAY_OPTIONS: Record< - SoftwareInstallDisplayStatus, + SoftwareInstallDisplayStatus | SoftwareScriptDisplayStatus, IStatusDisplayOption > = { installed: { @@ -73,6 +77,36 @@ const STATUS_DISPLAY_OPTIONS: Record< ), }, + ran_script: { + displayName: "Ran", + iconName: "success", + // TODO: Confirm tooltip + tooltip: <>Payload-free software script successfully ran on these hosts., + }, + pending_script: { + displayName: "Pending", + iconName: "pending-outline", + // TODO: Confirm tooltip + tooltip: ( + <> + Fleet is running or will do so +
+ when the host comes online. + + ), + }, + failed_script: { + displayName: "Failed", + iconName: "error", + // TODO: Confirm tooltip + tooltip: ( + <> + These hosts failed to run the payload-free software script. +
+ Click on a host to view error(s). + + ), + }, }; const generateSoftwareTitleDetailsTableConfig = ({ @@ -87,7 +121,9 @@ const generateSoftwareTitleDetailsTableConfig = ({ disableSortBy: true, title: isScriptPackage ? "Ran" : "Installed", Header: () => { - const displayData = STATUS_DISPLAY_OPTIONS.installed; + const displayData = isScriptPackage + ? STATUS_DISPLAY_OPTIONS.ran_script + : STATUS_DISPLAY_OPTIONS.installed; const titleWithTooltip = ( { - const displayData = STATUS_DISPLAY_OPTIONS.pending; + const displayData = isScriptPackage + ? STATUS_DISPLAY_OPTIONS.pending_script + : STATUS_DISPLAY_OPTIONS.pending; return ( { - const displayData = STATUS_DISPLAY_OPTIONS.failed; + const displayData = isScriptPackage + ? STATUS_DISPLAY_OPTIONS.failed_script + : STATUS_DISPLAY_OPTIONS.failed; return ( { pending: 8, failed: 3, }, + isScriptPackage: false, isSelfService: true, }); }); @@ -87,6 +88,7 @@ describe("SoftwareTitleDetailsPage helpers", () => { pending: 5, failed: 3, }, + isScriptPackage: false, isSelfService: false, }); }); diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx index 76b38bd51f6..82db230275c 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx @@ -4,7 +4,7 @@ import { InjectedRouter } from "react-router"; import { ISoftwareTitle, - NO_VERSION_OR_HOST_DATA_AVAIL_SOURCES, + NO_VERSION_OR_HOST_DATA_SOURCES, formatSoftwareType, isIpadOrIphoneSoftwareSource, } from "interfaces/software"; @@ -189,7 +189,7 @@ const generateTableHeaders = ( Cell: (cellProps: IViewAllHostsLinkProps) => { const { source } = cellProps.row.original; - const hostCountNotSupported = NO_VERSION_OR_HOST_DATA_AVAIL_SOURCES.includes( + const hostCountNotSupported = NO_VERSION_OR_HOST_DATA_SOURCES.includes( source ); diff --git a/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx b/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx index e49fe99a121..b5fdf0dbbbf 100644 --- a/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx +++ b/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx @@ -8,7 +8,7 @@ import React from "react"; import { getPathWithQueryParams, QueryParams } from "utilities/url"; import paths from "router/paths"; -import { NO_VERSION_OR_HOST_DATA_AVAIL_SOURCES } from "interfaces/software"; +import { NO_VERSION_OR_HOST_DATA_SOURCES } from "interfaces/software"; import DataSet from "components/DataSet"; import LastUpdatedHostCount from "components/LastUpdatedHostCount"; @@ -68,8 +68,7 @@ const SoftwareDetailsSummary = ({ // Remove host count for tgz_packages, sh_packages, and ps1_packages only // or if viewing details summary from edit icon preview modal const showHostCount = - source && - !NO_VERSION_OR_HOST_DATA_AVAIL_SOURCES.includes(source) && + !NO_VERSION_OR_HOST_DATA_SOURCES.includes(source || "") && iconPreviewUrl === undefined; const renderSoftwareIcon = () => { diff --git a/frontend/pages/SoftwarePage/components/forms/SoftwareOptionsSelector/SoftwareOptionsSelector.tsx b/frontend/pages/SoftwarePage/components/forms/SoftwareOptionsSelector/SoftwareOptionsSelector.tsx index 6a702c4dd28..f3f3e2880eb 100644 --- a/frontend/pages/SoftwarePage/components/forms/SoftwareOptionsSelector/SoftwareOptionsSelector.tsx +++ b/frontend/pages/SoftwarePage/components/forms/SoftwareOptionsSelector/SoftwareOptionsSelector.tsx @@ -125,14 +125,13 @@ const SoftwareOptionsSelector = ({ ); } - // TODO: Confirm with PM/Design if we want tooltip messaging for script packages if (isScriptPackage) { return ( <> Fleet can't create a policy to detect existing installations of - software installed by scripts. To automatically install script-based - packages, add a custom policy and enable the install software - automation on the Policies page. + payload-free packages. To automatically install these packages, add a + custom policy and enable the install software automation on the{" "} + Policies page. ); } diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 20a6a22edf1..5a8a56a3bfc 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -34,7 +34,7 @@ import { IQueryStats } from "interfaces/query_stats"; import { IHostSoftware, resolveUninstallStatus, - SoftwareInstallUninstallApiStatus, + SoftwareInstallUninstallStatus, } from "interfaces/software"; import { ITeam } from "interfaces/team"; import { ActivityType, IHostUpcomingActivity } from "interfaces/activity"; @@ -704,7 +704,7 @@ const HostDetailsPage = ({ setActivityVPPInstallDetails({ appName: details?.software_title || "", fleetInstallStatus: (details?.status || - "pending_install") as SoftwareInstallUninstallApiStatus, + "pending_install") as SoftwareInstallUninstallStatus, commandUuid: details?.command_uuid || "", // FIXME: It seems like the backend is not using the correct display name when it returns // upcoming install activities. As a workaround, we'll prefer the display name from diff --git a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx index 530271b559f..e9386558bc3 100644 --- a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx +++ b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx @@ -15,7 +15,7 @@ import { IDropdownOption } from "interfaces/dropdownOption"; import { IHostSoftwarePackage, IHostAppStoreApp, - SoftwareInstallUninstallUiStatus, + EnhancedSoftwareInstallUninstallStatus, IHostSoftwareWithUiStatus, } from "interfaces/software"; import { IconNames } from "components/icons"; @@ -35,7 +35,7 @@ interface IActionButtonState { export interface IGetActionButtonStateProps { hostScriptsEnabled: boolean; softwareId: number; - status: SoftwareInstallUninstallUiStatus | null; + status: EnhancedSoftwareInstallUninstallStatus | null; softwarePackage: IHostSoftwarePackage | null; appStoreApp: IHostAppStoreApp | null; hostMDMEnrolled?: boolean; diff --git a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx index f7927132045..fd25618675d 100644 --- a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx +++ b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx @@ -5,7 +5,7 @@ import { IHostSoftware, IHostSoftwareWithUiStatus, IHostSoftwareUiStatus, - SoftwareInstallUninstallApiStatus, + SoftwareInstallUninstallStatus, IVPPHostSoftware, SoftwareUninstallStatus, IAppLastInstall, @@ -28,7 +28,7 @@ const baseClass = "install-status-cell"; interface CommandUuid { command_uuid: string; software_title?: string; - status?: SoftwareInstallUninstallApiStatus; + status?: SoftwareInstallUninstallStatus; } interface InstallUuid { @@ -110,7 +110,7 @@ export const INSTALL_STATUS_DISPLAY_OPTIONS: Record< pending_install: { iconName: "pending-outline", displayText: ({ isSelfService, isHostOnline }) => - isSelfService || isHostOnline ? "Installing..." : "Install (pending)", // TODO: "Running..." for script packages / "Run (pending)" + isSelfService || isHostOnline ? "Installing..." : "Install (pending)", tooltip: ({ isSelfService, isHostOnline }) => isSelfService || isHostOnline ? ( "Fleet is installing software." @@ -285,6 +285,7 @@ export const INSTALL_STATUS_DISPLAY_OPTIONS: Record< ran_script: { iconName: "success", displayText: "Ran", + // TODO: Confirm tooltip tooltip: ({ lastInstalledAt }) => lastInstalledAt ? ( <> @@ -296,6 +297,7 @@ export const INSTALL_STATUS_DISPLAY_OPTIONS: Record< failed_script: { iconName: "error", displayText: "Failed run", + // TODO: Confirm tooltip tooltip: ({ lastInstalledAt, isSelfService }) => ( <> Payload-free software script failed to run @@ -317,11 +319,13 @@ export const INSTALL_STATUS_DISPLAY_OPTIONS: Record< running_script: { iconName: "pending-outline", displayText: "Run (pending)", + // TODO: Confirm tooltip tooltip: () => "Fleet is running the payload-free software script.", }, pending_script: { iconName: "pending-outline", displayText: "Run (pending)", + // TODO: Confirm tooltip tooltip: ({ isSelfService, isHostOnline }) => isSelfService || isHostOnline ? ( "Fleet is running the payload-free software script." diff --git a/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tsx b/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tsx index de204a0fc24..a1bd3465089 100644 --- a/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tsx +++ b/frontend/pages/hosts/details/cards/Software/SoftwareUpdateModal/SoftwareUpdateModal.tsx @@ -2,7 +2,7 @@ import React from "react"; import { IHostSoftware, - SoftwareInstallUninstallApiStatus, + SoftwareInstallUninstallStatus, } from "interfaces/software"; import Button from "components/buttons/Button"; import Modal from "components/Modal"; @@ -16,7 +16,7 @@ const baseClass = "software-update-modal"; interface IStatusMessageProps { hostDisplayName: string; isDeviceUser: boolean; - softwareStatus: SoftwareInstallUninstallApiStatus | null; + softwareStatus: SoftwareInstallUninstallStatus | null; softwareName: string; installerName: string; installerVersion?: string; From 44663488a521c597d92642c36db35b3812ffa076 Mon Sep 17 00:00:00 2001 From: RachelElysia Date: Fri, 10 Oct 2025 10:06:24 -0400 Subject: [PATCH 6/9] Hide install details dropdown for pending payload-free modal --- .../SoftwareScriptDetailsModal.tsx | 36 ++++++++++--------- .../InstallerStatusTableConfig.tsx | 8 ++++- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx index 769fe79c940..ed369639dbd 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx @@ -236,22 +236,26 @@ export const SoftwareInstallDetailsModal = ({ } ); - const renderScriptDetailsSection = () => ( - <> - - {showInstallDetails && swInstallResult?.output && ( - - )} - - ); + const renderScriptDetailsSection = () => { + if (swInstallResult?.status !== "pending_install") { + return ( + <> + + {showInstallDetails && swInstallResult?.output && ( + + )} + + ); + } + }; const hostDisplayname = swInstallResult?.host_display_name || detailsFromProps.host_display_name; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx index c7727292052..56c13e5e2b3 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx @@ -81,7 +81,13 @@ const STATUS_DISPLAY_OPTIONS: Record< displayName: "Ran", iconName: "success", // TODO: Confirm tooltip - tooltip: <>Payload-free software script successfully ran on these hosts., + tooltip: ( + <> + Payload-free software script successfully +
+ ran on these hosts. + + ), }, pending_script: { displayName: "Pending", From 9d1328f6182788d03b534d319e5c08aa864ae9ab Mon Sep 17 00:00:00 2001 From: RachelElysia Date: Tue, 14 Oct 2025 10:01:15 -0400 Subject: [PATCH 7/9] Copy changes, updates to script details modal, add tests --- .../SoftwareScriptDetailsModal.tests.tsx | 2 +- .../SoftwareScriptDetailsModal.tsx | 88 ++++----- .../SoftwareScriptDetailsModal/_styles.scss | 2 +- .../InstallerStatusTableConfig.tsx | 9 +- .../DeviceUserPage/DeviceUserPage.tests.tsx | 4 +- .../InstallStatusCell.tests.tsx | 181 +++++++++++++++++- .../InstallStatusCell/InstallStatusCell.tsx | 21 +- .../hosts/details/cards/Software/helpers.tsx | 1 - 8 files changed, 229 insertions(+), 79 deletions(-) diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx index 39567d200ba..f7e7e4a6e4d 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx @@ -149,7 +149,7 @@ describe("SoftwareScriptDetailsModal - ModalButtons component", () => { ).not.toBeInTheDocument(); }); - it("on device user page, shows Done button for ran payload-free software", () => { + it("on device user page, shows Done button for ran payload-free software script", () => { const onCancel = jest.fn(); render( {showInstallDetails && swInstallResult?.output && ( - )} @@ -268,65 +268,61 @@ export const SoftwareInstallDetailsModal = ({ : undefined; const renderContent = () => { - if (isInstalledByFleet) { - if (isLoading) { - return ; - } - - if (isError) { - if (error?.status === 404) { - return deviceAuthToken ? ( - - ) : ( - - ); - } - - if (error?.status === 401) { - return deviceAuthToken ? ( - - ) : ( - - ); - } - } + if (isLoading) { + return ; + } - if (!swInstallResult) { + if (isError) { + if (error?.status === 404) { return deviceAuthToken ? ( ) : ( - + ); } - if ( - !["installed", "pending_install", "failed_install"].includes( - swInstallResult.status - ) - ) { - return ( - + if (error?.status === 401) { + return deviceAuthToken ? ( + + ) : ( + ); } } - if (installResultWithHostDisplayName) { + if (!installResultWithHostDisplayName) { + return deviceAuthToken ? ( + + ) : ( + + ); + } + + if ( + !["installed", "pending_install", "failed_install"].includes( + installResultWithHostDisplayName.status + ) + ) { return ( -
- - {renderScriptDetailsSection()} -
+ ); } + + return ( +
+ + {renderScriptDetailsSection()} +
+ ); }; return ( diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/_styles.scss b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/_styles.scss index 74e08bf52bb..4fa30e811d9 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/_styles.scss +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/_styles.scss @@ -1,7 +1,7 @@ // TODO: Confirm all styling .software-script-details-modal { - overflow-wrap: anywhere; // Prevent long software name overflow + overflow-wrap: anywhere; &__modal-content { display: flex; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx index 56c13e5e2b3..91caafc69b8 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx @@ -80,10 +80,9 @@ const STATUS_DISPLAY_OPTIONS: Record< ran_script: { displayName: "Ran", iconName: "success", - // TODO: Confirm tooltip tooltip: ( <> - Payload-free software script successfully + The script successfully
ran on these hosts. @@ -92,10 +91,9 @@ const STATUS_DISPLAY_OPTIONS: Record< pending_script: { displayName: "Pending", iconName: "pending-outline", - // TODO: Confirm tooltip tooltip: ( <> - Fleet is running or will do so + Fleet is running the script or will do so
when the host comes online. @@ -104,10 +102,9 @@ const STATUS_DISPLAY_OPTIONS: Record< failed_script: { displayName: "Failed", iconName: "error", - // TODO: Confirm tooltip tooltip: ( <> - These hosts failed to run the payload-free software script. + These hosts failed to run the script.
Click on a host to view error(s). diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx index 9a30e42726a..7c23b8a44eb 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx @@ -188,8 +188,8 @@ describe("Device User Page", () => { expect( screen.getByText(SETTING_UP_YOUR_DEVICE_MATCHER) ).toBeInTheDocument(); - expect(screen.getByText(/Installing/)).toBeInTheDocument(); - expect(screen.getByText(/Running/)).toBeInTheDocument(); + expect(screen.getAllByText(/Install/i).length).toBeGreaterThan(0); + expect(screen.getAllByText(/Run/i).length).toBeGreaterThan(0); }); expect(screen.queryByText(REGULAR_DUP_MATCHER)).toBeNull(); diff --git a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tests.tsx b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tests.tsx index 98fb7286e98..37af6229ac7 100644 --- a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tests.tsx +++ b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tests.tsx @@ -82,12 +82,6 @@ describe("InstallStatusCell - component", () => { await user.hover(screen.getByText("Installed")); - // TODO: Confirm with design if there is a tooltip - // expect( - // screen.getByText(/Software was installed/i) - // ).toBeInTheDocument(); - - // There SHOULD be a button with this label expect( screen.queryByRole("button", { name: /installed/i }) ).toBeInTheDocument(); @@ -128,6 +122,38 @@ describe("InstallStatusCell - component", () => { ).not.toBeInTheDocument(); }); + it("renders 'Ran' status for a payload-free package", async () => { + const { user } = renderWithSetup( + + ); + + expect(screen.getByRole("button", { name: /ran/i })).toBeInTheDocument(); + expect(screen.getByTestId("success-icon")).toBeInTheDocument(); + + await user.hover(screen.getByText(/ran/i)); + + // No tooltip on install status + expect(screen.queryByText(/The script ran/i)).not.toBeInTheDocument(); + + // There SHOULD be a button with this label + expect(screen.queryByRole("button", { name: /ran/i })).toBeInTheDocument(); + }); + it("renders 'Install (pending)' status with tooltip if host is offline", async () => { const { user } = renderWithSetup( { }); }); + it("renders 'Running...' status for a payload-free package with tooltip if host is online", async () => { + const { user } = renderWithSetup( + + ); + + expect(screen.getByText("Running...")).toBeInTheDocument(); + expect(screen.getByTestId("spinner")).toBeInTheDocument(); + + await user.hover(screen.getByText("Running...")); + await waitFor(() => { + expect( + screen.getByText(/Fleet is running the script./i) + ).toBeInTheDocument(); + }); + + // Not clickable + expect( + screen.queryByRole("button", { name: /running/i }) + ).not.toBeInTheDocument(); + }); + + it("renders 'Run (pending)' for a payload-free package with tooltip if host is offline", async () => { + const { user } = renderWithSetup( + + ); + + expect( + screen.getByRole("button", { name: /Run \(pending\)/i }) + ).toBeInTheDocument(); + expect(screen.getByTestId("pending-outline-icon")).toBeInTheDocument(); + + await user.hover(screen.getByText("Run (pending)")); + await waitFor(() => { + expect( + screen.getByText(/Fleet will run the script/i) + ).toBeInTheDocument(); + }); + }); + it("renders 'Uninstalling...' status with tooltip if host is online", async () => { const { user } = renderWithSetup( { }); }); + it("renders 'Failed run' for a payload-free package that failed to run", async () => { + const { user } = renderWithSetup( + + ); + + expect( + screen.getByRole("button", { name: /Failed run/i }) + ).toBeInTheDocument(); + expect(screen.getByTestId("error-icon")).toBeInTheDocument(); + + await user.hover(screen.getByText(/Failed run/)); + await waitFor(() => { + expect(screen.getByText(/The script failed to run/i)).toBeInTheDocument(); + }); + }); + it("renders 'Failed (uninstall)' status with tooltip", async () => { const { user } = renderWithSetup( { await user.hover(screen.getByText("---")); await waitFor(() => { - expect(screen.getByText(/can be installed/i)).toBeInTheDocument(); + expect(screen.getByText(/can be/i)).toBeInTheDocument(); + expect(screen.getByText(/installed/i)).toBeInTheDocument(); }); // Not clickable @@ -503,7 +633,39 @@ describe("InstallStatusCell - component", () => { await user.hover(screen.getByText("---")); await waitFor(() => { - expect(screen.getByText(/can be installed/i)).toBeInTheDocument(); + expect(screen.getByText(/can be/i)).toBeInTheDocument(); + expect(screen.getByText(/installed/i)).toBeInTheDocument(); + }); + + // Not clickable + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); + + it("renders '---' for a payload-free package available for run", async () => { + const { user } = renderWithSetup( + + ); + + expect(screen.getByText("---")).toBeInTheDocument(); + + await user.hover(screen.getByText("---")); + await waitFor(() => { + expect(screen.getByText(/can be/i)).toBeInTheDocument(); + expect(screen.getByText(/ran/i)).toBeInTheDocument(); }); // Not clickable @@ -536,7 +698,8 @@ describe("InstallStatusCell - component", () => { await user.hover(screen.getAllByText("---")[0]); await waitFor(() => { - expect(screen.getByText(/can be installed/i)).toBeInTheDocument(); + expect(screen.getByText(/can be/i)).toBeInTheDocument(); + expect(screen.getByText(/installed/i)).toBeInTheDocument(); }); // Not clickable diff --git a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx index fd25618675d..962a768376e 100644 --- a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx +++ b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx @@ -76,7 +76,7 @@ export const INSTALL_STATUS_DISPLAY_OPTIONS: Record< > = { installed: { iconName: "success", - displayText: "Installed", // TODO: "Ran" for script packages + displayText: "Installed", tooltip: () => undefined, // No tooltip for installed state }, recently_updated: { @@ -285,22 +285,19 @@ export const INSTALL_STATUS_DISPLAY_OPTIONS: Record< ran_script: { iconName: "success", displayText: "Ran", - // TODO: Confirm tooltip tooltip: ({ lastInstalledAt }) => lastInstalledAt ? ( <> - Payload-free software script ran (finished with exit code 0){" "} - {dateAgo(lastInstalledAt)}. + The script ran (finished with exit code 0) {dateAgo(lastInstalledAt)}. ) : undefined, }, failed_script: { iconName: "error", displayText: "Failed run", - // TODO: Confirm tooltip tooltip: ({ lastInstalledAt, isSelfService }) => ( <> - Payload-free software script failed to run + The script failed to run {lastInstalledAt ? ` (${dateAgo(lastInstalledAt)})` : ""}.{" "} {isSelfService ? ( <> @@ -318,21 +315,19 @@ export const INSTALL_STATUS_DISPLAY_OPTIONS: Record< }, running_script: { iconName: "pending-outline", - displayText: "Run (pending)", - // TODO: Confirm tooltip - tooltip: () => "Fleet is running the payload-free software script.", + displayText: "Running...", + tooltip: () => "Fleet is running the script.", }, pending_script: { iconName: "pending-outline", displayText: "Run (pending)", - // TODO: Confirm tooltip tooltip: ({ isSelfService, isHostOnline }) => isSelfService || isHostOnline ? ( - "Fleet is running the payload-free software script." + "Fleet is running the script." ) : ( <> - Fleet will run the payload-free software -
script when the host comes online. + Fleet will run the script when the host +
comes online. ), }, diff --git a/frontend/pages/hosts/details/cards/Software/helpers.tsx b/frontend/pages/hosts/details/cards/Software/helpers.tsx index 9eb233d0fa4..673b9db9204 100644 --- a/frontend/pages/hosts/details/cards/Software/helpers.tsx +++ b/frontend/pages/hosts/details/cards/Software/helpers.tsx @@ -315,7 +315,6 @@ interface IButtonConfig { /** Display text and icon are shared across self-service and * host details > library action buttons */ -// TODO: "Retry" and "Reinstall" should be "Rerun" for script packages export const getInstallerActionButtonConfig = ( type: ButtonType, status: IHostSoftwareUiStatus From 34236ce0c1e5d009a2705840ffb9dd5a4a9f4619 Mon Sep 17 00:00:00 2001 From: RachelElysia Date: Tue, 14 Oct 2025 10:19:43 -0400 Subject: [PATCH 8/9] Differences in install modal vs script modal --- .../SoftwareScriptDetailsModal.tsx | 10 +--------- .../cards/HostSoftwareLibrary/HostSoftwareLibrary.tsx | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx index fd4a979ab2e..955a69ade79 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx @@ -186,11 +186,8 @@ export const ModalButtons = ({ }; interface ISoftwareInstallDetailsProps { - /** note that details.install_uuid is present in hostSoftware, but since it is always needed for - this modal while hostSoftware is not, as in the case of the activity feeds, it is specifically - necessary in the details prop */ details: IPackageInstallDetails; - hostSoftware?: IHostSoftware; // for inventory versions, and software name when not Fleet installed (not present on activity feeds) + hostSoftware?: IHostSoftware; // for software name when not Fleet installed (not present on activity feeds) deviceAuthToken?: string; // My Device Page only onCancel: () => void; onRerun?: (id: number) => void; // My Device Page only @@ -213,10 +210,6 @@ export const SoftwareInstallDetailsModal = ({ setShowInstallDetails((prev) => !prev); }; - const isInstalledByFleet = hostSoftware - ? !!hostSoftware.software_package?.last_install - : true; // if no hostSoftware passed in, can assume this is the activity feed, meaning this can only refer to a Fleet-handled install - const { data: swInstallResult, isLoading, isError, error } = useQuery< ISoftwareInstallResults, AxiosError, @@ -229,7 +222,6 @@ export const SoftwareInstallDetailsModal = ({ : softwareAPI.getSoftwareInstallResult(installUUID); }, { - enabled: !!isInstalledByFleet, ...DEFAULT_USE_QUERY_OPTIONS, staleTime: 3000, select: (data) => data.results as ISoftwareScriptResult, diff --git a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibrary.tsx b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibrary.tsx index 3bf4650cd51..8514e0f36d3 100644 --- a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibrary.tsx +++ b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibrary.tsx @@ -588,7 +588,7 @@ const HostSoftwareLibrary = ({ host_display_name: hostDisplayName, install_uuid: selectedHostSWScriptDetails.software_package?.last_install - ?.install_uuid, // slightly redundant, see explanation in `SoftwareInstallDetailsModal + ?.install_uuid, }} hostSoftware={selectedHostSWScriptDetails} onCancel={() => setSelectedHostSWScriptDetails(null)} From 6773a04db5f8146f68f52e528664ecd57cf0f5be Mon Sep 17 00:00:00 2001 From: RachelElysia Date: Tue, 14 Oct 2025 10:37:16 -0400 Subject: [PATCH 9/9] Typo --- .../SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx index 8a221f320fa..2106a0d74ac 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx @@ -1,5 +1,5 @@ /** For payload-free packages (e.g. software source is sh_packages or ps1_packages) - * we use SoftwareIScriptDetailsModal */ + * we use SoftwareScriptDetailsModal */ import React, { useState } from "react"; import { useQuery } from "react-query";