diff --git a/changes/20308-file-uploader b/changes/20308-file-uploader new file mode 100644 index 000000000000..675c212d4b51 --- /dev/null +++ b/changes/20308-file-uploader @@ -0,0 +1 @@ +- Update UI for software uploads to include upload progress bar. diff --git a/frontend/components/FileDetails/FileDetails.tsx b/frontend/components/FileDetails/FileDetails.tsx index 78d48d5bdee8..49c5da93aed7 100644 --- a/frontend/components/FileDetails/FileDetails.tsx +++ b/frontend/components/FileDetails/FileDetails.tsx @@ -1,19 +1,19 @@ import React from "react"; +import { IFileDetails } from "utilities/file/fileUtils"; + +import Button from "components/buttons/Button"; import { ISupportedGraphicNames } from "components/FileUploader/FileUploader"; import Graphic from "components/Graphic"; -import Button from "components/buttons/Button"; import Icon from "components/Icon"; interface IFileDetailsProps { graphicNames: ISupportedGraphicNames | ISupportedGraphicNames[]; - fileDetails: { - name: string; - platform?: string; - }; + fileDetails: IFileDetails; canEdit: boolean; onFileSelect: (e: React.ChangeEvent) => void; accept?: string; + progress?: number; } const baseClass = "file-details"; @@ -24,6 +24,7 @@ const FileDetails = ({ canEdit, onFileSelect, accept, + progress, }: IFileDetailsProps) => { return (
@@ -42,7 +43,7 @@ const FileDetails = ({ )}
- {canEdit && ( + {!progress && canEdit && (
)} + {!!progress && ( +
+
+
+
+
+ {Math.round(progress * 100)}% +
+
+ )}
); }; diff --git a/frontend/components/FileDetails/_styles.scss b/frontend/components/FileDetails/_styles.scss index 497dabcc3dbb..718574db0681 100644 --- a/frontend/components/FileDetails/_styles.scss +++ b/frontend/components/FileDetails/_styles.scss @@ -2,6 +2,7 @@ display: flex; justify-content: space-between; width: 100%; + gap: $pad-medium; &__info { display: flex; @@ -34,4 +35,31 @@ cursor: pointer; } } + + &__progress-wrapper { + display: flex; + width: 184px; // using fixed width to prevent jitter when text changes: 144px + $pad-medium + 24px for text + justify-content: space-between; + align-items: center; + gap: $pad-small; // shouldn't be necessary, but just in case + } + + &__progress-bar { + display: inline-block; + height: 6px; + width: 144px; + background-color: $ui-fleet-black-10; + border-radius: 32px; + overflow: hidden; + } + + &__progress-bar--uploaded { + background-color: $core-vibrant-blue; + height: 100%; + } + + &__progress-text { + font-size: $x-small; + color: $ui-fleet-black-50; + } } diff --git a/frontend/components/FileProgressModal/FileProgressModal.tsx b/frontend/components/FileProgressModal/FileProgressModal.tsx new file mode 100644 index 000000000000..b7f74822a3fb --- /dev/null +++ b/frontend/components/FileProgressModal/FileProgressModal.tsx @@ -0,0 +1,41 @@ +import Card from "components/Card"; +import FileDetails from "components/FileDetails"; +import Modal from "components/Modal"; +import { noop } from "lodash"; +import React from "react"; + +import { ISupportedGraphicNames } from "components/FileUploader/FileUploader"; + +import { IFileDetails } from "utilities/file/fileUtils"; + +const baseClass = "file-progress-modal"; + +const FileProgressModal = ({ + graphicNames = "file-pkg", + fileDetails, + fileProgress, +}: { + graphicNames?: ISupportedGraphicNames | ISupportedGraphicNames[]; + fileDetails: IFileDetails; + fileProgress: number; +}) => ( + + + + + +); + +export default FileProgressModal; diff --git a/frontend/components/FileProgressModal/_styles.scss b/frontend/components/FileProgressModal/_styles.scss new file mode 100644 index 000000000000..08cad626545f --- /dev/null +++ b/frontend/components/FileProgressModal/_styles.scss @@ -0,0 +1,7 @@ +.file-progress-modal { + // TODO: should we update the card component with this padding option? + &__card { + padding-top: $pad-medium; + padding-bottom: $pad-medium; + } +} diff --git a/frontend/components/FileProgressModal/index.ts b/frontend/components/FileProgressModal/index.ts new file mode 100644 index 000000000000..648a271fac75 --- /dev/null +++ b/frontend/components/FileProgressModal/index.ts @@ -0,0 +1 @@ +export { default } from "./FileProgressModal"; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx index dbb8bded4c07..d026e48a13c8 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx @@ -1,15 +1,19 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useContext } from "react"; import { Tab, TabList, Tabs } from "react-tabs"; import { InjectedRouter } from "react-router"; import { Location } from "history"; import PATHS from "router/paths"; import { buildQueryStringFromParams } from "utilities/url"; +import { QueryContext } from "context/query"; +import useToggleSidePanel from "hooks/useToggleSidePanel"; +import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team"; import MainContent from "components/MainContent"; import BackLink from "components/BackLink"; import TabsWrapper from "components/TabsWrapper"; -import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team"; +import SidePanelContent from "components/SidePanelContent"; +import QuerySidePanel from "components/side_panels/QuerySidePanel"; const baseClass = "software-add-page"; @@ -59,8 +63,14 @@ const SoftwareAddPage = ({ location, router, }: ISoftwareAddPageProps) => { + const { selectedOsqueryTable, setSelectedOsqueryTable } = useContext( + QueryContext + ); + const { isSidePanelOpen, setSidePanelOpen } = useToggleSidePanel(false); + const navigateToNav = useCallback( (i: number): void => { + setSidePanelOpen(false); // Only query param to persist between tabs is team id const teamIdParam = buildQueryStringFromParams({ team_id: location.query.team_id, @@ -69,7 +79,7 @@ const SoftwareAddPage = ({ const navPath = addSoftwareSubNav[i].pathname.concat(`?${teamIdParam}`); router.replace(navPath); }, - [location, router] + [location.query.team_id, router, setSidePanelOpen] ); // Quick exit if no team_id param. This page must have a team id to function @@ -84,41 +94,59 @@ const SoftwareAddPage = ({ return null; } + const onOsqueryTableSelect = (tableName: string) => { + setSelectedOsqueryTable(tableName); + }; + const backUrl = `${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams({ team_id: location.query.team_id, })}`; return ( - - <> - -

Add Software

- - - - {addSoftwareSubNav.map((navItem) => { - return ( - - {navItem.name} - - ); - })} - - - - {React.cloneElement(children, { - router, - currentTeamId: parseInt(location.query.team_id, 10), - })} - -
+ <> + + <> + +

Add Software

+ + + + {addSoftwareSubNav.map((navItem) => { + return ( + + {navItem.name} + + ); + })} + + + + {React.cloneElement(children, { + router, + currentTeamId: parseInt(location.query.team_id, 10), + isSidePanelOpen, + setSidePanelOpen, + })} + +
+ {isSidePanelOpen && ( + + setSidePanelOpen(false)} + /> + + )} + ); }; diff --git a/frontend/pages/SoftwarePage/components/AddPackage/AddPackage.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx similarity index 54% rename from frontend/pages/SoftwarePage/components/AddPackage/AddPackage.tsx rename to frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx index df20f7845af3..a56059134ada 100644 --- a/frontend/pages/SoftwarePage/components/AddPackage/AddPackage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx @@ -1,43 +1,46 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useContext, useEffect } from "react"; import { InjectedRouter } from "react-router"; -import { getErrorReason } from "interfaces/errors"; - import PATHS from "router/paths"; -import { NotificationContext } from "context/notification"; -import softwareAPI from "services/entities/software"; -import { QueryParams, buildQueryStringFromParams } from "utilities/url"; - import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; +import { getFileDetails, IFileDetails } from "utilities/file/fileUtils"; +import { buildQueryStringFromParams, QueryParams } from "utilities/url"; +import softwareAPI, { + MAX_FILE_SIZE_BYTES, + MAX_FILE_SIZE_MB, + UPLOAD_TIMEOUT, +} from "services/entities/software"; -import CustomLink from "components/CustomLink"; +import { NotificationContext } from "context/notification"; +import { getErrorReason } from "interfaces/errors"; -import PackageForm from "../PackageForm"; -import { IPackageFormData } from "../PackageForm/PackageForm"; -import { getErrorMessage } from "../AddSoftwareModal/helpers"; +import CustomLink from "components/CustomLink"; +import FileProgressModal from "components/FileProgressModal"; +import PackageForm from "pages/SoftwarePage/components/PackageForm"; +import { IPackageFormData } from "pages/SoftwarePage/components/PackageForm/PackageForm"; -const baseClass = "add-package"; +import { getErrorMessage } from "./helpers"; -// 8 minutes + 15 seconds to account for extra roundtrip time. -export const UPLOAD_TIMEOUT = (8 * 60 + 15) * 1000; -export const MAX_FILE_SIZE_MB = 500; -export const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; +const baseClass = "software-custom-package"; -interface IAddPackageProps { - teamId: number; +interface ISoftwarePackageProps { + currentTeamId: number; router: InjectedRouter; - onExit: () => void; - setAddedSoftwareToken: (token: string) => void; + isSidePanelOpen: boolean; + setSidePanelOpen: (isOpen: boolean) => void; } -const AddPackage = ({ - teamId, +const SoftwareCustomPackage = ({ + currentTeamId, router, - onExit, - setAddedSoftwareToken, -}: IAddPackageProps) => { + isSidePanelOpen, + setSidePanelOpen, +}: ISoftwarePackageProps) => { const { renderFlash } = useContext(NotificationContext); - const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = React.useState(0); + const [uploadDetails, setUploadDetails] = React.useState( + null + ); useEffect(() => { let timeout: NodeJS.Timeout; @@ -50,7 +53,7 @@ const AddPackage = ({ }; // set up event listener to prevent user from leaving page while uploading - if (isUploading) { + if (uploadDetails) { addEventListener("beforeunload", beforeUnloadHandler); timeout = setTimeout(() => { removeEventListener("beforeunload", beforeUnloadHandler); @@ -64,25 +67,47 @@ const AddPackage = ({ removeEventListener("beforeunload", beforeUnloadHandler); clearTimeout(timeout); }; - }, [isUploading]); + }, [uploadDetails]); + + const onCancel = () => { + router.push( + `${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams({ + team_id: currentTeamId, + })}` + ); + }; - const onAddPackage = async (formData: IPackageFormData) => { - setIsUploading(true); + const onSubmit = async (formData: IPackageFormData) => { + console.log("submit", formData); + if (!formData.software) { + renderFlash( + "error", + `Couldn't add. Please refresh the page and try again.` + ); + return; + } if (formData.software && formData.software.size > MAX_FILE_SIZE_BYTES) { renderFlash( "error", `Couldn't add. The maximum file size is ${MAX_FILE_SIZE_MB} MB.` ); - onExit(); - setIsUploading(false); return; } + setUploadDetails(getFileDetails(formData.software)); + // Note: This TODO is copied to onSaveSoftwareChanges in EditSoftwareModal // TODO: confirm we are deleting the second sentence (not modifying it) for non-self-service installers try { - await softwareAPI.addSoftwarePackage(formData, teamId, UPLOAD_TIMEOUT); + await softwareAPI.addSoftwarePackage({ + data: formData, + teamId: currentTeamId, + timeout: UPLOAD_TIMEOUT, + onUploadProgress: (progressEvent) => { + setUploadProgress(progressEvent.progress || 0); + }, + }); renderFlash( "success", <> @@ -93,14 +118,12 @@ const AddPackage = ({ ); - const newQueryParams: QueryParams = { team_id: teamId }; + const newQueryParams: QueryParams = { team_id: currentTeamId }; if (formData.selfService) { newQueryParams.self_service = true; } else { newQueryParams.available_for_install = true; } - // any unique string - triggers SW refetch - setAddedSoftwareToken(`${Date.now()}`); router.push( `${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams(newQueryParams)}` ); @@ -125,20 +148,26 @@ const AddPackage = ({ renderFlash("error", getErrorMessage(e)); } } - - onExit(); - setIsUploading(false); + setUploadDetails(null); }; return (
setSidePanelOpen(true)} + className={`${baseClass}__package-form`} + onCancel={onCancel} + onSubmit={onSubmit} /> + {uploadDetails && ( + + )}
); }; -export default AddPackage; +export default SoftwareCustomPackage; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/_styles.scss new file mode 100644 index 000000000000..6276b13e92c1 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/_styles.scss @@ -0,0 +1,14 @@ +.software-custom-package { + // formatting the form buttons to be on the left. Tshis is done here because + // this form can be used in other places where the buttons should be on + // the right. + &__package-form { + margin-top: $pad-xxlarge; + + .form-buttons { + display: flex; + flex-direction: row; + gap: $pad-large; + } + } +} diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/helpers.ts b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/helpers.ts new file mode 100644 index 000000000000..bf6bef64a8db --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/helpers.ts @@ -0,0 +1,13 @@ +import { getErrorReason } from "interfaces/errors"; + +const UPLOAD_ERROR_MESSAGES = { + default: { + message: "Couldn't add. Please try again.", + }, +}; + +// eslint-disable-next-line import/prefer-default-export +export const getErrorMessage = (err: unknown) => { + if (typeof err === "string") return err; + return getErrorReason(err) || UPLOAD_ERROR_MESSAGES.default.message; +}; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/index.ts b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/index.ts new file mode 100644 index 000000000000..729e678fe9b1 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/index.ts @@ -0,0 +1 @@ +export { default } from "./SoftwareCustomPackage"; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx index a97b7a1fc112..f368e2608ed1 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx @@ -184,7 +184,7 @@ const FleetMaintainedAppDetailsPage = ({ version={data.version} /> ; -} - -const SoftwarePackage = ({ - currentTeamId, - router, - location, -}: ISoftwarePackageProps) => { - return
Sofware package page
; -}; - -export default SoftwarePackage; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwarePackage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwarePackage/_styles.scss deleted file mode 100644 index e197624a521a..000000000000 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwarePackage/_styles.scss +++ /dev/null @@ -1,3 +0,0 @@ -.software-package { - -} diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwarePackage/index.ts b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwarePackage/index.ts deleted file mode 100644 index c8ed0ce44b96..000000000000 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwarePackage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./SoftwarePackage"; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx index 4b7cbc09c6a9..e1c23e43003a 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx @@ -1,27 +1,26 @@ import React, { useContext, useState, useEffect } from "react"; import { InjectedRouter } from "react-router"; import classnames from "classnames"; -import deepDifference from "utilities/deep_difference"; import { getErrorReason } from "interfaces/errors"; -import PATHS from "router/paths"; import { NotificationContext } from "context/notification"; -import softwareAPI from "services/entities/software"; -import { QueryParams, buildQueryStringFromParams } from "utilities/url"; +import softwareAPI, { + MAX_FILE_SIZE_BYTES, + MAX_FILE_SIZE_MB, + UPLOAD_TIMEOUT, +} from "services/entities/software"; import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; +import deepDifference from "utilities/deep_difference"; +import { getFileDetails } from "utilities/file/fileUtils"; import CustomLink from "components/CustomLink"; +import FileProgressModal from "components/FileProgressModal"; import Modal from "components/Modal"; import PackageForm from "pages/SoftwarePage/components/PackageForm"; import { IPackageFormData } from "pages/SoftwarePage/components/PackageForm/PackageForm"; -import { - UPLOAD_TIMEOUT, - MAX_FILE_SIZE_MB, - MAX_FILE_SIZE_BYTES, -} from "pages/SoftwarePage/components/AddPackage/AddPackage"; import { getErrorMessage } from "./helpers"; import ConfirmSaveChangesModal from "../ConfirmSaveChangesModal"; @@ -33,18 +32,14 @@ interface IEditSoftwareModalProps { router: InjectedRouter; software?: any; // TODO refetchSoftwareTitle: () => void; - onExit: () => void; - setAddedSoftwareToken: (token: string) => void; } const EditSoftwareModal = ({ softwareId, teamId, - router, software, onExit, - setAddedSoftwareToken, refetchSoftwareTitle, }: IEditSoftwareModalProps) => { const { renderFlash } = useContext(NotificationContext); @@ -62,16 +57,23 @@ const EditSoftwareModal = ({ installScript: "", selfService: false, }); + const [uploadProgress, setUploadProgress] = useState(0); // Work around to not lose Edit Software modal data when Save changes modal opens // by using CSS to hide Edit Software modal when Save changes modal is open useEffect(() => { setEditSoftwareModalClasses( classnames(baseClass, { - [`${baseClass}--hidden`]: showConfirmSaveChangesModal, + [`${baseClass}--hidden`]: + showConfirmSaveChangesModal || + (!!pendingUpdates.software && isUpdatingSoftware), }) ); - }, [showConfirmSaveChangesModal]); + }, [ + showConfirmSaveChangesModal, + pendingUpdates.software, + isUpdatingSoftware, + ]); useEffect(() => { let timeout: NodeJS.Timeout; @@ -120,12 +122,15 @@ const EditSoftwareModal = ({ // Note: This TODO is copied over from onAddPackage on AddPackage.tsx // TODO: confirm we are deleting the second sentence (not modifying it) for non-self-service installers try { - await softwareAPI.editSoftwarePackage( - formData, + await softwareAPI.editSoftwarePackage({ + data: formData, softwareId, teamId, - UPLOAD_TIMEOUT - ); + timeout: UPLOAD_TIMEOUT, + onUploadProgress: (progressEvent) => { + setUploadProgress(progressEvent.progress || 0); + }, + }); renderFlash( "success", @@ -182,7 +187,6 @@ const EditSoftwareModal = ({ const onlySelfServiceUpdated = Object.keys(updates).length === 1 && "selfService" in updates; if (!onlySelfServiceUpdated) { - console.log("non-self-service updates: ", updates); // Open the confirm save changes modal setShowConfirmSaveChangesModal(true); } else { @@ -205,8 +209,8 @@ const EditSoftwareModal = ({ width="large" > )} + {!!pendingUpdates.software && isUpdatingSoftware && ( + + )} ); }; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/_styles.scss index 6dd125240355..5b454abb0f8c 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/_styles.scss @@ -14,4 +14,18 @@ &--hidden { display: none; } + + + // formatting the form buttons to be on the right side of the modal. + // this is done here and not with .modal-cta-wrap because this form + // can be used in other places where the buttons should be on the left. + &__package-form { + .form-buttons { + align-self: flex-end; + display: flex; + flex-direction: row-reverse; + margin-top: $pad-xlarge; + gap: $pad-medium; + } + } } diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx index 30a0578191fd..0a8863abe646 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx @@ -15,7 +15,6 @@ import softwareAPI from "services/entities/software"; import { buildQueryStringFromParams } from "utilities/url"; import { internationalTimeFormat } from "utilities/helpers"; import { uploadedFromNow } from "utilities/date_format"; -import { noop } from "lodash"; // @ts-ignore import Dropdown from "components/forms/fields/Dropdown"; @@ -390,7 +389,6 @@ const SoftwarePackageCard = ({ software={softwarePackage} onExit={() => setShowEditSoftwareModal(false)} router={router} - setAddedSoftwareToken={noop} refetchSoftwareTitle={refetchSoftwareTitle} /> )} diff --git a/frontend/pages/SoftwarePage/components/AddPackage/_styles.scss b/frontend/pages/SoftwarePage/components/AddPackage/_styles.scss deleted file mode 100644 index 2387af120ac2..000000000000 --- a/frontend/pages/SoftwarePage/components/AddPackage/_styles.scss +++ /dev/null @@ -1,3 +0,0 @@ -.add-package { - margin-top: $pad-large; -} diff --git a/frontend/pages/SoftwarePage/components/AddPackage/index.ts b/frontend/pages/SoftwarePage/components/AddPackage/index.ts deleted file mode 100644 index 393d7d621b4d..000000000000 --- a/frontend/pages/SoftwarePage/components/AddPackage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./AddPackage"; diff --git a/frontend/pages/SoftwarePage/components/AdvancedOptionsFields/AdvancedOptionsFields.tsx b/frontend/pages/SoftwarePage/components/AdvancedOptionsFields/AdvancedOptionsFields.tsx index 95bf94be5e22..883647e7c9fd 100644 --- a/frontend/pages/SoftwarePage/components/AdvancedOptionsFields/AdvancedOptionsFields.tsx +++ b/frontend/pages/SoftwarePage/components/AdvancedOptionsFields/AdvancedOptionsFields.tsx @@ -46,7 +46,7 @@ const AdvancedOptionsFields = ({ const classNames = classnames(baseClass, className); const renderLabelComponent = (): JSX.Element | null => { - if (showSchemaButton) { + if (!showSchemaButton) { return null; } @@ -86,6 +86,7 @@ const AdvancedOptionsFields = ({ void; onChangePreInstallQuery: (value?: string) => void; onChangeInstallScript: (value: string) => void; onChangePostInstallScript: (value?: string) => void; @@ -76,12 +78,14 @@ interface IPackageAdvancedOptionsProps { } const PackageAdvancedOptions = ({ + showSchemaButton = false, errors, selectedPackage, preInstallQuery, installScript, postInstallScript, uninstallScript, + onClickShowSchema = noop, onChangePreInstallQuery, onChangeInstallScript, onChangePostInstallScript, @@ -99,7 +103,7 @@ const PackageAdvancedOptions = ({ return ( setShowAdvancedOptions(!showAdvancedOptions)} disabled={!selectedPackage} disabledTooltipContent={ - selectedPackage - ? "Choose a file to modify advanced options." - : undefined + <> + Choose a file to modify
+ advanced options. + } /> {showAdvancedOptions && !!selectedPackage && renderAdvancedOptions()} diff --git a/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx b/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx index 4e9d7aa74de0..0123e0130ac8 100644 --- a/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx +++ b/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx @@ -1,5 +1,6 @@ // Used in AddPackageModal.tsx and EditSoftwareModal.tsx import React, { useContext, useState } from "react"; +import classnames from "classnames"; import { NotificationContext } from "context/notification"; import { getFileDetails } from "utilities/file/fileUtils"; @@ -9,7 +10,6 @@ import getDefaultUninstallScript from "utilities/software_uninstall_scripts"; import Button from "components/buttons/Button"; import Checkbox from "components/forms/fields/Checkbox"; import FileUploader from "components/FileUploader"; -import Spinner from "components/Spinner"; import TooltipWrapper from "components/TooltipWrapper"; import PackageAdvancedOptions from "../PackageAdvancedOptions"; @@ -18,15 +18,6 @@ import { generateFormValidation } from "./helpers"; export const baseClass = "package-form"; -const UploadingSoftware = () => { - return ( -
- -

Uploading software. This may take a few minutes to finish.

-
- ); -}; - export interface IPackageFormData { software: File | null; preInstallQuery?: string; @@ -46,9 +37,10 @@ export interface IFormValidation { } interface IPackageFormProps { - isUploading: boolean; + showSchemaButton?: boolean; onCancel: () => void; onSubmit: (formData: IPackageFormData) => void; + onClickShowSchema?: () => void; isEditingSoftware?: boolean; defaultSoftware?: any; // TODO defaultInstallScript?: string; @@ -56,12 +48,14 @@ interface IPackageFormProps { defaultPostInstallScript?: string; defaultUninstallScript?: string; defaultSelfService?: boolean; + className?: string; } const ACCEPTED_EXTENSIONS = ".pkg,.msi,.exe,.deb,.rpm"; const PackageForm = ({ - isUploading, + showSchemaButton = false, + onClickShowSchema, onCancel, onSubmit, isEditingSoftware = false, @@ -71,6 +65,7 @@ const PackageForm = ({ defaultPostInstallScript, defaultUninstallScript, defaultSelfService, + className, }: IPackageFormProps) => { const { renderFlash } = useContext(NotificationContext); @@ -163,65 +158,65 @@ const PackageForm = ({ const isSubmitDisabled = !formValidation.isValid; + const classNames = classnames(baseClass, className); + return ( -
- {isUploading ? ( - // Note: Sarah is replacing uploading state as subsequent 4.57 feature - ) : ( -
- + + + + + End users can install from{" "} + Fleet Desktop {">"} Self-service. + } - /> - - - End users can install from{" "} - Fleet Desktop {">"} Self-service. - - } - > - Self-service - - - -
- - -
- - )} + Self-service +
+
+ +
+ + +
+
); }; diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index 68e7994f8048..89a802646f22 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -79,7 +79,7 @@ import SoftwareOSDetailsPage from "pages/SoftwarePage/SoftwareOSDetailsPage"; import SoftwareVulnerabilityDetailsPage from "pages/SoftwarePage/SoftwareVulnerabilityDetailsPage"; import SoftwareAddPage from "pages/SoftwarePage/SoftwareAddPage"; import SoftwareFleetMaintained from "pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained"; -import SoftwarePackage from "pages/SoftwarePage/SoftwareAddPage/SoftwarePackage"; +import SoftwareCustomPackage from "pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage"; import SoftwareAppStore from "pages/SoftwarePage/SoftwareAddPage/SoftwareAppStoreVpp"; import FleetMaintainedAppDetailsPage from "pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage"; @@ -284,7 +284,7 @@ const routes = ( path="fleet-maintained" component={SoftwareFleetMaintained} /> - + { + addSoftwarePackage: ({ + data, + teamId, + timeout, + onUploadProgress, + signal, + }: { + data: IPackageFormData; + teamId?: number; + timeout?: number; + onUploadProgress?: (progressEvent: AxiosProgressEvent) => void; + signal?: AbortSignal; + }) => { const { SOFTWARE_PACKAGE_ADD } = endpoints; if (!data.software) { @@ -262,21 +276,32 @@ export default { formData.append("post_install_script", data.postInstallScript); teamId && formData.append("team_id", teamId.toString()); - return sendRequest( - "POST", - SOFTWARE_PACKAGE_ADD, - formData, - undefined, + return sendRequestWithProgress({ + method: "POST", + path: SOFTWARE_PACKAGE_ADD, + data: formData, timeout, - true - ); + skipParseError: true, + onUploadProgress, + signal, + }); }, - editSoftwarePackage: ( - data: IPackageFormData, - softwareId: number, - teamId: number, - timeout?: number - ) => { + + editSoftwarePackage: ({ + data, + softwareId, + teamId, + timeout, + onUploadProgress, + signal, + }: { + data: IPackageFormData; + softwareId: number; + teamId: number; + timeout?: number; + onUploadProgress?: (progressEvent: AxiosProgressEvent) => void; + signal?: AbortSignal; + }) => { const { EDIT_SOFTWARE_PACKAGE } = endpoints; const formData = new FormData(); @@ -288,15 +313,17 @@ export default { formData.append("post_install_script", data.postInstallScript || ""); formData.append("uninstall_script", data.uninstallScript || ""); - return sendRequest( - "PATCH", - EDIT_SOFTWARE_PACKAGE(softwareId), - formData, - undefined, + return sendRequestWithProgress({ + method: "PATCH", + path: EDIT_SOFTWARE_PACKAGE(softwareId), + data: formData, timeout, - true - ); + skipParseError: true, + onUploadProgress, + signal, + }); }, + deleteSoftwarePackage: (softwareId: number, teamId: number) => { const { SOFTWARE_AVAILABLE_FOR_INSTALL } = endpoints; const path = `${SOFTWARE_AVAILABLE_FOR_INSTALL( diff --git a/frontend/services/index.ts b/frontend/services/index.ts index acca7f824f3c..a395d29b9191 100644 --- a/frontend/services/index.ts +++ b/frontend/services/index.ts @@ -1,7 +1,72 @@ -import axios, { isAxiosError, ResponseType as AxiosResponseType } from "axios"; +import axios, { + isAxiosError, + ResponseType as AxiosResponseType, + AxiosProgressEvent, +} from "axios"; import URL_PREFIX from "router/url_prefix"; import { authToken } from "utilities/local"; +export const sendRequestWithProgress = async ({ + method, + path, + data, + responseType = "json", + timeout, + skipParseError, + returnRaw, + onDownloadProgress, + onUploadProgress, + signal, +}: { + method: "GET" | "POST" | "PATCH" | "DELETE" | "HEAD"; + path: string; + data?: unknown; + responseType?: AxiosResponseType; + timeout?: number; + skipParseError?: boolean; + returnRaw?: boolean; + onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void; + onUploadProgress?: (progressEvent: AxiosProgressEvent) => void; + signal?: AbortSignal; +}) => { + const { origin } = global.window.location; + + const url = `${origin}${URL_PREFIX}/api${path}`; + const token = authToken(); + + try { + const response = await axios({ + method, + url, + data, + responseType, + timeout, + headers: { + Authorization: `Bearer ${token}`, + }, + onDownloadProgress, + onUploadProgress, + signal, + }); + + if (returnRaw) { + return response; + } + return response.data; + } catch (error) { + if (skipParseError) { + return Promise.reject(error); + } + let reason: unknown | undefined; + if (isAxiosError(error)) { + reason = error.response || error.message || error.code; + } + return Promise.reject( + reason || `send request: parse server error: ${error}` + ); + } +}; + export const sendRequest = async ( method: "GET" | "POST" | "PATCH" | "DELETE" | "HEAD", path: string, diff --git a/frontend/utilities/file/fileUtils.ts b/frontend/utilities/file/fileUtils.ts index 6853a68a69c3..30f752334039 100644 --- a/frontend/utilities/file/fileUtils.ts +++ b/frontend/utilities/file/fileUtils.ts @@ -36,3 +36,8 @@ export const getFileDetails = (file: File) => { platform: getPlatformDisplayName(file), }; }; + +export interface IFileDetails { + name: string; + platform?: string; +}