fleet/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx
jacobshandling d3ccb51755
UI - Improve UX of Flash messages (#22836)
## #22661 


![ezgif-6-71e48912ae](https://github.com/user-attachments/assets/01144620-0eba-48f0-9254-cc4795fde9fd)

- Update `FlashMessage` behavior to, by default, hide itself when the
user performs any URL-changing navigation
- Add `persistOnPageChange` option to `renderFlash` API and associated
notification context and reducer logic, allowing override of this
behavior on a per-call basis
- Ensure proper order of evaluation of URL changes and render flash
action dispatches on the event loop
- Clean up legacy unused "undo"-related arguments and logic
- Allow the user to click in the same horizontal dimension as a flash
message
- Other misc. cleanup and refactoring

[Demo - messages hidden on page (any URL)
change](https://www.loom.com/share/1e884b6ba11c4b59bc74f51df3690131?sid=9b53e78b-6535-4541-b676-377760366cf4)

- [x] Changes file added for user-visible changes in `changes/`,
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-10-22 10:52:20 -07:00

187 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useContext, useEffect } from "react";
import { InjectedRouter } from "react-router";
import { isAxiosError } from "axios";
import PATHS from "router/paths";
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,
} from "services/entities/software";
import { NotificationContext } from "context/notification";
import { AppContext } from "context/app";
import { getErrorReason } from "interfaces/errors";
import CustomLink from "components/CustomLink";
import FileProgressModal from "components/FileProgressModal";
import PremiumFeatureMessage from "components/PremiumFeatureMessage";
import PackageForm from "pages/SoftwarePage/components/PackageForm";
import { IPackageFormData } from "pages/SoftwarePage/components/PackageForm/PackageForm";
import { getErrorMessage } from "./helpers";
const baseClass = "software-custom-package";
interface ISoftwarePackageProps {
currentTeamId: number;
router: InjectedRouter;
isSidePanelOpen: boolean;
setSidePanelOpen: (isOpen: boolean) => void;
}
const SoftwareCustomPackage = ({
currentTeamId,
router,
isSidePanelOpen,
setSidePanelOpen,
}: ISoftwarePackageProps) => {
const { renderFlash } = useContext(NotificationContext);
const { isPremiumTier } = useContext(AppContext);
const [uploadProgress, setUploadProgress] = React.useState(0);
const [uploadDetails, setUploadDetails] = React.useState<IFileDetails | null>(
null
);
useEffect(() => {
const beforeUnloadHandler = (e: BeforeUnloadEvent) => {
e.preventDefault();
// Next line with e.returnValue is included for legacy support
// e.g.Chrome / Edge < 119
e.returnValue = true;
};
// set up event listener to prevent user from leaving page while uploading
if (uploadDetails) {
addEventListener("beforeunload", beforeUnloadHandler);
} else {
removeEventListener("beforeunload", beforeUnloadHandler);
}
// clean up event listener and timeout on component unmount
return () => {
removeEventListener("beforeunload", beforeUnloadHandler);
};
}, [uploadDetails]);
const onCancel = () => {
router.push(
`${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams({
team_id: currentTeamId,
})}`
);
};
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.`
);
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({
data: formData,
teamId: currentTeamId,
onUploadProgress: (progressEvent) => {
const progress = progressEvent.progress || 0;
// for large uploads it seems to take a bit for the server to finalize its response so we'll keep the
// progress bar at 97% until the server response is received
setUploadProgress(Math.max(progress - 0.03, 0.01));
},
});
const newQueryParams: QueryParams = { team_id: currentTeamId };
if (formData.selfService) {
newQueryParams.self_service = true;
} else {
newQueryParams.available_for_install = true;
}
router.push(
`${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams(newQueryParams)}`
);
renderFlash(
"success",
<>
<b>{formData.software?.name}</b> successfully added.
{formData.selfService
? " The end user can install from Fleet Desktop."
: ""}
</>
);
} catch (e) {
const isTimeout =
isAxiosError(e) &&
(e.response?.status === 504 || e.response?.status === 408);
const reason = getErrorReason(e);
if (isTimeout) {
renderFlash(
"error",
`Couldnt upload. Request timeout. Please make sure your server and load balancer timeout is long enough.`
);
} else if (reason.includes("Fleet couldn't read the version from")) {
renderFlash(
"error",
<>
{reason}{" "}
<CustomLink
newTab
url={`${LEARN_MORE_ABOUT_BASE_LINK}/read-package-version`}
text="Learn more"
iconColor="core-fleet-white"
/>
</>
);
} else {
renderFlash("error", getErrorMessage(e));
}
}
setUploadDetails(null);
};
if (!isPremiumTier) {
return (
<PremiumFeatureMessage className={`${baseClass}__premium-message`} />
);
}
return (
<div className={baseClass}>
<PackageForm
showSchemaButton={!isSidePanelOpen}
onClickShowSchema={() => setSidePanelOpen(true)}
className={`${baseClass}__package-form`}
onCancel={onCancel}
onSubmit={onSubmit}
/>
{uploadDetails && (
<FileProgressModal
fileDetails={uploadDetails}
fileProgress={uploadProgress}
/>
)}
</div>
);
};
export default SoftwareCustomPackage;