**Related issue:** Resolves #31973 Addresses a few closely related issues around determining the appropriate UI to display for installed software - Pass the inherited host's `softwareUpdatedAt` to the handler responsible for determining the software's "UI display status" so it can calculate whether it is "recently installed", as intended. - In the scenario where the above is _not_ passed in for some reason, default to "installed" ui display status any time the software's status is "installed" - Add a check that ensures the above default "installed" ui status is captured even when `installed_versions` is `null`. This scenario previously returned an "uninstalled" ui display status, which caused this bug initially <img width="1720" height="880" alt="Screenshot 2025-12-01 at 12 02 29 PM" src="https://github.com/user-attachments/assets/20874ff5-133c-4b74-8634-2cdc2d3a1497" /> - [x] Changes file added for user-visible changes in `changes/ - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually
678 lines
21 KiB
TypeScript
678 lines
21 KiB
TypeScript
import React, {
|
|
useCallback,
|
|
useContext,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
useEffect,
|
|
} from "react";
|
|
import { InjectedRouter } from "react-router";
|
|
import { useQuery } from "react-query";
|
|
import { AxiosError } from "axios";
|
|
|
|
import hostAPI, {
|
|
IGetHostSoftwareResponse,
|
|
IHostSoftwareQueryKey,
|
|
} from "services/entities/hosts";
|
|
import PATHS from "router/paths";
|
|
import {
|
|
IHostSoftware,
|
|
IVPPHostSoftware,
|
|
ISoftware,
|
|
} from "interfaces/software";
|
|
import { HostPlatform, isIPadOrIPhone, isAndroid } from "interfaces/platform";
|
|
|
|
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
|
|
import { getPathWithQueryParams } from "utilities/url";
|
|
|
|
import { NotificationContext } from "context/notification";
|
|
import { AppContext } from "context/app";
|
|
|
|
import CardHeader from "components/CardHeader";
|
|
import DataError from "components/DataError";
|
|
import Spinner from "components/Spinner";
|
|
import Button from "components/buttons/Button";
|
|
import Icon from "components/Icon";
|
|
import SoftwareInstallDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal";
|
|
import SoftwareIpaInstallDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareIpaInstallDetailsModal";
|
|
import SoftwareScriptDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal";
|
|
import VppInstallDetailsModal from "components/ActivityDetails/InstallDetails/VppInstallDetailsModal";
|
|
import SoftwareUninstallDetailsModal, {
|
|
ISWUninstallDetailsParentState,
|
|
} from "components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal";
|
|
|
|
import { generateHostSWLibraryTableHeaders } from "./HostSoftwareLibraryTable/HostSoftwareLibraryTableConfig";
|
|
import HostSoftwareLibraryTable from "./HostSoftwareLibraryTable";
|
|
import { getInstallErrorMessage, getUninstallErrorMessage } from "./helpers";
|
|
import { getUiStatus } from "../Software/helpers";
|
|
import SoftwareUpdateModal from "../Software/SelfService/components/SoftwareUpdateModal";
|
|
|
|
const baseClass = "host-software-library-card";
|
|
|
|
export interface ITableSoftware extends Omit<ISoftware, "vulnerabilities"> {
|
|
vulnerabilities: string[]; // for client-side search purposes, we only want an array of cve strings
|
|
}
|
|
|
|
interface IHostSoftwareLibraryProps {
|
|
/** This is the host id or the device token */
|
|
id: number | string;
|
|
platform: HostPlatform;
|
|
hostDisplayName: string;
|
|
softwareUpdatedAt?: string;
|
|
router: InjectedRouter;
|
|
queryParams: ReturnType<typeof parseHostSoftwareLibraryQueryParams>;
|
|
pathname: string;
|
|
hostTeamId: number;
|
|
hostName: string;
|
|
onShowInventoryVersions: (software?: IHostSoftware) => void;
|
|
isSoftwareEnabled?: boolean;
|
|
hostScriptsEnabled?: boolean;
|
|
hostMDMEnrolled?: boolean;
|
|
isHostOnline?: boolean;
|
|
refetchHostDetails: () => void;
|
|
isHostDetailsPolling: boolean;
|
|
}
|
|
|
|
const DEFAULT_SEARCH_QUERY = "";
|
|
const DEFAULT_SORT_DIRECTION = "asc";
|
|
const DEFAULT_SORT_HEADER = "name";
|
|
const DEFAULT_PAGE = 0;
|
|
const DEFAULT_PAGE_SIZE = 20;
|
|
|
|
export const parseHostSoftwareLibraryQueryParams = (queryParams: {
|
|
page?: string;
|
|
query?: string;
|
|
order_key?: string;
|
|
order_direction?: "asc" | "desc";
|
|
self_service?: string;
|
|
}) => {
|
|
const searchQuery = queryParams?.query ?? DEFAULT_SEARCH_QUERY;
|
|
const sortHeader = queryParams?.order_key ?? DEFAULT_SORT_HEADER;
|
|
const sortDirection = queryParams?.order_direction ?? DEFAULT_SORT_DIRECTION;
|
|
const page = queryParams?.page
|
|
? parseInt(queryParams.page, 10)
|
|
: DEFAULT_PAGE;
|
|
const pageSize = DEFAULT_PAGE_SIZE;
|
|
const selfService = queryParams?.self_service === "true";
|
|
|
|
return {
|
|
page,
|
|
query: searchQuery,
|
|
order_key: sortHeader,
|
|
order_direction: sortDirection,
|
|
per_page: pageSize,
|
|
available_for_install: true, // always true for host installers
|
|
self_service: selfService,
|
|
};
|
|
};
|
|
|
|
const HostSoftwareLibrary = ({
|
|
id,
|
|
platform,
|
|
hostDisplayName,
|
|
softwareUpdatedAt,
|
|
hostScriptsEnabled,
|
|
router,
|
|
queryParams,
|
|
pathname,
|
|
hostTeamId = 0,
|
|
hostName,
|
|
onShowInventoryVersions,
|
|
isSoftwareEnabled = false,
|
|
hostMDMEnrolled,
|
|
isHostOnline = false,
|
|
refetchHostDetails,
|
|
isHostDetailsPolling,
|
|
}: IHostSoftwareLibraryProps) => {
|
|
const { renderFlash } = useContext(NotificationContext);
|
|
const {
|
|
isGlobalAdmin,
|
|
isGlobalMaintainer,
|
|
isTeamAdmin,
|
|
isTeamMaintainer,
|
|
} = useContext(AppContext);
|
|
|
|
const isUnsupported = isAndroid(platform); // no Android software
|
|
const isWindowsHost = platform === "windows";
|
|
const isIPadOrIPhoneHost = isIPadOrIPhone(platform);
|
|
const isAndroidHost = isAndroid(platform);
|
|
const isMacOSHost = platform === "darwin";
|
|
|
|
const [hostSoftwareLibraryRes, setHostSoftwareLibraryRes] = useState<
|
|
IGetHostSoftwareResponse | undefined
|
|
>(undefined);
|
|
const [
|
|
selectedSoftwareUpdates,
|
|
setSelectedSoftwareUpdates,
|
|
] = useState<IHostSoftware | null>(null);
|
|
// these states and modal logic exist at this level instead of the page level to match the similar
|
|
// pattern on
|
|
// the device user page, which facilitates manipulating relevant UI states e.g.
|
|
// "updating..." when the user clicks "Retry" in the SoftwareInstallDetailsModal
|
|
const [
|
|
selectedHostSWInstallDetails,
|
|
setSelectedHostSWInstallDetails,
|
|
] = useState<IHostSoftware | null>(null);
|
|
const [
|
|
selectedHostSWIpaInstallDetails,
|
|
setSelectedHostSWIpaInstallDetails,
|
|
] = useState<IHostSoftware | null>(null);
|
|
const [
|
|
selectedHostSWScriptDetails,
|
|
setSelectedHostSWScriptDetails,
|
|
] = useState<IHostSoftware | null>(null);
|
|
const [
|
|
selectedHostSWUninstallDetails,
|
|
setSelectedHostSWUninstallDetails,
|
|
] = useState<ISWUninstallDetailsParentState | null>(null);
|
|
const [
|
|
selectedVPPInstallDetails,
|
|
setSelectedVPPInstallDetails,
|
|
] = useState<IVPPHostSoftware | null>(null);
|
|
|
|
const enhancedSoftware = useMemo(() => {
|
|
if (!hostSoftwareLibraryRes) return [];
|
|
return hostSoftwareLibraryRes.software.map((software) => ({
|
|
...software,
|
|
ui_status: getUiStatus(software, isHostOnline, softwareUpdatedAt),
|
|
}));
|
|
}, [hostSoftwareLibraryRes, isHostOnline, softwareUpdatedAt]);
|
|
|
|
const pendingSoftwareSetRef = useRef<Set<string>>(new Set()); // Track for polling
|
|
const pollingTimeoutIdRef = useRef<NodeJS.Timeout | null>(null);
|
|
const isAwaitingHostDetailsPolling = useRef(isHostDetailsPolling);
|
|
|
|
const queryKey = useMemo<IHostSoftwareQueryKey[]>(() => {
|
|
return [
|
|
{
|
|
scope: "host_software",
|
|
id: id as number,
|
|
softwareUpdatedAt,
|
|
...queryParams,
|
|
},
|
|
];
|
|
}, [queryParams, id, softwareUpdatedAt]);
|
|
|
|
const {
|
|
isLoading: hostSoftwareLibraryLoading,
|
|
isError: hostSoftwareLibraryError,
|
|
isFetching: hostSoftwareLibraryFetching,
|
|
refetch: refetchHostSoftwareLibrary,
|
|
} = useQuery<
|
|
IGetHostSoftwareResponse,
|
|
AxiosError,
|
|
IGetHostSoftwareResponse,
|
|
IHostSoftwareQueryKey[]
|
|
>(queryKey, () => hostAPI.getHostSoftware(queryKey[0]), {
|
|
...DEFAULT_USE_QUERY_OPTIONS,
|
|
enabled: isSoftwareEnabled && !isUnsupported,
|
|
keepPreviousData: true,
|
|
onSuccess: (response) => {
|
|
setHostSoftwareLibraryRes(response);
|
|
},
|
|
});
|
|
|
|
// After host details polling (in parent) finishes, refetch software data.
|
|
// Ensures self service data reflects updates to installed_versions from the latest host details.
|
|
useEffect(() => {
|
|
// Detect completion of the host details polling (in parent)
|
|
// Once host details polling completes, refetch software data to retreive updated installed_versions keyed from host details data
|
|
if (isAwaitingHostDetailsPolling.current && !isHostDetailsPolling) {
|
|
refetchHostSoftwareLibrary();
|
|
}
|
|
isAwaitingHostDetailsPolling.current = isHostDetailsPolling;
|
|
}, [isHostDetailsPolling, refetchHostSoftwareLibrary]);
|
|
|
|
// Poll for pending installs/uninstalls
|
|
const { refetch: refetchForPendingInstallsOrUninstalls } = useQuery<
|
|
IGetHostSoftwareResponse,
|
|
AxiosError
|
|
>(
|
|
["pending_installs", queryKey[0]],
|
|
() => hostAPI.getHostSoftware(queryKey[0]),
|
|
{
|
|
enabled: false,
|
|
onSuccess: (response) => {
|
|
// Get the set of pending software IDs
|
|
const newPendingSet = new Set(
|
|
response.software
|
|
.filter(
|
|
(software) =>
|
|
software.status === "pending_install" ||
|
|
software.status === "pending_uninstall"
|
|
)
|
|
.map((software) => String(software.id))
|
|
);
|
|
|
|
// Refresh host details if the number of pending installs or uninstalls has decreased
|
|
// To update the software library information of the newly installed/uninstalled software
|
|
if (newPendingSet.size < pendingSoftwareSetRef.current.size) {
|
|
refetchHostDetails();
|
|
}
|
|
|
|
// Compare new set with the previous set
|
|
const setsAreEqual =
|
|
newPendingSet.size === pendingSoftwareSetRef.current.size &&
|
|
[...newPendingSet].every((pendingId) =>
|
|
pendingSoftwareSetRef.current.has(pendingId)
|
|
);
|
|
|
|
if (newPendingSet.size > 0) {
|
|
// If the set changed, update and continue polling
|
|
if (!setsAreEqual) {
|
|
pendingSoftwareSetRef.current = newPendingSet;
|
|
setHostSoftwareLibraryRes(response);
|
|
}
|
|
|
|
// Continue polling
|
|
if (pollingTimeoutIdRef.current) {
|
|
clearTimeout(pollingTimeoutIdRef.current);
|
|
}
|
|
pollingTimeoutIdRef.current = setTimeout(() => {
|
|
refetchForPendingInstallsOrUninstalls();
|
|
}, 5000);
|
|
} else {
|
|
// No pending installs nor pending uninstalls, stop polling and refresh data
|
|
pendingSoftwareSetRef.current = new Set();
|
|
if (pollingTimeoutIdRef.current) {
|
|
clearTimeout(pollingTimeoutIdRef.current);
|
|
pollingTimeoutIdRef.current = null;
|
|
}
|
|
setHostSoftwareLibraryRes(response);
|
|
}
|
|
},
|
|
onError: () => {
|
|
pendingSoftwareSetRef.current = new Set();
|
|
renderFlash(
|
|
"error",
|
|
"We're having trouble checking pending installs. Please refresh the page."
|
|
);
|
|
},
|
|
}
|
|
);
|
|
|
|
// Stop polling if the host goes offline
|
|
// Polling will automatically resume when host is online with pending installs
|
|
useEffect(() => {
|
|
if (!isHostOnline) {
|
|
if (pollingTimeoutIdRef.current) {
|
|
clearTimeout(pollingTimeoutIdRef.current);
|
|
pollingTimeoutIdRef.current = null;
|
|
}
|
|
pendingSoftwareSetRef.current = new Set();
|
|
}
|
|
}, [isHostOnline]);
|
|
|
|
const startPollingForPendingInstallsOrUninstalls = useCallback(
|
|
(pendingIds: string[]) => {
|
|
if (isHostOnline) {
|
|
const newSet = new Set(pendingIds);
|
|
const setsAreEqual =
|
|
newSet.size === pendingSoftwareSetRef.current.size &&
|
|
[...newSet].every((pendingId) =>
|
|
pendingSoftwareSetRef.current.has(pendingId)
|
|
);
|
|
if (!setsAreEqual) {
|
|
pendingSoftwareSetRef.current = newSet;
|
|
|
|
// Clear any existing timeout to avoid overlap
|
|
if (pollingTimeoutIdRef.current) {
|
|
clearTimeout(pollingTimeoutIdRef.current);
|
|
}
|
|
// Starts polling for pending installs/uninstalls
|
|
refetchForPendingInstallsOrUninstalls();
|
|
}
|
|
}
|
|
},
|
|
[refetchForPendingInstallsOrUninstalls, isHostOnline]
|
|
);
|
|
|
|
// Cleanup on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
pendingSoftwareSetRef.current = new Set();
|
|
if (pollingTimeoutIdRef.current) {
|
|
clearTimeout(pollingTimeoutIdRef.current);
|
|
pollingTimeoutIdRef.current = null;
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
// On initial load or data change, check for pending installs/uninstalls
|
|
useEffect(() => {
|
|
const pendingSoftware = hostSoftwareLibraryRes?.software.filter(
|
|
(software) =>
|
|
software.status === "pending_install" ||
|
|
software.status === "pending_uninstall"
|
|
);
|
|
const pendingIds = pendingSoftware?.map((s) => String(s.id)) ?? [];
|
|
if (pendingIds.length > 0) {
|
|
startPollingForPendingInstallsOrUninstalls(pendingIds);
|
|
}
|
|
}, [hostSoftwareLibraryRes, startPollingForPendingInstallsOrUninstalls]);
|
|
|
|
const onAddSoftware = useCallback(() => {
|
|
// "Add Software" path dependent on host's platform
|
|
const addSoftwarePathForHostPlatform = () => {
|
|
if (isIPadOrIPhoneHost || isAndroidHost) {
|
|
return getPathWithQueryParams(PATHS.SOFTWARE_ADD_APP_STORE, {
|
|
platform: isAndroidHost ? "android" : "apple",
|
|
});
|
|
}
|
|
if (isMacOSHost || isWindowsHost) {
|
|
return PATHS.SOFTWARE_ADD_FLEET_MAINTAINED;
|
|
}
|
|
return PATHS.SOFTWARE_ADD_PACKAGE;
|
|
};
|
|
|
|
router.push(
|
|
getPathWithQueryParams(addSoftwarePathForHostPlatform(), {
|
|
team_id: hostTeamId,
|
|
})
|
|
);
|
|
}, [
|
|
hostTeamId,
|
|
isIPadOrIPhoneHost,
|
|
isAndroidHost,
|
|
isMacOSHost,
|
|
isWindowsHost,
|
|
router,
|
|
]);
|
|
|
|
const onShowUpdateDetails = useCallback(
|
|
(software?: IHostSoftware) => {
|
|
if (software) {
|
|
setSelectedSoftwareUpdates(software);
|
|
}
|
|
},
|
|
[setSelectedSoftwareUpdates]
|
|
);
|
|
|
|
const onSetSelectedHostSWInstallDetails = useCallback(
|
|
(hostSW?: IHostSoftware) => {
|
|
if (hostSW) {
|
|
setSelectedHostSWInstallDetails(hostSW);
|
|
}
|
|
},
|
|
[setSelectedHostSWInstallDetails]
|
|
);
|
|
|
|
const onSetSelectedHostSWIpaInstallDetails = useCallback(
|
|
(hostSW?: IHostSoftware) => {
|
|
if (hostSW) {
|
|
setSelectedHostSWIpaInstallDetails(hostSW);
|
|
}
|
|
},
|
|
[setSelectedHostSWIpaInstallDetails]
|
|
);
|
|
|
|
const onSetSelectedHostSWScriptDetails = useCallback(
|
|
(hostSW?: IHostSoftware) => {
|
|
if (hostSW) {
|
|
setSelectedHostSWScriptDetails(hostSW);
|
|
}
|
|
},
|
|
[setSelectedHostSWScriptDetails]
|
|
);
|
|
|
|
const onSetSelectedHostSWUninstallDetails = useCallback(
|
|
(uninstallDetails?: ISWUninstallDetailsParentState) => {
|
|
if (uninstallDetails) {
|
|
setSelectedHostSWUninstallDetails(uninstallDetails);
|
|
}
|
|
},
|
|
[setSelectedHostSWUninstallDetails]
|
|
);
|
|
|
|
const onSetSelectedVPPInstallDetails = useCallback(
|
|
(s?: IVPPHostSoftware) => {
|
|
if (s) {
|
|
setSelectedVPPInstallDetails(s);
|
|
}
|
|
},
|
|
[setSelectedVPPInstallDetails]
|
|
);
|
|
|
|
const onInstallOrUninstall = useCallback(() => {
|
|
// For online hosts, poll for change in pending statuses
|
|
// For offline hosts, refresh the data without polling
|
|
isHostOnline
|
|
? refetchForPendingInstallsOrUninstalls()
|
|
: refetchHostSoftwareLibrary();
|
|
}, [
|
|
refetchForPendingInstallsOrUninstalls,
|
|
refetchHostSoftwareLibrary,
|
|
isHostOnline,
|
|
]);
|
|
|
|
const hasSWWriteRole = Boolean(
|
|
isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer
|
|
);
|
|
|
|
// 4.77 Currently Android apps can only be installed via self-service by end user
|
|
const userHasSWWritePermission = hasSWWriteRole && !isAndroidHost;
|
|
|
|
const isMountedRef = useRef(false);
|
|
useEffect(() => {
|
|
isMountedRef.current = true;
|
|
return () => {
|
|
isMountedRef.current = false;
|
|
};
|
|
}, []);
|
|
|
|
const onClickInstallAction = useCallback(
|
|
async (softwareId: number, isScriptPackage = false) => {
|
|
try {
|
|
await hostAPI.installHostSoftwarePackage(id as number, softwareId);
|
|
if (isMountedRef.current) {
|
|
onInstallOrUninstall();
|
|
}
|
|
|
|
const message = () => {
|
|
switch (true) {
|
|
case isHostOnline && isScriptPackage:
|
|
return "Script is running.";
|
|
case isHostOnline && !isScriptPackage:
|
|
return "Software is installing.";
|
|
case !isHostOnline && isScriptPackage:
|
|
return "Script will run when the host comes online.";
|
|
default:
|
|
return "Software will install when the host comes online.";
|
|
}
|
|
};
|
|
|
|
renderFlash(
|
|
"success",
|
|
<>
|
|
{message()} To see details, go to <b>Details > Activity</b>.
|
|
</>
|
|
);
|
|
} catch (e) {
|
|
renderFlash("error", getInstallErrorMessage(e));
|
|
}
|
|
},
|
|
[id, renderFlash, onInstallOrUninstall, isHostOnline]
|
|
);
|
|
|
|
const onClickUninstallAction = useCallback(
|
|
async (softwareId: number) => {
|
|
try {
|
|
await hostAPI.uninstallHostSoftwarePackage(id as number, softwareId);
|
|
if (isMountedRef.current) {
|
|
onInstallOrUninstall();
|
|
}
|
|
renderFlash(
|
|
"success",
|
|
<>
|
|
Software{" "}
|
|
{isHostOnline
|
|
? "is uninstalling"
|
|
: "will uninstall when the host comes online"}
|
|
. To see details, go to <b>Details > Activity</b>.
|
|
</>
|
|
);
|
|
} catch (e) {
|
|
renderFlash("error", getUninstallErrorMessage(e));
|
|
}
|
|
},
|
|
[id, renderFlash, onInstallOrUninstall, isHostOnline]
|
|
);
|
|
|
|
const tableConfig = useMemo(() => {
|
|
return generateHostSWLibraryTableHeaders({
|
|
userHasSWWritePermission,
|
|
hostScriptsEnabled,
|
|
hostMDMEnrolled,
|
|
router,
|
|
teamId: hostTeamId,
|
|
hostName,
|
|
baseClass,
|
|
onShowInventoryVersions,
|
|
onShowUpdateDetails,
|
|
onSetSelectedHostSWInstallDetails,
|
|
onSetSelectedHostSWIpaInstallDetails,
|
|
onSetSelectedHostSWScriptDetails,
|
|
onSetSelectedHostSWUninstallDetails,
|
|
onSetSelectedVPPInstallDetails,
|
|
onClickInstallAction,
|
|
onClickUninstallAction,
|
|
isHostOnline,
|
|
});
|
|
}, [
|
|
userHasSWWritePermission,
|
|
hostScriptsEnabled,
|
|
hostMDMEnrolled,
|
|
router,
|
|
hostTeamId,
|
|
hostName,
|
|
onShowInventoryVersions,
|
|
onShowUpdateDetails,
|
|
onSetSelectedHostSWInstallDetails,
|
|
onSetSelectedHostSWIpaInstallDetails,
|
|
onSetSelectedHostSWScriptDetails,
|
|
onSetSelectedHostSWUninstallDetails,
|
|
onSetSelectedVPPInstallDetails,
|
|
onClickInstallAction,
|
|
onClickUninstallAction,
|
|
isHostOnline,
|
|
]);
|
|
|
|
const isLoading = hostSoftwareLibraryLoading;
|
|
const isError = hostSoftwareLibraryError;
|
|
const data = hostSoftwareLibraryRes;
|
|
const enhancedData = enhancedSoftware;
|
|
|
|
const renderHostSoftware = () => {
|
|
if (isLoading) {
|
|
return <Spinner />;
|
|
}
|
|
|
|
if (isError) {
|
|
return <DataError verticalPaddingSize="pad-xxxlarge" />;
|
|
}
|
|
|
|
return (
|
|
<HostSoftwareLibraryTable
|
|
isLoading={hostSoftwareLibraryFetching}
|
|
data={data}
|
|
enhancedData={enhancedData}
|
|
platform={platform}
|
|
router={router}
|
|
tableConfig={tableConfig}
|
|
sortHeader={queryParams.order_key}
|
|
sortDirection={queryParams.order_direction}
|
|
searchQuery={queryParams.query}
|
|
page={queryParams.page}
|
|
pagePath={pathname}
|
|
selfService={queryParams.self_service}
|
|
/>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className={baseClass}>
|
|
<div className={`${baseClass}__header`}>
|
|
<CardHeader subheader="Software available to be installed on this host" />
|
|
{userHasSWWritePermission && (
|
|
<Button variant="inverse" onClick={onAddSoftware}>
|
|
<Icon name="plus" />
|
|
<span>Add software</span>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
{renderHostSoftware()}
|
|
{selectedSoftwareUpdates && (
|
|
<SoftwareUpdateModal
|
|
hostDisplayName={hostDisplayName}
|
|
software={selectedSoftwareUpdates}
|
|
onUpdate={onClickInstallAction}
|
|
onExit={() => setSelectedSoftwareUpdates(null)}
|
|
/>
|
|
)}
|
|
{selectedHostSWInstallDetails && (
|
|
<SoftwareInstallDetailsModal
|
|
details={{
|
|
host_display_name: hostDisplayName,
|
|
install_uuid:
|
|
selectedHostSWInstallDetails.software_package?.last_install
|
|
?.install_uuid, // slightly redundant, see explanation in `SoftwareInstallDetailsModal
|
|
}}
|
|
hostSoftware={selectedHostSWInstallDetails}
|
|
onCancel={() => setSelectedHostSWInstallDetails(null)}
|
|
/>
|
|
)}
|
|
{selectedHostSWIpaInstallDetails && (
|
|
<SoftwareIpaInstallDetailsModal
|
|
details={{
|
|
hostDisplayName,
|
|
fleetInstallStatus: selectedHostSWIpaInstallDetails.status,
|
|
appName:
|
|
selectedHostSWIpaInstallDetails.display_name ||
|
|
selectedHostSWIpaInstallDetails.name,
|
|
commandUuid:
|
|
selectedHostSWIpaInstallDetails.software_package?.last_install
|
|
?.install_uuid, // slightly redundant, see explanation in `SoftwareInstallDetailsModal
|
|
}}
|
|
hostSoftware={selectedHostSWIpaInstallDetails}
|
|
onCancel={() => setSelectedHostSWIpaInstallDetails(null)}
|
|
/>
|
|
)}
|
|
{selectedHostSWScriptDetails && (
|
|
<SoftwareScriptDetailsModal
|
|
details={{
|
|
host_display_name: hostDisplayName,
|
|
install_uuid:
|
|
selectedHostSWScriptDetails.software_package?.last_install
|
|
?.install_uuid,
|
|
}}
|
|
hostSoftware={selectedHostSWScriptDetails}
|
|
onCancel={() => setSelectedHostSWScriptDetails(null)}
|
|
/>
|
|
)}
|
|
{selectedHostSWUninstallDetails && (
|
|
<SoftwareUninstallDetailsModal
|
|
{...selectedHostSWUninstallDetails}
|
|
hostDisplayName={hostDisplayName}
|
|
onCancel={() => setSelectedHostSWUninstallDetails(null)}
|
|
/>
|
|
)}
|
|
{selectedVPPInstallDetails && (
|
|
<VppInstallDetailsModal
|
|
details={{
|
|
fleetInstallStatus: selectedVPPInstallDetails.status,
|
|
hostDisplayName,
|
|
appName:
|
|
selectedVPPInstallDetails.display_name ||
|
|
selectedVPPInstallDetails.name,
|
|
commandUuid: selectedVPPInstallDetails.commandUuid,
|
|
}}
|
|
hostSoftware={selectedVPPInstallDetails}
|
|
onCancel={() => setSelectedVPPInstallDetails(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default React.memo(HostSoftwareLibrary);
|