From fa2b295a5e7f10ab947efee7ee4b8d257003465e Mon Sep 17 00:00:00 2001 From: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com> Date: Fri, 10 May 2024 10:57:47 -0500 Subject: [PATCH] Update UI to support filtering by software install status (#18888) --- changes/14921-software-installers-sg | 4 + frontend/hooks/useTeamIdParam.ts | 75 ++++++++++++++++++- frontend/interfaces/software.ts | 30 +++++--- .../SoftwarePackageCard.tsx | 6 +- .../SoftwareInstallDetails.tsx | 6 +- .../hosts/ManageHostsPage/HostsPageConfig.tsx | 2 + .../hosts/ManageHostsPage/ManageHostsPage.tsx | 55 +++++++++++++- .../HostsFilterBlock/HostsFilterBlock.tsx | 51 +++++++++++-- .../components/HostsFilterBlock/_styles.scss | 3 +- .../InstalledSoftwareActivityItem.tsx | 6 +- .../Software/HostSoftwareTableConfig.tsx | 4 +- .../InstallStatusCell/InstallStatusCell.tsx | 6 +- frontend/services/entities/host_count.ts | 6 ++ frontend/services/entities/hosts.ts | 21 +++++- frontend/utilities/url/index.ts | 62 ++++++++++++++- 15 files changed, 296 insertions(+), 41 deletions(-) create mode 100644 changes/14921-software-installers-sg diff --git a/changes/14921-software-installers-sg b/changes/14921-software-installers-sg new file mode 100644 index 0000000000..9bd8a1b260 --- /dev/null +++ b/changes/14921-software-installers-sg @@ -0,0 +1,4 @@ +- Added functionality to filter hosts by software installer status. +- Added endpoints to upload, delete, and download software installers. +- Added endpoints to get host software install results. +- Updated activity feeds to include software installer activities. diff --git a/frontend/hooks/useTeamIdParam.ts b/frontend/hooks/useTeamIdParam.ts index 51274a9e33..f9b92b6ef5 100644 --- a/frontend/hooks/useTeamIdParam.ts +++ b/frontend/hooks/useTeamIdParam.ts @@ -1,6 +1,6 @@ import { useCallback, useContext, useEffect, useMemo } from "react"; import { InjectedRouter } from "react-router"; -import { findLastIndex, trimStart } from "lodash"; +import { findLastIndex, over, trimStart } from "lodash"; import { AppContext } from "context/app"; import { TableContext } from "context/table"; @@ -16,6 +16,30 @@ import { import { IUser, IUserRole } from "interfaces/user"; import permissions from "utilities/permissions"; import sort from "utilities/sort"; +import { HOSTS_QUERY_PARAMS } from "services/entities/hosts"; + +type OnTeamChangeFuncShouldStripParam = ( + teamIdForApi: number | undefined +) => boolean; + +type OnTeamChangeFuncShouldReplaceParam = ( + teamIdForApi: number | undefined +) => [boolean, string]; + +/** + * This type is used to define functions that determine whether a query parameter should be stripped or replaced + * when the team id changes. + * + * The key is the name of the query parameter and the value is a function that receives the new team + * id with a return type of either: + * - boolean indicating whether the query parameter should be stripped + * - tuple of a boolean and a string, where the boolean indicates whether the query parameter should be replaced + * and the string is the new value for the query parameter + */ +export type IConfigOverrideParamsOnTeamChange = Record< + string, + OnTeamChangeFuncShouldReplaceParam | OnTeamChangeFuncShouldStripParam +>; const splitQueryStringParts = (queryString: string) => trimStart(queryString, "?") @@ -27,7 +51,8 @@ const joinQueryStringParts = (parts: string[]) => const rebuildQueryStringWithTeamId = ( queryString: string, - newTeamId: number + newTeamId: number, + configAdditionalParams?: IConfigOverrideParamsOnTeamChange ) => { const parts = splitQueryStringParts(queryString); @@ -67,6 +92,41 @@ const rebuildQueryStringWithTeamId = ( parts.splice(teamIndex, 1); // just remove the old team part } + if (configAdditionalParams) { + Object.entries(configAdditionalParams).forEach(([paramName, fn]) => { + let shouldStrip = false; + let shouldReplace = false; + let replaceString = ""; + + const val = fn(newTeamId); + if (Array.isArray(val)) { + [shouldReplace, replaceString] = val; + } else if (typeof val === "boolean") { + shouldStrip = val; + } + + if (shouldStrip || shouldReplace) { + const paramIndex = parts.findIndex((p) => + p.startsWith(`${paramName}=`) + ); + + if (shouldStrip && paramIndex !== -1) { + parts.splice(paramIndex, 1); + return; + } + + if (shouldReplace) { + const newPart = `${paramName}=${replaceString}`; + if (paramIndex === -1) { + parts.splice(paramIndex, 1, newPart); + } else { + parts.push(newPart); + } + } + } + }); + } + return joinQueryStringParts(parts); }; @@ -223,6 +283,7 @@ export const useTeamIdParam = ({ includeNoTeam, permittedAccessByTeamRole, resetSelectedRowsOnTeamChange = true, + overrideParamsOnTeamChange, }: { location?: { pathname: string; @@ -235,6 +296,7 @@ export const useTeamIdParam = ({ includeNoTeam: boolean; permittedAccessByTeamRole?: Record; resetSelectedRowsOnTeamChange?: boolean; + overrideParamsOnTeamChange?: IConfigOverrideParamsOnTeamChange; }) => { const { hash, pathname, query, search } = location; const { @@ -282,11 +344,18 @@ export const useTeamIdParam = ({ router.replace( pathname - .concat(rebuildQueryStringWithTeamId(search, teamId)) + .concat( + rebuildQueryStringWithTeamId( + search, + teamId, + overrideParamsOnTeamChange + ) + ) .concat(hash || "") ); }, [ + overrideParamsOnTeamChange, resetSelectedRowsOnTeamChange, router, pathname, diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts index 64737c89ba..68923d47b7 100644 --- a/frontend/interfaces/software.ts +++ b/frontend/interfaces/software.ts @@ -148,17 +148,29 @@ export const formatSoftwareType = ({ } return type; }; -/* - * SoftwareInstallStatus represents the possible states of software install operations. - * - */ -export type ISoftwareInstallStatus = "pending" | "installed" | "failed"; /** - * + * This list comprises all possible states of software install operations. + */ +export const SOFTWARE_INSTALL_STATUSES = [ + "failed", + "installed", + "pending", +] as const; + +/* + * SoftwareInstallStatus represents the possible states of software install operations. + */ +export type SoftwareInstallStatus = typeof SOFTWARE_INSTALL_STATUSES[number]; + +export const isValidSoftwareInstallStatus = ( + s: string | undefined +): s is SoftwareInstallStatus => + !!s && SOFTWARE_INSTALL_STATUSES.includes(s as SoftwareInstallStatus); + +/** * ISoftwareInstallResult is the shape of a software install result object * returned by the Fleet API. - * */ export interface ISoftwareInstallResult { install_uuid: string; @@ -167,7 +179,7 @@ export interface ISoftwareInstallResult { software_package: string; host_id: number; host_display_name: string; - status: ISoftwareInstallStatus; + status: SoftwareInstallStatus; detail: string; output: string; pre_install_query_output: string; @@ -200,7 +212,7 @@ export interface IHostSoftware { package_available_for_install?: string | null; source: string; bundle_identifier?: string; - status: ISoftwareInstallStatus | null; + status: SoftwareInstallStatus | null; last_install: ISoftwareLastInstall | null; installed_versions: ISoftwareInstallVersion[] | null; } diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx index d0548e8563..f4863e93c3 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx @@ -1,7 +1,7 @@ import React, { useContext, useState } from "react"; import endpoints from "utilities/endpoints"; -import { ISoftwareInstallStatus, ISoftwarePackage } from "interfaces/software"; +import { SoftwareInstallStatus, ISoftwarePackage } from "interfaces/software"; import PATHS from "router/paths"; import { AppContext } from "context/app"; import { buildQueryStringFromParams } from "utilities/url"; @@ -27,7 +27,7 @@ interface IStatusDisplayOption { } const STATUS_DISPLAY_OPTIONS: Record< - ISoftwareInstallStatus, + SoftwareInstallStatus, IStatusDisplayOption > = { installed: { @@ -49,7 +49,7 @@ const STATUS_DISPLAY_OPTIONS: Record< interface IPackageStatusCountProps { softwareId: number; - status: ISoftwareInstallStatus; + status: SoftwareInstallStatus; count: number; teamId?: number; } diff --git a/frontend/pages/SoftwarePage/components/SoftwareInstallDetails/SoftwareInstallDetails.tsx b/frontend/pages/SoftwarePage/components/SoftwareInstallDetails/SoftwareInstallDetails.tsx index 87c2b377da..4c9c5fa77b 100644 --- a/frontend/pages/SoftwarePage/components/SoftwareInstallDetails/SoftwareInstallDetails.tsx +++ b/frontend/pages/SoftwarePage/components/SoftwareInstallDetails/SoftwareInstallDetails.tsx @@ -4,7 +4,7 @@ import { useQuery } from "react-query"; import { ISoftwareInstallResult, ISoftwareInstallResults, - ISoftwareInstallStatus, + SoftwareInstallStatus, } from "interfaces/software"; import softwareAPI from "services/entities/software"; @@ -18,13 +18,13 @@ import { IconNames } from "components/icons"; const baseClass = "software-install-details"; -const STATUS_ICONS: Record = { +const STATUS_ICONS: Record = { pending: "pending-outline", installed: "success-outline", failed: "error-outline", } as const; -const STATUS_PREDICATES: Record = { +const STATUS_PREDICATES: Record = { pending: "will install", installed: "installed", failed: "failed to install", diff --git a/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx b/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx index 7d7b3be7f7..74b1723dcc 100644 --- a/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx +++ b/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx @@ -10,6 +10,7 @@ export const MANAGE_HOSTS_PAGE_FILTER_KEYS = [ "policy_response", "macos_settings", "software_id", + HOSTS_QUERY_PARAMS.SOFTWARE_STATUS, "status", "mdm_id", "mdm_enrollment_status", @@ -32,6 +33,7 @@ export const MANAGE_HOSTS_PAGE_LABEL_INCOMPATIBLE_QUERY_PARAMS = [ "software_id", "software_version_id", "software_title_id", + HOSTS_QUERY_PARAMS.SOFTWARE_STATUS, "bootstrap_package", "macos_settings", HOSTS_QUERY_PARAMS.OS_SETTINGS, diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index 8c6dd2a85f..adb7ffbdfc 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -23,6 +23,7 @@ import hostsAPI, { ILoadHostsResponse, ISortOption, MacSettingsStatusQueryParam, + HOSTS_QUERY_PARAMS, } from "services/entities/hosts"; import hostCountAPI, { IHostsCountQueryKey, @@ -49,7 +50,11 @@ import { getErrorReason } from "interfaces/errors"; import { ILabel } from "interfaces/label"; import { IOperatingSystemVersion } from "interfaces/operating_system"; import { IPolicy, IStoredPolicyResponse } from "interfaces/policy"; -import { ITeam } from "interfaces/team"; +import { + isValidSoftwareInstallStatus, + SoftwareInstallStatus, +} from "interfaces/software"; +import { API_NO_TEAM_ID, ITeam } from "interfaces/team"; import { IEmptyTableProps } from "interfaces/empty_table"; import { DiskEncryptionStatus, @@ -162,6 +167,11 @@ const ManageHostsPage = ({ router, includeAllTeams: true, includeNoTeam: true, + overrideParamsOnTeamChange: { + // remove the software status filter when selecting all teams or no team + [HOSTS_QUERY_PARAMS.SOFTWARE_STATUS]: (newTeamId?: number) => + !newTeamId || newTeamId < 1, + }, }); const hostHiddenColumns = localStorage.getItem("hostHiddenColumns"); @@ -232,6 +242,11 @@ const ManageHostsPage = ({ queryParams?.software_title_id !== undefined ? parseInt(queryParams.software_title_id, 10) : undefined; + const softwareStatus = isValidSoftwareInstallStatus( + queryParams?.[HOSTS_QUERY_PARAMS.SOFTWARE_STATUS] + ) + ? (queryParams[HOSTS_QUERY_PARAMS.SOFTWARE_STATUS] as SoftwareInstallStatus) + : undefined; const status = isAcceptableStatus(queryParams?.status) ? queryParams?.status : undefined; @@ -380,6 +395,7 @@ const ManageHostsPage = ({ softwareId, softwareTitleId, softwareVersionId, + softwareStatus, status, mdmId, mdmEnrollmentStatus, @@ -423,6 +439,7 @@ const ManageHostsPage = ({ softwareId, softwareTitleId, softwareVersionId, + softwareStatus, status, mdmId, mdmEnrollmentStatus, @@ -517,10 +534,10 @@ const ManageHostsPage = ({ useEffect(() => { if ( location.search.match( - /software_id|software_version_id|software_title_id/gi + /software_id|software_version_id|software_title_id|software_status/gi ) ) { - // regex matches any of "software_id", "software_version_id", or "software_title_id" + // regex matches any of "software_id", "software_version_id", "software_title_id", or "software_status" // so we don't set the filtered hosts path in those cases return; } @@ -712,6 +729,25 @@ const ManageHostsPage = ({ ); }; + const handleSoftwareInstallStatausChange = ( + newStatus: SoftwareInstallStatus + ) => { + handleResetPageIndex(); + + router.replace( + getNextLocationPath({ + pathPrefix: PATHS.MANAGE_HOSTS, + routeTemplate, + routeParams, + queryParams: { + ...queryParams, + [HOSTS_QUERY_PARAMS.SOFTWARE_STATUS]: newStatus, + page: 0, // resets page index + }, + }) + ); + }; + const onAddLabelClick = () => { router.push(`${PATHS.NEW_LABEL}`); }; @@ -815,6 +851,10 @@ const ManageHostsPage = ({ newQueryParams.software_version_id = softwareVersionId; } else if (softwareTitleId) { newQueryParams.software_title_id = softwareTitleId; + if (softwareStatus && teamIdForApi && teamIdForApi > 0) { + // software_status is only valid when software_title_id is present and a team is selected + newQueryParams[HOSTS_QUERY_PARAMS.SOFTWARE_STATUS] = softwareStatus; + } } else if (mdmId) { newQueryParams.mdm_id = mdmId; } else if (mdmEnrollmentStatus) { @@ -864,6 +904,7 @@ const ManageHostsPage = ({ softwareId, softwareVersionId, softwareTitleId, + softwareStatus, mdmId, mdmEnrollmentStatus, munkiIssueId, @@ -1062,6 +1103,7 @@ const ManageHostsPage = ({ softwareId, softwareTitleId, softwareVersionId, + softwareStatus, osName, osVersionId, osVersion, @@ -1114,6 +1156,7 @@ const ManageHostsPage = ({ softwareId, softwareTitleId, softwareVersionId, + softwareStatus, osName, osVersionId, osVersion, @@ -1325,6 +1368,7 @@ const ManageHostsPage = ({ softwareId, softwareTitleId, softwareVersionId, + softwareStatus, status, mdmId, mdmEnrollmentStatus, @@ -1527,6 +1571,7 @@ const ManageHostsPage = ({ softwareId || softwareTitleId || softwareVersionId || + softwareStatus || osName || osVersionId || osVersion || @@ -1665,6 +1710,7 @@ const ManageHostsPage = ({ softwareId, softwareTitleId, softwareVersionId, + softwareStatus, mdmId, mdmEnrollmentStatus, lowDiskSpaceHosts, @@ -1696,6 +1742,9 @@ const ManageHostsPage = ({ handleChangeBootstrapPackageStatusFilter } onChangeMacSettingsFilter={handleMacSettingsStatusDropdownChange} + onChangeSoftwareInstallStatusFilter={ + handleSoftwareInstallStatausChange + } onClickEditLabel={onEditLabelClick} onClickDeleteLabel={toggleDeleteLabelModal} isSandboxMode={isSandboxMode} diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx index b122344957..036f4f5e89 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx @@ -15,6 +15,8 @@ import { } from "interfaces/mdm"; import { IMunkiIssuesAggregate } from "interfaces/macadmins"; import { IPolicy } from "interfaces/policy"; +import { SoftwareInstallStatus } from "interfaces/software"; + import { HOSTS_QUERY_PARAMS, MacSettingsStatusQueryParam, @@ -70,6 +72,7 @@ interface IHostsFilterBlockProps { osSettingsStatus?: MdmProfileStatus; diskEncryptionStatus?: DiskEncryptionStatus; bootstrapPackageStatus?: BootstrapPackageStatus; + softwareStatus?: SoftwareInstallStatus; }; selectedLabel?: ILabel; isOnlyObserver?: boolean; @@ -84,6 +87,9 @@ interface IHostsFilterBlockProps { onChangeMacSettingsFilter: ( newMacSettingsStatus: MacSettingsStatusQueryParam ) => void; + onChangeSoftwareInstallStatusFilter: ( + newStatus: SoftwareInstallStatus + ) => void; onClickEditLabel: (evt: React.MouseEvent) => void; onClickDeleteLabel: () => void; isSandboxMode?: boolean; @@ -117,6 +123,7 @@ const HostsFilterBlock = ({ osSettingsStatus, diskEncryptionStatus, bootstrapPackageStatus, + softwareStatus, }, selectedLabel, isOnlyObserver, @@ -127,6 +134,7 @@ const HostsFilterBlock = ({ onChangeDiskEncryptionStatusFilter, onChangeBootstrapPackageStatusFilter, onChangeMacSettingsFilter, + onChangeSoftwareInstallStatusFilter, onClickEditLabel, onClickDeleteLabel, isSandboxMode = false, @@ -254,7 +262,7 @@ const HostsFilterBlock = ({ ); }; - const renderSoftwareFilterBlock = () => { + const renderSoftwareFilterBlock = (additionalClearParams?: string[]) => { if (!softwareDetails) return null; const { name, version } = softwareDetails; @@ -264,6 +272,16 @@ const HostsFilterBlock = ({ } label = label.trim() || "Unknown software"; + const clearParams = [ + "software_id", + "software_version_id", + "software_title_id", + ]; + + if (additionalClearParams?.length) { + clearParams.push(...additionalClearParams); + } + // const TooltipDescription = ( // // Hosts with {name || "Unknown software"}, @@ -275,13 +293,7 @@ const HostsFilterBlock = ({ return ( - handleClearFilter([ - "software_id", - "software_version_id", - "software_title_id", - ]) - } + onClear={() => handleClearFilter(clearParams)} // tooltipDescription={TooltipDescription} /> ); @@ -452,6 +464,26 @@ const HostsFilterBlock = ({ ); }; + const renderSoftwareInstallStatusBlock = () => { + const OPTIONS = [ + { value: "installed", label: "Installed" }, + { value: "failed", label: "Failed" }, + { value: "pending", label: "Pending" }, + ]; + + return ( + <> + + {renderSoftwareFilterBlock([HOSTS_QUERY_PARAMS.SOFTWARE_STATUS])} + + ); + }; + const showSelectedLabel = selectedLabel && selectedLabel.type !== "all"; if ( @@ -461,6 +493,7 @@ const HostsFilterBlock = ({ softwareId || softwareTitleId || softwareVersionId || + softwareStatus || mdmId || mdmEnrollmentStatus || lowDiskSpaceHosts || @@ -501,6 +534,8 @@ const HostsFilterBlock = ({ return renderPoliciesFilterBlock(); case !!macSettingsStatus: return renderMacSettingsStatusFilterBlock(); + case !!softwareStatus: + return renderSoftwareInstallStatusBlock(); case !!softwareId || !!softwareVersionId || !!softwareTitleId: return renderSoftwareFilterBlock(); case !!mdmId: diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/_styles.scss index 752858973b..b270d85184 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/_styles.scss @@ -14,7 +14,8 @@ // NOTE: Look more into this styling &__os_settings-dropdown, - &__macsettings-dropdown { + &__macsettings-dropdown, + &__sw-install-status-dropdown { .Select-value { display: flex; align-items: center; 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 d141754d75..fd0d7d3fa6 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,6 @@ import React from "react"; -import { ISoftwareInstallStatus } from "interfaces/software"; +import { SoftwareInstallStatus } from "interfaces/software"; import { IHostActivityItemComponentPropsWithShowDetails } from "../../ActivityConfig"; import HostActivityItem from "../../HostActivityItem"; @@ -8,7 +8,7 @@ import ShowDetailsButton from "../../ShowDetailsButton"; const baseClass = "installed-software-activity-item"; -const STATUS_PREDICATES: Record = { +const STATUS_PREDICATES: Record = { failed: "failed to install", installed: "installed", pending: "told Fleet to install", @@ -21,7 +21,7 @@ export const getSoftwareInstallStatusPredicate = ( return STATUS_PREDICATES.pending; } return ( - STATUS_PREDICATES[status as ISoftwareInstallStatus] || + STATUS_PREDICATES[status as SoftwareInstallStatus] || STATUS_PREDICATES.pending ); }; diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx index 307b91ec2e..ba724f6f46 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx @@ -5,7 +5,7 @@ import { cloneDeep } from "lodash"; import { IHostSoftware, - ISoftwareInstallStatus, + SoftwareInstallStatus, formatSoftwareType, } from "interfaces/software"; import { @@ -49,7 +49,7 @@ type IVulnerabilitiesCellProps = IInstalledVersionsCellProps; const generateActions = ( softwareId: number, - status: ISoftwareInstallStatus | null, + status: SoftwareInstallStatus | null, installingSoftwareId: number | null, canInstall: boolean, packageToInstall?: string | null diff --git a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx index c83d154ff2..abad97d41c 100644 --- a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx +++ b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from "react"; -import { ISoftwareInstallStatus } from "interfaces/software"; +import { SoftwareInstallStatus } from "interfaces/software"; import { dateAgo } from "utilities/date_format"; import Icon from "components/Icon"; @@ -9,7 +9,7 @@ import TextCell from "components/TableContainer/DataTable/TextCell"; const baseClass = "install-status-cell"; -type IStatusValue = ISoftwareInstallStatus | "avaiableForInstall"; +type IStatusValue = SoftwareInstallStatus | "avaiableForInstall"; type IStatusDisplayConfig = { iconName: "success" | "pending-outline" | "error" | "install"; @@ -56,7 +56,7 @@ const CELL_DISPLAY_OPTIONS: Record = { }; interface IInstallStatusCellProps { - status: ISoftwareInstallStatus | null; + status: SoftwareInstallStatus | null; packageToInstall?: string | null; installedAt?: string; } diff --git a/frontend/services/entities/host_count.ts b/frontend/services/entities/host_count.ts index a118b3698c..e0093e3bdb 100644 --- a/frontend/services/entities/host_count.ts +++ b/frontend/services/entities/host_count.ts @@ -42,6 +42,7 @@ export interface IHostCountLoadOptions { softwareId?: number; softwareTitleId?: number; softwareVersionId?: number; + softwareStatus?: string; lowDiskSpaceHosts?: number; mdmId?: number; mdmEnrollmentStatus?: string; @@ -67,6 +68,7 @@ export default { const softwareId = options?.softwareId; const softwareTitleId = options?.softwareTitleId; const softwareVersionId = options?.softwareVersionId; + const softwareStatus = options?.softwareStatus; const macSettingsStatus = options?.macSettingsStatus; const status = options?.status; const mdmId = options?.mdmId; @@ -89,7 +91,10 @@ export default { macSettingsStatus, osSettings, }), + // TODO: shouldn't macSettingsStatus be included in the mutually exclusive query params too? + // If so, this todo applies in other places. ...reconcileMutuallyExclusiveHostParams({ + teamId, label, policyId, policyResponse, @@ -98,6 +103,7 @@ export default { munkiIssueId, softwareId, softwareTitleId, + softwareStatus, softwareVersionId, lowDiskSpaceHosts, osName, diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts index 8ff6ab8789..a409e7577a 100644 --- a/frontend/services/entities/hosts.ts +++ b/frontend/services/entities/hosts.ts @@ -9,7 +9,12 @@ import { reconcileMutuallyInclusiveHostParams, } from "utilities/url"; import { SelectedPlatform } from "interfaces/platform"; -import { ISoftwareTitle, ISoftware, IHostSoftware } from "interfaces/software"; +import { + IHostSoftware, + ISoftwareTitle, + ISoftware, + SoftwareInstallStatus, +} from "interfaces/software"; import { DiskEncryptionStatus, BootstrapPackageStatus, @@ -46,6 +51,7 @@ export type IUnlockHostResponse = export const HOSTS_QUERY_PARAMS = { OS_SETTINGS: "os_settings", DISK_ENCRYPTION: "os_settings_disk_encryption", + SOFTWARE_STATUS: "software_status", } as const; export interface ILoadHostsQueryKey extends ILoadHostsOptions { @@ -67,6 +73,7 @@ export interface ILoadHostsOptions { softwareId?: number; softwareTitleId?: number; softwareVersionId?: number; + softwareStatus?: SoftwareInstallStatus; status?: HostStatus; mdmId?: number; mdmEnrollmentStatus?: string; @@ -97,6 +104,7 @@ export interface IExportHostsOptions { softwareId?: number; softwareTitleId?: number; softwareVersionId?: number; + softwareStatus?: SoftwareInstallStatus; status?: HostStatus; mdmId?: number; munkiIssueId?: number; @@ -126,6 +134,7 @@ export interface IActionByFilter { softwareId?: number | null; softwareTitleId?: number | null; softwareVersionId?: number | null; + softwareStatus?: SoftwareInstallStatus; osName?: string; osVersion?: string; osVersionId?: number | null; @@ -220,6 +229,7 @@ export default { softwareId, softwareTitleId, softwareVersionId, + softwareStatus, osName, osVersion, osVersionId, @@ -245,6 +255,7 @@ export default { software_id: softwareId, software_title_id: softwareTitleId, software_version_id: softwareVersionId, + [HOSTS_QUERY_PARAMS.SOFTWARE_STATUS]: softwareStatus, os_name: osName, os_version: osVersion, os_version_id: osVersionId, @@ -270,6 +281,7 @@ export default { const softwareId = options?.softwareId; const softwareTitleId = options?.softwareTitleId; const softwareVersionId = options?.softwareVersionId; + const softwareStatus = options?.softwareStatus; const macSettingsStatus = options?.macSettingsStatus; const osName = options?.osName; const osVersionId = options?.osVersionId; @@ -301,6 +313,7 @@ export default { osSettings, }), ...reconcileMutuallyExclusiveHostParams({ + teamId, label, policyId, policyResponse, @@ -310,6 +323,7 @@ export default { softwareId, softwareTitleId, softwareVersionId, + softwareStatus, osName, osVersionId, osVersion, @@ -354,6 +368,7 @@ export default { softwareId, softwareTitleId, softwareVersionId, + softwareStatus, status, mdmId, mdmEnrollmentStatus, @@ -388,6 +403,7 @@ export default { osSettings, }), ...reconcileMutuallyExclusiveHostParams({ + teamId, label, policyId, policyResponse, @@ -397,6 +413,7 @@ export default { softwareId, softwareTitleId, softwareVersionId, + softwareStatus, lowDiskSpaceHosts, osVersionId, osName, @@ -462,6 +479,7 @@ export default { softwareId, softwareTitleId, softwareVersionId, + softwareStatus, osName, osVersion, osVersionId, @@ -488,6 +506,7 @@ export default { software_id: softwareId, software_title_id: softwareTitleId, software_version_id: softwareVersionId, + [HOSTS_QUERY_PARAMS.SOFTWARE_STATUS]: softwareStatus, os_name: osName, os_version: osVersion, os_version_id: osVersionId, diff --git a/frontend/utilities/url/index.ts b/frontend/utilities/url/index.ts index 0d12e7071e..5171d25ac1 100644 --- a/frontend/utilities/url/index.ts +++ b/frontend/utilities/url/index.ts @@ -9,6 +9,7 @@ import { HOSTS_QUERY_PARAMS, MacSettingsStatusQueryParam, } from "services/entities/hosts"; +import { isValidSoftwareInstallStatus } from "interfaces/software"; type QueryValues = string | number | boolean | undefined | null; export type QueryParams = Record; @@ -23,6 +24,7 @@ interface IMutuallyInclusiveHostParams { } interface IMutuallyExclusiveHostParams { + teamId?: number; label?: string; policyId?: number; policyResponse?: string; @@ -33,6 +35,7 @@ interface IMutuallyExclusiveHostParams { softwareId?: number; softwareVersionId?: number; softwareTitleId?: number; + softwareStatus?: string; osVersionId?: number; osName?: string; osVersion?: string; @@ -77,6 +80,48 @@ export const buildQueryStringFromParams = (queryParams: QueryParams) => { return queryString; }; +export const reconcileSoftwareParams = ({ + teamId, + softwareId, + softwareVersionId, + softwareTitleId, + softwareStatus, +}: Pick< + IMutuallyExclusiveHostParams, + | "teamId" + | "softwareId" + | "softwareVersionId" + | "softwareTitleId" + | "softwareStatus" +>) => { + if ( + isValidSoftwareInstallStatus(softwareStatus) && + softwareTitleId && + teamId && + teamId > 0 + ) { + return { + software_title_id: softwareTitleId, + [HOSTS_QUERY_PARAMS.SOFTWARE_STATUS]: softwareStatus, + team_id: teamId, + }; + } + + if (softwareTitleId) { + return { software_title_id: softwareTitleId }; + } + + if (softwareVersionId) { + return { software_version_id: softwareVersionId }; + } + + if (softwareId) { + return { software_id: softwareId }; + } + + return {}; +}; + export const reconcileMutuallyInclusiveHostParams = ({ label, teamId, @@ -102,9 +147,12 @@ export const reconcileMutuallyInclusiveHostParams = ({ reconciled[HOSTS_QUERY_PARAMS.OS_SETTINGS] = osSettings; reconciled.team_id = teamId ?? 0; } + return reconciled; }; + export const reconcileMutuallyExclusiveHostParams = ({ + teamId, label, policyId, policyResponse, @@ -115,6 +163,7 @@ export const reconcileMutuallyExclusiveHostParams = ({ softwareId, softwareVersionId, softwareTitleId, + softwareStatus, osVersionId, osName, osVersion, @@ -147,8 +196,17 @@ export const reconcileMutuallyExclusiveHostParams = ({ return { mdm_enrollment_status: mdmEnrollmentStatus }; case !!munkiIssueId: return { munki_issue_id: munkiIssueId }; - case !!softwareTitleId: - return { software_title_id: softwareTitleId }; + case !!softwareStatus || + !!softwareTitleId || + !!softwareVersionId || + !!softwareId: + return reconcileSoftwareParams({ + teamId, + softwareId, + softwareVersionId, + softwareTitleId, + softwareStatus, + }); case !!softwareVersionId: return { software_version_id: softwareVersionId }; case !!softwareId: