Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MDS-5720] Order file upload parts ascending, fix errors when uploading multiple files #2881

Merged
merged 6 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 106 additions & 30 deletions services/common/src/components/forms/RenderFileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import withFeatureFlag from "@mds/common/providers/featureFlags/withFeatureFlag"
import { createRequestHeader } from "@mds/common/redux/utils/RequestHeaders";
import { FLUSH_SOUND, WATER_SOUND } from "@mds/common/constants/assets";
import { getSystemFlag } from "@mds/common/redux/selectors/authenticationSelectors";
import { MultipartDocumentUpload } from "@mds/common/utils/fileUploadHelper.interface";
import {
MultipartDocumentUpload,
UploadResult,
} from "@mds/common/utils/fileUploadHelper.interface";
import { HttpRequest, HttpResponse } from "tus-js-client";
import { BaseInputProps } from "./BaseInput";

Expand All @@ -33,7 +36,7 @@ type AfterSuccessActionType = [
interface FileUploadProps extends BaseInputProps {
uploadUrl: string;
acceptedFileTypesMap?: { [key: string]: string };
onFileLoad?: (fileName?: string, documentGuid?: string) => void;
onFileLoad?: (fileName?: string, documentGuid?: string, versionGuid?: string) => void;
chunkSize?: number;
onAbort?: () => void;
onUploadResponse?: (data: MultipartDocumentUpload) => void;
Expand Down Expand Up @@ -94,25 +97,40 @@ export const FileUpload = (props: FileUploadProps) => {
const system = useSelector(getSystemFlag);

const [showWhirlpool, setShowWhirlpool] = useState(false);
const [uploadResults, setUploadResults] = useState([]);
const [uploadData, setUploadData] = useState(null);
const [uploadProcess, setUploadProcess] = useState({
fieldName: null,
file: props.file || null,
metadata: null,
load: null,
error: null,
progress: null,
abort: null,
});

// Used to store intermittent results of upload parts to enable
// retries of parts that fail.
const [uploadResults, setUploadResults] = useState<{ [fileId: string]: UploadResult[] }>({});

// Used to store upload information about each upload and part
// including pre-signed urls to enable retries of parts that fail,
// and replace file functionality
const [uploadData, setUploadData] = useState<{ [fileId: string]: MultipartDocumentUpload }>({});

// Stores metadata and process function for each file, so we can manually
// trigger it. Currently, this is being used for the replace file functionality
// which dynamically changes the URL of the upload if you confirm the replacement
const [uploadProcess, setUploadProcess] = useState<{
[fileId: string]: {
fieldName: string;
file: File;
metadata: any;
load: (documentGuid: string) => void;
error: (file: File, err: any) => void;
progress: (file: File, progress: number) => void;
abort: () => void;
};
}>({});

let waterSound;
let flushSound;
let filepond;

const handleError = (file, err) => {
try {
const response = JSON.parse(err.originalRequest.getUnderlyingObject().response);
const response = err.originalRequest
? JSON.parse(err.originalRequest.getUnderlyingObject().response)
: err || {};

if (
!(
Expand All @@ -125,6 +143,7 @@ export const FileUpload = (props: FileUploadProps) => {
duration: 10,
});
}

if (props.onError) {
props.onError(file && file.name ? file.name : "", err);
}
Expand All @@ -136,7 +155,7 @@ export const FileUpload = (props: FileUploadProps) => {
}
};

const handleSuccess = (documentGuid, file, load, abort) => {
const handleSuccess = (documentGuid, file, load, abort, versionGuid?) => {
let intervalId; // eslint-disable-line prefer-const

const pollUploadStatus = async () => {
Expand All @@ -145,7 +164,7 @@ export const FileUpload = (props: FileUploadProps) => {
clearInterval(intervalId);
if (response.data.status === "Success") {
load(documentGuid);
props.onFileLoad(file.name, documentGuid);
props.onFileLoad(file.name, documentGuid, versionGuid);

if (props?.afterSuccess?.action) {
try {
Expand Down Expand Up @@ -191,34 +210,56 @@ export const FileUpload = (props: FileUploadProps) => {
intervalId = setInterval(pollUploadStatus, 1000);
};

function _s3MultipartUpload(uploadUrl, file, metadata, load, error, progress, abort) {
const setUploadResultsFor = (fileId, results) => {
setUploadResults({
...uploadResults,
[fileId]: results,
});
};

const setUploadProcessFor = (fileId, process) => {
setUploadProcess({
...uploadProcess,
[fileId]: process,
});
};
const setUploadDataFor = (fileId, data) => {
setUploadData({
...uploadData,
[fileId]: data,
});
};

function _s3MultipartUpload(fileId, uploadUrl, file, metadata, load, error, progress, abort) {
return new FileUploadHelper(file, {
uploadUrl: ENVIRONMENT.apiUrl + uploadUrl,
uploadResults: uploadResults,
uploadData: uploadData,
// Pass along results and upload configuration if exists from
// previous upload attempts for this file. Occurs if retrying a failed upload.
uploadResults: uploadResults[fileId],
uploadData: uploadData[fileId],
metadata: {
filename: file.name,
filetype: file.type || APPLICATION_OCTET_STREAM,
},
onError: (err, uploadResults) => {
setUploadResults(uploadResults);
setUploadResultsFor(fileId, uploadResults);
handleError(file, err);
error(err);
},
onInit: (uploadData) => {
setUploadData(uploadData);
setUploadDataFor(fileId, uploadData);
},
onProgress: (bytesUploaded, bytesTotal) => {
progress(true, bytesUploaded, bytesTotal);
},
onSuccess: (documentGuid) => {
handleSuccess(documentGuid, file, load, abort);
onSuccess: (documentGuid, versionGuid) => {
handleSuccess(documentGuid, file, load, abort, versionGuid);
},
onUploadResponse: props.onUploadResponse,
});
}

function _tusdUpload(uploadUrl, file, metadata, load, error, progress, abort) {
function _tusdUpload(fileId, uploadUrl, file, metadata, load, error, progress, abort) {
const upload = new tus.Upload(file, {
endpoint: ENVIRONMENT.apiUrl + uploadUrl,
retryDelays: [100, 1000, 3000],
Expand Down Expand Up @@ -269,10 +310,20 @@ export const FileUpload = (props: FileUploadProps) => {
}

const server = {
process: (fieldName, file, metadata, load, error, progress, abort) => {
process: (
fieldName,
file,
metadata,
load,
error,
progress,
abort,
transfer = null,
options = null
) => {
let upload;

setUploadProcess({
setUploadProcessFor(metadata.filepondid, {
fieldName,
file,
metadata,
Expand All @@ -281,15 +332,34 @@ export const FileUpload = (props: FileUploadProps) => {
progress,
abort,
});
setUploadData(null);
setUploadResults([]);

setUploadDataFor(metadata.filepondid, null);
setUploadResultsFor(metadata.filepondid, []);

const uploadUrl = props.shouldReplaceFile ? props.replaceFileUploadUrl : props.uploadUrl;

if (props.isFeatureEnabled("s3_multipart_upload")) {
upload = _s3MultipartUpload(uploadUrl, file, metadata, load, error, progress, abort);
upload = _s3MultipartUpload(
metadata.filepondid,
uploadUrl,
file,
metadata,
load,
error,
progress,
abort
);
} else {
upload = _tusdUpload(uploadUrl, file, metadata, load, error, progress, abort);
upload = _tusdUpload(
metadata.filepondid,
uploadUrl,
file,
metadata,
load,
error,
progress,
abort
);
}

upload.start();
Expand Down Expand Up @@ -335,6 +405,11 @@ export const FileUpload = (props: FileUploadProps) => {
};
}, []);

const handleFileAdd = (err, file) => {
// Add ID to file metadata so we can reference it later
file.setMetadata("filepondid", file.id);
};

const fileValidateTypeLabelExpectedTypesMap = invert(props.acceptedFileTypesMap);
const acceptedFileTypes = uniq(Object.values(props.acceptedFileTypesMap));

Expand Down Expand Up @@ -392,6 +467,7 @@ export const FileUpload = (props: FileUploadProps) => {
// maxFiles={props.maxFiles || undefined}
allowFileTypeValidation={acceptedFileTypes.length > 0}
acceptedFileTypes={acceptedFileTypes}
onaddfile={handleFileAdd}
onprocessfiles={props.onProcessFiles}
onprocessfileabort={props.onAbort}
// oninit={props.onInit}
Expand Down
37 changes: 22 additions & 15 deletions services/common/src/redux/customAxios.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import axios from "axios";
import { notification, Button } from "antd";
import * as String from "@mds/common/constants/strings";
import React from 'react';
import React from "react";
import * as API from "@mds/common/constants/API";
import { ENVIRONMENT } from "@mds/common";
import { createRequestHeader } from "./utils/RequestHeaders";
Expand All @@ -20,18 +20,19 @@ const formatErrorMessage = (errorMessage) => {
return errorMessage.replace("(psycopg2.", "(DatabaseError.");
};

const notifymAdmin = (error) => {
let CustomAxios;

const notifymAdmin = (error) => {
const business_message = error?.response?.data?.message;
const detailed_error = error?.response?.data?.detailed_error;

const payload = {
"business_error": business_message,
"detailed_error": detailed_error
business_error: business_message,
detailed_error: detailed_error,
};

// @ts-ignore
CustomAxios().post(ENVIRONMENT.apiUrl + API.REPORT_ERROR, payload, createRequestHeader())
CustomAxios()
.post(ENVIRONMENT.apiUrl + API.REPORT_ERROR, payload, createRequestHeader())
.then((response) => {
notification.success({
message: "Error details sent to Admin. Thank you.",
Expand All @@ -41,11 +42,10 @@ const notifymAdmin = (error) => {
})
.catch((err) => {
throw new Error(err);
})
});
};

// @ts-ignore
const CustomAxios = ({ errorToastMessage, suppressErrorNotification = false } = {}) => {
CustomAxios = ({ errorToastMessage = null, suppressErrorNotification = false } = {}) => {
const instance = axios.create();

instance.interceptors.response.use(
Expand All @@ -63,21 +63,29 @@ const CustomAxios = ({ errorToastMessage, suppressErrorNotification = false } =
(errorToastMessage === "default" || errorToastMessage === undefined) &&
!suppressErrorNotification
) {
console.error('Detailed Error: ', error?.response?.data?.detailed_error)
const notificationKey = 'errorNotification';
console.error("Detailed Error: ", error?.response?.data?.detailed_error);
const notificationKey = "errorNotification";

if (isFeatureEnabled(Feature.REPORT_ERROR)) {
notification.error({
key: notificationKey,
message: formatErrorMessage(error?.response?.data?.message ?? String.ERROR),
description: <p style={{ color: 'grey' }}>If you think this is a system error please help us to improve by informing the system Admin</p>,
description: (
<p style={{ color: "grey" }}>
If you think this is a system error please help us to improve by informing the
system Admin
</p>
),
duration: 10,
btn: (
<Button type="primary" size="small"
<Button
type="primary"
size="small"
onClick={() => {
notifymAdmin(error);
notification.close(notificationKey);
}}>
}}
>
Tell Admin
</Button>
),
Expand All @@ -89,7 +97,6 @@ const CustomAxios = ({ errorToastMessage, suppressErrorNotification = false } =
duration: 10,
});
}

} else if (errorToastMessage && !suppressErrorNotification) {
notification.error({
message: errorToastMessage,
Expand Down
2 changes: 1 addition & 1 deletion services/common/src/utils/fileUploadHelper.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface FileUploadHelperProps {
uploadData?: MultipartDocumentUpload;
onError: (err, uploadResults: UploadResult[]) => void;
onProgress: (bytesUploaded: number, bytesTotal: number) => void;
onSuccess: (documentManagerGuid: string) => void;
onSuccess: (documentManagerGuid: string, documentManagerVersionGuid?: string) => void;
onInit?: (uploadData: MultipartDocumentUpload) => void;
onUploadResponse?: (data: MultipartDocumentUpload) => void;
retryDelayMs?: number;
Expand Down
Loading
Loading