diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx index 0972192b77..cea4bfc05d 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx @@ -1,9 +1,16 @@ import React, { useCallback, useContext, useState } from "react"; -import endpoints from "utilities/endpoints"; -import { SoftwareInstallStatus, ISoftwarePackage } from "interfaces/software"; +import FileSaver from "file-saver"; + import PATHS from "router/paths"; + import { AppContext } from "context/app"; +import { NotificationContext } from "context/notification"; + +import { SoftwareInstallStatus, ISoftwarePackage } from "interfaces/software"; + +import softwareAPI from "services/entities/software"; + import { buildQueryStringFromParams } from "utilities/url"; import { internationalTimeFormat } from "utilities/helpers"; import { uploadedFromNow } from "utilities/date_format"; @@ -109,6 +116,8 @@ const SoftwarePackageCard = ({ isTeamMaintainer, } = useContext(AppContext); + const { renderFlash } = useContext(NotificationContext); + const [showAdvancedOptionsModal, setShowAdvancedOptionsModal] = useState( false ); @@ -122,18 +131,45 @@ const SoftwarePackageCard = ({ setShowDeleteModal(true); }; - const onSuccess = useCallback(() => { + const onDeleteSuccess = useCallback(() => { setShowDeleteModal(false); onDelete(); }, [onDelete]); + const onDownloadClick = useCallback(async () => { + try { + const resp = await softwareAPI.downloadSoftwarePackage( + softwareId, + teamId + ); + const contentLength = parseInt(resp.headers["content-length"], 10); + if (contentLength !== resp.data.size) { + throw new Error( + `Byte size (${resp.data.size}) does not match content-length header (${contentLength})` + ); + } + const filename = softwarePackage.name; + const file = new File([resp.data], filename, { + type: "application/octet-stream", + }); + if (file.size === 0) { + throw new Error("Downloaded file is empty"); + } + if (file.size !== resp.data.size) { + throw new Error( + `File size (${file.size}) does not match expected size (${resp.data.size})` + ); + } + FileSaver.saveAs(file); + } catch (e) { + console.log(e); + renderFlash("error", "Couldn’t download. Please try again."); + } + }, [renderFlash, softwareId, softwarePackage.name, teamId]); + const showActions = isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer; - const downloadUrl = `/api${endpoints.SOFTWARE_PACKAGE( - softwareId - )}?${buildQueryStringFromParams({ alt: "media", team_id: teamId })}`; - return (
@@ -185,13 +221,9 @@ const SoftwarePackageCard = ({ {/* TODO: make a component for download icons */} - - - + @@ -210,7 +242,7 @@ const SoftwarePackageCard = ({ softwareId={softwareId} teamId={teamId} onExit={() => setShowDeleteModal(false)} - onSuccess={onSuccess} + onSuccess={onDeleteSuccess} /> )} diff --git a/frontend/pages/SoftwarePage/components/AddSoftwareForm/AddSoftwareForm.tsx b/frontend/pages/SoftwarePage/components/AddSoftwareForm/AddSoftwareForm.tsx index 05ece55866..4f7fee9dbc 100644 --- a/frontend/pages/SoftwarePage/components/AddSoftwareForm/AddSoftwareForm.tsx +++ b/frontend/pages/SoftwarePage/components/AddSoftwareForm/AddSoftwareForm.tsx @@ -70,7 +70,6 @@ const AddSoftwareForm = ({ onCancel, onSubmit, }: IAddSoftwareFormProps) => { - console.log("rerender"); const [showPreInstallCondition, setShowPreInstallCondition] = useState(false); const [showPostInstallScript, setShowPostInstallScript] = useState(false); const [formData, setFormData] = useState({ diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts index f101759110..8ac710679f 100644 --- a/frontend/pages/SoftwarePage/components/icons/index.ts +++ b/frontend/pages/SoftwarePage/components/icons/index.ts @@ -17,6 +17,7 @@ import Word from "./Word"; import Zoom from "./Zoom"; import ChromeOS from "./ChromeOS"; import LinuxOS from "./LinuxOS"; +// import Falcon from "./Falcon"; // TODO: Add Falcon icon svg // SOFTWARE_NAME_TO_ICON_MAP list "special" applications that have a defined // icon for them, keys refer to application names, and are intended to be fuzzy @@ -33,6 +34,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = { "visual studio code": VisualStudioCode, "microsoft word": Word, zoom: Zoom, + // falcon: Falcon, darwin: MacOS, windows: WindowsOS, chrome: ChromeOS, diff --git a/frontend/pages/hosts/details/cards/Software/_styles.scss b/frontend/pages/hosts/details/cards/Software/_styles.scss index f74c1e4eac..780245ac89 100644 --- a/frontend/pages/hosts/details/cards/Software/_styles.scss +++ b/frontend/pages/hosts/details/cards/Software/_styles.scss @@ -1,10 +1,12 @@ .software-card { - .table-container__search-input { width: 325px; // Custom to fit placeholder text } - .data-table-block .data-table__wrapper { - overflow-x: scroll; - } + // // TODO: Addingoverflow-x: scroll to the table clips the actions dropdown at the bottom of the + // // table. Find a solution that allows dropdown menu to be displayed over the bottom of the table + // // in the y-axis when the table is scrollable in the x-axis. + // .data-table-block .data-table__wrapper { + // overflow-x: scroll; + // } } diff --git a/frontend/services/entities/software.ts b/frontend/services/entities/software.ts index b81539a288..40be25ddc5 100644 --- a/frontend/services/entities/software.ts +++ b/frontend/services/entities/software.ts @@ -1,3 +1,5 @@ +import { AxiosResponse } from "axios"; + import { snakeCase, reduce } from "lodash"; import sendRequest from "services"; @@ -211,6 +213,25 @@ export default { return sendRequest("DELETE", path); }, + downloadSoftwarePackage: ( + softwareTitleId: number, + teamId: number + ): Promise => { + const path = `${endpoints.SOFTWARE_PACKAGE( + softwareTitleId + )}?${buildQueryStringFromParams({ alt: "media", team_id: teamId })}`; + + return sendRequest( + "GET", + path, + undefined, + "blob", + undefined, + undefined, + true // return raw response + ); + }, + getSoftwareInstallResult: (installUuid: string) => { const { SOFTWARE_INSTALL_RESULTS } = endpoints; const path = SOFTWARE_INSTALL_RESULTS(installUuid); diff --git a/frontend/services/index.ts b/frontend/services/index.ts index 7152f042f2..acca7f824f 100644 --- a/frontend/services/index.ts +++ b/frontend/services/index.ts @@ -8,7 +8,8 @@ export const sendRequest = async ( data?: unknown, responseType: AxiosResponseType = "json", timeout?: number, - skipParseError?: boolean + skipParseError?: boolean, + returnRaw?: boolean ) => { const { origin } = global.window.location; @@ -27,6 +28,9 @@ export const sendRequest = async ( }, }); + if (returnRaw) { + return response; + } return response.data; } catch (error) { if (skipParseError) {