Fleet UI: VPP auto install software on failed policies, filter software compatible to policy's target platform, etc (#25202)

This commit is contained in:
RachelElysia 2025-01-13 19:45:16 -05:00 committed by GitHub
parent 588ccd967a
commit 43628439d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 235 additions and 149 deletions

View file

@ -0,0 +1,2 @@
- Improved software installation for failed policies: Added platform-specific filtering in the software dropdown, ensuring only compatible software are displayed based on each policy's targeted platforms
- Add VPP app to automatic installation dropdown for failed policies and auto install information on VPP app details page

View file

@ -9,7 +9,7 @@ import { find } from "lodash";
import { osqueryTables } from "utilities/osquery_tables";
import { IOsQueryTable, DEFAULT_OSQUERY_TABLE } from "interfaces/osquery_table";
import { SelectedPlatformString } from "interfaces/platform";
import { CommaSeparatedPlatformString } from "interfaces/platform";
enum ACTIONS {
SET_LAST_EDITED_QUERY_INFO = "SET_LAST_EDITED_QUERY_INFO",
@ -25,7 +25,7 @@ interface ISetLastEditedQueryInfo {
lastEditedQueryBody?: string;
lastEditedQueryResolution?: string;
lastEditedQueryCritical?: boolean;
lastEditedQueryPlatform?: SelectedPlatformString | null;
lastEditedQueryPlatform?: CommaSeparatedPlatformString | null;
defaultPolicy?: boolean;
}
@ -55,7 +55,7 @@ type InitialStateType = {
lastEditedQueryBody: string;
lastEditedQueryResolution: string;
lastEditedQueryCritical: boolean;
lastEditedQueryPlatform: SelectedPlatformString | null;
lastEditedQueryPlatform: CommaSeparatedPlatformString | null;
defaultPolicy: boolean;
setLastEditedQueryId: (value: number | null) => void;
setLastEditedQueryName: (value: string) => void;
@ -63,7 +63,9 @@ type InitialStateType = {
setLastEditedQueryBody: (value: string) => void;
setLastEditedQueryResolution: (value: string) => void;
setLastEditedQueryCritical: (value: boolean) => void;
setLastEditedQueryPlatform: (value: SelectedPlatformString | null) => void;
setLastEditedQueryPlatform: (
value: CommaSeparatedPlatformString | null
) => void;
setDefaultPolicy: (value: boolean) => void;
policyTeamId: number;
setPolicyTeamId: (id: number) => void;
@ -216,7 +218,9 @@ const PolicyProvider = ({ children }: Props): JSX.Element => {
[]
);
const setLastEditedQueryPlatform = useCallback(
(lastEditedQueryPlatform: SelectedPlatformString | null | undefined) => {
(
lastEditedQueryPlatform: CommaSeparatedPlatformString | null | undefined
) => {
dispatch({
type: ACTIONS.SET_LAST_EDITED_QUERY_INFO,
lastEditedQueryPlatform,

View file

@ -4,7 +4,7 @@ import { find } from "lodash";
import { osqueryTables } from "utilities/osquery_tables";
import { DEFAULT_QUERY } from "utilities/constants";
import { DEFAULT_OSQUERY_TABLE, IOsQueryTable } from "interfaces/osquery_table";
import { SelectedPlatformString } from "interfaces/platform";
import { CommaSeparatedPlatformString } from "interfaces/platform";
import { QueryLoggingOption } from "interfaces/schedulable_query";
import {
DEFAULT_TARGETS,
@ -26,7 +26,7 @@ type InitialStateType = {
lastEditedQueryObserverCanRun: boolean;
lastEditedQueryFrequency: number;
lastEditedQueryAutomationsEnabled: boolean;
lastEditedQueryPlatforms: SelectedPlatformString;
lastEditedQueryPlatforms: CommaSeparatedPlatformString;
lastEditedQueryMinOsqueryVersion: string;
lastEditedQueryLoggingType: QueryLoggingOption;
lastEditedQueryDiscardData: boolean;
@ -40,7 +40,7 @@ type InitialStateType = {
setLastEditedQueryObserverCanRun: (value: boolean) => void;
setLastEditedQueryFrequency: (value: number) => void;
setLastEditedQueryAutomationsEnabled: (value: boolean) => void;
setLastEditedQueryPlatforms: (value: SelectedPlatformString) => void;
setLastEditedQueryPlatforms: (value: CommaSeparatedPlatformString) => void;
setLastEditedQueryMinOsqueryVersion: (value: string) => void;
setLastEditedQueryLoggingType: (value: string) => void;
setLastEditedQueryDiscardData: (value: boolean) => void;

View file

@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from "react";
import { forEach } from "lodash";
import {
SelectedPlatformString,
CommaSeparatedPlatformString,
QUERYABLE_PLATFORMS,
QueryablePlatform,
} from "interfaces/platform";
@ -21,7 +21,7 @@ export interface IPlatformSelector {
}
const usePlatformSelector = (
platformContext: SelectedPlatformString | null | undefined,
platformContext: CommaSeparatedPlatformString | null | undefined,
baseClass = "",
disabled = false,
installSoftware: IPolicySoftwareToInstall | undefined,

View file

@ -55,7 +55,7 @@ export const VULN_SUPPORTED_PLATFORMS: Platform[] = ["darwin", "windows"];
export type SelectedPlatform = QueryablePlatform | "all";
export type SelectedPlatformString =
export type CommaSeparatedPlatformString =
| ""
| QueryablePlatform
| `${QueryablePlatform},${QueryablePlatform}`

View file

@ -1,5 +1,5 @@
import PropTypes from "prop-types";
import { SelectedPlatformString } from "interfaces/platform";
import { CommaSeparatedPlatformString } from "interfaces/platform";
import { IScript } from "./script";
// Legacy PropTypes used on host interface
@ -36,7 +36,7 @@ export interface IPolicy {
author_name: string;
author_email: string;
resolution: string;
platform: SelectedPlatformString;
platform: CommaSeparatedPlatformString;
team_id: number | null;
created_at: string;
updated_at: string;
@ -99,7 +99,7 @@ export interface IPolicyFormData {
description?: string | number | boolean | undefined;
resolution?: string | number | boolean | undefined;
critical?: boolean;
platform?: SelectedPlatformString;
platform?: CommaSeparatedPlatformString;
name?: string | number | boolean | undefined;
query?: string | number | boolean | undefined;
team_id?: number | null;
@ -118,6 +118,6 @@ export interface IPolicyNew {
query: string;
resolution: string;
critical: boolean;
platform: SelectedPlatformString;
platform: CommaSeparatedPlatformString;
mdm_required?: boolean;
}

View file

@ -4,7 +4,7 @@ import PropTypes from "prop-types";
import { IFormField } from "./form_field";
import { IPack } from "./pack";
import {
SelectedPlatformString,
CommaSeparatedPlatformString,
QueryablePlatform,
SelectedPlatform,
} from "./platform";
@ -19,7 +19,7 @@ export interface ISchedulableQuery {
query: string;
team_id: number | null;
interval: number;
platform: SelectedPlatformString; // Might more accurately be called `platforms_to_query` or `targeted_platforms` comma-sepparated string of platforms to query, default all platforms if ommitted
platform: CommaSeparatedPlatformString; // Might more accurately be called `platforms_to_query` or `targeted_platforms` comma-separated string of platforms to query, default all platforms if omitted
min_osquery_version: string;
automations_enabled: boolean;
logging: QueryLoggingOption;
@ -90,7 +90,7 @@ export interface ICreateQueryRequestBody {
discard_data?: boolean;
team_id?: number; // global query if ommitted
interval?: number; // default 0 means never run
platform?: SelectedPlatformString; // Might more accurately be called `platforms_to_query` comma-sepparated string of platforms to query, default all platforms if ommitted
platform?: CommaSeparatedPlatformString; // Might more accurately be called `platforms_to_query` comma-separated string of platforms to query, default all platforms if omitted
min_osquery_version?: string; // default all versions if ommitted
automations_enabled?: boolean; // whether to send data to the configured log destination according to the query's `interval`. Default false if ommitted.
logging?: QueryLoggingOption;
@ -109,7 +109,7 @@ export interface IModifyQueryRequestBody
observer_can_run?: boolean;
discard_data?: boolean;
frequency?: number;
platform?: SelectedPlatformString;
platform?: CommaSeparatedPlatformString;
min_osquery_version?: string;
automations_enabled?: boolean;
}
@ -144,7 +144,7 @@ export interface IEditQueryFormFields {
discard_data: IFormField<boolean>;
frequency: IFormField<number>;
automations_enabled: IFormField<boolean>;
platforms: IFormField<SelectedPlatformString>;
platforms: IFormField<CommaSeparatedPlatformString>;
min_osquery_version: IFormField<string>;
logging: IFormField<QueryLoggingOption>;
}

View file

@ -35,7 +35,7 @@ export interface ISoftware {
name: string; // e.g., "Figma.app"
version: string; // e.g., "2.1.11"
bundle_identifier?: string | null; // e.g., "com.figma.Desktop"
source: string; // "apps" | "ipados_apps" | "ios_apps" | "programs" | ?
source: string; // "apps" | "ipados_apps" | "ios_apps" | "programs" | "rpm_packages" | "deb_packages" | ?
generated_cpe: string;
vulnerabilities: ISoftwareVulnerability[] | null;
hosts_count?: number;
@ -56,7 +56,7 @@ export interface ISoftwareTitleVersion {
hosts_count?: number;
}
export interface ISoftwarePackagePolicy {
export interface ISoftwareInstallPolicy {
id: number;
name: string;
}
@ -82,7 +82,7 @@ export interface ISoftwarePackage {
pending_uninstall: number;
failed_uninstall: number;
};
automatic_install_policies?: ISoftwarePackagePolicy[] | null;
automatic_install_policies?: ISoftwareInstallPolicy[] | null;
install_during_setup?: boolean;
labels_include_any: ILabelSoftwareTitle[] | null;
labels_exclude_any: ILabelSoftwareTitle[] | null;
@ -105,6 +105,17 @@ export interface IAppStoreApp {
failed: number;
};
install_during_setup?: boolean;
automatic_install_policies?: ISoftwareInstallPolicy[] | null;
last_install?: {
install_uuid: string;
command_uuid: string;
installed_at: string;
} | null;
last_uninstall?: {
script_execution_id: string;
uninstalled_at: string;
} | null;
version?: string;
}
export interface ISoftwareTitle {
@ -185,6 +196,32 @@ export const SOURCE_TYPE_CONVERSION = {
export type SoftwareSource = keyof typeof SOURCE_TYPE_CONVERSION;
/** Map installable software source to platform */
export const INSTALLABLE_SOURCE_PLATFORM_CONVERSION = {
apt_sources: "linux",
deb_packages: "linux",
portage_packages: "linux",
rpm_packages: "linux",
yum_sources: "linux",
npm_packages: null,
atom_packages: null,
python_packages: null,
apps: "darwin",
ios_apps: "ios",
ipados_apps: "ipados",
chrome_extensions: null,
firefox_addons: null,
safari_extensions: null,
homebrew_packages: "darwin",
programs: "windows",
ie_extensions: null,
chocolatey_packages: "windows",
pkg_packages: "darwin",
vscode_extensions: null,
} as const;
export type InstallableSoftwareSource = keyof typeof INSTALLABLE_SOURCE_PLATFORM_CONVERSION;
const BROWSER_TYPE_CONVERSION = {
chrome: "Chrome",
chromium: "Chromium",

View file

@ -123,7 +123,7 @@ const FleetMaintainedAppDetailsPage = ({
isError: isErrorFleetApp,
} = useQuery(
["fleet-maintained-app", appId],
() => softwareAPI.getFleetMainainedApp(appId),
() => softwareAPI.getFleetMaintainedApp(appId),
{
...DEFAULT_USE_QUERY_OPTIONS,
enabled: isPremiumTier,

View file

@ -1,6 +1,6 @@
import React from "react";
import { ISoftwarePackagePolicy } from "interfaces/software";
import { ISoftwareInstallPolicy } from "interfaces/software";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
@ -11,7 +11,7 @@ const baseClass = "automatic-install-modal";
interface IPoliciesListItemProps {
teamId: number;
policy: ISoftwarePackagePolicy;
policy: ISoftwareInstallPolicy;
}
const PoliciesListItem = ({ teamId, policy }: IPoliciesListItemProps) => {
@ -24,7 +24,7 @@ const PoliciesListItem = ({ teamId, policy }: IPoliciesListItemProps) => {
interface IPoliciesListProps {
teamId: number;
policies: ISoftwarePackagePolicy[];
policies: ISoftwareInstallPolicy[];
}
const PoliciesList = ({ teamId, policies }: IPoliciesListProps) => {
@ -39,7 +39,7 @@ const PoliciesList = ({ teamId, policies }: IPoliciesListProps) => {
interface IAutomaticInstallModalProps {
teamId: number;
policies: ISoftwarePackagePolicy[];
policies: ISoftwareInstallPolicy[];
onExit: () => void;
}

View file

@ -18,7 +18,7 @@ const DELETE_SW_INSTALLED_DURING_SETUP_ERROR_MSG =
interface IDeleteSoftwareModalProps {
softwareId: number;
teamId: number;
softwarePackageName?: string;
softwareInstallerName?: string;
onExit: () => void;
onSuccess: () => void;
}
@ -26,7 +26,7 @@ interface IDeleteSoftwareModalProps {
const DeleteSoftwareModal = ({
softwareId,
teamId,
softwarePackageName,
softwareInstallerName,
onExit,
onSuccess,
}: IDeleteSoftwareModalProps) => {
@ -36,7 +36,7 @@ const DeleteSoftwareModal = ({
const onDeleteSoftware = useCallback(async () => {
setIsDeleting(true);
try {
await softwareAPI.deleteSoftwarePackage(softwareId, teamId);
await softwareAPI.deleteSoftwareInstaller(softwareId, teamId);
renderFlash("success", "Software deleted successfully!");
onSuccess();
} catch (error) {
@ -64,9 +64,9 @@ const DeleteSoftwareModal = ({
<p>
Software won&apos;t be uninstalled from existing hosts, but any
pending installs and uninstalls{" "}
{softwarePackageName ? (
{softwareInstallerName ? (
<>
for <b> {softwarePackageName}</b>{" "}
for <b> {softwareInstallerName}</b>{" "}
</>
) : (
""

View file

@ -8,7 +8,11 @@ import React, {
import PATHS from "router/paths";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import { ISoftwarePackage } from "interfaces/software";
import {
ISoftwarePackage,
IAppStoreApp,
isSoftwarePackage,
} from "interfaces/software";
import softwareAPI from "services/entities/software";
import { buildQueryStringFromParams } from "utilities/url";
@ -131,19 +135,19 @@ const STATUS_DISPLAY_OPTIONS: Record<
},
};
interface IPackageStatusCountProps {
interface IInstallerStatusCountProps {
softwareId: number;
status: SoftwareInstallDisplayStatus;
count: number;
teamId?: number;
}
const PackageStatusCount = ({
const InstallerStatusCount = ({
softwareId,
status,
count,
teamId,
}: IPackageStatusCountProps) => {
}: IInstallerStatusCountProps) => {
const displayData = STATUS_DISPLAY_OPTIONS[status];
const linkUrl = `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams({
software_title_id: softwareId,
@ -177,14 +181,14 @@ const PackageStatusCount = ({
};
interface IActionsDropdownProps {
isSoftwarePackage: boolean;
isPackage: boolean;
onDownloadClick: () => void;
onDeleteClick: () => void;
onEditSoftwareClick: () => void;
}
const SoftwareActionsDropdown = ({
isSoftwarePackage,
isPackage,
onDownloadClick,
onDeleteClick,
onEditSoftwareClick,
@ -212,7 +216,7 @@ const SoftwareActionsDropdown = ({
onChange={onSelect}
placeholder="Actions"
options={
isSoftwarePackage
isPackage
? [...SOFTWARE_PACKAGE_DROPDOWN_OPTIONS]
: [...APP_STORE_APP_DROPDOWN_OPTIONS]
}
@ -222,7 +226,7 @@ const SoftwareActionsDropdown = ({
);
};
interface ISoftwarePackageCardProps {
interface ISoftwareInstallerCardProps {
name: string;
version: string;
uploadedAt: string; // TODO: optional?
@ -234,8 +238,7 @@ interface ISoftwarePackageCardProps {
isSelfService: boolean;
softwareId: number;
teamId: number;
// NOTE: we will only have this if we are working with a software package.
softwarePackage?: ISoftwarePackage;
softwareInstaller: ISoftwarePackage | IAppStoreApp;
onDelete: () => void;
refetchSoftwareTitle: () => void;
}
@ -243,18 +246,19 @@ interface ISoftwarePackageCardProps {
// NOTE: This component is dependent on having either a software package
// (ISoftwarePackage) or an app store app (IAppStoreApp). If we add more types
// of packages we should consider refactoring this to be more dynamic.
const SoftwarePackageCard = ({
const SoftwareInstallerCard = ({
name,
version,
uploadedAt,
status,
isSelfService,
softwarePackage,
softwareInstaller,
softwareId,
teamId,
onDelete,
refetchSoftwareTitle,
}: ISoftwarePackageCardProps) => {
}: ISoftwareInstallerCardProps) => {
const isPackage = isSoftwarePackage(softwareInstaller);
const {
isGlobalAdmin,
isGlobalMaintainer,
@ -304,7 +308,7 @@ const SoftwarePackageCard = ({
}, [renderFlash, softwareId, name, teamId]);
const renderIcon = () => {
return softwarePackage ? (
return isPackage ? (
<Graphic name="file-pkg" />
) : (
<SoftwareIcon name="appStore" size="medium" />
@ -338,13 +342,13 @@ const SoftwarePackageCard = ({
<div className={`${baseClass}__main-info`}>
{renderIcon()}
<div className={`${baseClass}__info`}>
<SoftwareName name={softwarePackage?.name || name} />
<SoftwareName name={softwareInstaller?.name || name} />
<span className={`${baseClass}__details`}>{renderDetails()}</span>
</div>
</div>
<div className={`${baseClass}__actions-wrapper`}>
{softwarePackage?.automatic_install_policies &&
softwarePackage?.automatic_install_policies.length > 0 && (
{softwareInstaller?.automatic_install_policies &&
softwareInstaller?.automatic_install_policies.length > 0 && (
<TooltipWrapper
showArrow
position="top"
@ -361,7 +365,7 @@ const SoftwarePackageCard = ({
{isSelfService && <Tag icon="user" text="Self-service" />}
{showActions && (
<SoftwareActionsDropdown
isSoftwarePackage={!!softwarePackage}
isPackage={isPackage}
onDownloadClick={onDownloadClick}
onDeleteClick={onDeleteClick}
onEditSoftwareClick={onEditSoftwareClick}
@ -369,31 +373,31 @@ const SoftwarePackageCard = ({
)}
</div>
</div>
<div className={`${baseClass}__package-statuses`}>
<PackageStatusCount
<div className={`${baseClass}__installer-statuses`}>
<InstallerStatusCount
softwareId={softwareId}
status="installed"
count={status.installed}
teamId={teamId}
/>
<PackageStatusCount
<InstallerStatusCount
softwareId={softwareId}
status="pending"
count={status.pending}
teamId={teamId}
/>
<PackageStatusCount
<InstallerStatusCount
softwareId={softwareId}
status="failed"
count={status.failed}
teamId={teamId}
/>
</div>
{showEditSoftwareModal && softwarePackage && (
{showEditSoftwareModal && isPackage && (
<EditSoftwareModal
softwareId={softwareId}
teamId={teamId}
software={softwarePackage}
software={softwareInstaller}
onExit={() => setShowEditSoftwareModal(false)}
refetchSoftwareTitle={refetchSoftwareTitle}
/>
@ -401,18 +405,18 @@ const SoftwarePackageCard = ({
{showDeleteModal && (
<DeleteSoftwareModal
softwareId={softwareId}
softwarePackageName={softwarePackage?.name}
softwareInstallerName={softwareInstaller?.name}
teamId={teamId}
onExit={() => setShowDeleteModal(false)}
onSuccess={onDeleteSuccess}
/>
)}
{showAutomaticInstallModal &&
softwarePackage?.automatic_install_policies &&
softwarePackage?.automatic_install_policies.length > 0 && (
softwareInstaller?.automatic_install_policies &&
softwareInstaller?.automatic_install_policies.length > 0 && (
<AutomaticInstallModal
teamId={teamId}
policies={softwarePackage.automatic_install_policies}
policies={softwareInstaller.automatic_install_policies}
onExit={() => setShowAutomaticInstallModal(false)}
/>
)}
@ -420,4 +424,4 @@ const SoftwarePackageCard = ({
);
};
export default SoftwarePackageCard;
export default SoftwareInstallerCard;

View file

@ -35,7 +35,7 @@
font-size: $xx-small;
}
&__package-statuses {
&__installer-statuses {
display: flex;
align-items: flex-start;
align-self: stretch;

View file

@ -139,7 +139,7 @@ const SoftwareTitleDetailsPage = ({
const packageCardData = getPackageCardInfo(title);
return (
<SoftwarePackageCard
softwarePackage={packageCardData.softwarePackage}
softwareInstaller={packageCardData.softwarePackage}
name={packageCardData.name}
version={packageCardData.version}
uploadedAt={packageCardData.uploadedAt}

View file

@ -71,7 +71,7 @@ describe("SoftwareTitleDetailsPage helpers", () => {
};
const packageCardInfo = getPackageCardInfo(softwareTitle);
expect(packageCardInfo).toEqual({
softwarePackage: undefined,
softwarePackage: softwareTitle.app_store_app,
name: "Test Software", // apps should display the software title name (backend should ensure the app name and software title name match)
version: "1.0.1",
uploadedAt: "",

View file

@ -25,7 +25,7 @@ export const getPackageCardInfo = (softwareTitle: ISoftwareTitleDetails) => {
const isPackage = isSoftwarePackage(packageData);
return {
softwarePackage: isPackage ? packageData : undefined,
softwarePackage: packageData,
name: (isPackage && packageData.name) || softwareTitle.name,
version:
(isSoftwarePackage(packageData)

View file

@ -75,18 +75,20 @@ const getSoftwareNameCellData = (
if (software_package) {
hasPackage = true;
isSelfService = software_package.self_service;
if (
installType =
software_package.automatic_install_policies &&
software_package.automatic_install_policies.length > 0
) {
installType = "automatic";
} else {
installType = "manual";
}
? "automatic"
: "manual";
} else if (app_store_app) {
hasPackage = true;
isSelfService = app_store_app.self_service;
iconUrl = app_store_app.icon_url;
installType =
app_store_app.automatic_install_policies &&
app_store_app.automatic_install_policies.length > 0
? "automatic"
: "manual";
}
const isAllTeams = teamId === undefined;

View file

@ -84,7 +84,7 @@ const [
DEFAULT_AUTOMATION_UPDATE_ERR_MSG,
] = [
"Successfully updated policy automations.",
"Could not update policy automations. Please try again.",
"Could not update policy automations.",
];
const baseClass = "manage-policies-page";

View file

@ -1,9 +1,14 @@
import React, { useCallback, useState } from "react";
import React, { useCallback, useMemo, useState } from "react";
import { useQuery } from "react-query";
import { omit } from "lodash";
import { IPolicyStats } from "interfaces/policy";
import {
CommaSeparatedPlatformString,
Platform,
PLATFORM_DISPLAY_NAMES,
} from "interfaces/platform";
import softwareAPI, {
ISoftwareTitlesQueryKey,
ISoftwareTitlesResponse,
@ -19,30 +24,21 @@ import Checkbox from "components/forms/fields/Checkbox";
import TooltipTruncatedText from "components/TooltipTruncatedText";
import CustomLink from "components/CustomLink";
import Button from "components/buttons/Button";
import { ISoftwareTitle } from "interfaces/software";
import {
INSTALLABLE_SOURCE_PLATFORM_CONVERSION,
InstallableSoftwareSource,
ISoftwareTitle,
} from "interfaces/software";
import TooltipWrapper from "components/TooltipWrapper";
const getPlatformDisplayFromPackageExtension = (ext: string | undefined) => {
switch (ext) {
case "pkg":
case "zip":
case "dmg":
return "macOS";
case "deb":
case "rpm":
return "Linux";
case "exe":
case "msi":
return "Windows";
default:
return null;
}
};
const AFI_SOFTWARE_BATCH_SIZE = 1000;
const SOFTWARE_TITLE_LIST_LENGTH = 1000;
const baseClass = "install-software-modal";
const formatSoftwarePlatform = (source: InstallableSoftwareSource) => {
return INSTALLABLE_SOURCE_PLATFORM_CONVERSION[source] || null;
};
interface ISwDropdownField {
name: string;
value: number;
@ -52,10 +48,16 @@ interface IFormPolicy {
id: number;
installSoftwareEnabled: boolean;
swIdToInstall?: number;
platform: CommaSeparatedPlatformString;
}
export type IInstallSoftwareFormData = IFormPolicy[];
interface IEnhancedSoftwareTitle extends ISoftwareTitle {
platform: Platform | null;
extension?: string;
}
interface IInstallSoftwareModal {
onExit: () => void;
onSubmit: (formData: IInstallSoftwareFormData) => void;
@ -63,6 +65,7 @@ interface IInstallSoftwareModal {
policies: IPolicyStats[];
teamId: number;
}
const InstallSoftwareModal = ({
onExit,
onSubmit,
@ -76,6 +79,7 @@ const InstallSoftwareModal = ({
id: policy.id,
installSoftwareEnabled: !!policy.install_software,
swIdToInstall: policy.install_software?.software_title_id,
platform: policy.platform,
}))
);
@ -84,32 +88,40 @@ const InstallSoftwareModal = ({
);
const {
data: titlesAFI,
isLoading: isTitlesAFILoading,
isError: isTitlesAFIError,
data: titlesAvailableForInstall,
isLoading: isTitlesAvailableForInstallLoading,
isError: isTitlesAvailableForInstallError,
} = useQuery<
ISoftwareTitlesResponse,
Error,
ISoftwareTitle[],
IEnhancedSoftwareTitle[],
[ISoftwareTitlesQueryKey]
>(
[
{
scope: "software-titles",
page: 0,
perPage: AFI_SOFTWARE_BATCH_SIZE,
perPage: SOFTWARE_TITLE_LIST_LENGTH,
query: "",
orderDirection: "desc",
orderKey: "hosts_count",
teamId,
availableForInstall: true,
packagesOnly: true,
platform: "darwin,windows,linux",
},
],
({ queryKey: [queryKey] }) =>
softwareAPI.getSoftwareTitles(omit(queryKey, "scope")),
{
select: (data) => data.software_titles,
select: (data): IEnhancedSoftwareTitle[] =>
data.software_titles.map((title) => {
const extension = title.software_package?.name.split(".").pop();
return {
...title,
platform: formatSoftwarePlatform(title.source),
extension,
};
}),
...DEFAULT_USE_QUERY_OPTIONS,
}
);
@ -152,19 +164,55 @@ const InstallSoftwareModal = ({
[formData]
);
const availableSoftwareOptions = titlesAFI?.map((title) => {
const splitName = title.software_package?.name.split(".") ?? "";
const ext =
splitName.length > 1 ? splitName[splitName.length - 1] : undefined;
const platformString = ext
? `${getPlatformDisplayFromPackageExtension(ext)} (.${ext}) • `
: "";
return {
label: title.name,
value: title.id,
helpText: `${platformString}${title.software_package?.version ?? ""}`,
// Filters and transforms software titles into dropdown options
// to include only software compatible with the policy's platform(s)
const availableSoftwareOptions = useCallback(
(policy: IFormPolicy) => {
const policyPlatforms = policy.platform.split(",");
return titlesAvailableForInstall
?.filter(
(title) => title.platform && policyPlatforms.includes(title.platform)
)
.map((title) => {
const vppOption = title.source === "apps" && !!title.app_store_app;
const platformString = () => {
if (vppOption) {
return "macOS (App Store) • ";
}
return title.extension
? `${
title.platform && PLATFORM_DISPLAY_NAMES[title.platform]
} (.${title.extension}) `
: "";
};
const versionString = () => {
return vppOption
? title.app_store_app?.version
: title.software_package?.version ?? "";
};
return {
label: title.name,
value: title.id,
helpText: `${platformString()}${versionString()}`,
};
});
},
[titlesAvailableForInstall]
);
// Cache availableSoftwareOptions for each unique platform
const memoizedAvailableSoftwareOptions = useMemo(() => {
const cache = new Map();
return (policy: IFormPolicy) => {
const key = policy.platform;
if (!cache.has(key)) {
cache.set(key, availableSoftwareOptions(policy));
}
return cache.get(key);
};
});
}, [availableSoftwareOptions]);
const renderPolicySwInstallOption = (policy: IFormPolicy) => {
const {
@ -194,7 +242,7 @@ const InstallSoftwareModal = ({
</Checkbox>
{enabled && (
<Dropdown
options={availableSoftwareOptions}
options={memoizedAvailableSoftwareOptions(policy)} // Options filtered for policy's platform(s)
value={swIdToInstall}
onChange={onSelectPolicySoftware}
placeholder="Select software"
@ -208,13 +256,13 @@ const InstallSoftwareModal = ({
};
const renderContent = () => {
if (isTitlesAFIError) {
if (isTitlesAvailableForInstallError) {
return <DataError />;
}
if (isTitlesAFILoading) {
if (isTitlesAvailableForInstallLoading) {
return <Spinner />;
}
if (!titlesAFI?.length) {
if (!titlesAvailableForInstall?.length) {
return (
<div className={`${baseClass}__no-software`}>
<b>No software available for install</b>
@ -226,16 +274,6 @@ const InstallSoftwareModal = ({
);
}
const compatibleTipContent = (
<>
.pkg for macOS.
<br />
.msi or .exe for Windows.
<br />
.deb for Linux.
</>
);
return (
<div className={`${baseClass} form`}>
<div className="form-field">
@ -246,11 +284,8 @@ const InstallSoftwareModal = ({
)}
</ul>
<span className="form-field__help-text">
Selected software, if{" "}
<TooltipWrapper tipContent={compatibleTipContent}>
compatible
</TooltipWrapper>{" "}
with the host, will be installed when hosts fail the chosen policy.{" "}
Selected software, if compatible with the host, will be installed
when hosts fail the chosen policy.{" "}
<CustomLink
url="https://fleetdm.com/learn-more-about/policy-automation-install-software"
text="Learn more"

View file

@ -15,7 +15,7 @@ import usePlatformCompatibility from "hooks/usePlatformCompatibility";
import usePlatformSelector from "hooks/usePlatformSelector";
import { IPolicy, IPolicyFormData } from "interfaces/policy";
import { SelectedPlatformString } from "interfaces/platform";
import { CommaSeparatedPlatformString } from "interfaces/platform";
import { DEFAULT_POLICIES } from "pages/policies/constants";
import Avatar from "components/Avatar";
@ -253,7 +253,7 @@ const PolicyForm = ({
const newPlatformString = selectedPlatforms.join(
","
) as SelectedPlatformString;
) as CommaSeparatedPlatformString;
if (!defaultPolicy) {
setLastEditedQueryPlatform(newPlatformString);

View file

@ -6,7 +6,7 @@ import { AppContext } from "context/app";
import { PolicyContext } from "context/policy";
import { IPlatformSelector } from "hooks/usePlatformSelector";
import { IPolicyFormData } from "interfaces/policy";
import { SelectedPlatformString } from "interfaces/platform";
import { CommaSeparatedPlatformString } from "interfaces/platform";
import useDeepEffect from "hooks/useDeepEffect";
// @ts-ignore
@ -96,7 +96,7 @@ const SaveNewPolicyModal = ({
const newPlatformString = platformSelector
.getSelectedPlatforms()
.join(",") as SelectedPlatformString;
.join(",") as CommaSeparatedPlatformString;
setLastEditedQueryPlatform(newPlatformString);
const { valid: validName, errors: newErrors } = validatePolicyName(

View file

@ -1,7 +1,7 @@
import { IPolicyNew } from "interfaces/policy";
import { SelectedPlatformString } from "interfaces/platform";
import { CommaSeparatedPlatformString } from "interfaces/platform";
const DEFAULT_POLICY_PLATFORM: SelectedPlatformString = "";
const DEFAULT_POLICY_PLATFORM: CommaSeparatedPlatformString = "";
export const DEFAULT_POLICY = {
id: 1,

View file

@ -15,7 +15,7 @@ import {
import {
isScheduledQueryablePlatform,
ScheduledQueryablePlatform,
SelectedPlatformString,
CommaSeparatedPlatformString,
} from "interfaces/platform";
import { API_ALL_TEAMS_ID } from "interfaces/team";
@ -85,7 +85,7 @@ interface IBoolCellProps extends IRowProps {
}
interface IPlatformCellProps extends IRowProps {
cell: {
value: SelectedPlatformString;
value: CommaSeparatedPlatformString;
};
}

View file

@ -45,7 +45,7 @@ import {
ICreateQueryRequestBody,
QueryLoggingOption,
} from "interfaces/schedulable_query";
import { SelectedPlatformString } from "interfaces/platform";
import { CommaSeparatedPlatformString } from "interfaces/platform";
import queryAPI from "services/entities/queries";
@ -278,12 +278,12 @@ const EditQueryForm = ({
// else if Remove OS if All is chosen
if (valArray.indexOf("") === 0 && valArray.length > 1) {
setLastEditedQueryPlatforms(
pull(valArray, "").join(",") as SelectedPlatformString
pull(valArray, "").join(",") as CommaSeparatedPlatformString
);
} else if (valArray.length > 1 && valArray.indexOf("") > -1) {
setLastEditedQueryPlatforms("");
} else {
setLastEditedQueryPlatforms(values as SelectedPlatformString);
setLastEditedQueryPlatforms(values as CommaSeparatedPlatformString);
}
},
[setLastEditedQueryPlatforms]

View file

@ -12,7 +12,7 @@ import {
SCHEDULE_PLATFORM_DROPDOWN_OPTIONS,
} from "utilities/constants";
import { SelectedPlatformString } from "interfaces/platform";
import { CommaSeparatedPlatformString } from "interfaces/platform";
import {
ICreateQueryRequestBody,
ISchedulableQuery,
@ -77,7 +77,7 @@ const SaveQueryModal = ({
const [
selectedPlatformOptions,
setSelectedPlatformOptions,
] = useState<SelectedPlatformString>(existingQuery?.platform ?? "");
] = useState<CommaSeparatedPlatformString>(existingQuery?.platform ?? "");
const [
selectedMinOsqueryVersionOptions,
setSelectedMinOsqueryVersionOptions,
@ -149,12 +149,12 @@ const SaveQueryModal = ({
if (valArray.indexOf("") === 0 && valArray.length > 1) {
// TODO - inmprove type safety of all 3 options
setSelectedPlatformOptions(
pull(valArray, "").join(",") as SelectedPlatformString
pull(valArray, "").join(",") as CommaSeparatedPlatformString
);
} else if (valArray.length > 1 && valArray.indexOf("") > -1) {
setSelectedPlatformOptions("");
} else {
setSelectedPlatformOptions(values as SelectedPlatformString);
setSelectedPlatformOptions(values as CommaSeparatedPlatformString);
}
},
[setSelectedPlatformOptions]

View file

@ -12,6 +12,7 @@ import {
IFleetMaintainedAppDetails,
ISoftwarePackage,
} from "interfaces/software";
import { CommaSeparatedPlatformString } from "interfaces/platform";
import {
buildQueryStringFromParams,
convertParamsToSnakeCase,
@ -20,7 +21,6 @@ import { IPackageFormData } from "pages/SoftwarePage/components/PackageForm/Pack
import { IEditPackageFormData } from "pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal";
import { IAddFleetMaintainedData } from "pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage";
import { listNamesFromSelectedLabels } from "components/TargetLabelSelector/TargetLabelSelector";
import { join } from "path";
export interface ISoftwareApiParams {
page?: number;
@ -75,6 +75,7 @@ export interface ISoftwareVersionsQueryKey extends ISoftwareApiParams {
export interface ISoftwareTitlesQueryKey extends ISoftwareApiParams {
// used to trigger software refetches from sibling pages
addedSoftwareToken?: string | null;
platform?: CommaSeparatedPlatformString;
scope: "software-titles";
}
@ -373,7 +374,8 @@ export default {
});
},
deleteSoftwarePackage: (softwareId: number, teamId: number) => {
// Endpoint for deleting packages or VPP
deleteSoftwareInstaller: (softwareId: number, teamId: number) => {
const { SOFTWARE_AVAILABLE_FOR_INSTALL } = endpoints;
const path = `${SOFTWARE_AVAILABLE_FOR_INSTALL(
softwareId
@ -407,7 +409,7 @@ export default {
return sendRequest("GET", path);
},
getFleetMainainedApp: (id: number): Promise<IFleetMaintainedAppResponse> => {
getFleetMaintainedApp: (id: number): Promise<IFleetMaintainedAppResponse> => {
const { SOFTWARE_FLEET_MAINTAINED_APP } = endpoints;
const path = `${SOFTWARE_FLEET_MAINTAINED_APP(id)}`;
return sendRequest("GET", path);