From 5319156399b24c5169247df9f25ebddea67043eb Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 27 Dec 2024 14:19:31 -0500 Subject: [PATCH] Fleet UI: Add automatic install to custom packages upload (#24729) --- frontend/interfaces/software.ts | 1 + .../SoftwareCustomPackage/helpers.tsx | 23 +++ .../FleetAppDetailsForm.tsx | 133 +++++++++++++----- .../FleetAppDetailsForm/_styles.scss | 7 +- .../FleetMaintainedAppDetailsPage.tsx | 1 + frontend/pages/SoftwarePage/SoftwarePage.tsx | 6 +- .../EditSoftwareModal/EditSoftwareModal.tsx | 7 +- .../components/PackageForm/PackageForm.tsx | 43 +++++- .../components/PackageForm/_styles.scss | 16 +++ .../components/PackageForm/helpers.ts | 6 +- frontend/services/entities/software.ts | 8 +- 11 files changed, 201 insertions(+), 50 deletions(-) diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts index f67eb25140ef..5f8797f27fb3 100644 --- a/frontend/interfaces/software.ts +++ b/frontend/interfaces/software.ts @@ -72,6 +72,7 @@ export interface ISoftwarePackage { uninstall_script: string; pre_install_query?: string; post_install_script?: string; + automatic_install?: boolean; // POST only self_service: boolean; icon_url: string | null; status: { diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/helpers.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/helpers.tsx index d9370edc9406..99a09e229bcc 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/helpers.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/helpers.tsx @@ -31,6 +31,29 @@ export const getErrorMessage = (err: unknown) => { ); } else if (reason.includes("Secret variable")) { return reason.replace("missing from database", "doesn't exist"); + } else if (reason.includes("Unable to extract necessary metadata")) { + return ( + <> + Couldn't add. Unable to extract necessary metadata.{" "} + + + ); + } else if (reason.includes("Fleet couldn't read the version from")) { + return ( + <> + {reason}{" "} + + + ); } return reason || DEFAULT_ERROR_MESSAGE; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/FleetAppDetailsForm.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/FleetAppDetailsForm.tsx index 7b2504fd73c9..d70e229e3646 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/FleetAppDetailsForm.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/FleetAppDetailsForm.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { ILabelSummary } from "interfaces/label"; +import { PackageType } from "interfaces/package_type"; import Checkbox from "components/forms/fields/Checkbox"; import TooltipWrapper from "components/TooltipWrapper"; @@ -8,6 +9,9 @@ import RevealButton from "components/buttons/RevealButton"; import Button from "components/buttons/Button"; import Radio from "components/forms/fields/Radio"; import TargetLabelSelector from "components/TargetLabelSelector"; +import InfoBanner from "components/InfoBanner"; +import CustomLink from "components/CustomLink"; +import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; import AdvancedOptionsFields from "pages/SoftwarePage/components/AdvancedOptionsFields"; @@ -19,13 +23,14 @@ import { const baseClass = "fleet-app-details-form"; +export type InstallType = "manual" | "automatic"; export interface IFleetMaintainedAppFormData { selfService: boolean; installScript: string; preInstallQuery?: string; postInstallScript?: string; uninstallScript?: string; - installType: string; + installType: InstallType; targetType: string; customTarget: string; labelTargets: Record; @@ -39,6 +44,7 @@ export interface IFormValidation { interface IFleetAppDetailsFormProps { labels: ILabelSummary[] | null; + name: string; defaultInstallScript: string; defaultPostInstallScript: string; defaultUninstallScript: string; @@ -48,8 +54,94 @@ interface IFleetAppDetailsFormProps { onSubmit: (formData: IFleetMaintainedAppFormData) => void; } +interface IInstallTypeSection { + installType: InstallType; + onChangeInstallType: (value: string) => void; + isCustomPackage?: boolean; + isExeCustomPackage?: boolean; +} + +// Also used in custom package form (PackageForm.tsx) +export const InstallTypeSection = ({ + installType, + onChangeInstallType, + isCustomPackage = false, + isExeCustomPackage = false, +}: IInstallTypeSection) => { + const isAutomaticDisabled = isExeCustomPackage; + const AUTOMATIC_DISABLED_TOOLTIP = ( + <> + Fleet can't create a policy to detect existing installations for +
.exe packages. To automatically install an .exe, add a custom +
policy and enable the install software automation on the +
Policies page. + + ); + + return ( +
+ Install +
+ + + Automatically install on each host that's{" "} + + If the host already has any version of this +
software, it won't be installed. + + } + > + missing this software +
+ . Policy that triggers install can be customized after software is + added. + + } + /> +
+ {installType === "automatic" && isCustomPackage && ( + + } + > + Installing software over existing installations might cause issues. + Fleet's policy may not detect these existing installations. + Please create a test team in Fleet to verify a smooth installation. + + )} +
+ ); +}; + const FleetAppDetailsForm = ({ labels, + name: appName, defaultInstallScript, defaultPostInstallScript, defaultUninstallScript, @@ -107,7 +199,8 @@ const FleetAppDetailsForm = ({ }; const onChangeInstallType = (value: string) => { - const newData = { ...formData, installType: value }; + const installType = value as InstallType; + const newData = { ...formData, installType }; setFormData(newData); }; @@ -140,38 +233,10 @@ const FleetAppDetailsForm = ({ return (
-
- Install -
- - - Automatically install on each host that's{" "} - - missing this software. - {" "} - Policy that triggers install can be customized after software is - added. - - } - /> -
-
+ { } }, [currentTeamId, router]); - // NOTE: used to reset page number to 0 when modifying filters - // NOTE: Solution reused from ManageHostPage.tsx + // Used to reset page number to 0 when modifying filters + // Solution reused from ManageHostPage.tsx useEffect(() => { setResetPageIndex(false); }, [queryParams, page]); @@ -312,7 +312,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => { const onTeamChange = useCallback( (teamId: number) => { handleTeamChange(teamId); - // NOTE: used to reset page number to 0 when modifying filters + // Used to reset page number to 0 when modifying filters setResetPageIndex(true); }, [handleTeamChange] diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx index 4d27f16a0567..98f2ee9bc69e 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx @@ -32,6 +32,9 @@ import ConfirmSaveChangesModal from "../ConfirmSaveChangesModal"; const baseClass = "edit-software-modal"; +// Install type used on add but not edit +export type IEditPackageFormData = Omit; + interface IEditSoftwareModalProps { softwareId: number; teamId: number; @@ -57,7 +60,7 @@ const EditSoftwareModal = ({ showConfirmSaveChangesModal, setShowConfirmSaveChangesModal, ] = useState(false); - const [pendingUpdates, setPendingUpdates] = useState({ + const [pendingUpdates, setPendingUpdates] = useState({ software: null, installScript: "", selfService: false, @@ -121,7 +124,7 @@ const EditSoftwareModal = ({ setShowConfirmSaveChangesModal(!showConfirmSaveChangesModal); }; - const onSaveSoftwareChanges = async (formData: IPackageFormData) => { + const onSaveSoftwareChanges = async (formData: IEditPackageFormData) => { setIsUpdatingSoftware(true); if (formData.software && formData.software.size > MAX_FILE_SIZE_BYTES) { diff --git a/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx b/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx index d728b72805f0..0408ab150bbc 100644 --- a/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx +++ b/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx @@ -1,5 +1,5 @@ // Used in AddPackageModal.tsx and EditSoftwareModal.tsx -import React, { useContext, useState } from "react"; +import React, { useContext, useState, useEffect } from "react"; import classnames from "classnames"; import { NotificationContext } from "context/notification"; @@ -7,11 +7,16 @@ import { getFileDetails } from "utilities/file/fileUtils"; import getDefaultInstallScript from "utilities/software_install_scripts"; import getDefaultUninstallScript from "utilities/software_uninstall_scripts"; import { ILabelSummary } from "interfaces/label"; +import { PackageType } from "interfaces/package_type"; import Button from "components/buttons/Button"; import Checkbox from "components/forms/fields/Checkbox"; import FileUploader from "components/FileUploader"; import TooltipWrapper from "components/TooltipWrapper"; +import { + InstallType, + InstallTypeSection, +} from "pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/FleetAppDetailsForm"; import TargetLabelSelector from "components/TargetLabelSelector"; import PackageAdvancedOptions from "../PackageAdvancedOptions"; @@ -36,9 +41,10 @@ export interface IPackageFormData { targetType: string; customTarget: string; labelTargets: Record; + installType: InstallType; // Used on add but not edit } -export interface IFormValidation { +export interface IPackageFormValidation { isValid: boolean; software: { isValid: boolean }; preInstallQuery?: { isValid: boolean; message?: string }; @@ -80,7 +86,7 @@ const PackageForm = ({ }: IPackageFormProps) => { const { renderFlash } = useContext(NotificationContext); - const [formData, setFormData] = useState({ + const initialFormData: IPackageFormData = { software: defaultSoftware || null, installScript: defaultInstallScript || "", preInstallQuery: defaultPreInstallQuery || "", @@ -90,8 +96,11 @@ const PackageForm = ({ targetType: getTargetType(defaultSoftware), customTarget: getCustomTarget(defaultSoftware), labelTargets: generateSelectedLabels(defaultSoftware), - }); - const [formValidation, setFormValidation] = useState({ + installType: "manual", + }; + + const [formData, setFormData] = useState(initialFormData); + const [formValidation, setFormValidation] = useState({ isValid: false, software: { isValid: false }, }); @@ -163,6 +172,12 @@ const PackageForm = ({ setFormValidation(generateFormValidation(newData)); }; + const onChangeInstallType = (value: string) => { + const installType = value as InstallType; + const newData = { ...formData, installType }; + setFormData(newData); + }; + const onToggleSelfServiceCheckbox = (value: boolean) => { const newData = { ...formData, selfService: value }; setFormData(newData); @@ -194,6 +209,18 @@ const PackageForm = ({ const classNames = classnames(baseClass, className); + const ext = formData?.software?.name.split(".").pop() as PackageType; + const isExePackage = ext === "exe"; + + // If a user preselects automatic install and then uploads a .exe + // which automatic install is not supported, the form will default + // back to manual install + useEffect(() => { + if (isExePackage && formData.installType === "automatic") { + onChangeInstallType("manual"); + } + }, [isExePackage]); + return (
@@ -210,6 +237,12 @@ const PackageForm = ({ formData.software ? getFileDetails(formData.software) : undefined } /> + string; type IValidationMessage = string | IMessageFunc; -type IFormValidationKey = keyof Omit; +type IFormValidationKey = keyof Omit; interface IValidation { name: string; @@ -74,7 +74,7 @@ const getErrorMessage = ( }; export const generateFormValidation = (formData: IPackageFormData) => { - const formValidation: IFormValidation = { + const formValidation: IPackageFormValidation = { isValid: true, software: { isValid: false, diff --git a/frontend/services/entities/software.ts b/frontend/services/entities/software.ts index 9f4ead95b145..0ef303dc719a 100644 --- a/frontend/services/entities/software.ts +++ b/frontend/services/entities/software.ts @@ -17,6 +17,7 @@ import { convertParamsToSnakeCase, } from "utilities/url"; import { IPackageFormData } from "pages/SoftwarePage/components/PackageForm/PackageForm"; +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"; @@ -279,6 +280,11 @@ export default { formData.append("pre_install_query", data.preInstallQuery); data.postInstallScript && formData.append("post_install_script", data.postInstallScript); + data.installType && + formData.append( + "automatic_install", + (data.installType === "automatic").toString() + ); teamId && formData.append("team_id", teamId.toString()); if (data.targetType === "Custom") { @@ -314,7 +320,7 @@ export default { onUploadProgress, signal, }: { - data: IPackageFormData; + data: IEditPackageFormData; orignalPackage: ISoftwarePackage; softwareId: number; teamId: number;